Style中的轻量级插件化方案
wr0416
7年前
<h2>阅读之前</h2> <ul> <li>建议 <a href="/misc/goto?guid=4959750917859281067" rel="nofollow,noindex">下载使用</a> Style动态壁纸应用</li> <li>文章后面会给出相应引用的链接</li> <li>如需评论请F墙后刷新页面</li> </ul> <p>文章主要讲以下几个方面:</p> <ul> <li>插件化背景及优缺点</li> <li>Style中的轻量级插件</li> <li>Style壁纸插件开发者SDK介绍</li> <li>构建第一个Style壁纸组件</li> <li>其他注意问题</li> </ul> <h2>Android插件化背景</h2> <p>Android中的插件化,旨在无需安装、修改的情况下运行APK。例如某应用包含非常多的功能,除了核心功能之外,其他功能都与主要业务比较独立,那么其他功能模块就可以考虑用插件方式开发。</p> <p>目前国内比较大的几款应用都有类似的场景。各大厂也有相应的插件框架,例如BAT、滴滴的VirtualAPK、360的DroidPlugin等等。而且VirtualAPK和DroidPlugin是开源框架,大家有兴趣可以深入学习研究。</p> <p>从我自身对插件化的接触,对它的优缺点做了一些总结:</p> <p>优点:</p> <ul> <li>减少宿主包大小,插件模块按需下载</li> <li>实现多团队协作开发,由独立团队对插件进行开发</li> <li>插件动态更新,插件包可以随时用户无感知的进行更新</li> </ul> <p>缺点:</p> <ul> <li>插件框架开发难度较大,插件框架开发需要深入了解Android的系统源码,并需要对系统级别方法进行大量hook,处理各种高难度问题</li> <li>插件的内存不易管理,如果插件和宿主存在同一进程中,那么宿主的内存使用是宿主+插件两者的内存使用之和,甚至导致宿主OOM,通常需要将插件放在独立进程中</li> <li>对插件包的大小、插件的质量要求比较严格。插件包太大宿主在首次加载时会消耗太多流量下载。插件如果运行出现异常,可能会导致宿主崩溃,影响用户体验</li> <li>插件很难做到完全不修改,插件框架也很难兼容系统的所有组件</li> </ul> <p>上述问题虽然难度较大,但并非不能解决,相信各大公司都有比较完善但插件框架。小公司如果没有大牛,在使用的时候也须谨慎。</p> <h2>Style中的轻量级插件</h2> <p>Style中包含三类壁纸:特效壁纸、Style艺术图片、自定义照片。其中Style艺术图片和自定义照片都是将一张图片渲染成壁纸,因此两者的渲染逻辑是一样的。而特效壁纸每一个都有不一样的效果,渲染逻辑代码都不一样。</p> <p>考虑到这一点,Style将特效壁纸做成插件的形式。有新的壁纸增加时,Style能及时更新并动态加载新的壁纸。另外,这种插件不需要是一个完整的APK,因为Style只会加载里面的 WallpaperService 类以使用它的渲染逻辑。</p> <p>因此Style中的插件就不需要太完整,这样能大大简化插件框架的开发,简化插件的开发。Style中将这种不完整的插件称之为壁纸组件,下面我会用“组件”这个词来表示Style中的插件。</p> <p><img src="https://simg.open-open.com/show/8d5037ad798472b1fdc8b0c3458bbf93.jpg"></p> <p>“engine”模块包含了运行组件的所有代码。 Manifest 中注册的 WallpaperService 类在“presentation”模块中,依赖了“engine”模块。代码如下:</p> <pre> <code class="language-javascript">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(); } } }</code></pre> <p>可以看出系统的 WallpaperService 简单的将所有逻辑交给我们的代理Service, onCreateEngine() 方法中也是由代理返回 Engine 对象。</p> <p>我们一共有两个代理类: WallpaperServiceProxy 和 GLWallpaperServiceProxy ,他们提供了不同的渲染支持。以下将这两个代理类简称为Proxy</p> <pre> <code class="language-javascript">public class WallpaperServiceProxy extends WallpaperService { public WallpaperServiceProxy(Context host) { attachBaseContext(host); } @Override public Engine onCreateEngine() { return null; } public class ActiveEngine extends Engine { } }</code></pre> <pre> <code class="language-javascript">public class GLWallpaperServiceProxy extends GLWallpaperService { public GLWallpaperServiceProxy(Context host) { attachBaseContext(host); } public class GLActiveEngine extends GLEngine { } }</code></pre> <p>在Proxy中,我们会有一个带有 Context 对象参数的构造方法,并在构造方法中利用 attachBaseContext(host) 指定了Proxy对象的Context。但是这个 Context 对象是一个特殊的 Context ,组件会通过它来获取 ClassLoader 和 Resource ,来加载组件中的类和资源。</p> <p>那么下面我们来看看这个 Context 到底有什么特殊:</p> <pre> <code class="language-javascript">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; } }</code></pre> <p>首先它是 ContextWrapper 的子类,并且有一个构造方法,构造方法第一个参数是宿主的 Application Context 对象,第二个参数是组件包的存放路径。</p> <p>然后它重写了四个常用的方法: getResources() 、 getClassLoader() 、 getAssets() 、 getApplicationContext() ,前三方法返回了跟组件相关的类加载器、资源、和 AssetsManager ,最后一个方法是为了兼容组件中 getApplicationContext() 的使用。</p> <p>除了这四个方法外,其他的 Context 中的方法均有使用宿主现有的方法。轻量级可以说就是这个意思,意味着宿主只关心组件中的类和资源,不关心里面的系统组件和其他东西。</p> <p>我们再来看看 IProvider 这个接口。组件中通过实现它来返回组件中的Proxy实现</p> <pre> <code class="language-javascript">public interface IProvider { WallpaperService provideProxy(Context host); }</code></pre> <p>那么我们在宿主中是如何获取到它的实现呢?</p> <pre> <code class="language-javascript">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; } }</code></pre> <p>在 ProxyApi 类的 getProvider() 方法中,通过先前的 ComponentContext 对象获取到组件的 ClassLoader ,再通过 IProvider 实现的类名来加载其实现。在 getProxy() 方法中构造了 ComponentContext 实例,并通过 IProvider 的实现获取到组件中的Proxy对象。</p> <p>将上面的逻辑串联起来,我们发现 StyleWallpaperService 可以和组件中的Proxy对象直接交互了。那么Proxy对象就来完成壁纸的渲染工作。就是说壁纸的渲染工作我们交给了组件来处理。</p> <p><img src="https://simg.open-open.com/show/411d88ff0fe038428cf94d06caaaea46.jpg"></p> <p>图中粉色部分是宿主,也就是Style。紫色部分是组件,也就是壁纸具体的实现。插件化方案将它们解耦,让壁纸的实现实现了动态部署、热更新、热修复。</p> <h2>Style壁纸插件开发者SDK介绍</h2> <p>插件化的一个好处是可以跨团队协作,由其他团队进行插件开发。因此Style的特效壁纸便开放给开发者,任何人都可以参与开发。</p> <p>Style提供了一套简易的SDK,供开发者以Style组件的规范进行壁纸开发。SDK可以从 <a href="/misc/goto?guid=4959751341071630632" rel="nofollow,noindex">Github</a> 上找到。它包含三个模块:sdk、壳、具体的实现。sdk和壳模块不需要任何的修改,开发者主要在实现模块进行开发。三者的依赖关系如下: <img src="https://simg.open-open.com/show/e04c8f781a9e215823e7acfa07421908.jpg"></p> <p>实现模块编译时依赖sdk模块来使用 IProvder 和Proxy类,但必须是使用“Provided”构建,防止将sdk的代码打入组件包中。它们都是 library 模块。</p> <p>壳模块没有代码、资源,是一个简单的 application 模块。目的是将实现模块编译进去,以生成APK包。</p> <p>相对于宿主运行环境(“engine”模块),sdk模块提供的代码则精简很多,它只是提供了编译环境,不需要任何实现。 <img src="https://simg.open-open.com/show/6cd010da8a19bfc8c4ebfad59119e2c9.jpg"></p> <p>实现模块也只有两个东西, IProvider 的实现类、Proxy的实现类。代码量视渲染逻辑的复杂度有所区别。 <img src="https://simg.open-open.com/show/2d87fe8972d86b8cdf57573f93fd6df9.jpg"></p> <p>可能你会有个疑问,就是构建好壁纸组件之后我们如何对它进行测试?直接用Style应用吗?</p> <p>在sdk工程根目录中,我提供了一个测试应用:sdk_test.apk。它包含了Style运行壁纸组件的完整环境。简单的说,如果它能加载壁纸组件并成功运行,那么Style也能。完整的测试步骤可以参看 <a href="/misc/goto?guid=4959751341164525468" rel="nofollow,noindex">说明</a></p> <h2>构建第一个Style壁纸组件</h2> <p>好了,花了大篇幅讲述Style中组件的原理和开发者sdk。现在我们来尝试使用sdk构建一款Style壁纸组件。</p> <p><a href="/misc/goto?guid=4959751341250121649" rel="nofollow,noindex">上一篇文章</a> 我讲了Android如何创建动态壁纸。里面的第一个例子显示了一些圆点。完整的代码可以看 <a href="/misc/goto?guid=4959750917947425728" rel="nofollow,noindex">这里</a> 。 <img src="https://simg.open-open.com/show/2446daedf0f4cb28c6661ece827fac5b.gif"> 下面我就用这个例子来说明如何利用sdk来构建壁纸组件。</p> <p>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</p> <pre> <code class="language-javascript">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); } } } }</code></pre> <p>这里是用了 MyPoint 类,代码如下:</p> <pre> <code class="language-javascript">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; } }</code></pre> <p>5、实现 IProvider 并返回第四步的 PointWallpaperServiceProxy 实例</p> <pre> <code class="language-javascript">public class ProviderImpl implements IProvider { @Override public WallpaperService provideProxy(Context host) { return new PointWallpaperServiceProxy(host); } }</code></pre> <p>6、将新建工程时的app模块当作壳模块,引用point_wallpaper模块, <em>compile project(':point_wallpaper')</em> 。 7、运行 <em>./gradlew assemble</em> ,生成apk文件,有没有签名没有关系,并将它放到/sdcard/style/目录下,假设名称叫point.apk。 8、安装测试应用(sdk中的sdk_test.apk) 9、创建配置文件config.json,加入下面json。也将它放到/sdcard/style/目录下。point.apk是/sdcard/style/中组件的文件名。“provider_name”是 IProvider 实现类的完整类名。</p> <pre> <code class="language-javascript">{ "component_name": "point.apk", "provider_name": "com.yalin.wallpaper.point.ProviderImpl" }</code></pre> <p>10、运行测试应用,点击设置壁纸按钮。出现下面的界面 <img src="https://simg.open-open.com/show/bd9de41dd42abf8d6f21ce4ac86106bf.jpg"></p> <p>简单的几步,第一个组件应用建好了,并能在宿主中运行。你也可以将这些方法运用到你的项目中去。你可以对比组件中的渲染逻辑和原来demo中的渲染逻辑,他们完全是一样的。也就印证了上面说的简化组件开发(因为可以直接把现成的移植过来)。</p> <p>其他注意问题</p> <ul> <li>组件混淆 -keep class * implements com.yalin.style.engine.IProvider</li> <li>打包时尽可能删掉其他不需要的依赖(例如appcompat-v7),以减小组件包大小。</li> <li>组件包中可以包含资源和Assets,但是现在不支持运行.so。</li> <li>运行测试应用时记得为它开启读取外存权限。</li> </ul> <p>总结</p> <p>也许我们潜移默化的认为,只有大的项目才有机会用到插件化,毕竟小公司业务相对不复杂、也不一定有那么多人去维护插件框架。但是通过这次实验,我们完全可以将一些功能做成轻量级插件,以实现动态的更新发布、分团队开发、解耦。而且宿主中插件相关的代码量非常少,几百来行,易于维护。那么,何不在你的项目中试试呢。</p> <p> </p> <p>来自:http://www.jcodecraeer.com/a/anzhuokaifa/2017/0807/8350.html</p> <p> </p>