意义
研究插件框架的意义在于以下几点:
· 减小安装包的体积,通过网络选择性地进行插件下发
· 模块化升级,减小网络流量
· 静默升级,用户无感知情况下进行升级
· 解决低版本机型方法数超限导致无法安装的问题
· 代码解耦
现状
Android中关于插件框架的技术已经有过不少讨论和实现,插件通常打包成apk或者dex的形式。
dex形式的插件往往提供了一些功能性的接口,这种方式类似于java中的jar形式,只是由于Android的Dalvik VM无法直接动态加载Java的Byte Code,所以需要我们提供Dalvik Byte Code,而dex就是Dalvik Byte Code形式的jar。
apk形式的插件提供了比dex形式更多的功能,例如可以将资源打包进apk,也可实现插件内的Activity或者Service等系统组件。
本文主要讨论apk形式的插件框架,对于apk形式又存在安装和不安装两种方式
· 安装apk的方式实现相对简单,主要原理是通过将插件apk和主程序共享一个UserId,主程序通过createPackageContext
构造插件的context,通过context即可访问插件apk中的资源,很多app的主题框架就是通过安装插件apk的形式实现,例如Go主题。这种方式的缺点就是需要用户手动安装,体验并不是很好。
· 不安装apk的方式解决了用户手动安装的缺点,但实现起来比较复杂,主要通过DexClassloader
的方式实现,同时要解决如何启动插件中Activity等Android系统组件,为了保证插件框架的灵活性,这些系统组件不太好在主程序中提前声明,实现插件框架真正的难点在此。
DexClassloader
这里引用《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版里对java类加载器的一段描述:
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
Android虚拟机的实现参考了java的JVM,因此在Android中加载类也用到了类加载器的概念,只是相对于JVM中加载器加载class文件而言,Android的Dalvik虚拟机加载的是Dex格式,而具体完成Dex加载的主要是PathClassloader
和Dexclassloader
。
PathClassloader
默认会读取/data/dalvik-cache
中缓存的dex文件,未安装的apk如果用PathClassloader
来加载,那么在/data/dalvik-cache
目录下找不到对应的dex,因此会抛出ClassNotFoundException
。
DexClassloader
可以加载任意路径下包含dex和apk文件,通过指定odex生成的路径,可加载未安装的apk文件。下面一段代码展示了DexClassloader
的使用方法:
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
DexClassloader
解决了类的加载问题,如果插件apk里只是一些简单的API调用,那么上面的代码已经能满足需求,不过这里讨论的插件框架还需要解决资源访问和Android系统组件的调用。
插件内系统组件的调用
Android Framework中包含Activity
,Service
,Content Provider
以及BroadcastReceiver
等四大系统组件,这里主要讨论如何在主程序中启动插件中的Activity,其它3种组件的调用方式类似。
大家都知道Activity需要在AndroidManifest.xml中进行声明,apk在安装的时候PackageManagerService
会解析apk中的AndroidManifest.xml文件,这时候就决定了程序包含的哪些Activity,启动未声明的Activity会报ActivityNotFound
异常,相信大部分Android开发者曾经都遇到过这个异常。
启动插件里的Activity必然会面对如何在主程序中的AndroidManifest.xml中声明这个Activity,然而为了保证插件框架的灵活性,我们是无法预知插件中有哪些Activity,所以也无法提前声明。
为了解决上述问题,这里介绍一种基于Proxy思想的解决方法,大致原理是在主程序的AndroidManifest.xml中声明一些ProxyActivity
,启动插件中的Activity会转为启动主程序中的一个ProxyActivity
,ProxyActivity
中所有系统回调都会调用插件Activity中对应的实现,最后的效果就是启动的这个Activity实际上是主程序中已经声明的一个Activity,但是相关代码执行的却是插件Activity中的代码。这就解决了插件Activity未声明情况下无法启动的问题,从上层来看启动的就是插件中的Activity。下面具体分析整个过程。
PluginSDK
所有的插件和主程序需要依赖PluginSDK进行开发,所有插件中的Activity继承自PluginSDK中的PluginBaseActivity
,PluginBaseActivity
继承自Activity
并实现了IActivity
接口。
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 |
|
[代码]java代码:
?
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
|
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
|
PluginBaseActivity
和ProxyActivity
在整个插件框架的核心,下面简单分析一下代码:
首先看一下ProxyActivity#onResume
:、
[代码]java代码:
1 2 3 4 5 6 7 |
|
变量mPluginActivity
的类型是IActivity
,由于插件Activity实现了IActivity
接口,因此可以猜测mPluginActivity.IOnResume()
最终执行的是插件Activity的onResume
中的代码,下面我们来证实这种猜测。
PluginBaseActivity
实现了IActivity
接口,那么这些接口具体是怎么实现的呢?看代码:
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 |
|
接口实现非常简单,只是调用了和接口对应的回调函数,那这里的回调函数最终会调到哪里呢?前面提到过所有插件Activity都会继承自PluginBaseActivity
,也就是说这里的回调函数最终会调到插件Activity中对应的回调,比如IOnResume
执行的是插件Activity中的onResume
中的代码,这也证实了之前的猜测。
上面的一些代码片段揭示了插件框架的核心逻辑,其它的代码更多的是为实现这种逻辑服务的,后面会提供整个工程的源码,大家可自行分析理解。
插件内资源获取
实现加载插件apk中的资源的一种思路是将插件apk的路径加入主程序资源查找的路径中,下面的代码展示了这种方法:
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 |
|
为了让插件Activity访问资源时使用我们自定义的Context,我们需要在PluginBaseActivity
的初始化中做一些处理:
[代码]java代码:
1 2 3 4 5 6 7 |
|
PluginContext
中通过重载getAssets
来实现包含插件apk查找路径的Context:
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
总结
本文介绍了一种基于Proxy思想的插件框架,所有的代码都在Github中,代码只是抽取了整个框架的核心部分,如果要用在生产环境中还需要完善,比如Content Provider
和BroadcastReceiver
组件的Proxy类未实现,Activity的Proxy实现也是不完整的,包括不少回调都没有处理。同时我也无法保证这套框架没有致命缺陷,本文主要是以总结、学习和交流为目的,欢迎大家一起交流。