Android之插件化

目标:实现运行时加载插件.apk文件

实现原理简介:

1、插件apk的class

通过构造插件apk的Dexclassloader来加载插件apk中的类。

DexClassLoader的parent设置为宿主程序的classloader,即可将主程序和插件程序的class贯通。

若是独立插件,将parent设置为宿主程序的classloader的parent,可隔离宿主class和插件class,此时宿主和插件可包含同名的class。

2、插件apk的Resource

直接构造插件apk的AssetManager和Resouce对象即可,需要注意的是,

通过addAssetsPath方法添加资源的时候,需要同时添加插件程序的资源文件和宿主程序的资源,

以及其依赖的资源。这样可以将Resource合并到一个Context里面去,解决资源访问时需要切换上下文的问题。

3、插件apk中的资源id冲突

完成上述第二点以后,宿主程序资源id和插件程序id可能有重复而参数冲突。
我们知道,资源id是在编译时生成的,其生成的规则是0xPPTTNNNN

PP段,是用来标记apk的,默认情况下系统资源PP是01f,应用程序的PP是07f

TT段,是用来标记资源类型的,比如图标、布局等,相同的类型TT值相同,但是同一个TT值

不代表同一种资源,例如这次编译的时候可能使用03作为layout的TT,那下次编译的时候可能
会使用06作为TT的值,具体使用那个值,实际上和当前APP使用的资源类型的个数是相关联的。
NNNN则是某种资源类型的资源id,默认从1开始,依次累加。

那么我们要解决资源id问题,就可从TT的值开始入手,只要将每次编译时的TT值固定,即可是资
源id达到分组的效果,从而避免重复。例如将宿主程序的layout资源的TT固定为33,将插件程序
资源的layout的TT值固定为03(也可不对插件程序的资源id做任何处理,使其使用编译出来的原生的值), 即可解决资源id重复的问题了。

固定资源id的TT值的办法也非常简单,提供一份public.xml,在public.xml中指定什么资源类型以
什么TT值开头即可。具体public.xml如何编写,可参考FairyPlugin/public.xml,是用来固定宿主程序资源id范围的。

还有一个方法是通过定制过的aapt在编译插件时指定id范围来解决冲突(For-gradle-with-aapt分支采用的方案)
此方案需要替换sdk原生的aapt,且要区分多平台,buildTools版本更新后需同步升级aapt。
定制的aapt由 openAtlasExtention@github 项目提供,目前的版本是基于22.0.1,将项目中的BuildTools替换
到本地Android Sdk中相应版本的BuildTools中,并指定gradle的buildTools version为对应版本即可。

4、插件apk的Context和LayoutInfalter

构造一个Context对象即可,具体的Context实现请参考PluginContextTheme.java
关键是要重写几个获取资源、主题的方法,以及重写getClassLoader方法,再从构造粗来的context中获取LayoutInfalter

6、插件代码无约定无规范约束。

要做到这一点,主要有几点:

    1、上诉第4步骤,

    2、在classloader树中插入自己的Classloader,在loadclass时进行映射

    3、替换ActivityThread的的Instrumentation对象和Handle CallBack对象,用来拦截组件的创建过程。

    4、利用反射修改成员变量,注入Context。利用反射调用隐藏方法。

    5、插件中Activity等不在宿主manifest中注册即拥有完整生命周期的方法。

由于Activity等是系统组件,必须在manifest中注册才能被系统唤起并拥有完整生命周期。
通过反射代理方式实现的实际是伪生命周期,并非完整生命周期。要实现插件组件免注册有2个方法。

前提:宿主中预注册几个组件。预注册的组件可实际存在也可不存在。

a、替换classloader。适用于所有组件。
    App安装时,系统会扫描app的Manifest并缓存到一个xml中,activity启动时,系统会现在查找缓存的xml,
    如果查到了,再通过classLoad去load这个class,并构造一个activity实例。那么我们只需要将classload
    加载这个class的时候做一个简单的映射,让系统以为加载的是A class,而实际上加载的是B class,达到挂羊头买狗肉的效果,
    即可将预注册的A组件替换为未注册的插件中的B组件,从而实现插件中的组件
    完全被系统接管,而拥有完整生命周期。其他组件同理。


b、替换Instrumention。

    这种方式仅适用于Activity。通过修改Instrumentation进行拦截,可以利用Intent传递参数。

    如果是Receiver和Service,利用Handler Callback进行拦截,再配合Classloader在loadclass时进行映射

8、通过activity代理方式实现加载插件中的activity是如何实现的

要实现这一点,同样是基于上述第4点,构造出插件的Context后,通过attachBaseContext的方式,
替换代理Activiyt的context即可。

另外还需要在获得插件Activity对象后,通过反射给Activity的attach()方法中attach的成员变量赋值。

更新:activity代理方式已放弃,不再支持,要了解实现可以查看历史版本

9、插件主题

重要实现原理仍然基于上述第2、3点。

10、插件Activity的LaunchMode

要实现插件Activity的LaunchMode,需要在宿主程序中预埋若干个(standard只需1个)相应launchMode的Activity(预注
册的组件可实际存在也可不存在),在运行时进行动态映射选择。core工程的manifest中配置

11、对多Service的支持

Service的启动模式类似于Activity的singleInstance,因此为了支持插件多service,采用了和上述第12像类似的做法。

实现

Activity
注册一个Activity基类,供插件中的Activity继承,在这个基类里做动态加载的核心逻辑,这就要求插件必须依赖某种API类库。 依赖倒转,不让插件依赖框架API,而是反过来,自动生成一个Activity类依赖(继承)插件中的Activity,这个自动生成的类就叫PluginActivity 并且声明在框架的清单文件中,如下:
插件里面Activity可不止一个,这里就注册一个? 是的,就一个,自动生成的Activity类名都是PluginActivity,不过放在不同的文件中,最简单的映射,原始Activity类名.dex文件中存储对应的子类:PluginActivity 其实也是偷梁换柱了,如果你想启动插件里的Activity,如com.test.MyPlugActivity, 我就把启动目标修改为androidx.pluginmgr.PluginActivity类, 然后从com.test.MyPlugActivity.dex文件中找到 public class androidx.pluginmgr.PluginActivity extends com.test.MyPlugActivity{....} 以启动SthActivity为例: 下面介绍如何让系统按你说的路径去找类文件,这涉及到类加载器。自定义类加载器比较简单,继承java.lang.ClassLoader即可. 在我的开源项目源码中对应的类是 FrameworkClassLoader, PluginManager初始化时就去修改Application的类加载器,替换为 FrameworkClassLoader. FrameworkClassLoader 其实不干什么实际加载工作,只是分发任务:
public Class loadClass(String className){ if(当前上下文插件不为空) { if( className 是 PluginActivity){ 找到当前实际要加载的原始 Activity return 使用插件对应的 ActivityClassLoader 从 (自动生成的)原始Activity类名.dex 文件 加载PluginActivity }else{ return 使用对应的 PluginClassLoader 加载普通类 }
}else{ return super.loadClass()//即委派给宿主Application的原始类加载器加载 }
} 其中, PluginClassLoader 是一个DexClassLoader, parent 指向 FrameworkClassLoader, ActivityClassLoader 也是一个DexClassLoader, parent 指向 PluginClassLoader

插图2(类加载器结构图):

http://v.youku.com/vshow/idXNTMzMjYzMzM2.html http://blog.csdn.net/hkxxx/article/details/42194387
https://github.com/limpoxe/Android-Plugin-Framework
http://blog.csdn.net/u010687392/article/details/47121729?hmsr=toutiao.io&utmmedium=toutiao.io&utmsource=toutiao.io

张鹏宇

继续阅读此作者的更多文章