Android插件化之Small框架原理

Small是一个轻量化的插件化框架,Small的使用介绍可以查看官网文档)了解,Small项目接入总结可以查看博客的另一篇《Small框架实践总结》。这篇文章主要从以下几个角度来看Small框架的实现原理。

  1. Small如何实现插件代码打包和资源打包
  2. Small加载插件代码和资源的原理
  3. Small代理插件activity生命周期的原理

关于Small插件化的其他文档:

《Android插件化之Small框架实践总结》
《Android插件化之资源加载机制》
《Android插件化之从入门到放弃》

0x01 Small如何打包module代码?

small插件化中的三种组件角色,分别是app.*lib.*宿主。small在打包过程中会根据不同module的类型针对不同组件使用不同编译插件做处理,分别是:

  1. AppPlugin –> app.*
  2. LibraryPlugin –> lib.*
  3. HostPlugin –> app
  4. AssetPlugin –> 其他

在处理模块依赖上,首先区分gradle的两种依赖方式:

  1. Compile: compile是对所有的build type以及flavors都会参与编译并且打包到最终的apk文件中。
  2. Provided: Provided是对所有的build type以及flavors只在编译时使用,只参与编译,不打包到最终apk。

在打包app.*插件时,将app.*对其他module的依赖转换成provided依赖。当执行插件打包时可以看做是插件模块执行assembleRelease

在打包lib.*插件时,LibraryPlugin会修改插件模块的build.gradle文件,apply plugin: 'com.android.library'修改成'com.android.application',将对lib的方式转换成app的打包,再执行assembleRelease任务。

0x02 Small如何解决资源id冲突?

Android App资源id的格式是0xPPTTNNNN,其中:

  • PP 资源的package id
  • TT 资源类型的id,类型是attr、layout、string等等
  • NNNN 资源的entry id

Android App资源的默认packageId是0x7f,当同时加载多个插件apk时。必然会有插件间资源id冲突的情况。业界解决资源id冲突主要通过package id的分段,实现方式一般有两种方式,第一种方式是修改aapt工具源码,让aapt针对插件打包时每个插件的packageId都不相同,重新编译出aapt,而达到解决资源id冲突的问题。small框架使用的是另外一种方式,那就是读取并重写resources.arsc文件。大致的处理流程如下(出自small原作者在github issues的回答):

  1. 编译完整的资源包,利用symbol/R.txt搜集完整包entries的资源信息
  2. 搜集当前插件包res目录的entry信息
  3. 根据(2),通过重新排序分配各个entry的资源id,并结合(1),形成旧id到新id的映射
  4. 解析(1)生成的resources.arsc文件,利用(3)进行过滤输出

0x03 Small如何加载插件代码?

Small的加载插件代码的方式基于android dex分包方案的,简单的说small通过将多个dex文件塞入到app的classloader中,达到加载插件代码的目的。Small并不会更换系统的classloader,所以这样在面对多种不同机型时可能就不会遇到的兼容性问题。

怎样把多个dex文件塞入系统classloader中,可以看下QQ空间的热修复方案

0x04 Small如何加载插件资源?

Android资源是由 AssetManager 加载的。应用启动时系统创建一个AssetManager实例,并通过addAssetPath方法添加资源路径,默认添加:

  • “/framework/base.apk” - Android base resources (base)
  • “/data/app/*.apk” - The launching apk resources (host)

那么如何让插件的资源能够被系统加载?Small的方式是自己创建的AssetManager,调用AssetManger的addAssetPath方法添加插件资源路径,再将系统创建的AssetManager替换掉。

Hook AssetManager路径如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ActivityThread
|
|---ResourcesManager mResourcesManager
|
|--- ArrayList<WeakReference<Resources>> mResourceReferences
|
|--- ResourcesImpl mResourcesImpl
|
|--- AssetManager mAssets

|--- ArrayMap<ResourcesKey, WeakReference<ResourcesImpl>> mResourceImpls
|
|--- ResourcesImpl mResourcesImpl
|
|--- AssetManager mAssets

0x05 Small如何代理插件Activity的生命周期?

先看Andriod中Activity是如何启动的。

1
2
3
4
5
6
7
8
9
10
MyActivity.startActivity()
|
|-->Activity.startActivity()
|
|-->Activity.startActivityForResult()
|
|-->Instrumentation.execStartActivty()
|
|-->ActivityManagerNative.getDefault().startActivityAsUser()

通过 Activity 的启动流程可以发现,Activity是由 ActivityThreadInstrumentation 启动的,Instrumentation有关Activity启动相关的方法大概有:execStartActivity、newActivity等等。Small是这样操作的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MyActivity.startActivity()
|
|-->Activity.startActivity()
|
|-->Activity.startActivityForResult()
|
|-->InstrumentationWrapper.execStartActivty()
|
|--> InstrumentationWrapper.wrapIntent() 将 Intent { cmp=net.wequick.example.small/.app.main.MainActivity }
|--> 转换成 Intent { cat=[>net.wequick.example.small.app.main.MainActivity] cmp=net.wequick.example.small/net.wequick.small.A }
|
|-->Instrumentation.execStartActivty()
|
|-->ActivityManagerNative.getDefault().startActivityAsUser()

Small首先在宿主manifest中注册一个命名特殊的占坑activity来欺骗系统获取生命周期,在封装一个Instrumentation替换掉宿主的,系统启动的是A这个activity,但classloader实际加载的代码却是 MainActivity.class 这个类。

1
2
<!-- Stub Activities -->
<activity android:name=".A" android:launchMode="standard"/>

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ActivityThread thread = currentActivityThread();
Instrumentation base = thread.@mInstrumentation;
Instrumentation wrapper = new InstrumentationWrapper(base);
thread.@mInstrumentation = wrapper;

class InstrumentationWrapper extends Instrumentation {
public ActivityResult execStartActivity(..., Intent intent, ...) {
fakeToStub(intent);
base.execStartActivity(args);
}

@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) {
className = restoreToReal(intent, className);
return base.newActivity(cl, className, intent);
}
}

0x06 Small如何动态更新插件模块?

查看github sample实现方式

0x07 参考文档

Small 官方 wiki

QQ空间热补丁动态修复技术介绍

转载请标明出处病已blog https://ivonhoe.github.io/