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

 

 
posted @ 2016-08-30 19:28  我在途中  阅读(339)  评论(0编辑  收藏  举报