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)
                 //字节码操作
                 ......
              
          
        
    

1. 名字

通过Transform#getName指定的当前Transform名字,当工程注册过后的Transform会在gradle的other菜单中可以找到

同样,在运行完app后,在Transform出来的目录也会在build目录显示cat的名字

2. 作用域

通过Transform#getScopes指定的作用对象

QualifiedContent.Scope作用域
EXTERNAL_LIBRARIES只包含外部库
PROJECT只作用于project本身内容
PROVIDED_ONLY支持compileOnly的远程依赖
SUB_PROJECTS子模块内容
TESTED_CODE当前变体测试的代码以及包括测试的依赖项
3. 作用对象

通过Transform#getInputTypes指定的作用对象

QualifiedContent.ContentType作用对象
CLASSESJava代码编译后的内容, 包括文件夹以及Jar包内的编译后的类文件
RESOURCES基于资源获取到的内容
4. 转换transform

Transform插桩主要是在override fun transform(transformInvocation: TransformInvocation?) 执行完成,对于有代码的地方都需要扫描到,Transform分2个情况去插桩

  • 项目中编写的代码:在编译过程中,开发者编写的代码编译过后的class文件,直接取出来通过AMS修改class源码,然后放回去
  • 项目中引入第三方Jar包的代码:在编译过程中,将旧的jar包解压出来后,取出class文件,通过AMS修改class源码,然后放入新的jar包中,然后放回去
5. 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的文件操作显得尤为必要,其重要的角色为ClassReaderClassWriterClassVisitor

  • 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 visitField(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        value: Any?
    ): FieldVisitor 
        return super.visitField(access, name, descriptor, signature, value)
    
    
    /**
     * 在这里访问方法,进行篡改
     */
    override fun visitMethod(
        access: Int,
        name: String?,
        desc: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor 
        return super.visitMethod(access, name, desc, signature, exceptions)
    

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、定义如何统计耗时的方案

首先我们要思考如何进行埋点,对方案的要怎么落笔去写代码,假如我们要对activity的创建进行埋点计时方法耗时

public void onCreate(Bundle savedInstanceState) 
    super.onCreate(savedInstanceState);
    setContentView((int) R.layout.activity_main);
    initList();
    initView();
    initBanner();
    testLog();

最简单的办法就是在开始时标记为开始,并记下当前系统时间,当方法执行完成后,标记为结束,计时相差算出整个方法的运行时间

public void onCreate(Bundle savedInstanceState) 
    int start = MethodManager.start();
    long nanoTime = System.nanoTime();
    super.onCreate(savedInstanceState);
    setContentView((int) R.layout.activity_main);
    initList();
    initView();
    initBanner();
    testLog();
    MethodManager.end((Object) null, "com/dreamer/uidemo/MainActivity", "onCreate", nanoTime, start);

2、方案的实施

针对上面的方法,我们就开始编写我们的代码,首先定义需要统计的耗时实体

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("└───────────────────────────────────------───────────────────────────────────------")
    

在自定义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))
        
    

创建自定义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中我们要思考要做哪些事情:

  1. 要支持非增量和支持增量两种方式
  2. 由于MethodManagerMethodInfo是放在插件内部,属于编译时的的产物,如果没有打进到包体里面,则会报错类找不到的问题,所以要主动将这两个类打进到包体中
  3. 对所有方法,或者是指定方法,进行无痕埋点,即转换

    以上是关于Transform+ASM插桩系列——Transform+ASM的实战的主要内容,如果未能解决你的问题,请参考以下文章

    Transform+ASM插桩系列——熟悉Java字节码

    Transform+ASM插桩系列——熟悉Java字节码

    Android Gradle 中的字节码插桩之ASM

    Android ASM 插桩实践

    Android ASM 插桩实践

    ASM的基础使用 Android 自动化埋点方案原理剖析