背景
2021年政府加强了对用户隐私的保护,App 的上架更新必须通过隐私合规扫描,而对 App 开发者来说就是必须在用户同意隐私政策前不能调用政府规定的隐私相关的 api。对于大家自己项目内部的代码来说,可以自己手动改掉,但是对于一些用到的第三方库就束手无策了,也许这些库有隐私版本的更新,但是对大家来说,可能版本差距太大,项目改动会比较多,当公司老项目比较多时,这种修改方案就耗费时间太久了。
而使用Gradle插件+ASM的方式可以很好的解决我们的困难。
ASM 是什么
ASM是AOP编程中一种比较成熟的框架,而 AOP是面向切面编程,具体概念比较复杂,我暂时理解为函数插桩,简单来说就是在代码中插入代码。
ASM是一个通用的Java字节码操作和分析框架。它可以用于修改现有类或直接以二进制形式动态生成类。ASM提供了一些常见的字节码转换和分析算法,可以从中构建定制的复杂转换和代码分析工具。ASM提供了与其他Java字节码框架类似的功能,但侧重于性能。由于它的设计和实现尽可能小和快,因此非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)
ASM 中我们主要用到下面几个类
**ClassReader:**一个解析器,可以让ClassVisitor像访问jvm中的Java文件一样访问class文件的结构
**ClassVisitor:**Java class文件的访问器,可以访问class文件的结构
**MethodVisitor:**Java 方法的访问器
**ClassWriter:**一个类访问器ClassVisitor,可以生成相应的class文件结构。
这里我们暂时只要知道 ASM 是一个可以修改 java .class文件的库就可以了
Gradle插件 Transform API
Android 的打包流程如下图
1、使用aapt工具将res资源文件生成R.java文件
2、使用aidl工具将aidl文件生成对应java文件
3、使用javac命令编译工程源代码和上面两步生成的文件,生成class文件
4、通过dex工具将class文件和第三方jar包打成dex文件
5、用res下编译过的二进制文件,以及assets中的原始资源文件,以及dex文件,通过apkbuilder工具打包成apk文件
6、通过jarsigner对apk进行签名
7、利用zipalign工具对apk进行字节对齐优化操作
在上面第3-4步的过程中,如果我们对.class 文件使用 ASM 进行处理就可以做到对任意 .class文件的修改,如下图
而 Android Studio 使用的构建工具 Gradle 是可以让我们自定义插件的,自定义 Gradle 插件有一个Transform API 可以实现让我们对所有的.class进行处理
关于如何自定义插件可以看这篇文章https://www.jianshu.com/p/d14f24c4a807
关于Transform API 可以参考这个文章http://quinnchen.cn/2018/09/13/2018-09-13-asm-transform/
自定义 Gradle 插件和Transform API 不是我们这篇文章的内容,看到这里默认大家已经对这些知识有一定了解了,不知道的请看上面文章
下面是插件实现的代码
public class HookPlugin implements Plugin<Project> {
@SuppressWarnings("NullableProblems")
@Override
public void apply(Project project) {
AppExtension appExtension = (AppExtension) project.getProperties().get("android");
//注册一个Transform
appExtension.registerTransform(new HookTransform(project), Collections.EMPTY_LIST);
}
}
Hunter库
Hunter是这么一个框架,帮你快速开发插件,在编译过程中修改字节码,它底层基于ASM 和 Gradle Transform API 实现。…
其实这个库就是对 ASM 和 Gradle 的再一层封装,让开发者更加将更多重心放在需要实现的业务上
我们在自定义Transform时,通过继承Hunter库里的 HunterTransform来实现,它对 ASM 的功能进行了封装
然后我们在自定义的HunterTransform里提供一个BaseWeaver类即可做到对字节码的访问
public final class HookTransform extends HunterTransform {
private HookExtension hookExtension;
public HookTransform(Project project) {
super(project);
//这个类用来存储插件的提供给用户的配置信息
hookExtension = project.getExtensions().create("macHookExt", HookExtension.class);
//我们需要给父类的bytecodeWeaver属性一个赋值,BaseWeaver帮我们封装了对压缩文件等的访问,
// 我们只需要提供一个ClassVisitor来决定如何对一个 class 文件访问就可以
this.bytecodeWeaver = new HookWeaver(hookExtension);
HookLog.info("HookExtension:" + hookExtension.toString());
}
@Override
public void transform(Context context, Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider,
boolean isIncremental) throws IOException, TransformException, InterruptedException {
bytecodeWeaver.setExtension(hookExtension);
HookLog.info("HookExtension:" + hookExtension.toString());
super.transform(context, inputs, referencedInputs, outputProvider, isIncremental);
}
}
BaseWeaver帮我们封装了对压缩文件(jar文件)等的访问,我们只需要提供一个ClassVisitor来决定如何对一个 class 文件访问就可以
public final class HookWeaver extends BaseWeaver {
private HookExtension hookExtension;
public HookWeaver(HookExtension hookExtension) {
this.hookExtension = hookExtension;
}
/**
* @param fullQualifiedClassName class的全限定名
* 是否要访问这个 class 文件
*/
@Override
public boolean isWeavableClass(String fullQualifiedClassName) {
return super.isWeavableClass(fullQualifiedClassName);
}
/**
* @return 提供一个类的访问者
*/
@Override
protected ClassVisitor wrapClassWriter(ClassWriter classWriter) {
//这是一个我们自定义的ClassVisitor,对 class 的访问业务就写在这里
return new HookClassAdapter(classWriter, hookExtension);
}
}
业务实现
上面我们提到我们对class 的访问业务写在HookClassAdapter这个 ClassVisitor 里,现在我们就看下它是如何实现的。
public final class HookClassAdapter extends ClassVisitor {
private String className = "";
private String superClassName = "";
private HookExtension hookExtension;
HookClassAdapter(final ClassVisitor cv, HookExtension hookExtension) {
super(Opcodes.ASM6, cv);
this.hookExtension = hookExtension;
}
/**
* 用于标记是否拦截对这个类的访问
*/
private boolean needIntercept = false;
/**
* 访问类的标题
*/
@Override
public void visit(int version, int access, String name, String signature, String superName,
String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
if (name != null) {
this.className = name.replace("/", ".");
}
if (superName != null) {
this.superClassName = superName.replace("/", ".");
}
// 过滤自定义接口
needIntercept = false;
for (String face : interfaces) {
if (HOOK_INTERFACE_PATH.equals(face.replace("/", ".")) || HOOK_INTERFACE_PATH2.equals(face.replace("/", "."))) {
needIntercept = true;
break;
}
}
//过滤插件中使用到的相关类
//|| HOOK_INTERFACE_ADAPTER_PATH.equals(face.replace("/", ".")
if (HOOK_INTERFACE_ADAPTER_PATH.equals(superClassName)
|| HOOK_INTERFACE_ADAPTER_PATH.equals(className)
|| HOOK_INTERFACE_ADAPTER_PATH2.equals(superClassName)
|| HOOK_INTERFACE_ADAPTER_PATH2.equals(className)
) {
needIntercept = true;
}
if (HOOK_CLASS.equals(className)) {
needIntercept = true;
}
}
/**
* 提供一个用于访问访问方法的 MethodVisitor
*/
@Override
public MethodVisitor visitMethod(final int access, final String name,
final String desc, final String signature, final String[] exceptions) {
if (needIntercept) {
//需要跳过的类就不走我们自己的MethodVisitor了
return super.visitMethod(access, name, desc, signature, exceptions);
}
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
//提供一个我们自己处理业务的MethodVisitor
return mv == null ? null
: new HookMethodAdapter(className, superClassName, name, access, desc, mv, hookExtension);
}
}
HookClassAdapter继承了一个ClassVisitor,这里我们主要覆写了 visit 方法和 visitMethod 方法。如上面注释, visit 方法用于访问类的标题,我们可以在这里获取当前访问 class 的类名等信息,
注意,ClassVisitor使用了访问者模式,可以简单的理解为,通过ClassVisitor我们就可以访问 class 文件的各个部分
一些解释看这些文章
https://www.jianshu.com/p/e4b8cb0b3204
https://blog.51cto.com/u_15064638/2874093
https://wenku.baidu.com/view/391d899e1937f111f18583d049649b6648d709c9.html
visitMethod 主要是提供一个用于访问方法的 MethodVisitor,和这里的 ClassVisitor 类似,不过MethodVisitor用于处理对方法的访问
public final class HookMethodAdapter extends LocalVariablesSorter implements Opcodes {
public final class HookMethodAdapter extends LocalVariablesSorter implements Opcodes {
private String classNamePath;
private String className;
private String superClassName;
private String methodDes;
private String methodName;
private HookExtension hookExtension;
public HookMethodAdapter(String className, String superClassName, String methodName, int access,
String desc,
MethodVisitor mv, HookExtension hookExtension) {
super(Opcodes.ASM5, access, desc, mv);
this.classNamePath = className.replace(".", "/");
this.className = className;
this.superClassName = superClassName;
this.methodName = methodName;
this.methodDes = desc;
this.hookExtension = hookExtension;
}
@Override
public void visitMethodInsn(int opcode, String owner, String methodName, String desc, boolean itf) {
//判断是在调用WifiInfo的getMacAddress方法
if ("android/net/wifi/WifiInfo".equals(owner) && ("getMacAddress").equals(methodName) && "()Ljava/lang/String;".equals(desc)) {
//修改为调用HOOK_CLASS_PATH(com.quvideo.mobile.platform.machook.MacHook)类的getTestMac方法
mv.visitMethodInsn(INVOKESTATIC, HookExtension.HOOK_CLASS_PATH, "getTestMac", "(L" + owner + ";)Ljava/lang/String;", false);
return;
}
...
super.visitMethodInsn(opcode, owner, methodName, desc, itf);
}
}
}
上面的代码完成业务是 使用我们自定义的MacHook.getTestMac
方法 替换WifiInfo
调用getMacAddress
方法获取 mac 地址,将每个地方这样的调用都替换成调用我们自己的方法,然后再在我们自定义方法里编写逻辑,比如隐私同意前给一个空值,隐私同意后再调用WifiInfo
的getMacAddress
获取真实的地址。
上面的owner表示类名,methodName就是方法名,desc是方法的描述,表示结构为
([参数类型],[参数类型]...)返回类型;
参数类型和返回类型要用字节码的表示方式,可以参考下面两张图
帮助插件
visitMethodInsn 的各种代码看不懂,写不出来怎么办,不用怕用ASM Bytecode Viewer Support Kotlin插件自动生成
我们再简单看下插件怎么用
找到你要修改的类,在编辑界面右键,然后在菜单中选择ASM Bytecode Viewer
然后 AndroidStudio 会开始 build,等 build 结束后会弹出如下窗口
这里显示的字节码指令,红色划线处的意思是 调用类为android/net/wifi/WifiInfo
的getMacAddress
方法,方法描述为()Ljava/lang/String;
这里就与上面的判断逻辑对上了
而我们要改成的代码则这样操作,先写好想要调用的代码,比如下面
void test() {
WifiInfo wifiInfo = null;
MacHook.getTestMac(wifiInfo);
}
然后再次用 ASM Bytecode Viewer插件
可以发现红线处就是我们要实现业务写的代码,是不是 so easy
补充
其他的比如插件的发布等大家可以自己找下插件相关的文章看下,这里就不多做介绍了
本次描述的插件下 build.gradle 配置如下
apply plugin: 'groovy'
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:3.1.4'
implementation 'com.android.tools.build:gradle-api:3.1.4'
implementation 'commons-io:commons-io:2.6'
implementation 'commons-codec:commons-codec:1.10'
//noinspection GradleDependency
implementation 'org.ow2.asm:asm:6.2.1'
//noinspection GradleDependency
implementation 'org.ow2.asm:asm-util:6.2.1'
//noinspection GradleDependency
implementation 'org.ow2.asm:asm-commons:6.2.1'
implementation 'com.quinn.hunter:hunter-transform:0.9.0'
}
repositories {
google()
jcenter()
mavenCentral()
}
参考以下文章
具体原理啥的可以看下这里https://zhuanlan.zhihu.com/p/359299148
http://quinnchen.cn/2018/09/13/2018-09-13-asm-transform/
https://www.jianshu.com/p/e3ca9ca3a3d3
https://maimai.cn/article/detail?fid=1674572771&efid=xtzpWBxgH0Iv9zVTBsZ_Ww
参考以下文章
具体原理啥的可以看下这里https://zhuanlan.zhihu.com/p/359299148
http://quinnchen.cn/2018/09/13/2018-09-13-asm-transform/
https://www.jianshu.com/p/e3ca9ca3a3d3
https://maimai.cn/article/detail?fid=1674572771&efid=xtzpWBxgH0Iv9zVTBsZ_Ww
ASM 讲解视频 https://www.bilibili.com/video/BV1Py4y1g7dN/?spm_id_from=333.788&vd_source=03ff970824e8707a9d0450978c8351ec