android 动态加载
编译期:资源和代码的编译
运行时:资源和代码的加载
解决了以上2个关键问题,之后如何实现插件呢
分析android是如何编译的
1.aapt 资源编译依赖这个命令行
1)android.jar
2)引用一个已经存在的apk包作为依赖资源参与编译
资源编译中,对组件的类名、方法引用会导致运行期的反射调用
3)java层面的常量ID会记录在R.java中,参与他们之后的代码编译阶段
,R类生成的每一个Int类型由4个字节组成,第一个字节代表package id,第二个字节为分类,三四字节为类内ID
//android.jar中的资源,其PackageID为0x01 public static final int cancel = 0x01040000; //用户app中的资源,PackageID总是0x7F public static final int zip_code = 0x7f090f2e;
所以我们修改aapt后,可以给每个子apk中的资源分配不同头字节packageid,这样就不会产生冲突
代码编译
classpath:java源码编译中需要找齐所有依赖项目,用来指定去哪个目录、文件、jar包寻找依赖。
混淆:安全起见参考手册
实现
两个问题需要解决:资源如何访问和代码如何访问
两类:针对插件子工程的编译流程改造和运行时动态加载改造
针对插件的资源编译,我们需要考虑到以下几点:
-
使用
-I
参数对宿主的apk进行引用。据此,插件的资源、xml布局中就可以使用宿主的资源和控件、布局类了。
-
为aapt增加
--apk-module
参数。如前所述,资源ID其实有一个PackageID的内部字段。我们为每个插件工程指定独特的PackageID字段,这样根据资源ID就很容易判明,此资源需要从哪个插件apk中去查找并加载了。在后文的资源加载部分会有进一步阐述。
-
为aapt增加
--public-R-path
参数。按照对android.jar包中资源使用的常规手段,引用系统资源可使用它的R类的全限定名
android.R
来引用具体ID,以便和当前项目中的R类区分。插件对于宿主的资源引用,当然也可以使用base.package.name.R
来完成。但由于历史原因,各子BU的“插件”代码是从主app中解耦独立出去的,资源引用还是直接使用当前工程的R。如果改为标准模式,则当前大量遗留代码中R
都需要酌情改为base.R
,工程量大并且容易出错,未来对bu开发人员的使用也有点不够“透明”。因此我们在设计上做了让步,额外增加--public-R-path
参数,为aapt指明了base.R
的位置,让它在编译期间把base的资源ID定义在插件的R类中完整复制一份,这样插件工程即可和之前一样,完全不用在乎资源来自于宿主或者自身,直接使用即可。当然这样做带来的副作用就是宿主和插件的资源不应有重名
针对插件的代码编译,需要考虑以下几点:
-
classpath
对于插件的编译来说,除了对android.jar以及自己需要的第三方库进行依赖之外,还需要依赖宿主导出的base.jar类库。同时对宿主的混淆也提出了要求:宿主的所有public/protected都可能被插件依赖,所以这些接口都不允许被混淆。
-
混淆。
插件工程在混淆的时候,当然也要把宿主的混淆后jar包作为参考库导入。
自此,编译期所有重要步骤的技术方案都已经确定,剩下的工作就只是把插件apk导入到先一步生成好的base.apk中并重新进行签名对齐而已。
万事俱备,只欠表演。接下来我们看看在运行时插件们是如何登台亮相的。
运行时资源的加载
平常我们使用资源,都是通过AssetManager类和Resources类来访问的。获取它们的方法位于Context类中。
Context.java
/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();
它们是两个抽象方法,具体的实现在ContextImpl类中。ContextImpl类中初始化Resources对象后,后续Context各子类包括Activity、Service等组件就都可以通过这两个方法读取资源了。
ContextImpl.java
private final Resources mResources;
@Override
public AssetManager getAssets() {
return getResources().getAssets();
}
@Override
public Resources getResources() {
return mResources;
}
既然我们已经知道一个资源ID应该从哪个apk去读取(前面在编译期我们已经在资源ID第一个字节标记了资源所属的package),那么只要我们重写这两个抽象方法,即可指导应用程序去正确的地方读取资源。
至于读取资源,AssetManager有一个隐藏方法addAssetPath,可以为AssetManager添加资源路径。
/**
* Add an additional set of assets to the asset manager. This can be
* either a directory or ZIP file. Not for use by applications. Returns
* the cookie of the added asset, or 0 on failure.
* {@hide}
*/
public final int addAssetPath(String path) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
我们只需反射调用这个方法,然后把插件apk的位置告诉AssetManager类,它就会根据apk内的resources.arsc和已编译资源完成资源加载的任务了。
以上我们已经可以做到加载插件资源了,但使用了一大堆定制类实现。要做到“无缝”体验,还需要一步:使用Instrumentation来接管所有Activity、Service等组件的创建(当然也就包含了它们使用到的Resources类)。
话说Activity、Service等系统组件,都会经由android.app.ActivityThread类在主线程中执行。ActivityThread类有一个成员叫mInstrumentation,它会负责创建Activity等操作,这正是注入我们的修改资源类的最佳时机。通过篡改mInstrumentation为我们自己的InstrumentationHook,每次创建Activity的时候顺手把它的mResources类偷天换日为我们的DelegateResources,以后创建的每个Activity都拥有一个懂得插件、懂得委托的资源加载类啦!
当然,上述替换都会针对Application的Context来操作。
运行时类的加载
类的加载相对比较简单。与Java程序的运行时classpath概念类似,Android的系统默认类加载器PathClassLoader也有一个成员pathList,顾名思义它从本质来说是一个List,运行时会从其间的每一个dex路径中查找需要加载的类。既然是个List,一定就会想到,给它追加一堆dex路径不就得了?实际上,Google官方推出的MultiDex库就是用以上原理实现的。下面代码片段展示了修改pathList路径的细节:
MultiDex.java
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
/* The patched class loader is expected to be a descendant of
* dalvik.system.BaseDexClassLoader. We modify its
* dalvik.system.DexPathList pathList field to append additional DEX
* file entries.
*/
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
}
当然,针对不同Android版本,类加载方式略有不同,可以参考MultiDex源码做具体的区别处理。
至此,之前提出的四个根本性问题,都已经有了具体的解决方案。剩下的就是编码!
编码主要分为三部分:
-
对aapt工具的修改。
-
gradle打包脚本的实现。
-
运行时加载代码的实现。
具体实现可以参考我们在GitHub上的开源项目DynamicAPK