前言
第一次看到插桩,是在Android开发高手课中。看完去查了一下:“咦!还有这东西,有点意思”。
本着不断学习和探索的精神,便走上学习函数插桩的“不归路”。
函数插桩
是什么函数插桩
插桩:目标程序代码中某些位置插入或修改成一些代码,从而在目标程序运行过程中获取某些程序状态并加以分析。简单来说就是在代码中插入代码。
那么函数插桩,便是在函数中插入或修改代码。
本文将介绍在Android编译过程中,往字节码里插入自定义的字节码,所以也可以称为字节码插桩。
作用
函数插桩可以帮助我们实现很多手术刀式的代码设计,如无埋点统计上报、轻量级AOP等。
应用到在Android中,可以用来做用行为统计、方法耗时统计等功能。
技术点
在动手之前,需要掌握以下相关知识:
Android打包流程
相关资料:Apk 打包流程梳理、Android APK打包流程Java字节码
相关资料:一文让你明白Java字节码、Java字节码(维基百科)、如何阅读JAVA 字节码(一)、《深入理解Java虚拟机》第6章(有条件的话,推荐看书)自定义Gradle插件、Transform API
相关资料:在AndroidStudio中自定义Gradle插件、深入理解Android之Gradle、打包Apk过程中的Transform API 、Transform官方文档
一定要先熟悉上面的知识
一定要先熟悉上面的知识
一定要先熟悉上面的知识
以下内容涉及知识过多,需熟练掌握以上知识。否则,可能会引起头大、目眩、烦躁等一系列不良反应。请在大人的陪同下阅读
实战
需求
你可能会遇到一个这样需求:在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之后执行。
动手实现
主要实现以下功能:
自定义Gradle插件
处理class文件
替换
(以下为部分关键代码,完整源码点击这里)
自定义Gradle插件
如何自定义插件这里就不详细介绍了,具体参考在AndroidStudio中自定义Gradle插件、打包Apk过程中的Transform API。
目录结构
目录结构分为两部分:插件部分(src/main/groovy
中)、ASM部分(src/main/java
中)
目录结构
LifecyclePlugin.groovy
继承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()
@Overridevoid 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 ?请指教
处理class文件
在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()
方法中插入对应的代码即可。代码如下:
@Overridepublic void visitInsn(int opcode) { //判断RETURN if (opcode == Opcodes.RETURN) { //在这里插入代码 ... } super.visitInsn(opcode); }
如果对字节码不是很了解,看到上面visitCode()
中的代码可能会觉得既熟悉又陌生,那是ASM插入字节码的用法。
如果你写不来,没关系,这里介绍一个插件——ASM Bytecode Outline,包教包会。
通过ASM Bytecode Outline插件生成代码
1、在Android Studio中安装ASM Bytecode Outline插件;
2、安装后,在编译器中,点击右键,选择Show Bytecode outLine;
3、在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会有新的认识。
作者:带心情去旅行
链接:https://www.jianshu.com/p/16ed4d233fd1