Transform+ASM插桩系列——Transform+ASM的实战
Posted 许英俊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Transform+ASM插桩系列——Transform+ASM的实战相关的知识,希望对你有一定的参考价值。
回顾
在上一章讲到创建完buildSrc之后,实现了项目的plugin之后,就可以在plugin注册我们的Transform。这期文章将正式进入重头戏,今天的学习内容有【认识Transform】、【认识AMS】、【插桩实战】
前言
插桩的技巧中,我们要知道
- Transform的作用:是用来替换(或转换)Class
- AMS的作用:是用来修改Class字节码
两者配合起来,利用Transform将旧的class文件取出来,再用AMS修改class的字节码,最后替换成我们新的class文件
认识Transform
android gradle插件自从1.5.0-beta1
版本开始就包含了一个Transform API,允许第三方插件在编译后的类文件转换为dex文件之前做处理操作。从这里可以知道Transform还在混淆之前,所以完全不用担心混淆的问题。在使用Transform API, 开发者完全可以不用去关注相关task的生成与执行流程,我们只聚焦在对输入的类文件进行处理,处理完后输出文件即可。
一、注册Transform
通过android.registerTransform(theTransform)
就可以进行注册。
class PhoenixPlugin : Plugin<Project>
override fun apply(project: Project)
val appExtension = project.extensions.getByName("android")
if (appExtension is AppExtension)
appExtension.registerTransform(CatTransform(project))
二、Transform的使用
自定义的Transform
继承于com.android.build.api.transform.Transform
class CatTransform(val project: Project) : Transform()
private var SCOPES: MutableSet<QualifiedContent.Scope> = mutableSetOf()
init
SCOPES.add(QualifiedContent.Scope.PROJECT)
SCOPES.add(QualifiedContent.Scope.SUB_PROJECTS)
SCOPES.add(QualifiedContent.Scope.EXTERNAL_LIBRARIES)
/**
* transform 名字
*/
override fun getName(): String
return "cat"
/**
* 输入文件的类型
*/
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType>
return TransformManager.CONTENT_CLASS
/**
* 指定作用范围
*/
override fun getScopes(): MutableSet<in QualifiedContent.Scope>
return SCOPES
/**
* 是否支持增量
*/
override fun isIncremental(): Boolean
return false
/**
* transform的执行
*/
override fun transform(transformInvocation: TransformInvocation?)
transformInvocation?.inputs?.forEach
// 项目中编写的代码
it.directoryInputs.forEach directoryInput->
with(directoryInput)
//字节码操作
......
// 项目中引入第三方Jar包的代码
it.jarInputs.forEach jarInput->
with(jarInput)
//字节码操作
......
Transform插桩对于有代码的地方都需要扫描到,Transform分2个情况去插桩
- 项目中编写的代码:在编译过程中,开发者编写的代码编译过后的class文件,直接取出来通过AMS修改class源码,然后放回去
- 项目中引入第三方Jar包的代码:在编译过程中,将旧的jar包解压出来后,取出class文件,通过AMS修改class源码,然后放入新的jar包中,然后放回去
三、字段的解读
1. 作用域
通过Transform#getScopes
指定的作用对象
QualifiedContent.Scope | 作用域 |
---|---|
EXTERNAL_LIBRARIES | 只包含外部库 |
PROJECT | 只作用于project本身内容 |
PROVIDED_ONLY | 支持compileOnly的远程依赖 |
SUB_PROJECTS | 子模块内容 |
TESTED_CODE | 当前变体测试的代码以及包括测试的依赖项 |
2. 作用对象
通过Transform#getInputTypes
指定的作用对象
QualifiedContent.ContentType | 作用对象 |
---|---|
CLASSES | Java代码编译后的内容, 包括文件夹以及Jar包内的编译后的类文件 |
RESOURCES | 基于资源获取到的内容 |
3. TransformInvocation
我们通过实现Transform#transform
方法来处理我们的中间转换过程, 而中间相关信息都是通过TransformInvocation
对象来传递
public interface TransformInvocation
// transform的上下文
@NonNull
Context getContext();
// 返回transform的输入源
@NonNull
Collection<TransformInput> getInputs();
// 返回引用型输入源
@NonNull Collection<TransformInput> getReferencedInputs();
//额外输入源
@NonNull Collection<SecondaryInput> getSecondaryInputs();
//输出源
@Nullable
TransformOutputProvider getOutputProvider();
//是否增量
boolean isIncremental();
关于输入源, 我们可以大致分为消费型和引用型和额外的输入源
- 消费型(getInputs()获取):需要transform操作的类型, 这类对象在处理后我们必须指定输出传给下一级
- 引用型(getReferencedInputs()获取):指我们不进行transform操作, 但可能存在查看时候使用, 所以这类我们也不需要输出给下一级
- 额外的输入源(通过getSecondaryInputs()获取):正常开发中我们很少用到, 不过像是ProGuardTransform中, 就会指定创建mapping.txt传给下一级,这类额外增加的文件就归属到额外输入源
- 输出源(通过getOutputProvider()获取):在消费完文件后,需要指定输出传给下一级的出口
认识AMS
ASM是一个通用的Java字节码操作和分析框架。它可以用于修改现有类或直接以二进制形式动态生成类。ASM的使用需要一定的学习成本,由于是基于汇编语言指令来改写class文件的,很多Api命名都是通过汇编指令命名,阅读起来只对汇编工程师友好。
一、Class文件操作
由于是对Class文件的操作,掌握AMS的文件操作显得尤为必要,其重要的角色为ClassReader
、ClassWriter
、ClassVisitor
- ClassReader:class的输入流,类似文件的输入流,负责《读Class》
- ClassWriter:class的输出流,类似文件的输出流,负责《写Class》
- ClassVisitor:class字节码的访问者,负责《转换Class》
1. ClassVisitor
在AMS中最为重要的角色当然是负责篡改字节码的ClassVisitor,在转换过程中ClassVisitor已经帮我封装好了接口,我们只需实现对应的方法就能访问到字节码对应的数据
- ClassVisitor.visit():访问类自身信息,可以修改当前类、父类、接口的信息
- ClassVisitor.visitField():访问字段本身,可以添加一个新的字段或者删除已有的字段
- ClassVisitor.visitMethod():访问方法,可以添加一个新的方或者删除已有的方法
class CatClassVisitor(private val project: Project, classVisitor: ClassVisitor) :
ClassVisitor(Opcodes.ASM6, classVisitor)
/**
* 在这里访问类,进行篡改
*/
override fun visit(
version: Int,
access: Int,
name: String,
signature: String?,
superName: String?,
interfaces: Array<out String>?
)
super.visit(version, access, name, signature, superName, interfaces)
/**
* 在这里访问方法,进行篡改
*/
override fun visitMethod(
access: Int,
name: String?,
desc: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor
return super.visitMethod(access, name, desc, signature, exceptions)
/**
* 在这里访问字段,进行篡改
*/
override fun visitField(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
value: Any?
): FieldVisitor
return super.visitField(access, name, descriptor, signature, value)
2. 方法解读
ClassVisitor.visit(int version, int access, String name, String signature, String superName, String[] interfaces)
- version: 当前Class版本的信息
- access: 当前类的访问修饰符
- name: 当前类的名字
- signature:改当前类的泛型信息
- superName: 当前的父类
- interfaces: 当前的接口信息
ClassVisitor.visitField(int access, String name, String descriptor, String signature, Object value)
- access: 当前字段的访问标识(access flag)信息
- name: 当前字段的名字
- descriptor: 当前字段的描述符
- signature: 当前字段的泛型信息
- value: 当前字段的常量值
ClassVisitor.visitMethod(int access, String name, String descriptor, String signature, String[] exceptions)
- access: 当前方法的访问修饰符
- name: 当前方法的名字
- descriptor: 当前方法的描述符,例如:()I、()V
- signature: 当前方法的泛型信息
- exceptions: 当前方法抛出的异常信息
3. class类完整的读写写法
通过ClassReader->ClassVisitor->ClassWriter(读取字节码->访问并修改字节码->输出字节码),你可以理解类似于文件流操作一样,这就是一种固定的写法
//1、通过ClassReader准备输入流
val reader = ClassReader(file.readBytes())
//2、通过ClassWriter准备输出流
val writer = ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
//3、将输出流交给ClassVisitor修改字节码
val visitor = CatClassVisitor(project, writer)
//4、将输入流的内容交给Visitor去处理
reader.accept(visitor, ClassReader.EXPAND_FRAMES)
//5、通过文件流的方式,将字节码输出,并写入原有的class文件
val code = writer.toByteArray()
val classPath = file.parentFile.absolutePath + File.separator + name
val fos = FileOutputStream(classPath)
fos.write(code)
fos.close()
插桩实战——无痕的耗时统计
我们通过一个简单的插桩无痕统计方法耗时的例子来实战一下Transform+AMS,我们先看看最终的结果把
I/System.out: ┌───────────────────────────────────------───────────────────────────────────------
I/System.out: │ [类名] com/dreamer/uidemo/MainActivity
I/System.out: │ [函数] <init>
I/System.out: │ [参数] []
I/System.out: │ [返回] null
I/System.out: │ [耗时] 0.025938
I/System.out: └───────────────────────────────────------───────────────────────────────────------
I/System.out: ┌───────────────────────────────────------───────────────────────────────────------
I/System.out: │ [类名] com/dreamer/uidemo/MainActivity
I/System.out: │ [函数] initList
I/System.out: │ [参数] []
I/System.out: │ [返回] null
I/System.out: │ [耗时] 0.067448
I/System.out: └───────────────────────────────────------───────────────────────────────────------
I/System.out: ┌───────────────────────────────────------───────────────────────────────────------
I/System.out: │ [类名] com/dreamer/uidemo/MainActivity
I/System.out: │ [函数] initView
I/System.out: │ [参数] []
I/System.out: │ [返回] null
I/System.out: │ [耗时] 6.508906
I/System.out: └───────────────────────────────────------───────────────────────────────────------
项目结构展示
1、定义如何统计耗时的方案
先定义需要统计的耗时实体
data class MethodInfo(
var className: String = "", //类名
var methodName: String = "", //函数
var returnParam: Any? = "", //返回
var time: Float = 0f, //耗时
var params: ArrayList<Any?> = ArrayList() //参数
)
override fun equals(other: Any?): Boolean
val m: MethodInfo = other as MethodInfo
return m.methodName == this.methodName
override fun toString(): String
return "MethodInfo(className='$className', methodName='$methodName', returnParam=$returnParam, time=$time, params=$params)"
定义耗时统计的方法,在函数执行前调用start方法,在函数结束时调用end方法
object MethodManager
private val methodWareHouse = Vector<MethodInfo>(1024)
@JvmStatic
fun start(): Int
methodWareHouse.add(MethodInfo())
return methodWareHouse.size - 1
@JvmStatic
fun addParams(param: Any?, index: Int)
val method = methodWareHouse[index]
method.params.add(param)
@JvmStatic
fun end(result: Any?, className: String, methodName: String, startTime: Long, index: Int)
val method = methodWareHouse[index]
method.className = className
method.methodName = methodName
method.returnParam = result
method.time = (System.nanoTime() - startTime) / 1000000f
println("┌───────────────────────────────────------───────────────────────────────────------")
println("│ [类名] $method.className")
println("│ [函数] $method.methodName")
println("│ [参数] $method.params")
println("│ [返回] $method.returnParam")
println("│ [耗时] $method.time")
println("└───────────────────────────────────------───────────────────────────────────------")
2、注册Transform
在自定义Gradle插件中,通过android.registerTransform()
将transform注册到app中
class PhoenixPlugin : Plugin<Project>
override fun apply(project: Project)
project.afterEvaluate
println()
println("===================================PhoenixPlugin===============begin==================")
println()
println()
println("===================================PhoenixPlugin===============end==================")
println()
registerTransform(project)
private fun registerTransform(project: Project)
val appExtension = project.extensions.getByName("android")
if (appExtension is AppExtension)
appExtension.registerTransform(CatTransform(project))
3、创建自定义Transform
class CatTransform(val project: Project) : Transform()
private var SCOPES: MutableSet<QualifiedContent.Scope> = mutableSetOf()
init
SCOPES.add(QualifiedContent.Scope.PROJECT)
SCOPES.add(QualifiedContent.Scope.SUB_PROJECTS)
SCOPES.add(QualifiedContent.Scope.EXTERNAL_LIBRARIES)
override fun getName(): String
return "cat"
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType>
return TransformManager.CONTENT_CLASS
override fun getScopes(): MutableSet<in QualifiedContent.Scope>
return SCOPES
override fun isIncremental(): Boolean
return false
override fun transform(transformInvocation: TransformInvocation?)
super.transform(transformInvocation)
//字节码操作
......
通过代码可以看到我们直接是全量的扫描整个项目的class文件,包括工程和jar,关键的点在transform
上,在transform中需要解决几个问题:
- 非全量的情况下要清楚已有产物,防止文件错乱
- 由于
MethodManager
和MethodInfo
是放在插件内部,属于编译时的的产物,如果没有打进到包体里面,则会报错类找不到的问题,所以要主动将这两个类打进到包体中 - 把字节码打进包里面(分文件夹和jar包)
override fun transform(transformInvocation: TransformInvocation?)
super.transform(transformInvocation)
val outputProvider = transformInvocation?.outputProvider
//非增量则全部删除输出文件,全量清空
if (!isIncremental)
outputProvider?.deleteAll()
//知识点一:把类打进进包里面
addClass(transformInvocation)
//知识点二:把字节码打进包里面(分文件夹和jar包)
transformInvocation?.inputs?.forEach input ->
//1、遍历输入的文件路径Dir
input.directoryInputs.forEach directoryInput ->
println("--directoryInput = $directoryInput")
if (directoryInput.file.isDirectory)
//2、遍历输入的所有文件File
FileUtils.getAllFiles(directoryInput.file).forEach it ->
println("----file = $it")
val file = it
val name = file.name
if (name.endsWith(".class"))
//3、通过ClassReader->ClassVisitor->ClassWriter(读取字节码->访问并修改字节码->输出字节码)
val reader = ClassReader(file.readBytes())
val writer = ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
val visitor = CatClassVisitor(project, writer)
reader.accept(visitor, ClassReader.EXPAND_FRAMES)
//4、将输出的字节码写入原有的class文件
val code = writer.toByteArray()
以上是关于Transform+ASM插桩系列——Transform+ASM的实战的主要内容,如果未能解决你的问题,请参考以下文章