往期参考:
插件框架代码已经开源,欢迎使用并反馈。https://github.com/cmguo/android-plugins
ClassLoader
代码插入通过自定义的ClassLoader来实现。在Android平台,系统提供从Apk中加载Class的功能(DexClassLoader)。插件Apk中的代码在运行时解析成Dex格式,存放在程序指定的目录,插件Apk更新时,DexClassLoader会自动更新Dex文件。
Java虚拟机在第一次引用到某个类时,会使用ClassLoader来加载类信息,继而初始化类的静态成员,然后才能插件类的实例。
在确定使用哪个ClassLoader来加载某个类时,Java有一个简单的机制,那就是看调用者是谁,哪个类,这个类的ClassLoader就是目标ClassLoader。这个加载简单有效。
插件中第一个被加载的类需要外部明确地通过插件的ClassLoader加载,从这个类开始,可以调用到插件内的所有类,插件内部的类可以相互调用。
插件中的代码可以调用主程序中的类,这是通过ClassLoader的父ClassLoader机制实现的。注意:这里的父子关系不是继承关系。插件的ClassLoader的父亲被指定为主程序的ClassLoader。
插件中的代码要做到被主程序使用,就需要注册自己的功能到主程序中,一般通过工厂框架实现。
插件之间的代码依赖也需要通过自定义的ClassLoader来实现。插件的依赖关系就是插件的ClassLoader的依赖关系,这种关系不同于父子关系,因为依赖关系可以依赖多个,父子关系只有一个父亲。实际上,ClassLoader的父子关系不需要很严格,需要严格的是查找类时访问ClassLoader的顺序,不应该通过自定义ClassLoader来覆盖父ClassLoader的类。同理,不应该覆盖被依赖插件中的类。
目前处理依赖关系,采用的是非严格方案。首先在当前插件中查找类,没有找到的时候,在依赖的插件中查找。
Context
在Android系统中处理资源,离不开Context。Context包含资源,也包含对资源的本地化选择策略。
平常我们不关心Context是怎么创建出来的,直接就用了。有Application,Service、Activity也是Context。当然,构建插件的Context,也不需要完全从头实现整个Context,只需要继承某个Context,再替换一些东西就可以。
Android提供ContextWrapper帮助自定义Context,Application、Service、Activity当然是继承ContextWrapper的。插件的Context也继承ContextWrapper,但是实际上是基于ApplicationContext实现。
需要自定义替换的模块:
项目 | 用途 | 说明 |
Assets | 加载资源 | 创建一个AssetManager实例,增加插件Apk。 其实AssetManager默认就包含了Android系统资源库的资源了(framework-res.apk)。 |
Resources | 资源 | 构建Resources实例,构造函数需要的部分参数从ApplicationContext的Resources里面获取。 |
PackageName | 包名 | 插件的包名。 |
ClassLoader | 类加载器 | 上面的插件ClassLoader。 |
PolicyManager | 信号源错误 | 与View相关的PolicyManager中反过来引用了Context,所以ApplicationContext中的PolicyManager是不能用了,需要重新创建。Android 6.0前后的类名都变了,需要判断当前SDK Level。 |
Theme | 主题 | 好像没有什么用,但是需要一个,从Resources类获取默认主题的资源Id,然后插件一个新的Theme实例。 |
。。。 | 。。。 | 可能还有更多需要定制替换的模块。 |
NativeLibrary
处理插件中的本地库(NativeLibrary)时,又要回到ClassLoader。本地库也是代码,包含某些类的native方法的实现,所以由ClassLoader来处理也说得通。
ClassLoader提供方法寻找本地库的位置。Runtime有两种方法来加载本地库:load和loadLibrary,前者需要ClassLoader提供搜索路径(getLdLibraryPath),后者由ClassLoader直接提供具体本地库的文件名(findLibrary)。按接口文档说明,前者需要调用者指定完整路径,推荐使用后者。
插件的本地库被解压出来,存放在特定目录,一旦插件更新,就意味着本地库也需要更新。可以在方法findLibrary中判断是否要更新(比较插件Apk的文件时间与本地库的文件时间)。
本地库可能会加载另一个本地库(一般没有jni相关代码),这种使用情形在Android 4.4之后才得到较好的支持。Runtime会使用ClassLoader提供的搜索路径搜索间接加载的库。
实际上,在Android 4.4之后,通过Runtime的load方法也不一定需要指定完整路径,也会搜索ClassLoader提供的搜索路径。
再来看CPU体系,一般64位体系也兼容32位的本地库,但是程序安装时就决定了具体使用哪个CPU体系。程序运行时可以通过Build.CPU_ABI获取当前的CPU体系。然后解压相应目录的本地库供加载。
Android6.0以上支持内置Native库,不需要解压部署到文件系统中。后续可以利用该机制节省空间。具体原理是本地库在APK中的位置是对齐到文件系统块的,并且没有压缩,可以直接映射APK中某个文件数据到内存页面。在集成到应用以及通过网络分发,插件APK是被压缩的,所以不影响应用体积和网络流量。