Small的原理
Small的基本知识可以参考使用Small进行Android模块化开发
这里重复下基本原理:
- APK包运行时仅仅是一个宿主工程,Java部分一般只包括一个Applicaton和一个入口Activity,还需要在asset的bundle.json中声明会用到的插件
- 公共库插件和业务插件以so文件的形式动态加载,Small提供了命令来生成这些插件so文件
- 插件可通过网络下载,因此可以做到热更新
- 如果需要新增插件,或者更新老插件的协议,还需要更新宿主插件声明信息(bundle.json)。
要做到以上几点,需要处理以下三方面的问题
- 动态加载类
- 动态加载资源
- 动态注册组件
动态加载类
这部分主要是通过向BaseDexClassLoader
中的DexPathList
添加插件类实现的。有很多文章解释这部分的原理,这里就不赘述了。
前面提到的插件so包可以像一个apk文件被解压,但不能独立运行。Small就是从这些插件so文件中加载Element,并添加到DexPathList
中的。
动态加载资源
Android是通过AssetManager来加载资源的,默认情况下只会添加
- /framework/base.apk" - Android基本资源
- /data/app/*.apk" - 应用程序资源
编译过程中aapt会为资源分配id,并存储为PPTTNNNN的16进制整数
- PP代表包信息
- TT代表资源类型
- NNNN定位具体的资源
Small是通过修改PP字段来避免模块间的资源冲突的。在后面Small的编译过程中会介绍这部分的逻辑。
动态注册组件
- Activty受Instrumentation监控,都需要通过
Instrumentation#execStartActivity
来启动并激活声明周期 - 而Activity的实例则是在
ActivityThread
中,通过Instrumentation#newActivty
来构建的。
因此要动态注册Activity,需要在宿主的Manifest中注册一系列的假Activity,来获取Activity的声明周期。
再通过反射的方式修改系统的Instrumentation
,在系统启动假Activity之前,将Activity信息替换为真正的Activity。这部分会在Small运行阶段中详细介绍。
Small项目的编译过程
了解完Small的基本原理后,就该Gradle出场了。因为没有插件so包,Small是没法运行的。
下面介绍Small插件的几个方面:
- Gradle插件的初始化过程
- 如何为每个模块分配资源ID
- 打出的so包到底是什么
我们这里只介绍与此相关的Gradle知识,如需补充更多Gradle基础知识可参考最后的几篇文章。对Small源码的分析也仅限于此。
Gradle插件的初始化过程
在Small的Smaple目录下的build.gradle中有apply plugin: 'net.wequick.small'
和classpath 'net.wequick.tools.build:gradle-small:1.1.0-beta3'
。前者是指定编译插件,后者是编译插件在maven仓库中的位置。
而在DevSmaple目录中,我们没有指定classpath也能使用这个插件。这是因为DevSmaple下有一个buildSrc工程,Sample中使用的脚本正是来自buildSrc。
buildSrc下是一个默认的Groovy工程,专门用于存放相对比较复杂的Groovy脚本,避免build.gradle过大里面主要包括一些properties和Groovy源文件。
net.wequick.small.properties
中指定apply plugin: 'net.wequick.small'
时需要运行的Groovy类implementation-class=net.wequick.gradle.RootPlugin
,apply是所有Plugin被调用的入口。
class RootPlugin extends BasePlugin {
void apply(Project project) {
super.apply(project)
}
[...]
}
public abstract class BasePlugin implements Plugin<Project> {
void apply(Project project) {
this.project = project
[...]
createExtension()
configureProject()
createTask()
}
[...]
}
这里可以看到RootPlugin主要做了3件事:创建Extension,配置Project,创建Task。
创建Task
@Override
protected void createTask() {
super.createTask()
project.task('cleanLib', group: 'small', description: 'Clean all libraries', type: Delete) {
delete small.preBuildDir
}
project.task('buildLib', group: 'small', description: 'Build all libraries').doFirst {
buildingLibIndex = 1
}
project.task('cleanBundle', group: 'small', description: 'Clean all bundles')
project.task('buildBundle', group: 'small', description: 'Build all bundles')
project.task('small') << {
[...]
}
}
这里我们定义了之前用到的buildLib和buildTask任务,我们才能在Android Studio中看到这些任务。当然Small中用到的任务远远不止这四个,而且他们之间有很强的依赖关系。
创建Extention
Groovy中定义的Extention就是在Gradle中可以直接配置的一些扩展信息。Android插件的Extention中一定包括compileSdkVersion
和buildToolsVersion
两个字段,我们才能在Gradle中使用
android {
compileSdkVersion 22
buildToolsVersion "22.0.1"
}
Small中的Gradle配置和Groovy的RootExtention也是对应的
small {
buildToAssets = false
aarVersion = '1.1.0-beta9'
android {
compileSdkVersion = 23
buildToolsVersion = "23.0.3"
supportVersion = "23.4.0"
}
}
//RootExtention
boolean buildToAssets = false
String aarVersion
protected AndroidConfig android
class AndroidConfig {
int compileSdkVersion
String buildToolsVersion
String supportVersion
}
配置Project
这里只截取了configureProject,这里可以看到,根据项目子模块的类型,还会继续加载对应的Plugin。
@Override
protected void configureProject() {
[...]
switch (type) {
case 'app':
it.apply plugin: AppPlugin
rootExt.appProjects.add(it)
break;
case 'lib':
it.apply plugin: LibraryPlugin
rootExt.libProjects.add(it)
break;
default: // Default to Asset
it.apply plugin: AssetPlugin
break;
}
[...]
}
如何为每个模块分配资源ID
修改PP字段就是在AppPlugin中实现的,LibraryPlugin也是继承AppPlugin,所以这个策略对公共库插件生效。
protected void initPackageId() {
Integer pp
String ppStr = null
Integer usingPP = sPackageIds.get(project.name)
boolean addsNewPP = true
// Get user defined package id
if (project.hasProperty('packageId')) {
def userPP = project.packageId
if (userPP instanceof Integer) {
// Set in build.gradle with 'ext.packageId=0x7e' as an Integer
pp = userPP
} else {
// Set in gradle.properties with 'packageId=7e' as a String
ppStr = userPP
pp = Integer.parseInt(ppStr, 16)
}
if (usingPP != null && pp != usingPP) {
// TODO: clean last build
throw new Exception("Package id for ${project.name} has changed! " +
"You should call clean first.")
}
} else {
if (usingPP != null) {
pp = usingPP
addsNewPP = false
} else {
pp = genRandomPackageId(project.name)
}
}
small.packageId = pp
small.packageIdStr = ppStr != null ? ppStr : String.format('%02x', pp)
if (!addsNewPP) return
// Check if the new package id has been used
sPackageIds.each { name, id ->
if (id == pp) {
throw new Exception("Duplicate package id 0x${String.format('%02x', pp)} " +
"with $name and ${project.name}!\nPlease redefine one of them " +
"in build.gradle (e.g. 'ext.packageId=0x7e') " +
"or gradle.properties (e.g. 'packageId=7e').")
}
}
sPackageIds.put(project.name, pp)
}
可以看到通过一个sPackageIds保存每个插件对应的PP值。
打出的so包到底是什么
这里我们主要关注LibraryPlugin <- AppPlugin <- BundlePlugin,三者是继承关系。
- LibraryPlugin:会将库工程转换为普通工程,从而能生成apk包。除此之外,这里还将生成的资源id都保存在一个public.txt文件中
- AppPlugin:分配PP字段,合并Manifest,合并R文件
- 将apk文件命名为so,并放到正确的位置
Small项目的运行
Small.preSetUp
这里主要是Small框架的初始化,主要注册了三种Launcher
public static void preSetUp(Application context) {
if (sContext != null) {
return;
}
sContext = context;
// Register default bundle launchers
registerLauncher(new ActivityLauncher());
registerLauncher(new ApkBundleLauncher());
registerLauncher(new WebBundleLauncher());
Bundle.onCreateLaunchers(context);
}
这里主要关注ApkBundleLauncher#onCreate
的一小部分,这里通过反射完成两件事
- 获取
ActivityThread#mInstrumentation
,并修改为ApkBundleLauncher#InstrumentationWrapper
:这是为了在启动Activity时,将Activity改为Manifest中声明的假Activity,从而能通过Instrumentation的检查。 - 获取
ActivityThread#mCallback
,并修改为ApkBundleLauncher#ActivityThreadHandlerCallback
:这是在ActivityThread创建Activity对象前,将ActivityInfo改为真正的Activity。
// Get activity thread
thread = ReflectAccelerator.getActivityThread(app);
// Replace instrumentation
try {
f = thread.getClass().getDeclaredField("mInstrumentation");
f.setAccessible(true);
base = (Instrumentation) f.get(thread);
wrapper = new ApkBundleLauncher.InstrumentationWrapper(base);
f.set(thread, wrapper);
} catch (Exception e) {
throw new RuntimeException("Failed to replace instrumentation for thread: " + thread);
}
// Inject message handler
try {
f = thread.getClass().getDeclaredField("mH");
f.setAccessible(true);
Handler ah = (Handler) f.get(thread);
f = Handler.class.getDeclaredField("mCallback");
f.setAccessible(true);
f.set(ah, new ApkBundleLauncher.ActivityThreadHandlerCallback());
} catch (Exception e) {
throw new RuntimeException("Failed to replace message handler for thread: " + thread);
}
Small.setUp和Small.openUri
Small.setUp是根据bundle.json去加载插件模块。插件的实际加载过程,在ApkBundleLauncher中完成。
- 解析apk文件
- 加载apk中的资源和类
- 调用模块
Application#onCreate
除此之外,Small.setUp还做了更新检测,这里不再赘述。
Small.openUri更简单,主要是根据uri去匹配对应的Activity。
总结
这篇文章的编译部分写得有点多,但是关联性有不是太强。这是为了在这部分介绍一些Gradle插件开发的内容。但由于笔者在这方面经验尚浅,所以写完自己也觉得有些凌乱,有空的时候可能会再修改。