前言
一般情况下,在Java层可以使用动态代理实现方法hook,如下:
interface Animal {
void eat();
}
Object proxy = Proxy.newProxyInstance(getClassLoader(), new Class[]{Animal.class},
new InvocationHandler() {
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
return null;
}
});
// 反射替换为动态代理对象proxy
//
这种方法只能对接口有效,因为newProxyInstance只能创建针对某个interface的动态代理对象,而无法生成针对某个class的动态代理对象。
对于class对象的方法hook,我们可以继承自该class然后重写对应的方法,然后直接创建新对象去把旧对象替换掉就可以。
如果class是公开的那就没问题,但很多情况下class并不是公开的,比如该class是在Framework层里,虽然它也是public但被@hide标记,导致应用层开发无法引用到,因此需要想方法解决该问题。
针对@hide的class对象实现方法调用hook
比如我们调用Context的getExternalFilesDir方法,会返回APP对应的外部File目录,而它里面具体的调用流程如下:
Context(getExternalFilesDir)
ContextImpl(getExternalFilesDir)
Environment(buildExternalStorageAppFilesDirs)
UserEnvironment(buildExternalStorageAppFilesDirs)
UserEnvironment(buildExternalStorageAppFilesDirs)
UserEnvironment(getExternalDirs)
在Environment类里,有个UserEnvironment对象sCurrentUser,所有的路径其实都是由这个sCurrentUser生成的:
private static UserEnvironment sCurrentUser;
/** {@hide} */
public static class UserEnvironment {
@UnsupportedAppUsage
public File[] getExternalDirs() {
final StorageVolume[] volumes = StorageManager.getVolumeList(mUserId,
StorageManager.FLAG_FOR_WRITE);
final File[] files = new File[volumes.length];
for (int i = 0; i < volumes.length; i++) {
files[i] = volumes[i].getPathFile();
}
return files;
}
......
}
如果我们现在需要getExternalDirs的返回路径,比如默认是sd卡根目录,现在需要返回sd卡根目录下的123文件夹目录,就需要hook这个UserEnvironment的getExternalDirs方法,但这里有两个问题,一个就是UserEnvironment是class而不是interface,无法用动态代理,另一个是UserEnvironment是@hide的App中无法直接引用到。
为了解决@hide这个问题,可以先做一个system.jar,里面放Environment和UserEnvironment:
package android.os;
import java.io.File;
public class Environment {
public static class UserEnvironment {
public File[] getExternalDirs() {
return null;
}
}
}
编译成system.jar,需要在App工程中引入,只需要在编译时即可:
compileOnly fileTree(dir: 'libs', includes: ['system.jar'])
按照正常的思路,接着应该在App工程中定义CustomUserEnvironment类,继承自android.os.Environment.UserEnvironment,然后去实现getExternalDirs方法即可,但这里会有个问题:Android系统也有android.os.Environment,会默认去找这个,而不是找我们system.jar里的Environment,因此也就找不到里面的UserEnvironment,因此出现编译失败。
为了解决这个问题,需要再引入一个bridge.jar,里面方法UserEnvironmentBridge:
package com.hook.bridge;
import android.os.Environment;
import java.io.File;
public class UserEnvironmentBridge extends Environment.UserEnvironment {
@Override
public File[] getExternalDirs() {
return super.getExternalDirs();
}
}
编译成bridge.jar,然后引入App工程,这个需要编译到apk中:
implementation fileTree(dir: 'libs', includes: ['bridge.jar'])
然后在App工程中定义CustomUserEnvironment类:
public class CustomUserEnvironment extends UserEnvironmentBridge {
private static final String TAG = "CustomUserEnvironment";
@Override
public File[] getExternalDirs() {
File[] files = super.getExternalDirs();
if (files != null) {
for (File file : files) {
// files[0] = new File("/sdcard/", "123");
Log.i(TAG, file.getAbsolutePath());
}
files[0] = new File("/storage/emulated/0/123");
}
return files;
}
}
这样就不会有编译不过的问题了,这里的继承关系为:CustomUserEnvironment -> UserEnvironmentBridge -> android.os.Environment.UserEnvironment
最后创建一个CustomUserEnvironment对象然后把sCurrentUser替换掉即可:
try {
Field field = Environment.class.getDeclaredField("sCurrentUser");
field.setAccessible(true);
field.set(null, new CustomUserEnvironment());
} catch (Throwable t) {
Log.w(TAG, t);
}
替换之后,外部调用getExternalFilesDir方法,就会走到CustomUserEnvironment的getExternalDirs方法里,然后在这里实现自定义逻辑。
总结
对于某个Class对象,如果要hook其中的方法,可以创建一个继承自该Class的对象,然后把原有的对象替换掉就可以,而如果该Class在Framework层且应用层引用不到,那么就可以采用上述的方法实现替换。
相对于在native层的hook方案(比如Xposed),该方案都是在Java层实现,代码实现简单,不会有机型适配和兼容问题,因此实际项目中会优先使用该方案。