android 插件框架实现原理

未来已来2018-09-14 09:25

作者:马保祥


为了满足产品中一些功能动态部署的需求,我们在android插件化开发方面进行了一些探索,并完成了一套比较成熟的插件管理框架。

这套框架的主要特点:
1 支持插件的热更新(即插件更新后无需重启框架);
2 低侵入性(插件中资源加载及Activity跳转与原生应用开发基本一致)
下面结合以上两个特性对相关技术进行一些讲解希望能抛砖引玉。

1 插件的热更新
插件框架的设计类似OSGI,每一个插件使用一个独立的类加载器进行加载。插件升级时会创建一个新的加载器来加载新版本所以即使框架不重启也不影响新版本的使用。
在DVM中ClassLoader符合双亲委托加载模式,DexclassLoader同样遵守了这规则,android中常用的类加载器关系如下:
BootClassLoader: parentClassLoader=null,             负责加载系统类(比如:java.lang.*  、android.app.Activity等)
PathClassLoader: parentClassLoader=BootClassLoader   负责加载我们自己的apk包中的类, 为了后面描述方便,这里我们将其命名为 HostAppClassLoader
ClassLoader.getSystemClassLoader()  parentClassLoader=BootClassLoader   该类加载器自身并不加载任何类,根据具体使用场景可以在创建自定义ClassLoader时使用,但该框架中我们并没用到这个类。
该框架中类加载器的关系如下:
BootClassLoader->HostAppClassLoader-> FrameworkClassloader(DexCalssLoader)->APKPlugigClassLoader(DexCalssLoader)
FrameworkClassloader 负责加载插件框架的实现类。为了使插件SDK可以自动升级,插件框架自身也是动态加载的,我们开发时加到项目中的jar包只是框架接口。
ApkPluginClassLoader          负责加载每一个插件。
根据上面的类加载依赖关系,我们可以看出,插件中使用到的公共包只在主程序中放一份就可以。比如: V4 包。在代码混淆时也要注意公共包不能被混淆。

2 低侵入性
  资源加载 如果想加载插件中的资源,我们需要创建自己的ResourceManager,这里需要使用一个私有API
 try {
      AssetManager am = (AssetManager) AssetManager.class.newInstance();
      Method add = am.getClass().getMethod("addAssetPath", String.class);
      add.setAccessible(true);
      add.invoke(am, path);//这里的path路径指向我们插件APK的地址
      assetManager = am;
    } catch (Exception e) {
      throw new RuntimeException(e);
    }

    Resources superRes = cnx.getResources();

    resources = new Resources(assetManager, superRes.getDisplayMetrics(),
        superRes.getConfiguration());
通过阅读源码我们可以了解到context在资源加载时也类似类加载一样会委托给自己的BaseContext去加载,所以如果想在插件Activity插件中直接使用 setContent() 和getResource()等方法,我们需要构造自己的BaseContext然后通过反射替换掉系统原有的BaseContext和resource对象。下面直接给出需要替换的一些变量
window - > mLayoutInflater
Activity-> mBase , mResource
//创建自己的layoutInflater
LayoutInflater inflater = activity.getWindow().getLayoutInflater().cloneInContext(myContext);
ClassUtils.replaceField("com.android.internal.policy.impl.PhoneWindow",
"mLayoutInflater", activity.getWindow(), inflater );
启动Activity  如何启动一个没有在manifest中配置的Activity?
通过阅读系统Activity启动相关的源码,我整理了下图:


从图中可以看出每个Activity的启动都通过Instrumentation进行一步代理,我们常用的自动化测试也正是基于重写Instrumentation来得到Activity生命周期的各个事件。通过分析Instrumentation源码,我们发现可以重写 newActivity 方法构造自己的Activity对象返回给系统,这样以后onCreate等事件都入自动传入我们的Activity。
现在插件框架中的实现原理是:
预先在主程序的manifest中声明一个PluginActivity,当启动插件中activity A 时我们在Instrumentation的execStartActivity方法中告诉系统我要启动PluginActivity,但是当系统真正构造PluginActivity对象时我们在newActivity方法中 new A()对象返回系统。通过这种方式我们欺骗了系统框架启动了我们真正想要的Activity
至此,资源加载和Activity跳转2个最关键的问题都解决了,这里还有很多细节问题不在此一一列举,详情可以查看源码。
插件框架目前适用于一些与主业务相对比较独立的模块开发,插件中可以使用.so库,界面开发与原生应用开发基本一致最终也是生成一个apk包,无需安装直接启动。现在还有很多不足之处,比如:插件中无法使用系统service 、插件所需权限需要在主程序中定义、插件故障隔离未处理等等。


本文来自网易实践者社区,经作者马保祥授权发布