Style中的轻量级插件化方案
阅读之前
- 建议下载使用Style动态壁纸应用
- 文章后面会给出相应引用的链接
- 如需评论请翻墙后刷新页面
文章主要讲以下几个方面:
- 插件化背景及优缺点
- Style中的轻量级插件
- Style壁纸插件开发者SDK介绍
- 构建第一个Style壁纸组件
- 其他注意问题
Android插件化背景
Android中的插件化,旨在无需安装、修改的情况下运行APK。例如某应用包含非常多的功能,除了核心功能之外,其他功能都与主要业务比较独立,那么其他功能模块就可以考虑用插件方式开发。
目前国内比较大的几款应用都有类似的场景。各大厂也有相应的插件框架,例如BAT、滴滴的VirtualAPK、360的DroidPlugin等等。而且VirtualAPK和DroidPlugin是开源框架,大家有兴趣可以深入学习研究。
从我自身对插件化的接触,对它的优缺点做了一些总结:
优点:
- 减少宿主包大小,插件模块按需下载
- 实现多团队协作开发,由独立团队对插件进行开发
- 插件动态更新,插件包可以随时用户无感知的进行更新
缺点:
- 插件框架开发难度较大,插件框架开发需要深入了解Android的系统源码,并需要对系统级别方法进行大量hook,处理各种高难度问题
- 插件的内存不易管理,如果插件和宿主存在同一进程中,那么宿主的内存使用是宿主+插件两者的内存使用之和,甚至导致宿主OOM,通常需要将插件放在独立进程中
- 对插件包的大小、插件的质量要求比较严格。插件包太大宿主在首次加载时会消耗太多流量下载。插件如果运行出现异常,可能会导致宿主崩溃,影响用户体验
- 插件很难做到完全不修改,插件框架也很难兼容系统的所有组件
上述问题虽然难度较大,但并非不能解决,相信各大公司都有比较完善但插件框架。小公司如果没有大牛,在使用的时候也须谨慎。
Style中的轻量级插件
Style中包含三类壁纸:特效壁纸、Style艺术图片、自定义照片。其中Style艺术图片和自定义照片都是将一张图片渲染成壁纸,因此两者的渲染逻辑是一样的。而特效壁纸每一个都有不一样的效果,渲染逻辑代码都不一样。
考虑到这一点,Style将特效壁纸做成插件的形式。有新的壁纸增加时,Style能及时更新并动态加载新的壁纸。另外,这种插件不需要是一个完整的APK,因为Style只会加载里面的WallpaperService
类以使用它的渲染逻辑。
因此Style中的插件就不需要太完整,这样能大大简化插件框架的开发,简化插件的开发。Style中将这种不完整的插件称之为壁纸组件,下面我会用“组件”这个词来表示Style中的插件。
“engine”模块包含了运行组件的所有代码。Manifest
中注册的WallpaperService
类在“presentation”模块中,依赖了“engine”模块。代码如下:
public class StyleWallpaperService extends WallpaperService {
private ProxyProvider proxyProvider = new ProxyProvider();
private WallpaperService proxy;
@Override
public void onCreate() {
super.onCreate();
proxy = proxyProvider.provideProxy(this);
if (proxy != null) {
proxy.onCreate();
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (proxy != null) {
proxy.onDestroy();
}
}
@Override
public Engine onCreateEngine() {
if (proxy != null) {
return proxy.onCreateEngine();
} else {
return new Engine();
}
}
}
可以看出系统的WallpaperService
简单的将所有逻辑交给我们的代理Service,onCreateEngine()
方法中也是由代理返回Engine
对象。
我们一共有两个代理类:WallpaperServiceProxy
和GLWallpaperServiceProxy
,他们提供了不同的渲染支持。以下将这两个代理类简称为Proxy
public class WallpaperServiceProxy extends WallpaperService {
public WallpaperServiceProxy(Context host) {
attachBaseContext(host);
}
@Override
public Engine onCreateEngine() {
return null;
}
public class ActiveEngine extends Engine {
}
}
public class GLWallpaperServiceProxy extends GLWallpaperService {
public GLWallpaperServiceProxy(Context host) {
attachBaseContext(host);
}
public class GLActiveEngine extends GLEngine {
}
}
在Proxy中,我们会有一个带有Context
对象参数的构造方法,并在构造方法中利用attachBaseContext(host)
指定了Proxy对象的Context。但是这个Context
对象是一个特殊的Context
,组件会通过它来获取ClassLoader
和Resource
,来加载组件中的类和资源。
那么下面我们来看看这个Context
到底有什么特殊:
public class ComponentContext extends ContextWrapper {
private String componentPath;
private Resources mResources;
private ClassLoader mClassLoader;
public ComponentContext(Context base, String componentPath) {
super(base.getApplicationContext());
this.componentPath = componentPath;
}
@Override
public Resources getResources() {
if (mResources == null) {
mResources = ResourcesManager.createResources(getBaseContext(), componentPath);
}
return mResources;
}
@Override
public ClassLoader getClassLoader() {
return getClassLoader(componentPath);
}
private ClassLoader getClassLoader(String componentFilePath) {
if (mClassLoader == null) {
mClassLoader = new DexClassLoader(componentFilePath, getCacheDir().getAbsolutePath(),
null, getBaseContext().getClassLoader());
}
return mClassLoader;
}
@Override
public AssetManager getAssets() {
return getResources().getAssets();
}
@Override
public Context getApplicationContext() {
return this;
}
}
首先它是ContextWrapper
的子类,并且有一个构造方法,构造方法第一个参数是宿主的Application Context
对象,第二个参数是组件包的存放路径。
然后它重写了四个常用的方法:getResources()
、getClassLoader()
、getAssets()
、getApplicationContext()
,前三方法返回了跟组件相关的类加载器、资源、和AssetsManager
,最后一个方法是为了兼容组件中getApplicationContext()
的使用。
除了这四个方法外,其他的Context
中的方法均有使用宿主现有的方法。轻量级可以说就是这个意思,意味着宿主只关心组件中的类和资源,不关心里面的系统组件和其他东西。
我们再来看看IProvider
这个接口。组件中通过实现它来返回组件中的Proxy实现
public interface IProvider {
WallpaperService provideProxy(Context host);
}
那么我们在宿主中是如何获取到它的实现呢?
public class ProxyApi {
private static IProvider getProvider(ComponentContext context, String providerName)
throws Exception {
synchronized (ProxyApi.class) {
IProvider provider;
Class providerClazz = context.getClassLoader().loadClass(providerName);
if (providerClazz != null) {
provider = (IProvider) providerClazz.newInstance();
return provider;
} else {
throw new IllegalStateException("Load Provider error.");
}
}
}
public static WallpaperService getProxy(Context context,
String componentPath, String providerName) {
try {
ComponentContext componentContext = new ComponentContext(context, componentPath);
IProvider provider = getProvider(componentContext, providerName);
return provider.provideProxy(componentContext);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
在ProxyApi
类的getProvider()
方法中,通过先前的ComponentContext
对象获取到组件的ClassLoader
,再通过IProvider
实现的类名来加载其实现。在getProxy()
方法中构造了ComponentContext
实例,并通过IProvider
的实现获取到组件中的Proxy对象。
将上面的逻辑串联起来,我们发现StyleWallpaperService
可以和组件中的Proxy对象直接交互了。那么Proxy对象就来完成壁纸的渲染工作。就是说壁纸的渲染工作我们交给了组件来处理。
图中粉色部分是宿主,也就是Style。紫色部分是组件,也就是壁纸具体的实现。插件化方案将它们解耦,让壁纸的实现实现了动态部署、热更新、热修复。
Style壁纸插件开发者SDK介绍
插件化的一个好处是可以跨团队协作,由其他团队进行插件开发。因此Style的特效壁纸便开放给开发者,任何人都可以参与开发。
Style提供了一套简易的SDK,供开发者以Style组件的规范进行壁纸开发。SDK可以从Github上找到。它包含三个模块:sdk、壳、具体的实现。sdk和壳模块不需要任何的修改,开发者主要在实现模块进行开发。三者的依赖关系如下:
实现模块编译时依赖sdk模块来使用IProvder
和Proxy类,但必须是使用“Provided”构建,防止将sdk的代码打入组件包中。它们都是library
模块。
壳模块没有代码、资源,是一个简单的application
模块。目的是将实现模块编译进去,以生成APK包。
相对于宿主运行环境(“engine”模块),sdk模块提供的代码则精简很多,它只是提供了编译环境,不需要任何实现。
实现模块也只有两个东西,IProvider
的实现类、Proxy的实现类。代码量视渲染逻辑的复杂度有所区别。
可能你会有个疑问,就是构建好壁纸组件之后我们如何对它进行测试?直接用Style应用吗?
在sdk工程根目录中,我提供了一个测试应用:sdk_test.apk。它包含了Style运行壁纸组件的完整环境。简单的说,如果它能加载壁纸组件并成功运行,那么Style也能。完整的测试步骤可以参看说明
构建第一个Style壁纸组件
好了,花了大篇幅讲述Style中组件的原理和开发者sdk。现在我们来尝试使用sdk构建一款Style壁纸组件。
上一篇文章我讲了Android如何创建动态壁纸。里面的第一个例子显示了一些圆点。完整的代码可以看这里。 下面我就用这个例子来说明如何利用sdk来构建壁纸组件。
1、我们新建一个工程,Activity什么的都不需要。 2、在新工程中新建sdk(library)模块,并将sdk的代码放进去。注意包名必须是com.yalin.style.engine
和net.rbgrn.android.glwallpaperservice
,后面我会将它放到Maven仓库中,一句代码就可以搞定了。 3、新建实现模块point_wallpaper(library),并在它的build.gradle中添加依赖provided project(':sdk')。 4、在point_wallpaper模块中创建自己的WallpaperService
实现类,PointWallpaperServiceProxy
继承自WallpaperServiceProxy
public class PointWallpaperServiceProxy extends WallpaperServiceProxy {
public PointWallpaperServiceProxy(Context host) {
super(host);
}
@Override
public Engine onCreateEngine() {
return new MyWallpaperEngine();
}
private class MyWallpaperEngine extends ActiveEngine {
private final Handler handler = new Handler();
private final Runnable drawRunner = new Runnable() {
@Override
public void run() {
draw();
}
};
private List<MyPoint> circles;
private Paint paint = new Paint();
private int width;
int height;
private boolean visible = true;
private int maxNumber;
private boolean touchEnabled;
public MyWallpaperEngine() {
maxNumber = 4;
touchEnabled = true;
circles = new ArrayList<>();
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeWidth(10f);
handler.post(drawRunner);
}
@Override
public void onVisibilityChanged(boolean visible) {
this.visible = visible;
if (visible) {
handler.post(drawRunner);
} else {
handler.removeCallbacks(drawRunner);
}
}
@Override
public void onSurfaceDestroyed(SurfaceHolder holder) {
super.onSurfaceDestroyed(holder);
this.visible = false;
handler.removeCallbacks(drawRunner);
}
@Override
public void onSurfaceChanged(SurfaceHolder holder, int format,
int width, int height) {
this.width = width;
this.height = height;
super.onSurfaceChanged(holder, format, width, height);
}
@Override
public void onTouchEvent(MotionEvent event) {
if (touchEnabled) {
float x = event.getX();
float y = event.getY();
SurfaceHolder holder = getSurfaceHolder();
Canvas canvas = null;
try {
canvas = holder.lockCanvas();
if (canvas != null) {
canvas.drawColor(Color.BLACK);
circles.clear();
circles.add(new MyPoint(
String.valueOf(circles.size() + 1), (int) x, (int) y));
drawCircles(canvas, circles);
}
} finally {
if (canvas != null)
holder.unlockCanvasAndPost(canvas);
}
super.onTouchEvent(event);
}
}
private void draw() {
SurfaceHolder holder = getSurfaceHolder();
Canvas canvas = null;
try {
canvas = holder.lockCanvas();
if (canvas != null) {
if (circles.size() >= maxNumber) {
circles.clear();
}
int x = (int) (width * Math.random());
int y = (int) (height * Math.random());
circles.add(new MyPoint(String.valueOf(circles.size() + 1),
x, y));
drawCircles(canvas, circles);
}
} finally {
if (canvas != null)
holder.unlockCanvasAndPost(canvas);
}
handler.removeCallbacks(drawRunner);
if (visible) {
handler.postDelayed(drawRunner, 1000);
}
}
// Surface view requires that all elements are drawn completely
private void drawCircles(Canvas canvas, List<MyPoint> circles) {
canvas.drawColor(Color.BLACK);
for (MyPoint point : circles) {
canvas.drawCircle(point.x, point.y, 20.0f, paint);
}
}
}
}
这里是用了MyPoint
类,代码如下:
public class MyPoint {
String text;
int x;
int y;
public MyPoint(String text, int x, int y) {
this.text = text;
this.x = x;
this.y = y;
}
}
5、实现IProvider
并返回第四步的PointWallpaperServiceProxy
实例
public class ProviderImpl implements IProvider {
@Override
public WallpaperService provideProxy(Context host) {
return new PointWallpaperServiceProxy(host);
}
}
6、将新建工程时的app模块当作壳模块,引用point_wallpaper模块,compile project(':point_wallpaper')。 7、运行_./gradlew assemble_,生成apk文件,有没有签名没有关系,并将它放到/sdcard/style/目录下,假设名称叫point.apk。 8、安装测试应用(sdk中的sdk_test.apk) 9、创建配置文件config.json,加入下面json。也将它放到/sdcard/style/目录下。point.apk是/sdcard/style/中组件的文件名。“provider_name”是IProvider
实现类的完整类名。
{
"component_name": "point.apk",
"provider_name": "com.yalin.wallpaper.point.ProviderImpl"
}
10、运行测试应用,点击设置壁纸按钮。出现下面的界面
简单的几步,第一个组件应用建好了,并能在宿主中运行。你也可以将这些方法运用到你的项目中去。你可以对比组件中的渲染逻辑和原来demo中的渲染逻辑,他们完全是一样的。也就印证了上面说的简化组件开发(因为可以直接把现成的移植过来)。
其他注意问题
- 组件混淆
-keep class * implements com.yalin.style.engine.IProvider
- 打包时尽可能删掉其他不需要的依赖(例如appcompat-v7),以减小组件包大小。
- 组件包中可以包含资源和Assets,但是现在不支持运行.so。
- 运行测试应用时记得为它开启读取外存权限。
总结
也许我们潜移默化的认为,只有大的项目才有机会用到插件化,毕竟小公司业务相对不复杂、也不一定有那么多人去维护插件框架。但是通过这次实验,我们完全可以将一些功能做成轻量级插件,以实现动态的更新发布、分团队开发、解耦。而且宿主中插件相关的代码量非常少,几百来行,易于维护。那么,何不在你的项目中试试呢。
插件相关开源项目
VirtualApk(滴滴)
DroidPlugin(360)