最新强大!ASM 插桩实现 Android 端无埋点性能监控!(1),面试官最后应该说什么

更多学习和讨论,欢迎加入我们!

有许多来自一线的技术大牛,也有在小厂或外包公司奋斗的码农,我们致力打造一个平等,高质量的Android交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。

这里有2000+小伙伴,让你的学习不寂寞~·

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

ASM插桩方案

我们知道,class文件是按照JVM规范格式存储的二进制文件,本质上是一个表,记录了类的常量池、访问标志、属性和方法等。ASM库不仅能够对class文件进行解读,还提供了方便的API进行字节码的修改,支持直接产生二进制class文件。

ASM提供了基于事件的API,ClassReader用于读取class文件的二进制流,ClassVisitor以事件的形式输出class的结构信息, ClassWriter则用于把修改后的字节码生成二进制流。

我们先以Java工程的方式演示对Class文件的处理,不考虑集成打包。

我们定义一个简单的页面MainActivity,增加一个加了编译期注解的方法

public class MainActivity extends AppCompatActivity {

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

}

@TraceTime

public void fun(){

Log.d(“tt_apm”, “annotated function”);

}

}

它的class文件在工程的app/build/intermediates/classes目录下,用ASM读取分析

public static void main(String[] args) {

try {

File classFile = new File(“./source/MainActivity.class”);

File dir = new File(“.”);

transformClassFile(dir, classFile)

} catch (Exception e){}

}

private static File transformClassFile(File dir, File sourceFile){

String className = sourceFile.getName();

// 得到class文件二进制流

FileInputStream fileInputStream = new FileInputStream(sourceFile);

byte[] sourceClassBytes = IOUtils.toByteArray(fileInputStream);

// 定义classWriter,用于输出修改后的二进制流

ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);

// 自定义ClassVisitor, 负责字节码的消费

MyClassVisitor myClassVisitor = new MyClassVisitor(classWriter);

// ClassReader负责字节码的读取

ClassReader classReader = new ClassReader(sourceClassBytes);

// 开始字节码处理

classReader.accept(myClassVisitor, 0);

// 生成二进制流并保存成新的文件

byte[] destByte = classWriter.toByteArray();

File modified = new File(dir, className)

if (modified.exists()) {

modified.delete()

}

modified.createNewFile();

new FileOutputStream(modified).write(destByte)

return modified;

}

private static class MyClassVisitor extends ClassVisitor {

public MyClassVisitor(ClassVisitor classVisitor) {

super(Opcodes.ASM6, classVisitor);

}

@Override

public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {

System.out.println(“visit:access: " + access + " ,name: " + name + " , superName: " + superName + " ,singature: " + signature + “, interfaces: " + interfaces.join(”/”));

super.visit(version, access, name, signature, superName, interfaces);

}

@Override

public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {

System.out.println("visitMethod:access: " + access + " ,name: " + name + " , desc: " + descriptor + " ,singature: " + signature);

MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);

MethodVisitor myMv = new MethodVisitor(Opcodes.ASM6, mv) {

@Override

AnnotationVisitor visitAnnotation(String desc, boolean visible) {

System.out.println("visitAnnotation: desc: " + desc);

return super.visitAnnotation(desc, visible)

}

@Override

void visitCode() {

super.visitCode()

}

}

return myMv;

}

}

我们用ClassReader读取了MainActivity.java的class文件,并用自定义的ClassVisitor接收事件。查看输出:

visit:access: 33 ,name: com/example/wangkai/MainActivity , superName: android/support/v7/app/AppCompatActivity ,singature: null, interfaces:

visitMethod:access: 1 ,name: , desc: ()V ,singature: null

visitMethod:access: 4 ,name: onCreate , desc: (Landroid/os/Bundle;)V ,singature: null

visitMethod:access: 1 ,name: fun , desc: ()V ,singature: null

visitAnnotation: desc: Lcom/example/wangkai/annotation/TraceTime;

我们通过visit回调可以读取到class的名字、父类名和接口,这样就可以判断出一个类是否是我们要插桩的白名单页面,是不是Activity子类以及是否实现了点击事件接口View$onClickListener(实现对点击事件的监控)

通过visitMethod我们拿到了方法名,这样就可以判断这个方法是不是我们要监控的生命周期方法。

通过在visitMethod方法里返回自定义的MethodVisitor对象,我们拿到了方法上的注解,从而可以知道这个方法是否是要插桩的方法。

visitCode表示方法开始执行,如果能在这里插入代码,那我们的代码就能在原始代码执行前执行。

我们已经找到了切入点,下一步就是插入代码了。插入代码要难一些,因为我们是在字节码层面操作,插入的也只能是字节码,这就需要对字节码有一定了解。包括局部变量表和操作数栈的概念,常见指令(ALOAD, INVOKEVIRTUAL等)的含义[参考5]

这里以实现监听点击事件为例。手动埋点时,我们需要插入这样的代码:

private static class MyClickListener implements View.OnClickListener{

@Override

public void onClick(View v) {

ClickAgent.click(v); //待插入代码,方法里获取view的ID和当前时间,实现对点击事件的记录

Log.d(TAG, "onClick: ");

}

}

我们要做的是,通过ASM methodVisitor提供的API,把ClickAgent.click(v)的字节码,注入到原始onClick方法里。查看字节码:

L0

LINENUMBER 27 L0

ALOAD 1

INVOKESTATIC com/example/wangkai/ClickAgent.click (Landroid/view/View;)V

L1

LINENUMBER 28 L1

LDC “MainActivity”

LDC "onClick: "

INVOKESTATIC android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I

可以看到ClickAgent.click(v)对应的字节码是两行

ALOAD 1表示把局部变量表里索引为1的值,推到操作数栈上,也就是参数值View v。对应到ASM,是methodVisitor.visitVarInsn(Opcodes.ALOAD, 1);

INVOKESTATIC com/example/wangkai/ClickAgent.click (Landroid/view/View;)V就是调用ClickAgent的静态方法click。对应到ASM,是methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, “com/example/wangkai/ClickAgent”, “click”, “(Landroid/view/View;)V”, false)

当我们在visitorMethod回调里判断name、desc和signature和原始方法一致,并且该类实现的interfaces包含了View$onClickListener时,就可以注入了。

怎么注入进去呢?这样写就可以了:

@Override

void visitCode() {

super.visitCode()

mv.visitVarInsn(Opcodes.ALOAD, 1);

mv.visitMethodInsn(Opcodes.INVOKESTATIC, “com/example/wangkai/ClickAgent”, “click”, “(Landroid/view/View;)V”, false)

}

修改后执行,会生成插桩后的class文件,可以用JD_GUI查看插桩后的效果,

实际编码中,我们可以借助于Bydecode Outline插件。

怎么实现AspectJ方案里演示的对JSONObject的反序列化监控呢?只需要将JSONObject对应的构造函数替换成我们的函数

@Override

void visitMethodInsn(int opcode, String owner, String methodName, String descriptor, boolean isInterface) {

if(opcode == Opcodes.INVOKESPECIAL && owner.equals(“org/json/JSONObject”)

&& methodName.equals(“”) && descriptor.equals(“(Ljava/lang/String;)V”)) {

super.visitMethodInsn(Opcodes.INVOKESTATIC, “com.example.apm.JSONObjectAgent”,

“init”, “(Ljava/lang/String;)Lorg/json/JSONObject;”, false);

} else {

super.visitMethodInsn(opcode, owner, methodName, descriptor, isInterface)

}

}

插桩的问题已经解决了!

Oh, we have solved one problem.

我们再把插桩的处理过程集成到Gradle打包里就可以。

我们知道,通过在build.gradle里配置apply plugin: 'xxplugin',可以实现调用自定义的plugin。自定义plugin:

class ApmPlugin implements Plugin {

@Override

void apply(Project project) {

android = project.extensions.getByType(AppExtension);

ApmTransform transform = new ApmTransform(project)

android.registerTransform(transform)

}

}

apply方法会在build.gradle apply plugin: 'xxplugin'行执行时被调用。我们在apply方方法里注册了自定义的Transform,实现对class文件的处理。

Transform是Android gradle提供的修改class的一套API,Transform每次都是将一个输入进行处理,然后将处理结果输出,而输出的结果将会作为另一个Transform的输入。

我们在回调里可以拿到输入

class ApmTransform extends Transform {

@Override

void transform(

@NonNull Context context,

@NonNull Collection inputs,

如何做好面试突击,规划学习方向?

面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。

学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。

同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节

image

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

image

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

分支细节**。

[外链图片转存中…(img-4v12d3Ld-1715377916230)]

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

[外链图片转存中…(img-vKyS2Jy3-1715377916230)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值