Style中的轻量级插件化方案

wr0416 2年前
   <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>