2024年Android最全强大!ASM 插桩实现 Android 端无埋点性能监控!,2024年最新阿里巴巴客服面试题及答案

《设计思想解读开源框架》

第一章、 热修复设计

  • 第一节、 AOT/JIT & dexopt 与 dex2oat

  • 第二节、 热修复设计之 CLASS_ISPREVERIFIED 问题

  • 第三节、热修复设计之热修复原理

  • 第四节、Tinker 的集成与使用(自动补丁包生成)

    第二章、 插件化框架设计

  • 第一节、 Class 文件与 Dex 文件的结构解读

  • 第二节、 Android 资源加载机制详解

  • 第三节、 四大组件调用原理

  • 第四节、 so 文件加载机制

  • 第五节、 Android 系统服务实现原理

    第三章、 组件化框架设计

  • 第一节、阿里巴巴开源路由框——ARouter 原理分析

  • 第二节、APT 编译时期自动生成代码&动态类加载

  • 第三节、 Java SPI 机制

  • 第四节、 AOP&IOC

  • 第五节、 手写组件化架构

    第四章、图片加载框架

  • 第一节、图片加载框架选型

  • 第二节、Glide 原理分析

  • 第三节、手写图片加载框架实战

    第五章、网络访问框架设计

  • 第一节、网络通信必备基础

  • 第二节、OkHttp 源码解读

  • 第三节、Retrofit 源码解析

    第六章、 RXJava 响应式编程框架设计

  • 第一节、链式调用

  • 第二节、 扩展的观察者模式

  • 第三节、事件变换设计

  • 第四节、Scheduler 线程控制

    第七章、 IOC 架构设计

  • 第一节、 依赖注入与控制反转

  • 第二节、ButterKnife 原理上篇、中篇、下篇

  • 第三节、Dagger 架构设计核心解密

    第八章、 Android 架构组件 Jetpack

  • 第一节、 LiveData 原理

  • 第二节、 Navigation 如何解决 tabLayout 问题

  • 第三节、 ViewModel 如何感知 View 生命周期及内核原理

  • 第四节、 Room 架构方式方法

  • 第五节、 dataBinding 为什么能够支持 MVVM

  • 第六节、 WorkManager 内核揭秘

  • 第七节、 Lifecycles 生命周期


    本文包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…

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

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

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

思路

实现自动监控需要解决2个问题

You solve one problem… and you solve the next one… and then the next. And If you solve enough problems, you get to come home – The Martian

1. 如何计算方法耗时

统计方法耗时是典型的面向切面编程(Aspect-Oriented Programming,AOP)的应用场景。实现AOP有一些成熟的技术方案

  • 静态代理 和 运行期注解 + 动态代理

  • 编译时代码生成(APT),案例:ButterKnifeDagger2Room

  • 切面编程库(AspectJ),案例:Hugo

  • 字节码注入(ASM),案例:GrowingIO

**方案1:**手动代理模式实现AOP显然不适用本场景。

**方案2:**在编译时根据Annotation生成相关的代码是非常便利的技术,但APT主要适合用来生成辅助类,用户仍然需要通过手动调用方法使生成的代码在切入点执行。这一点其实不算AOP编程,也不适合本场景。

**方案3:**AspectJ[参考1]是广泛应用于JavaEE开发的AOP方案,简单易用,功能强大。它提供了简便的语法让我们定义切面逻辑,再通过提供的AJC编译器,在Java文件编译成class文件的过程里,把切面代码织入到目标业务代码里。本质上,仍然是以代理的方式实现AOP。我们通过AspectJ就能方便的在目标方法执行前后执行我们的计时代码。

**方案4:**我们还可以直接对class文件进行修改,ASM[参考2]是字节码操作库,支持对字节码进行编辑,实现类、属性和方法的增删改查。字节码操作库还有Javaassit库可以选择,但ASM灵活度和效率都是最高的。利用操作字节码实现方法计时,可以的做法是修改class文件,在目标方法开始和结束时插入本来需要手动埋点的计时代码(称之为字节码插桩)。

注解的作用是提供插入点,AspectJASM既支持以注解作为切入点,也支持根据类方法名/类继承关系等规则来确定切入点。

2. 如何集成到打包流程

Android工程的构建工具是Gradle, 构建过程由一系列Task构成。Gradle支持自定义Task加入到原有的构建流程,以实现自己的处理逻辑[参考3]

Hugo pluginjavaCompile Task最后插入一个Action,调用ajc函数对class文件进行处理,把AspectJ的能力引入到了Android打包流程,AspectJx[参考4]是参考Hugo实现的一个在Android上通用的使用AspectJ的开源库,方案3利用这个库使用AspectJ。

Android官方提供了Transform API支持在class文件到dex转换期间修改class文件,这个阶段正是ASM字节码操作库工作的阶段,所以,我们可以通过在自定义插件中使用Transform的方式,把插桩过程集成到打包流程,方案4使用这个处理方式。

实现

下面分别用AspectJ方案和ASM插桩方案进行Demo实现。

AspectJ方案

AspecJ完整给出了AOP编程里的一些概念:切面(Aspect)通知(Advice)切入点(Pointcut),这些概念通过代码可以很清晰的理解。

网上有较多的统计Activity生命周期耗时的例子。本文以统计JSON反序列化耗时为例。

通过new JSONObject(String jsonStr)方法可以把JSON格式的字符串反序列化为JSON对象处理,我们要切入的点就是JSONObject的构造函数,需要做的处理是在构造函数执行前后插入我们的计时代码

ke-width: 0px; background-color: rgb(255, 255, 255); border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.54902) 0px 2px 10px;">@Aspect // 代码1

public class JsonAspect {

private static final String TAG = “JsonAspect”;

@Around(“call(org.json.JSONObject.new(…))”) // 代码2

public JSONObject onJsonConstruct(ProceedingJoinPoint joinPoint) throws Throwable { // 代码3

JSONObject result = null;

long start = System.currentTimeMillis();

result = (JSONObject) joinPoint.proceed(); // 代码4

long end = System.currentTimeMillis();

Log.d(TAG, "onJsonConstruct: " + (end - start) + (joinPoint.getArgs()[0].toString()));

return result;

}

}

  • **代码1:**通过@Aspect注解,告知ajc编译期这个类是一个Aspect, 我们在这个类里定义在哪里切入,如何切入

  • **代码2:**这里定义了一个匿名的Pointcut,@Around是一个Advice, 表示要在pointcut的前后进行插入,对应的还有beforeafter@Around里的字符串定义了怎么寻找这个pointcut,"call(org.json.JSONObject.new(..))"表示pointcut是当JSONObject的构造函数被调用的时候

  • **代码3:**我们定义了一个方法,进行我们的逻辑处理。需要了解的是方法的参数joinPoint, joinPoint表达的是连接点对象.

  • **代码4:**通过joinPoint.proceed()实现对原有逻辑的调用,我们正是在这一处前后插入我们的执行逻辑

上面的代码就已经实现了无埋点进行JSON反序列化耗时统计。

通过注解来统计方法耗时,可以参照Hugo的源码。

可以看出,AspectJ方案写起来很简单,非常适合做一些Android里需要的AOP编程操作,比如动态权限检查。但AspectJ还是有一些局限,我们统计Activity页面生命周期耗时需要以生命周期为切点,在实际工程代码里,我们最终使用的页面Activity类一般是经过多次抽象后继承实现的,代码里已经不包含OnCreate/onResume方法了,这时候AspectJ就无能为力了。另外查看处理后的class文件,可以发现除了桩点代码外,还会增加额外的一些代码,对包大小限制不利。

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

最后是今天给大家分享的一些独家干货:

【Android开发核心知识点笔记】

【Android思维脑图(技能树)】

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【Android高级架构视频学习资源】

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

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

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

gent.click (Landroid/view/View;)V

L1

最后是今天给大家分享的一些独家干货:

【Android开发核心知识点笔记】

[外链图片转存中…(img-WErc8PmK-1714886532479)]

【Android思维脑图(技能树)】

[外链图片转存中…(img-oYovKomj-1714886532479)]

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

[外链图片转存中…(img-qhLHYyvq-1714886532479)]

【Android高级架构视频学习资源】

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

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

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

  • 15
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值