插件化原理解析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的启动,使其有了别的功能。