前言

我们都知道,在Android编译过程中,Java代码会被编译成Class文件,Class文件再被打进Dex文件,虚拟机最终会加载Dex文件去执行。

插桩,就是干涉代码的编译过程,在编译期间生成新的代码或者修改已有的代码。

常用的EventBus、ARouter,内部就是使用了APT(Annotation Process Tool),在编译的最开始解析Java文件中的注解,并生成新的Java文件。

但如果有以下两个需求:

  • 给Activity的attach()方法加日志
  • 将第三方库中所有调用getDeviceId()的地方替换为我们自己的方法,使其符合隐私规范

这两个需求一个是需要修改Android SDK的Activity文件,一个是需要修改三方库中的某个方法。而我们集成它们的方式是通过Jar包/AAR,本质上也就是Class文件。这时候就需要我们能够在编译阶段去修改Class文件,这也就是ASM发挥作用的地方。

通过本文,你可以解决如下问题:

  1. ASM的作用是什么?
  2. 如何使用ASM?
  3. 如何将ASM运用到我们的实际项目中来?

ASM的作用是什么?

在介绍ASM插桩之前,首先来回顾一下Java Class文件。在AS中,我们可以看到打开一个Class文件是这样的:

但其实这是IDE为了方便开发者查阅,特意解析渲染了CLASS文件。如果直接拖进编辑器查看这个文件的话,我们可以看到它其实是这样的:

上图是CLASS文件的16进制代码。一般人都看不懂这些代码的含义...但既然AS可以将这些代码解析成开发者可以看懂的样子,说明CLASS文件肯定是遵循某个格式规范的。所以,一个熟悉CLASS文件格式规范的开发者,是完全有能力解析所有的CLASS文件,甚至修改CLASS文件的。

ASM的开发者就是这么做的,并且提供一套完整的API帮助我们 在不需要了解CLASS文件格式规范的情况下,可以解析并修改CLASS文件 。

如何使用ASM?

基本使用方式

下面我们就来使用一下ASM,看一下它能达到的效果。假设现在我们需要统计MainActivity所有方法的耗时,原先的MainActivity.Class文件是这样的:

用ASM修改过后的MainActivity.Class文件:

具体的实现代码:

// 读取Class文件
String clazzFilePath = "/Users/xiaozhi/AndroidStudioProjects/ASMTest/app/build/intermediates/javac/debug/classes/com/xiaozhi/asmtest/MainActivity.class";
ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath));
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
MethodTimeConsumeClassVisitor methodTimeConsumeClassVisitor = new MethodTimeConsumeClassVisitor(Opcodes.ASM5, classWriter);
classReader.accept(methodTimeConsumeClassVisitor, ClassReader.SKIP_FRAMES);// 写入Class文件
byte[] bytes = classWriter.toByteArray();
FileOutputStream fos = new FileOutputStream(clazzFilePath);
fos.write(bytes);
fos.flush();
fos.close();
复制代码

首先在第6行,通过ClassReader.accept(classVisitor, parsingOptions)读取Class文件。然后将修改完的字节码用FileOutputStream写回原文件,原先的Class代码也就被修改了。但这里我们看不到是怎么修改的,因为 修改其实就发生在读取阶段,ClassReader负责读取解析Class文件,遇到相应节点后,调用ClassVisitor中的方法去修改相应的节点代码 (4、5行)。

这里涉及到两个类,ClassWriter与MethodTimeConsumeClassVisitor,这两个类都继承于ClassVisitor。结合第9行我们可以猜测,ClassWriter肯定可以记录我们修改后的字节码。既然ClassWriter是用来记录的,而第6行ClassReader.accept(classVisitor, parsingOptions)读取Class文件又只能接收一个classVisitor,那我们怎么用另一个ClassVisitor去修改Class文件呢?

我们可以看到ClassVisitor有这么一个构造函数:

public ClassVisitor(final int api, final ClassVisitor classVisitor)
复制代码

所以我们第5行的代码,实际上是用自定义的ClassVisitor-MethodTimeConsumeClassVisitor,代理了ClassWriter,在需要修改的Class节点复写方法进行修改就可以了。

另外我们额外了解一下构造函数中的几个参数。

// 接收Flag参数,用于设置方法的操作数栈的深度。COMPUTE_MAXS可以自动帮我们计算stackSize。
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);// 接收api与ClassVisitor。 Opcodes.ASM4~Opcodes.ASM9标识了ASM的版本信息
MethodTimeConsumeClassVisitor methodTimeConsumeClassVisitor = new MethodTimeConsumeClassVisitor(Opcodes.ASM5, classWriter);// 接收ClassVisitor与parsingOptions参数。 parsingOptions用来决定解析Class的方式,SKIP_FRAMES代表跳过MethodVisitor.visitFrame方法
classReader.accept(methodTimeConsumeClassVisitor, ClassReader.SKIP_FRAMES);
复制代码

自定义ClassVisitor

下面我们具体看一下怎么通过自定义ClassVisitor修改Class文件。

public class MethodTimeConsumeClassVisitor extends ClassVisitor {private String mOwner;public MethodTimeConsumeClassVisitor(int api, ClassVisitor classVisitor) {super(api, classVisitor);}@Overridepublic void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {super.visit(version, access, name, signature, superName, interfaces);mOwner = name;}@Overridepublic MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);return new TimeConsumeMethodVisitor(mOwner, api, methodVisitor, access, name, descriptor);}
}
复制代码

我们可以看到第9行与第15行,分别是visit方法与visitMethod方法,对应的是访问Class文件头部与Class文件方法这两个节点。类似的还有很多节点:

visit visitSource? visitOuterClass? ( visitAnnotation |visitAttribute )*( visitInnerClass | visitField | visitMethod )*visitEnd
复制代码

我们要统计MainActivity所有方法的耗时,就需要重写visitMethod方法。第15行的visitMethod返回了一个MethodVisitor,顾名思义就是用来遍历修改Method,跟ClassVisitor是一个道理只不过维度不同罢了。类似的还有AnnotationVisitor与FiledVisitor,它们分别在visitAnnotation和visitField方法中返回,用来访问修改注解与字段。然后我们来看这个MethodVisitor是怎么修改方法的:

static class TimeConsumeMethodVisitor extends AdviceAdapter {private final String methodName;private final int access;private final String descriptor;private final String owner;private static final String METHOD_TIME_CONSUME_LOG_TAG = "METHOD_TIME_CONSUME_ASM_HOOK";private static final String METHOD_TIME_CONSUME_LOG = "method time consume:";protected TimeConsumeMethodVisitor(String owner, int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {super(api, methodVisitor, access, name, descriptor);this.owner = owner;this.methodName = name;this.access = access;this.descriptor = descriptor;}@Overrideprotected void onMethodEnter() {System.out.println("TimeConsumeMethodVisitor onMethodEnter. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor:" + descriptor);visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);visitVarInsn(LSTORE, 1);super.onMethodEnter();}@Overrideprotected void onMethodExit(int opcode) {System.out.println("TimeConsumeMethodVisitor onMethodExit. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor:" + descriptor);visitLdcInsn(METHOD_TIME_CONSUME_LOG_TAG);visitTypeInsn(NEW, "java/lang/StringBuilder");visitInsn(DUP);visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);visitLdcInsn(METHOD_TIME_CONSUME_LOG);visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);visitVarInsn(LLOAD, 1);visitInsn(LSUB);visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);visitLdcInsn("ms" + ", clazz:" + owner + ", method:" + methodName);visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);visitMethodInsn(INVOKESTATIC, "com/taobao/yyds/common/utils/Logger", "d", "(Ljava/lang/String;Ljava/lang/String;)V", false);super.onMethodExit(opcode);}
}
复制代码

在第19行onMethodEnter()方法中,我们实现了

long startTime = System.currentTimeMillis();
复制代码

在第29行onMethodExit()方法中,我们实现了

Logger.d("METHOD_TIME_CONSUME_ASM_HOOK", "method time consume:" + (System.currentTimeMillis() - var1) + "ms, clazz:com/xiaozhi/asmtest/MainActivity, method:");
复制代码

至于其中所用的API,我们可以通过函数名大致推断出什么意思,感兴趣的话可以去学习这些API的使用。但因为刚入门,我这里使用了一个偷懒的方式:ASM ByteCode Viewer。

ASM ByteCode Viewer

ASM ByteCode Viewer是专门用于ASM插桩的AS插件。安装该插件后,我们可以很方便地查看一个Class文件怎么用ASM的API去写出来:

如果我想很快速地知道如何通过ASM API去给方法加耗时日志,只需要先在本地Java文件中写好这一段逻辑,然后通过插件查看对应的API是怎么样的就可以了。但建议还是要多了解下这些API,因为我们写的Java代码可能不是通用的,在其它Java文件中不一定就能顺利地编译成功,因此往往会有需要进行适配的地方,排查的过程中就需要我们了解API才行了。

到这里,ASM的基本使用就已经讲好了。如果感兴趣可以参考官方文档 asm.ow2.io/asm4-guide.… 去实践。

如何将ASM运用都我们的实际项目中来?

上一节我们已经知道如何用ASM对一个Class文件进行修改,那么怎么运用到我们的项目中来呢?Android打包过程中会将Class文件打包成Dex文件,在这个阶段我们可以借助AGP(Android Gradle Plugin)与Android Transform来遍历访问到所有需要的Class文件,再通过ASM去修改。

引入工程

Android Gradle Plugin

自定义插件一共分为5个步骤:

  • 创建插件项目
  • 配置插件
  • 实现插件
  • 发布插件
  • 应用插件

创建插件项目

跟其它子模块一样,我们需要创建一个插件模块,然后在根目录的settings.gradle中引入该模块。

配置插件

首先,插件模块的文件目录需要严格遵守以下目录结构(因为我们选择用groovy实现插件,所以要用groovy文件夹):

main
├── groovy
├── resources ├── META-INF├── gradle-plugins├── *.properties
复制代码

上图中的配置文件需要特别注意,该配置文件代表着插件id->插件实现类的映射。

其它项目应用插件时所用的插件id,就是配置文件的文件前缀com.yyds.asm.plugin。而实际的实现类就是com.xiaozhi.plugin.ASMPlugin。

另外,我们需要在build.gradle中配置如下内容:

plugins {id 'groovy'
}dependencies {implementation gradleApi()implementation localGroovy()
}sourceSets {main {groovy {srcDir 'src/main/groovy'}resources {srcDir 'src/main/resources'}}
}
复制代码

实现插件

接下来就是实现我们自定义的插件了,我们可以在ASMPlugin中写我们需要执行的逻辑:

class ASMPlugin implements Plugin<Project> {@Overridevoid apply(Project project) {println("ASMPlugin apply")}
}
复制代码

发布插件

插件项目写完后,我们需要将其发布到maven仓库中去(这里可以选择先将其发布到本地maven仓库),从而让其它模块可以方便地进行依赖。

我们需要在build.gradle中添加以下代码:

uploadArchives {repositories {mavenDeployer {//设置插件的GAV参数pom.groupId = 'com.xiaozhi.plugin.asm'pom.artifactId = 'asmArt'pom.version = '1.0.1'//文件发布到下面目录repository(url: uri('../maven_repo'))}}
}
复制代码

sync后,我们就可以在gradle tasks中看到上传插件的task:

执行task,插件就会发布到本地的maven仓库了,我们可以在本地的maven_repo文件夹中找到。

应用插件

现在,我们的项目就可以很方便地依赖这个插件了。只用做两个步骤:

  1. 在工程根目录build.gralde添加maven仓库与插件依赖:
buildscript {repositories {···maven { url uri('./maven_repo') }···}dependencies {···classpath "com.xiaozhi.plugin.asm:asmArt:1.0.1"···}
}
复制代码
  1. 在想要依赖插件的项目的build.gradle中应用插件:
plugins {id 'com.xiaozhi.plugin'
}
复制代码

可以看到, 这个id就是对应的插件项目中配置文件的前缀名 。

Android Transform

现在假设app模块已经应用了我们的ASM插件,那么还需要使用Transform才能访问到app模块在编译过程中产生/依赖的所有Class文件。自定义一个Transform:

public class ASMTransform extends Transform {// transfrom名称@Overridepublic String getName() {return this.getClass().getSimpleName();}// 输入源,class文件@Overridepublic Set<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS;}// 文件范围,整个工程@Overridepublic Set<? super QualifiedContent.Scope> getScopes() {return TransformManager.SCOPE_FULL_PROJECT;}// 是否增量编译,可用于编译优化@Overridepublic boolean isIncremental() {return false;}// 核心方法@Overridepublic void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {}
}
复制代码

我们主要看第29行transform()方法,在这里我们就能访问到app模块的所有Class文件。实现如下:

@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {super.transform(transformInvocation);if (!transformInvocation.isIncremental()) {//不是增量编译删除所有的outputProvidertransformInvocation.getOutputProvider().deleteAll();}// 获取输入源Collection<TransformInput> inputs = transformInvocation.getInputs();inputs.forEach(transformInput -> {Collection<DirectoryInput> directoryInputs = transformInput.getDirectoryInputs();Collection<JarInput> jarInputs = transformInput.getJarInputs();directoryInputs.forEach(new Consumer<DirectoryInput>() {@Overridepublic void accept(DirectoryInput directoryInput) {try {// 处理输入源handleDirectoryInput(directoryInput);} catch (IOException e) {System.out.println("handleDirectoryInput error:" + e.toString());}}});for (DirectoryInput directoryInput : directoryInputs) {// 获取output目录File dest = transformInvocation.getOutputProvider().getContentLocation(directoryInput.getName(),directoryInput.getContentTypes(),directoryInput.getScopes(),Format.DIRECTORY);//这里执行字节码的注入,不操作字节码的话也要将输入路径拷贝到输出路径try {FileUtils.copyDirectory(directoryInput.getFile(), dest);} catch (IOException e) {System.out.println("output copy error:" + e.toString());}}for (JarInput jarInput : jarInputs) {// 获取output目录File dest = transformInvocation.getOutputProvider().getContentLocation(jarInput.getName(),jarInput.getContentTypes(),jarInput.getScopes(),Format.JAR);//这里执行字节码的注入,不操作字节码的话也要将输入路径拷贝到输出路径try {FileUtils.copyFile(jarInput.getFile(), dest);} catch (IOException e) {System.out.println("output copy error:" + e.toString());}}});
}
复制代码

这里的逻辑主要是三端:

  1. 获取输入源

第9行获取到的TransformInput中可以访问到所有的DirectoryInput和JarInput,分别代表着我们项目中的Class文件与依赖的JAR包/AAR包中的Class文件。DirectoryInput和JarInput都继承于QualifiedContent,调用getFile()方法就可以拿到Class文件的所有信息了。

  1. 处理输入源

获取Class文件后,其实我们就可以用ASM去修改它了。相当于把我们之前ASM修改Class文件的代码复制过来就可以了,这一部分留到下一节中讲。

  1. 将输入源文件拷贝到目标文件中

处理完之后,我们还要记得把输入源文件拷贝到输出路径中去,否则下一个transform可能就要失败了,因为它找不到输入源了。第27行transformInvocation.getOutputProvider().getContentLocation()可以确保我们获取到最终的输出路径。

现在Transform写好了,但我们还没有应用。应用很简单,只需要在插件中注册一下就好了:

class ASMPlugin implements Plugin<Project> {@Overridevoid apply(Project project) {def android = project.getExtensions().findByType(AppExtension)android.registerTransform(new ASMTransform())}
}
复制代码

然后,重新发布插件到maven仓库,sync一下,我们就可以在app模块的gradle tasks中看到我们刚写好的transform了:

至此,所有链路都已经走通了,我们知道ASM如何修改Class文件,并可以利用AGP与Transfrom应用到我们的工程中来。下面我们就用这条链路来实现一下方法节流。

方法节流

Android中最常见的方法节流就是防重复点击。假设当用户在首页2s内频繁点击了商品或者误触了商品时,我们期望只打开一次商详页面,这时候就需要对点击事件做节流。

首先我们需要定义一个注解,并声明到点击事件上,同时支持设置节流时长duration:

private final View.OnClickListener mOnClickListener = new View.OnClickListener() {@Override@MethodThrottle(duration = 3000)public void onClick(View view) {}
}
复制代码

接下来就是ASM的舞台了。回顾我们上一节中处理输入源相关的代码:

Collection<DirectoryInput> directoryInputs = transformInput.getDirectoryInputs();
directoryInputs.forEach(new Consumer<DirectoryInput>() {@Overridepublic void accept(DirectoryInput directoryInput) {try {// 处理输入源handleDirectoryInput(directoryInput);} catch (IOException e) {System.out.println("handleDirectoryInput error:" + e.toString());}}
});
复制代码

在第7行handleDirectoryInput()方法中,我们利用ASM修改Class文件:

/*** 处理文件目录下的class文件*/
private static void handleDirectoryInput(DirectoryInput directoryInput) throws IOException {List<File> files = new ArrayList<>();//列出目录所有文件(包含子文件夹,子文件夹内文件)listFiles(files, directoryInput.getFile());for (File file: files) {ClassReader classReader = new ClassReader(new FileInputStream(file.getAbsolutePath()));ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);MethodThrottleClassVisitor methodThrottleClassVisitor = new MethodThrottleClassVisitor(Opcodes.ASM5, classWriter);classReader.accept(methodThrottleClassVisitor, ClassReader.SKIP_FRAMES);byte[] code = classWriter.toByteArray();FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());fos.write(code);fos.close();}
}
复制代码

关键是MethodThrottleClassVisitor类,我们看它主要是怎么实现的:

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);throttleMethodVisitor = new ThrottleMethodVisitor(mOwner, api, methodVisitor, access, name, descriptor);return throttleMethodVisitor;
}
复制代码

visitMethod()方法返回一个自定义的MethodVisitor。ThrottleMethodVisitor在访问每个方法时,若发现方法声明了@MethodThrottle注解,就会插入节流代码:

@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {System.out.println("ThrottleMethodVisitor visitAnnotation. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor" + descriptor + ", annotationDesc:" + desc);throttle = "Lcom/taobao/yyds/common/annotation/MethodThrottle;".equals(desc);···
}@Override
protected void onMethodEnter() {System.out.println("ThrottleMethodVisitor onMethodEnter. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor" + descriptor + ", throttle:" + throttle);if (throttle) {// 插入方法节流代码}
}
复制代码

具体的代码就不放上去了,通过ASM ByteCode Viewer可以很方便地生成。这样我们在平时代码中要做方法节流时,只需要给方法声明一个注解就可以了。

方法耗时日志

类似的,我们可以用这个链路实现很多AOP逻辑。给方法加耗时日志这一点,在上面的ASM基本使用那一节中其实已经讲过了。运用到工程中来,其实就跟方法节流一样,自定义一个注解,然后在Transform中加一点处理输入流的逻辑就好了:

/*** 处理文件目录下的class文件*/
private static void handleDirectoryInput(DirectoryInput directoryInput) throws IOException {List<File> files = new ArrayList<>();//列出目录所有文件(包含子文件夹,子文件夹内文件)listFiles(files, directoryInput.getFile());for (File file: files) {// 方法节流methodThrottleASM(file);// 方法耗时methodTimeConsumeASM(file);}
}
复制代码

如何调试

另外有一点我个人觉得还是挺重要的,那就是Gradle插件该如何调试。即使有ASM ByteCode Viewer插件的帮助,我们在插件中写的ASM代码也不可能一键完成,很大概率会碰到各种各样的编译错误问题。本地打日志又比较麻烦,所以调试手段是必不可少的。篇幅受限,这里就不额外写了,可以参考 juejin.cn/post/684490…

发布线上的额外工作

虽然我们已经集成了ASM插件模块,但并不意味着这样就能上线了。至少还需要完成以下的工作才行。

插件项目的maven仓库

因为在调试阶段,插件的maven仓库是用的本地的maven仓库。但如果要集成进CI,肯定是需要线上的maven仓库的,所以到时候需要申请上传到某个maven仓中才行。

编译影响评估

第一点是必须保证CI打包时没问题。第二点就是看这样做是否会影响到编译时长,毕竟Transform是在编译阶段加了一个Task。如果对编译时长有比较大的影响,还需要额外做一些编译优化的工作。可以用 ./gradlew --profile --rerun-tasks assembleDebug 命令查看各环节的编译耗时。

Tips

在做这个Demo的过程中,因为自己也是第一次接触,遇到了不少坑,拿几点贴一下:

  1. 每次plugin改动都要重新发布一下,否则plugin中transform、asm的逻辑都不会更新,因为拿的是maven仓中的。
  2. transform每次debug前都要clean一下,否则debug不进去。
  3. 即使transform没修改任何东西,也需要将源文件jar directory拷贝到目标文件。否则最后编译出的build/transforms文件夹中会少很多jar包,造成启动时找不到各种Class而崩溃。
  4. classpath引入后,造成启动后某so崩溃。一直以为是插件写的有什么问题,排查很久后也没找到原因。最后拿崩溃栈去找so库接口人,升级so版本后问题就解决了。

总结

通过本篇文章,我们了解到ASM的作用,学会ASM基本API的使用,进而利用AGP与Transform将ASM运用到实际项目中来。实战中,ASM可以实现常见的AOP逻辑,如方法节流与方法耗时日志。不仅如此,当我们以后为需要修改三方库代码而发愁时,或许可以想想,ASM能帮助我们搞定吗?

文章不足之处,还望大家多多海涵,多多指点,先行谢过!

Android ASM插桩探索及实战相关推荐

  1. Android ASM 插桩实践

    上一章知道了如何获取 class 文件,那该如何进行插桩呢?本章告诉你! 什么是 ASM? ASM 是一个字节码操作库,它可以直接修改已经存在的 class 文件或者生成 class 文件. ASM ...

  2. android 插桩工具,Android Asm 插桩 教学项目

    AndroidAutoTrack 本项目主要就是给大家一个参考学习的demo而已,主要是打算简化学习gradle插件的成本,以及对于android transform的一次抽象,将增量更新等等进行一次 ...

  3. 【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )

    文章目录 一." 字节码插桩 " 技术简介 二.AspectJ 插桩工具 三.ASM 插桩工具 一." 字节码插桩 " 技术简介 性能优化 , 插件化 , 热修 ...

  4. Android编译插桩

    背景:这一次分享一下关于android编译插桩这个话题,在正常编写代码实现程序的逻辑外,还要使用一点点黑科技,拿起操作代码无所不能的武器. 一.Android常用的能动态改变代码逻辑的方法有两种 1. ...

  5. Android源码设计模式探索与实战【建造者模式】

    IT行业,一直讲一句话,拼到最后都拼的是"内功",而内功往往就是指我们处理问题的思路.经验.想法,而对于开发者来说,甚至对于产品也一样,都离不开一个"宝典",就 ...

  6. Android源码设计模式探索与实战【外观模式】

    IT行业,一直讲一句话,拼到最后都拼的是"内功",而内功往往就是指我们处理问题的思路.经验.想法,而对于开发者来说,甚至对于产品也一样,都离不开一个"宝典",就 ...

  7. 动态二进制插桩原理与实战

    二进制插桩~ 说点什么 插桩是啥 为啥要插桩 源代码插桩 二进制插桩 如何插桩 两种主要方式和三种执行模式 方式1: 方式2: 第一种模式: 第二种模式: 第三种模式: 插桩实例 Pin 动态二进制插 ...

  8. Android源码设计模式探索与实战【原型模式】

    IT行业,一直讲一句话,拼到最后都拼的是"内功",而内功往往就是指我们处理问题的思路.经验.想法,而对于开发者来说,甚至对于产品也一样,都离不开一个"宝典",就 ...

  9. 深入探索编译插桩技术(四、ASM 探秘,android中文api文档

    从字节码的视角中,一个 Java 类由很多组件凝聚而成,而这之中便包括超类.接口.属性.域和方法等等.当我们在使用 ASM 进行操控时,可以将它们视为一个个与之对应的事件.因此 ASM 提供了一个 类 ...

  10. 吹爆系列:Android 插桩之美,全面掌握~

    作者:阿明的小蝴蝶 插桩 插桩是什么?你在开发中有用过插桩的技术吗? 所谓的插桩就是在代码编译期间修改已有的代码或者生成新代码. 插桩具体在编译的哪个流程介入呢? 插桩的作用与场景 代码生成 代码监控 ...

最新文章

  1. LMAX Disruptor – High Performance, Low Latency and Simple Too 转载
  2. C语言 编写程序:请将Fibonacci数列前30项中的偶数值找出来,存储到一维数组中。其中,Fibonacci数列如下:1,1,2,3,5,8,13,21,34...该数列除前两项之外,其他任意
  3. 【JavaSE_06】Java中的数组(array)-思维导图
  4. 想要 24 小时自学编程,那是不可能的,先自学 10000 小时再说!
  5. 配置LINUX的DNS主辅服务器
  6. 为什么选择 npm script?
  7. css单位介绍em ex ch rem vw vh vm cm mm in pt pc px
  8. C# 中XML序列化与反序列化学习笔记
  9. Discuz!NT 模板机制分析
  10. 250个jquery 插件
  11. 洛谷 3203 HNOI2010 BOUNCE 弹飞绵羊
  12. feedsky 话题 营销
  13. 修理牧场( 哈夫曼算法 ,贪心 )
  14. LaTeX之双栏模板表格布局(单双栏满宽+不满宽)
  15. 关于laravel下composer安装excel插件
  16. git branch分支创建、切换、合并,git tag标签
  17. 十、C#接口、抽象、密封、开放封闭原则
  18. JUC并发编程第十四篇,StampedLock(邮戳锁)为什么比ReentrantReadWriteLock(读写锁)更快!
  19. itextpdf 怎么下划线_java – 带有粗体和下划线的Itext新字体
  20. 使用scp上传文件到服务器或从服务器下载文件(支持跨越跳板机)

热门文章

  1. hightopo学习笔记---入门
  2. 【愚公系列】2022年02月 攻防世界-进阶题-MISC-86(picture2)
  3. MATLAB之牛顿插值法
  4. 计算机网络第二章学习通题目及答案
  5. Vue select默认选中第一个
  6. win10 ie浏览器安装Flash Player Debugger解决方案
  7. 从拉新、促活/留存和营收说起,做运营到底是在做什么?
  8. docx4j文档差异比较
  9. MovieLens 1M 数据集
  10. 产品经理面试必备常见问题及解析