其实从这一节开始,就需要区分两种插件化的方案:
网路上大多是研究不需要注册的方案,需要 hook 各种 Activity、Service 的启动流程和生命周期。一般来说 hook 的原则是越少越好,越少越不会和系统的变动有冲突,自然也就不会出问题。
当然,也有不做深度 hook 的方案,比如被反编译出来的 Atlas (现在改名叫 ACDD,https://github.com/zjf1023/ACDDExtension)。下面都是按预先注册的方案来解释,这样的方案较为简单,hook的量极少,稳定可靠,当然也就牺牲了一定的动态性。
<!--more-->
启动流程的分析网路上很多很多,这边摘了一个比较精简的版本:
- 每个Activity的启动过程都是通过startActivityForResult() 最终都会调用Instrument.execStartActivity()
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
- 这句中handleLaunchActivity()又调用了performLaunchActivity(r, customIntent); 而最终又调用了这句:
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
- 兜了一圈又回到Instrumentation了。结果终于找到了可以hook的点了,就是这个mInstrumentation.newActivity()
因为我们提前注册了 Activity,所以其实不会碰到校验的问题。剩下的问题就只有,我们的插件 Activity 代码不在当前 Classloader 里。
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
Hook 的点也是显而易见的,在 Instrumentation 里把 ClassLoader 换掉。
Application里:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
installDex();
hookInstrumentation();
}
private void installDex() {
// 外部路径
File optimizedDexOutputPath = new File("/system/dex/" + "3-Plugin.apk");
// 无法直接从外部路径加载.dex文件,需要指定APP内部路径作为缓存目录(.dex文件会被解压到此目录)
File dexOutputDir = this.getDir("dex", 0);
dexClassLoader = new DexClassLoader(
optimizedDexOutputPath.getAbsolutePath(),
dexOutputDir.getAbsolutePath(),
null,
getClassLoader());
}
private void hookInstrumentation() {
try {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 拿到原始的 mInstrumentation字段
Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
//如果没有注入过,就执行替换
if (!(mInstrumentation instanceof CustomInstrumentation)) {
CustomInstrumentation pluginInstrumentation = new CustomInstrumentation(mInstrumentation, dexClassLoader);
mInstrumentationField.set(currentActivityThread, pluginInstrumentation);
}
} catch (Exception e) {
e.printStackTrace();
}
}
CustomInstrumentation:
public class CustomInstrumentation extends Instrumentation {
private ClassLoader customClassloader;
private Instrumentation base;
public CustomInstrumentation(Instrumentation base, ClassLoader classLoader) {
this.base = base; // 如果要不注册 Activity 就能启动的方式,那么还需要 hook execStartActivity 等方法,此时会用到这个 base 的 Instrumentation
customClassloader = classLoader;
}
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
// 替换了 ClassLoader
return super.newActivity(customClassloader, className, intent);
}
}
至此,我们就能启动插件的 Activity 了。
本系列为笔记文,文中有大量的源码解析都是引用的其他作者的成果,详见下方参考资料。
欢迎关注我的公众号和微博。