Kaede Akatsuki

中二病也要开发 Android

动态创建 Activity 模式

还记得我们在代理 Activity 模式里谈到启动插件 APK 里的 Activity 的两个难题吗,由于插件里的 Activity 没在主项目的 Manifest 里面注册,所以无法经历系统 Framework 层级的一系列初始化过程,最终导致获得的 Activity 实例并没有生命周期和无法使用 res 资源。

使用代理 Activity 能够解决这两个问题,但是有一些限制:

  • 实际运行的 Activity 实例其实都是 ProxyActivity,并不是真正想要启动的 Activity;
  • ProxyActivity 只能指定一种 LaunchMode,所以插件里的 Activity 无法自定义 LaunchMode;
  • 不支持静态注册的 BroadcastReceiver;
  • 往往不是所有的 APK 都可作为插件被加载,插件项目需要依赖特定的框架,还有需要遵循一定的 “开发规范”;

特别是最后一个,无法直接把一个普通的 APK 作为插件使用。这个其实也不算是限制,如果我们需要进行插件化开发,我们总希望能够通过一些框架限制和规范插件的行为,在加载插件前就知道插件大概有哪些功能,这样不仅能方便对插件行为的控制,还能在一定程度上确保插件的安全(运行一个完全未知的可执行文件鬼知道它会做些什么)。不过这样做就要求插件必须依赖特定的框架,这对插件是一种侵入式开发,也就是说,开发插件时不能像开发普通 APP 那样自由。

那么有办法避开这些限制,做到完全非侵入式开发吗?比如,通过动态加载框架,不用安装就直接运行《Flappy Bird》的 APK 安装包。这听起来好像是只有获得 ROOT 权限才能做到的事情,要不然随便写个空壳的 APK 加载别人的游戏安装包就直接能运行了。不过,确实有人做到了,通过动态生成 Activity 类的方式。

基本信息

动态创建 Activity 模式

插件的 Activity 不是标准的 Activity 对象才会有上述的这些限制,使其成为标准的 Activity 是解决问题的关键,而要使其成为标准的 Activity,则需要在主项目里注册这些 Activity。总不能把插件 APK 所有的 Activity 都事先注册到宿主项目里面吧,想到代理模式需要注册一个代理的 ProxyActivity,那么能不能在主项目里 注册一个通用的 Activity(比如 TargetActivity)给插件里所有的 Activity 用呢?解决对策就是,在需要启动插件的某一个 Activity(比如 PlugActivity)的时候,动态创建一个 TargetActivity,新创建的 TargetActivity 会继承 PlugActivity 的所有共有行为,而这个 TargetActivity 的包名与类名刚好与我们事先注册的 TargetActivity 一致,我们就能以标准的方式启动这个 Activity。

运行时动态创建并编译一个 Activity 类,这种想法不是天方夜谭,动态创建类的工具有 dexmakerasmdex,二者均能实现动态字节码操作,最大的区别是前者是创建 DEX 文件,而后者是创建 CLASS 文件。

使用 DexMaker 动态创建一个类

这种运行时创建一个编译好并能运行的类的方式叫做 “动态字节码操作”(runtime bytecode manipulation),使用 DexMaker 工具能创建一个 DEX 文件,之后我们再反编译这个 DEX 看看创建出来的类是什么样子。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

public void onMakeDex(View view){
try {
DexMaker dexMaker = new DexMaker();
// Generate a HelloWorld class.
TypeId<?> helloWorld = TypeId.get("LHelloWorld;");
dexMaker.declare(helloWorld, "HelloWorld.generated", Modifier.PUBLIC, TypeId.OBJECT);
generateHelloMethod(dexMaker, helloWorld);
// Create the dex file and load it.
File outputDir = new File(Environment.getExternalStorageDirectory() + File.separator + "dexmaker");
if (!outputDir.exists())outputDir.mkdir();
ClassLoader loader = dexMaker.generateAndLoad(this.getClassLoader(), outputDir);
Class<?> helloWorldClass = loader.loadClass("HelloWorld");
// Execute our newly-generated code in-process.
helloWorldClass.getMethod("hello").invoke(null);
} catch (Exception e) {
Log.e("MainActivity","[onMakeDex]",e);
}
}

/**
* Generates Dalvik bytecode equivalent to the following method.
* public static void hello() {
* int a = 0xabcd;
* int b = 0xaaaa;
* int c = a - b;
* String s = Integer.toHexString(c);
* System.out.println(s);
* return;
* }
*/private static void generateHelloMethod(DexMaker dexMaker, TypeId<?> declaringType) {
// Lookup some types we'll need along the way.
TypeId<System> systemType = TypeId.get(System.class);
TypeId<PrintStream> printStreamType = TypeId.get(PrintStream.class);

// Identify the 'hello()' method on declaringType.
MethodId hello = declaringType.getMethod(TypeId.VOID, "hello");

// Declare that method on the dexMaker. Use the returned Code instance// as a builder that we can append instructions to.
Code code = dexMaker.declare(hello, Modifier.STATIC | Modifier.PUBLIC);

// Declare all the locals we'll need up front. The API requires this.
Local<Integer> a = code.newLocal(TypeId.INT);
Local<Integer> b = code.newLocal(TypeId.INT);
Local<Integer> c = code.newLocal(TypeId.INT);
Local<String> s = code.newLocal(TypeId.STRING);
Local<PrintStream> localSystemOut = code.newLocal(printStreamType);

// int a = 0xabcd;
code.loadConstant(a, 0xabcd);

// int b = 0xaaaa;
code.loadConstant(b, 0xaaaa);

// int c = a - b;
code.op(BinaryOp.SUBTRACT, c, a, b);

// String s = Integer.toHexString(c);
MethodId<Integer, String> toHexString
= TypeId.get(Integer.class).getMethod(TypeId.STRING, "toHexString", TypeId.INT);
code.invokeStatic(toHexString, s, c);

// System.out.println(s);
FieldId<System, PrintStream> systemOutField = systemType.getField(printStreamType, "out");
code.sget(systemOutField, localSystemOut);
MethodId<PrintStream, Void> printlnMethod = printStreamType.getMethod(
TypeId.VOID, "println", TypeId.STRING);
code.invokeVirtual(printlnMethod, null, localSystemOut, s);

// return;
code.returnVoid();
}

}

运行后在 SD 卡的 dexmaker 目录下找到刚创建的文件 “Generated1532509318.jar”,把里面的 “classes.dex” 解压出来,然后再用 “dex2jar” 工具转化成 jar 文件,最后再用 “jd-gui” 工具反编译 jar 的源码。

至此,我们已经成功在运行时创建一个编译好的类(的 DEX)。

修改需要启动的目标 Activity

接下来的问题是如何把需要启动的、在 Manifest 里面没有注册的 PlugActivity 换成有注册的 TargetActivity。
在 Android,虚拟机加载类的时候,是通过 ClassLoader 的 loadClass 方法,而 loadClass 方法并不是 final 类型的,这意味着我们可以创建自己的类去继承 ClassLoader,以重载 loadClass 方法并改写类的加载逻辑,在需要加载 PlugActivity 的时候,偷偷把其换成 TargetActivity。

大致思路如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CJClassLoader extends ClassLoader{

@override
public Class loadClass(String className){
if(当前上下文插件不为空) {
if( className 是 TargetActivity){
找到当前实际要加载的原始PlugActivity,动态创建类(TargetActivity extends PlugActivity )的dex文件
return 从dex文件中加载的TargetActivity
}else{
return 使用对应的PluginClassLoader加载普通类
}
}else{
return super.loadClass()//使用原来的类加载方法
}
}
}

这样就能把启动插件里的 PlugActivity 变成启动动态创建的 TargetActivity。

不过还有一个问题,主项目启动插件 Activity 的时候,我们可以替换 Activity,但是如果在插件 Activity(比如 MainActivity)启动另一个 Activity(SubActivity)的时候怎么办?插件时普通的第三方 APK,我们无法更改里面跳转 Activity 的逻辑。其实,从主项目启动插件 MainActivity 的时候,其实启动的是我们动态创建的 TargetActivity(extends MainActivity),而我们知道 Activity 启动另一个 Activity 的时候都是使用其 “startActivityForResult” 方法,所以我们可以在创建 TargetActivity 时,重写其 “startActivityForResult” 方法,让它在启动其他 Activity 的时候,也采用动态创建 Activity 的方式,这样就能解决问题。

动态创建 Activity 开源项目 android-pluginmgr

这种脑洞大开的动态加载思路来自于 houkx 的开源项目 android-pluginmgr

android-pluginmgr 项目中有三种 ClassLoader,一是用于替换宿主 APK 的 Application 的 CJClassLoader,二是用于加载插件 APK 的 PluginClassLoader,再来是用于加载启动插件 Activity 时动态生成的 PlugActivity 的 dex 包的 DexClassLoader(存放在 Map 集合 proxyActivityLoaderMap 里面)。其中 CJClassLoaderPluginClassLoader 的 Parent,而 PluginClassLoader 又是第三种 DexClassLoader 的 Parent。

ClassLoader 类加载 Class 的时候,会先使用 Parent 的 ClassLoader,但 Parent 不能完成加载工作时,才会调用 Child 的 ClassLoader 去完成工作。

java.lang.ClassLoader Loads classes and resources from a repository. One or more class loaders are installed at runtime. These are consulted whenever the runtime system needs a specific class that is not yet available in-memory. Typically, class loaders are grouped into a tree where child class loaders delegate all requests to parent class loaders. Only if the parent class loader cannot satisfy the request, the child class loader itself tries to handle it.

具体分析请参考 Android 动态加载基础 ClassLoader 的工作机制

所以每加载一个 Activity 的时候都会调用到最上级的 CJClassLoaderloadClass 方法,从而保证启动插件 Activity 的时候能顺利替换成 PlugActivity。当然如何控制着三种 ClassLoader 的加载工作,也是 pluginmgr 项目的设计难度之一。

存在的问题

动态类创建的方式,使得注册一个通用的 Activity 就能给多给 Activity 使用,对这种做法存在的问题也是明显的:

  1. 使用同一个注册的 Activity,所以一些需要在 Manifest 注册的属性无法做到每个 Activity 都自定义配置;
  2. 插件中的权限,无法动态注册,插件需要的权限都得在宿主中注册,无法动态添加权限;
  3. 插件的 Activity 无法开启独立进程,因为这需要在 Manifest 里面注册;
  4. 动态字节码操作涉及到 Hack 开发,所以相比代理模式起来不稳定;

其中不稳定的问题出现在对 Service 的支持上,使用动态创建类的方式可以搞定 Activity 和 Broadcast Receiver,但是使用类似的方式处理 Service 却不行,因为 “ContextImpl.getApplicationContext” 期待得到一个非 ContextWrapper 的 context,如果不是则继续下次循环,目前的 Context 实例都是 wrapper,所以会进入死循环。

houkx 称他现在有另外的思路实现 “启动为安装的普通第三方 APK” 的目的,而且不是基于动态类创建的原理,期待他的开源项目的更新。

代理 Activity 模式与动态创建 Activity 模式的区别

简单地说,最大的不同是代理模式使用了一个 代理的 Activity,而动态创建 Activity 模式使用了一个 通用的 Activity

代理模式中,使用一个代理 Activity 去完成本应该由插件 Activity 完成的工作,这个代理 Activity 是一个标准的 Android Activity 组件,具有生命周期和上下文环境(ContextWrapper 和 ContextCompl),但是它自身只是一个空壳,并没有承担什么业务逻辑;而插件 Activity 其实只是一个普通的 Java 对象,它没有上下文环境,但是却能正常执行业务逻辑的代码。代理 Activity 和不同的插件 Activity 配合起来,就能完成不同的业务逻辑了。所以代理模式其实还是使用常规的 Android 开发技术,只是在处理插件资源的时候强制调用了系统的隐藏 API(除非某些 ROM 蛋疼修改了这个 API),因此这种模式还是可以稳定工作和升级的。

动态创建 Activity 模式,被动态创建出来的 Activity 类是有在主项目里面注册的,它是一个标准的 Activity,它有自己的 Context 和生命周期,不需要代理的 Activity。

(这个系列的下个文章开始介绍插件化框架 “Android Frontia” 的设计和开发过程。)

参考文章