*本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布 java
第一次看到插桩,是在Android开发高手课中。看完去查了一下:“咦!还有这东西,有点意思”。android
本着不断学习和探索的精神,便走上学习函数插桩的“不归路”。git
插桩:目标程序代码中某些位置插入或修改成一些代码,从而在目标程序运行过程当中获取某些程序状态并加以分析。简单来讲就是在代码中插入代码。 那么函数插桩,即是在函数中插入或修改代码。程序员
本文将介绍在Android编译过程当中,往字节码里插入自定义的字节码,因此也能够称为字节码插桩。github
函数插桩能够帮助咱们实现不少手术刀式的代码设计,如无埋点统计上报、轻量级AOP等。 应用到在Android中,能够用来作用行为统计、方法耗时统计等功能。api
在动手以前,须要掌握如下相关知识:缓存
Android打包流程 相关资料:Apk 打包流程梳理、Android APK打包流程bash
Java字节码 相关资料:一文让你明白Java字节码、Java字节码(维基百科)、如何阅读JAVA 字节码(一)、《深刻理解Java虚拟机》第6章(有条件的话,推荐看书)微信
自定义Gradle插件、Transform API 相关资料:在AndroidStudio中自定义Gradle插件、深刻理解Android之Gradle、打包Apk过程当中的Transform API 、Transform官方文档app
必定要先熟悉上面的知识 必定要先熟悉上面的知识 必定要先熟悉上面的知识
如下内容涉及知识过多,需熟练掌握以上知识。不然,可能会引发头大、目眩、烦躁等一系列不良反应。请在大人的陪同下阅读
你可能会遇到一个这样需求:在Android应用中,记录每一个页面的打开\关闭。
记录页面被打开\关闭,通常来讲就是记录**Activity
的建立和销毁**(这里以Activity
区分页面)。因此,咱们只要在Activity
的onCreate()
和onDestroy()
中插入对应的代码便可。
这时候就会遇到一个问题:如何为Activity插入代码? **一个个写?不可能!毕竟咱们是高(懒)效(惰)**的程序员; **写在BaseActivity中?**好像能够,不过项目中若是有第三方的页面就显得有些无力了,并且不通用;
咱们但愿实现一个能够自动在Activity
的onCreate()
和onDestroy()
中插入代码的工具,能够在任意工程中使用。
因而,自定义Gradle插件 + ASM便成了一个不错的选择
对Android打包过程和自定义Gradle插件了解后发现,java文件会先转化为class
文件,而后在转化为dex
文件。而经过Gradle
插件提供的Transform API
,能够在编译成dex
文件以前获得class
文件。 获得class
文件以后,即可以经过ASM对字节码进行修改,便可完成字节码插桩。
步骤以下:
了解Android打包过程,在过程当中找插入点(class
转换成 .dex
过程);
了解自定义Gradle插件、Transform API,在Transform#transform()
中获得class
文件;
找到FragmentActivity
的class
文件,经过ASM库,在onCreate()
中插入代码;(为何是FragmentActivity
而不是Activity
后面会说到)
将原文件替换为修改后的class
文件。
以下图:
class文件:java源文件通过
javac
后生成一种紧凑的8位字节的二进制流文件。 插入点:“dex”节点,表示将class
文件打包到dex
文件的过程,其输入包括class
文件以及第三方依赖的class
文件。
关于Transform API:从
1.5.0-beta1
开始,Gradle插件包含一个Transform API,容许第三方插件在将编译后的类文件转换为dex
文件以前对其进行操做。
关于混淆:关于混淆能够不用小心。混淆实际上是个
ProguardTransform
,在自定义的Transform以后执行。
主要实现如下功能:
(如下为部分关键代码,完整源码点击这里)
如何自定义插件这里就不详细介绍了,具体参考在AndroidStudio中自定义Gradle插件、打包Apk过程当中的Transform API。
目录结构分为两部分:插件部分(src/main/groovy
中)、ASM部分(src/main/java
中)
继承Transform
,实现Plugin
接口,经过Transform#transform()
获得Collection<TransformInput> inputs
,里面有咱们想要的class
文件。
class LifecyclePlugin extends Transform implements Plugin<Project> {
@Override
void apply(Project project) {
//registerTransform
def android = project.extensions.getByType(AppExtension)
android.registerTransform(this)
}
@Override
String getName() {
return "LifecyclePlugin"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(@NonNull TransformInvocation transformInvocation) {
...
...
...
}
}
复制代码
主要看方法transform()
@Override
void transform(@NonNull TransformInvocation transformInvocation) {
println '--------------- LifecyclePlugin visit start --------------- '
def startTime = System.currentTimeMillis()
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
//删除以前的输出
if (outputProvider != null)
outputProvider.deleteAll()
//遍历inputs
inputs.each { TransformInput input ->
//遍历directoryInputs
input.directoryInputs.each { DirectoryInput directoryInput ->
//处理directoryInputs
handleDirectoryInput(directoryInput, outputProvider)
}
//遍历jarInputs
input.jarInputs.each { JarInput jarInput ->
//处理jarInputs
handleJarInputs(jarInput, outputProvider)
}
}
def cost = (System.currentTimeMillis() - startTime) / 1000
println '--------------- LifecyclePlugin visit end --------------- '
println "LifecyclePlugin cost : $cost s"
}
复制代码
经过参数inputs
能够拿到全部的class
文件。inputs
中包括directoryInputs
和jarInputs
,directoryInputs
为文件夹中的class
文件,而jarInputs
为jar包中的class
文件。
对应两个处理方法handleDirectoryInput
、handleJarInputs
LifecyclePlugin#handleDirectoryInput()
/** * 处理文件目录下的class文件 */
static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
//是不是目录
if (directoryInput.file.isDirectory()) {
//列出目录全部文件(包含子文件夹,子文件夹内文件)
directoryInput.file.eachFileRecurse { File file ->
def name = file.name
if (name.endsWith(".class") && !name.startsWith("R\$")
&& !"R.class".equals(name) && !"BuildConfig.class".equals(name)
&& "android/support/v4/app/FragmentActivity.class".equals(name)) {
println '----------- deal with "class" file <' + name + '> -----------'
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new LifecycleClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
}
}
}
//处理完输入文件以后,要把输出给下一个任务
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
复制代码
LifecyclePlugin#handleJarInputs()
/** * 处理Jar中的class文件 */
static void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
//重名名输出文件,由于可能同名,会覆盖
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
JarFile jarFile = new JarFile(jarInput.file)
Enumeration enumeration = jarFile.entries()
File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
//避免上次的缓存被重复插入
if (tmpFile.exists()) {
tmpFile.delete()
}
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
//用于保存
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = jarFile.getInputStream(jarEntry)
//插桩class
if (entryName.endsWith(".class") && !entryName.startsWith("R\$")
&& !"R.class".equals(entryName) && !"BuildConfig.class".equals(entryName)
&& "android/support/v4/app/FragmentActivity.class".equals(entryName)) {
//class文件处理
println '----------- deal with "jar" class file <' + entryName + '> -----------'
jarOutputStream.putNextEntry(zipEntry)
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new LifecycleClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
jarOutputStream.write(code)
} else {
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
//结束
jarOutputStream.close()
jarFile.close()
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}
复制代码
这两个方法都在作同一件事,就是遍历directoryInputs
、jarInputs
,获得对应的class
文件,而后交给ASM处理,最后覆盖原文件。
发现:在
input.jarInputs
中并无android.jar
。本想在Activity
中作处理,由于找不到android.jar
,只好退而求其次选择android.support.v4.app
中的FragmentActivity
。 那么,因此如何的到android.jar ?请指教
在handleDirectoryInput
和handleJarInputs
中,能够看到ASM的部分代码了。这里以handleDirectoryInput
为例。
handleDirectoryInput
中ASM代码:
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new LifecycleClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)
复制代码
其中,关键处理类LifecycleClassVisitor
LifecycleClassVisitor
用于访问class
的工具,在visitMethod()
里对类名和方法名进行判断是否须要处理。若须要,则交给MethodVisitor
。
public class LifecycleClassVisitor extends ClassVisitor implements Opcodes {
private String mClassName;
public LifecycleClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
System.out.println("LifecycleClassVisitor : visit -----> started :" + name);
this.mClassName = name;
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("LifecycleClassVisitor : visitMethod : " + name);
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
//匹配FragmentActivity
if ("android/support/v4/app/FragmentActivity".equals(this.mClassName)) {
if ("onCreate".equals(name) ) {
//处理onCreate
return new LifecycleOnCreateMethodVisitor(mv);
} else if ("onDestroy".equals(name)) {
//处理onDestroy
return new LifecycleOnDestroyMethodVisitor(mv);
}
}
return mv;
}
@Override
public void visitEnd() {
System.out.println("LifecycleClassVisitor : visit -----> end");
super.visitEnd();
}
}
复制代码
在visitMethod()
中判断是否为FragmentActivity
,且为方法onCreate
或onDestroy
,而后交给LifecycleOnDestroyMethodVisitor
或LifecycleOnCreateMethodVisitor
处理。
回到需求,咱们但愿在onCreate()
中插入对应的代码,来记录页面被打开。(这里经过Log代替)
Log.i("TAG", "-------> onCreate : " + this.getClass().getSimpleName());
复制代码
因而,在LifecycleOnCreateMethodVisitor
中以下处理 (LifecycleOnDestroyMethodVisitor
与LifecycleOnCreateMethodVisitor
类似,完整代码点击这里 )
LifecycleOnCreateMethodVisitor
public class LifecycleOnCreateMethodVisitor extends MethodVisitor {
public LifecycleOnCreateMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM4, mv);
}
@Override
public void visitCode() {
//方法执行前插入
mv.visitLdcInsn("TAG");
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("-------> onCreate : ");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
super.visitCode();
//方法执行后插入
}
@Override
public void visitInsn(int opcode) {
super.visitInsn(opcode);
}
}
复制代码
只须要在visitCode()
中插入上面的代码,便可实现onCreate()
内容执行以前,先执行咱们插入的代码。
若是想在onCreate()
内容执行以后插入代码,该怎么作? 和上面类似,只要在visitInsn()
方法中插入对应的代码便可。代码以下:
@Override
public void visitInsn(int opcode) {
//判断RETURN
if (opcode == Opcodes.RETURN) {
//在这里插入代码
...
}
super.visitInsn(opcode);
}
复制代码
若是对字节码不是很了解,看到上面visitCode()
中的代码可能会以为既熟悉又陌生,那是ASM插入字节码的用法。 若是你写不来,不要紧,这里介绍一个插件——ASM Bytecode Outline,包教包会。
经过ASM Bytecode Outline插件生成代码 一、在Android Studio中安装ASM Bytecode Outline插件; 二、安装后,在编译器中,点击右键,选择Show Bytecode outLine;
三、在 ASM标签中选择 ASMified,便可在右侧看到当前类对应的 ASM代码。(能够忽略 Label相关的代码,如下选框的内容为对应的代码) ![]()
![]()
提示:
ClassVisitor#visitMethod()
只能访问当前类定义的method
(一开始想访问父类的方法,陷入误区)。 如,在MainActivity
中只重写了onCreate()
,没有重写onDestroy()
。那么在visitMethod()
中只会出现onCreate()
,不会有onDestroy()
。
class
文件的插桩已经说完,剩下最后一步——替换。眼尖的同窗应该发现,代码上面已经出现过了。仍是以LifecyclePlugin#handleDirectoryInput()
中的代码为例:
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
复制代码
从classWriter
获得class
修改后的byte
流,而后经过流的写入覆盖原来的class
文件。 (Jar包的覆盖会稍微复杂一点,这里就不细说了)
File.separator
:文件的分隔符。不一样系统分隔符可能不同。 如:一样一个文件,Windows下是C:\tmp\test.txt
;Linux 下倒是/tmp/test.txt
插件写完,即可以投入使用了。
建立一个Android项目app
,在app.gradle
中引用插件。(完整代码点击这里)
apply plugin: 'com.gavin.gradle'
复制代码
运行后,按步骤操做: 打开MainActivity
——>打开SecondActivity
——>返回MainActivity
。
查看效果:
com.gavin.asmdemo I/TAG: -------> onCreate : MainActivity
com.gavin.asmdemo I/TAG: -------> onCreate : SecondActivity
com.gavin.asmdemo I/TAG: -------> onDestroy : SecondActivity
复制代码
能够发现,页面打开\关闭都会打印对应的log。说明咱们插入的代码被执行了,并且,使用时对项目没有任何“入侵”。
本文内容涉及知识较多,在熟悉Android打包过程、字节码、Gradle Transform API、ASM等以前,阅读起来会很困难。不过,在了解并学习这些知识的以后,相信你对Android会有新的认识。
经过Gradle的Transform配合ASM实战路由框架和统计方法耗时
以上有错误之处,感谢指出