0%

Android 插件化学习

作者:Oz

插件化技术(也叫动态加载技术)在技术驱动型的公司中扮演着相当重要的角色,当项目越来越庞大的时候,需要通过插件化来给应用瘦身,还可以实现热插拔,即在不发布新版本的情况下更新某些模块。对于业务迭代较快的公司,可以减少发包次数而且有很好的覆盖度。

以下为这段时间学习插件化的一些笔记或摘要及自己的一些理解。

Java类加载器

提到Android插件化,一个基础的知识点就是java的类加载机制。

类加载器的树状组织结构

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:

  • 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader
  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

类加载器的代理模式

类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。在介绍代理模式背后的动机之前,首先需要说明一下 Java 虚拟机是如何判定两个 Java 类是相同的。Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。

加载类的过程

在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。

动态代理

代理及动态代理在插件化的技术中也起到了至关重要的作用。

Java 实现动态代理

Java实现动态代理的大致步骤如下:

  • 定义一个委托类和公共接口。

  • 自己定义一个类(调用处理器类,即实现 InvocationHandler 接口),这个类的目的是指定运行时将生成的代理类需要完成的具体任务(包括Preprocess和Postprocess),即代理类调用任何方法都会经过这个调用处理器类。

  • 生成代理对象(当然也会生成代理类),需要为他指定(1)委托对象(2)实现的一系列接口(3)调用处理器类的实例。因此可以看出一个代理对象对应一个委托对象,对应一个调用处理器实例。

Java 实现动态代理主要涉及以下几个类:

  • java.lang.reflect.Proxy: 这是生成代理类的主类,通过 Proxy 类生成的代理类都继承了 Proxy 类,即 DynamicProxyClass extends Proxy

  • java.lang.reflect.InvocationHandler: 这里称他为”调用处理器”,他是一个接口,我们动态生成的代理类需要完成的具体内容需要自己定义一个类,而这个类必须实现 InvocationHandler 接口。

Proxy 类主要方法为:

1
2
//创建代理对象  
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler)

这个静态函数的第一个参数是类加载器对象(即哪个类加载器来加载这个代理类到 JVM 的方法区),第二个参数是接口(表明你这个代理类需要实现哪些接口),第三个参数是调用处理器类实例(指定代理类中具体要干什么)。这个函数是 JDK 为了程序员方便创建代理对象而封装的一个函数,因此你调用newProxyInstance()时直接创建了代理对象(略去了创建代理类的代码)。其实他主要完成了以下几个工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler)
{
//源代码中关键代码为下面注释的代码,将其拆解为三个过程方便阅读
//return getProxyClass(loader, interfaces)
// .getConstructor(InvocationHandler.class)
// .newInstance(invocationHandler);

//1. 根据类加载器和接口创建代理类
Class clazz = Proxy.getProxyClass(loader, interfaces);
//2. 获得代理类的带参数的构造函数
Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class });
//3. 创建代理对象,并制定调用处理器实例为参数传入
Interface Proxy = (Interface)constructor.newInstance(new Object[] {handler});
}

Android动态加载技术三个关键问题

不同的插件化方案各有各的特色,但是它们都必须要解决三个基础性问题:ClassLoader的管理、生命周期的管理以及资源的访问。 

ClassLoader的管理

针对ClassLoader的管理有两种方案:

  • 可以对每一个插件分配一个ClassLoader(这是目前最常见的一种方式)
  • 也可以动态得把插件加载到当前运行环境的classloader中

生命周期的管理

通过ClassLoader加载了外部dex,但是通过这种方式却不能启动插件中的应该具有生命周期的组件,例如:Activity。

Activity等组件是需要在AndroidManifest中注册后才能以标准Intent的方式启动的,通过ClassLoader加载并实例化的Activity实例只是一个普通的Java对象,能调用对象的方法,但是它没有生命周期,ActivityService等组件是有生命周期的,它们统一由系统服务AMS管理,所以在Android系统上,仅仅完成动态类加载是不够的,我们需要想办法把我们加载进来的Activity等组件交给系统管理,让AMS赋予组件生命周期。所以摆在眼前的一个困难就是如何让组件获得生命活力。

针对组件生命周期的管理,大约有以下两(三)种方案:

  • 通过代理模式管理插件生命周期(有同学评价为牵线木偶式管理)

    • 在代理组件中通过反射调用插件相应的生命周期(性能开销较大,不推荐使用)
    • 将生命周期抽象为接口,在代理组件中调用生命周期
  • 通过Hook机制管理插件生命周期(瞒天过海、偷梁换柱、借尸还魂,指的就是这种方式)

    DroidPlugin及天才少年lody开源的VirtualApp中均通过动态代理Hook后的系统Binder服务,从而巧妙的掌控了上帝视角。不同的是VirtualApp基于注解的依赖注入处理Hook,在代码上更清晰。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    private IBinder baseBinder;
    private Interface mBaseObject;
    private Interface mProxyObject;
    ...
    public final void bind() {
    this.baseBinder = queryBaseBinder();
    if (baseBinder != null) {
    this.mBaseObject = createInterface(baseBinder);
    mProxyObject = (Interface) Proxy.newProxyInstance(mBaseObject.getClass().getClassLoader(),
    mBaseObject.getClass().getInterfaces(), new HookHandler());

    }
    }
    @Override
    public IInterface queryLocalInterface(String descriptor) {
    return getProxyObject();
    }

    Hook机制不但解决了生命周期的问题,而且使得插件能够无缝地使用这些系统服务,对于插件apk的开发就跟平常开发app并无区别,不用关心一些规则的限制。

资源的访问

对于资源的访问,我们来追寻一下源码中是通过什么方式来获取资源的。

我们知道Activity是继承自Context,而Context唯一实现类是ContxtImpl,Activity中有一个叫mBase的成员变量,它的类型就是ContextImpl。注意到Context中有如下两个抽象方法,看起来是和资源有关 的,实际上Context就是通过它们来获取资源的。这两个抽象方法的真正实现在ContextImpl中,也就是说,只要实现这两个方法,就可以解决资源问题了。

1
2
3
4
5
/** 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();

查看系统的Resources源码,我们发现这个类主要做了两件事,首当其冲的当然是访问资源,另外一件就是管理资源配置信息。对于资源的动态加载来说,我们关心的是它如何做第一件事的。我们发现,Resources对资源的访问,全部代理给了另一个重要的对象AssetManager。那么问题转化成了,AssetManager是如何做到对资源的访问的。Resources类在它的构造函数里对AssetManager做了一些重要的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Creates a new Resources object with CompatibilityInfo.
*
* @param assets Previously created AssetManager.
* @param metrics Current display metrics to consider when
* selecting/computing resource values.
* @param config Desired device configuration to consider when
* selecting/computing resource values (optional).
* @param compatInfo this resource's compatibility info. Must not be null.
* @param token The Activity token for determining stack affiliation. Usually null.
* @hide
*/
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config,
CompatibilityInfo compatInfo, IBinder token) {
mAssets = assets;
mMetrics.setToDefaults();
if (compatInfo != null) {
mCompatibilityInfo = compatInfo;
}
mToken = new WeakReference<IBinder>(token);
updateConfiguration(config, metrics);
assets.ensureStringBlocks();
}

其中的重点就是调用了AssetManager对象的ensureStringBlocks()函数,这个函数的实现如下:

1
2
3
4
5
6
7
8
9
/*package*/ final void ensureStringBlocks() {
if (mStringBlocks == null) {
synchronized (this) {
if (mStringBlocks == null) {
makeStringBlocks(sSystem.mStringBlocks);
}
}
}
}

先判断mStringBlocks变量是否为空,如果不为空的话,表示需要被初始化,于是调用makeStringBlocks函数初始化mStringBlocks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*package*/ final void makeStringBlocks(StringBlock[] seed) {
final int seedNum = (seed != null) ? seed.length : 0;
final int num = getStringBlockCount();
mStringBlocks = new StringBlock[num];
if (localLOGV) Log.v(TAG, "Making string blocks for " + this
+ ": " + num);
for (int i=0; i<num; i++) {
if (i < seedNum) {
mStringBlocks[i] = seed[i];
} else {
mStringBlocks[i] = new StringBlock(getNativeStringBlock(i), true);
}
}
}

这里的mStringBlocks对象是一个StringBlock数组,这个类被标记为@hide,表示应用层根本不需要关心它的存在。那么它是做什么用的呢,它就是AssetManager能够访问资源的奥秘所在,AssetManager所有访问资源的函数,例如getResourceTextArray(),都最终通过StringBlock再代理到native进行访问的。看到这里,依然没有任何看到能够指示为什么开发者可以访问自己应用的资源,那么我们再往前看一点,看看传入Resources的构造函数之前,asset参数是不是被“做过手脚”。我们现在来看一下Resources被实例化的地方,经过翻阅代码发现,在ResourceManagergetTopLevelResources函数被实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  /**
* Creates the top level Resources for applications with the given compatibility info.
*
* @param resDir the resource directory.
* @param overlayDirs the resource overlay directories.
* @param libDirs the shared library resource dirs this app references.
* @param compatInfo the compability info. Must not be null.
* @param token the application token for determining stack bounds.
*/
public Resources getTopLevelResources(String resDir, String[] splitResDirs,
String[] overlayDirs, String[] libDirs, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
...
Resources r;
...
AssetManager assets = new AssetManager();
// resDir can be null if the 'android' package is creating a new Resources object.
// This is fine, since each AssetManager automatically loads the 'android' package
// already.
if (resDir != null) {
if (assets.addAssetPath(resDir) == 0) {
return null;
}
}
...
r = new Resources(assets, dm, config, compatInfo, token);
}

代码有点多,截取最重要的部分,那就是系统通过调用AssetManageraddAssetPath函数,将需要加载的资源路径加了进去。addAssetPath函数返回一个int类型,它指示了每个被添加的资源路径在native层一个数组中的位置,这个数组保存了系统资源路径(framework-res.apk),和应用自己添加的所有的资源路径。再回过头看makeStringBlocks函数,就豁然开朗了:

  1. makeStringBlocks函数的参数也是一个StringBlock数组,它表示系统资源,首先它调用getStringBlockCount函数得到当前应用所有要加载的资源路径数量。
  2. 然后进入循环,如果属于系统资源,就直接用传入参数seed中的对象来赋值。
  3. 如果是应用自己的资源,就实例化一个新的StringBlock对象来赋值。并在StringBlock的构造函数中调用getNativeStringBlock函数来获取一个native层的对象指针,这个指针被java层StringBlock对象用来调用native函数,最终达到访问资源的目的。

插件化技术比较受到国内开发者青睐,国外则更多的热衷于Facebook开源的react native技术,出于快速迭代的目的其实还有另一种做法,在早几年已经有行业内一线公司采用,使用本地H5配合WebView达到快速迭代,H5代码则通过线上打包更新(全量或增量)的方式,将其最新版本更新至本地加载,这一方面解决了公司对业务快速迭代的需求,另一方面也缓解了线上H5对网络环境的依赖,也算是一种成熟可用的折中方案。

参考文档