插件化原理解析 Hook 动态代理

使用动态代理能够达到 AOP 编程的效果。现在流行的插件化框架也广泛使用了动态代理。本文主要讲解动态代理的 Hook 机制

代理

代理可以实现方法增强,主要分为静态代理和动态代理。

静态代理

所有的原始类,代理类都提供好了

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
//目标对象接口
interface DoSth {
fun doSomething(sth: Long): Array<Any>
}

//目标对象实现
class DoSthImpl : DoSth {

override fun doSomething(sth: Long): Array<Any> {
println("DoSthImpl " + sth)
return arrayOf("a", "b", "c");
}
}

//代理对象实现
class ProxyDoSth(val base: DoSth) : DoSth {

override fun doSomething(sth: Long): Array<Any> {
println("ProxyDoSth " + (sth * 10))
val sth = base.doSomething(sth)
sth[0] = "d"
return sth
}
}

//测试程序
val sthImpl = DoSthImpl()
val proxyDoSth = ProxyDoSth(sthImpl)
println(Arrays.toString(proxyDoSth.doSomething(10)))

代理对象实现将目标对象的实现行为改变了,将传入的值扩大 10 倍,并将返回的数组内容改变。因此代理模式是可以改变目标对象的行为的

动态代理

静态代理使用简单,但是比较繁琐。当需要代理的类较多时,会很麻烦。JDK 提供了动态代理,运行时生成对应的代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//代理方法
class InvocationHandlerImpl(val base: Any) : InvocationHandler {
override fun invoke(p0: Any?, p1: Method?, p2: Array<out Any>?): Any {

if ("doSomething" == (p1!!.name)) {
val value: Long = p2!![0] as Long
val doSthValue = value * 5
println(doSthValue)
val invoke: Array<Any> = p1.invoke(base, doSthValue) as Array<Any>
invoke[0] = "d"
return invoke
}

return Unit
}
}

//测试程序
val sthImpl = DoSthImpl()
val result = Proxy.newProxyInstance(DoSth::class.java.classLoader, sthImpl.javaClass.interfaces, InvocationHandlerImpl(sthImpl))
val ds = (result as DoSth).doSomething(10)

代理模式,运行时生成对应代理接口的代理实现类,当目标方法调用时,触发 Invocation.invoke.

代理 Hook

Hook 之前,得先找到 Hook 点,一般是静态变量或者单例,因为这些对象不会经常变化。下面我们来分析如何 Hook Activity。都知道 Activity 的启动都是通过 Instrumentation。而其 ActivityThread 内部,一个进程只有一个 ActivityThread。因此只要 Hook 住这个对象那么就可以操作 Instrumentation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SuppressLint("PrivateApi")
class HookHelper {
companion object {

@Throws(Exception::class)
fun attachContext() {
val activityThreadClass = Class.forName("android.app.ActivityThread")
val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread")
currentActivityThreadMethod.isAccessible = true
val currentActivityThread = currentActivityThreadMethod.invoke(null)
val instrumentationField = activityThreadClass.getDeclaredField("mInstrumentation")
instrumentationField.isAccessible = true
val instrumentation = instrumentationField.get(currentActivityThread) as Instrumentation

//代理instrumentation
val proxyInstrumentation = ProxyInstrumentation(instrumentation)
instrumentationField.set(currentActivityThread, proxyInstrumentation)
}
}
}

HookHelper 通过反射获取当前进程的 ActivityThread,然后替换内部的 Instrumentation 字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ProxyInstrumentation(val instrumentation: Instrumentation) : Instrumentation() {
companion object {
val TAG = ProxyInstrumentation::class.java.canonicalName
}

fun execStartActivity(
who: Context, contextThread: IBinder?, token: IBinder?, target: Activity?,
intent: Intent, requestCode: Int, options: Bundle?): Instrumentation.ActivityResult? {
Log.d(TAG, "\nstartActivity, who = [" + who + "], " +
"\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
"\ntarget = [" + target + "], \nintent = [" + intent +
"], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]")

try {
val execStartActivity = Instrumentation::class.java.getDeclaredMethod("execStartActivity", Context::class.java, IBinder::class.java, IBinder::class.java, Activity::class.java, Intent::class.java, Int::class.javaPrimitiveType, Bundle::class.java)
execStartActivity.isAccessible = true
return execStartActivity.invoke(instrumentation, who, contextThread, token, target, intent, requestCode, options) as Instrumentation.ActivityResult?
} catch (e: Exception) {
throw RuntimeException("don't support hook")
}

}
}

ProxyInstrumentation 继承自 Instrumentation,用于增强 Instrumentation。在每个 Activity 启动之前打印日志

App 启动的时候修改替换 Instrumentation

1
2
3
4
5
6
7
8
9
10
class MyApp : Application() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
try {
HookHelper.attachContext()
} catch (e: Exception) {
e.printStackTrace()
}
}
}

这个时候启动 App,然后重新启动一个 Activity, 那么会打印日志

1
startActivity, who = [cn.binea.pluginframeworkdemo.MyApp@3cc96c5], contextThread = [android.app.ActivityThread$ApplicationThread@56a681a], token = [null], target = [null], intent = [Intent { act=android.intent.action.VIEW dat=http://www.baidu.com/... flg=0x10000000 }], requestCode = [-1], options = [null]

至此,这个例子简单的 HookActivity 的启动,使其有了别的功能。