*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
最近把Activity启动流程整体看了一遍,估摸着弄个啥来巩固下,发现插件化正好是这块技术的实践,而说道插件化其实有好几种实现方式,这里我用的是hook的方式实现,主要目的呢是为了对activity启动流程有个整体的认识,当然了本篇的插件化也只是一个demo版本并没有任何兼容适配,重在流程和原理的理解。
概述
插件化顾名思义,就是将一个APK拆成多个,当需要的时候下载对应插件APK加载的技术。本文demo中除了下载是通过adb命令,其他都是模拟真实环境的,这里先理下流程。
- 将插件工程打包为APK,然后通过adb push命令发送到宿主APK目录(模拟下载流程)。
- 利用ClassLoader加载插件APK中的类文件。
- hook Activity启动流程中部分类,利用占坑Activity帮助PluginActivity绕过AMS验证,在真正启动的时候又替换回PluginActivity。
- 创建插件Apk的Resources对象,完成插件资源的加载。
对整体流程有个大概认识后,下面将结合源码和Demo来详细讲解,本文贴出的源码基于API27。
初始化插件APK类文件
既然插件APK是通过网络下载下来的,那么APK中的类文件就需要我们自己加载了,这里我们要用到DexClassLoader去加载插件APK中的类文件,然后将DexClassLoader中的Element数组和宿主应用的PathClassLoader的Element数组合并再设置回PathClassLoader,完成插件APK中类的加载。对ClassLoader不太熟悉的可以看下我另篇Android ClassLoader浅析
public class InjectUtil {
private static final String TAG = "InjectUtil";
private static final String CLASS_BASE_DEX_CLASSLOADER = "dalvik.system.BaseDexClassLoader";
private static final String CLASS_DEX_PATH_LIST = "dalvik.system.DexPathList";
private static final String FIELD_PATH_LIST = "pathList";
private static final String FIELD_DEX_ELEMENTS = "dexElements";
public static void inject(Context context, ClassLoader origin) throws Exception {
File pluginFile = context.getExternalFilesDir("plugin");// /storage/emulated/0/Android/data/$packageName/files/plugin
if (pluginFile == null || !pluginFile.exists() || pluginFile.listFiles().length == 0) {
Log.i(TAG, "插件文件不存在");
return;
}
pluginFile = pluginFile.listFiles()[0];//获取插件apk文件
File optimizeFile = context.getFileStreamPath("plugin");// /data/data/$packageName/files/plugin
if (!optimizeFile.exists()) {
optimizeFile.mkdirs();
}
DexClassLoader pluginClassLoader = new DexClassLoader(pluginFile.getAbsolutePath(), optimizeFile.getAbsolutePath(), null, origin);
Object pluginDexPathList = FieldUtil.getField(Class.forName(CLASS_BASE_DEX_CLASSLOADER), pluginClassLoader, FIELD_PATH_LIST);
Object pluginElements = FieldUtil.getField(Class.forName(CLASS_DEX_PATH_LIST), pluginDexPathList, FIELD_DEX_ELEMENTS);//拿到插件Elements
Object originDexPathList = FieldUtil.getField(Class.forName(CLASS_BASE_DEX_CLASSLOADER), origin, FIELD_PATH_LIST);
Object originElements = FieldUtil.getField(Class.forName(CLASS_DEX_PATH_LIST), originDexPathList, FIELD_DEX_ELEMENTS);//拿到Path的Elements
Object array = combineArray(originElements, pluginElements);//合并数组
FieldUtil.setField(Class.forName(CLASS_DEX_PATH_LIST), originDexPathList, FIELD_DEX_ELEMENTS, array);//设置回PathClassLoader
Log.i(TAG, "插件文件加载成功");
}
private static Object combineArray(Object pathElements, Object dexElements) {
//合并数组
Class<?> componentType = pathElements.getClass().getComponentType();
int i = Array.getLength(pathElements);
int j = Array.getLength(dexElements);
int k = i + j;
Object result = Array.newInstance(componentType, k);
System.arraycopy(dexElements, 0, result, 0, j);
System.arraycopy(pathElements, 0, result, j, i);
return result;
}
}
这里我们约定将插件APK放在/storage/emulated/0/Android/data/$packageName/files/plugin目录,然后为了尽早加载所以在Application中执行加载逻辑。
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
InjectUtil.inject(this, getClassLoader());//加载插件Apk的类文件
} catch (Exception e) {
e.printStackTrace();
}
}
}
Hook启动流程
在说之前我们得先了解下Activity的启动流程。
上图抽象的给出了Acticity的启动过程。在应用程序进程中的Activity向AMS请求创建Activity(步骤1),AMS会对这个Activty的生命周期栈进行管理,校验Activity等等。如果Activity满足AMS的校验,AMS就会请求应用程序进程中的ActivityThread去创建并启动Activity。
那么在上一步我们已经将插件Apk的类文件加载进来了,但是我们并不能通过startActivity的方式去启动PluginActivity,因为PluginActivity并没有在AndroidManifest中注册过不了AMS的验证,既然这样我们换一个思路。
- 在宿主项目中提前弄一个SubActivity占坑,在启动PluginActivity的时候替换为启动这个SubActivity绕过验证。
- 在AMS处理完相应验证通知我们ActivityThread创建Activty的时候在替换为PluginActivity。
占坑SubActivity非常简单
public class SubActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
然后在AndroidManifest注册好即可
<activity android:name=".SubActivity"/>
对于startActivity()最终都会调到ActivityManagerService的startActivity()方法。
ActivityManager.getService()//获取AMS
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
那么我们可以通过动态代理hook ActivityManagerService,然后在startActivity()的时候将PluginActivity替换为SubActivity,不过对于ActivityManagerService的获取不同版本方式有所不同。
在Android7.0以下会调用ActivityManagerNative的getDefault方法获取,如下所示。
static public IActivityManager getDefault(