插件框架实现思路及原理
一、技术可行性
a) apk的安装处理流程
i. apk会copy到/data/app;
ii. 解压apk中的class.dex,并对其进行优化,获得odex(即JIT)。最后保存到/data/dalvik_cache;
iii. 还有一些权限和包信息,会缓存到/data/system中的packages.list和packages.xml中。
b) 在android上,对apk包的加载逻辑
i. 加载逻辑
Zygote(孵化器)在成功启动一Android进程后,会根据packages.list的内容(启动时会加载到system_process中的pakcagemanager中),把odex文件,加载到dalvik中,完成逻辑的加载;
ii. 资源读取
资源读取,主要有两三个类,分别是Resource、AssertManager和LayoutInflater。
当在显示界面时,就通过这三个类读取资源。
c) 结论和猜想
i. apk相对于整个android系统而言,其本身就是一种插件形式体现。根据上面关于逻辑和资源的读取概述,完全是可以静默实现的。其次,class.dex并没有包含Android SDK的代码,只是保留对Android SDK接口的调用。 可以这样想象,Android SDK即插件框架,而Android OS即为整个插件的宿主环境。因此这就可以解释了,为什么在1.x编译的代码,在2.x甚至3.x都可以运行,因为只要插件宿主的接口(即Android SDK)不变,插件运行时所调用的接口都可以被找到。
ii. 为了减少内存占用,Resource、AssertManager和LayoutInflater必然不会把apk中的所有资源都加载进来,而是用时才加载并缓存,而且还有一些的处理机制(如最不常用清除等)。因此这些类当中,必然存在一个指明资源路径的字段或者结构。
iii. 要保证兼容性,插件框架公开给插件的接口,必须遵守Open-Close(开发-封闭)原则。另外,一些已经废弃掉接口,同样需要保留。比如Service中的setForeground和JDK的中关于Thread的一些接口等。
iv. 可以尝试通过反射,修改Resource、AssertManager和LayoutInflater中指明资源路径的字段;另外,还可以查看源码,查找设置资源路径的方法。
二、技术实现要点
a) 逻辑加载
i. 针对接口编程。这个是所有插件框架的基本设计模型。
ii. 通过DexClassLoader加载插件所实现的插件接口,详细可参考PluginManagerImpl中的parserPlugin方法实现,关键代码如下:
b) AssertManager的实现
经查阅Android源码,发现AssertManager的实例生成,用到两个隐藏的方法,如下所示:
通过以上代码,我们就可以得到我们插件的AssertManager了,关键代码如下所示:
c) Resource的实现
有了插件专用的AssertManager,那么插件的Resource也轻易得到了,关键代码如下所示:
d) LayoutInflater的实现
一般我们要获取LayoutInflater,都必须通过Context来获得,即是说LayoutInflater的资源读取,都是通过Context的getResoure以及getAssert读取,是直接跟宿主挂勾的。这里有两个方法可选选择:
其一,自己重写Context类,并把getResoure和getAssert的返回值改为上面所得的插件资源相关的实例,即包装法;
其二,考虑到平时我们用LayoutInflater时,主要是用来加载布局文件(XML),因此可以投机取巧点,只针对inflater进行修改。
目前框架采用的是第二种方法。先看看LayoutInflater的源码实现,如下所示:
因此,只调用插件的Resources,并调用其第二个方法,并可实现加载插件的布局问题,为了方便使用,定义了一个ILayoutInflater接口,封装实现细节,关键代码如下:
事实上,通过以上的方法,还是无法完成插件XML布局文件的加载,通过跟踪源码,会发生View的生成,还需要因为利用到当前Context(或Activity)的一个类型Theme的实例。跟踪过程如下:
l View(Context context, AttributeSet arrts, int defStyle)
l Context.obtainStyledAttributes(AttrobiteSet arrts, int[] attrs, int defStyleAttr, int defStyleRes)
l Context.getTheme()
l ......
而这个Theme类型,是Resource的一个内部类,不单可以直接引用Resource的,还通过Context保存AssertManager的引用。源码如下:
因此,我们还需要通过反射的方式,把当前Theme的实例,替换成我们插件的。当XML布局文件解释成功后,再恢复过来。留意上面代码中的begin和end方法,就是这个过程的封装。关键代码如下:
而插件的Theme实例,可通过Resource的newTheme获得,关键代码如下:
e) 混合资源解析的实现
基本上,上面的有了上面的三个类,就可以完全加载插件的读取插件的资源和逻辑。但往往事情并不是这么简单。
考虑到UI的可重用性,插件里,往往会很多的用到宿主所提示的UI库接口。因此,就需要考虑,当在解释插件的xml布局时,如何混合使用两方的资源。从上面的过程可得,无论是Resources、AssertManager还是LayoutInflater,都是同时只能针对一方资源。当在插件的布局文件中,使用了两方的文件,必然会因为找不到资源,解释出错。
不过,LayoutInflater,提示了一个setFactory,可以在解释XML布局文件时,优先解释。关键代码如下:
三、待完善的地方
l 在宿主中定义的资源,目前除了自定义view之外,都不可以使用。比如文字、颜色值、样式等。
l 插件的权限,必须是宿主的子集。
l 插件中包含so的加载逻辑,还没有实现。
l 目前插件的加载,都是加载dex,而不是odex。还需要查阅源码,手工实现这个优化过程。