Transform+ASM插桩系列(3)——Transform+ASM的实战

news2024/11/30 8:32:16

回顾

在上一章讲到创建完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个情况去插桩

  1. 项目中编写的代码:在编译过程中,开发者编写的代码编译过后的class文件,直接取出来通过AMS修改class源码,然后放回去
  2. 项目中引入第三方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作用对象
CLASSESJava代码编译后的内容, 包括文件夹以及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的文件操作显得尤为必要,其重要的角色为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 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中需要解决几个问题:

  1. 非全量的情况下要清楚已有产物,防止文件错乱
  2. 由于MethodManagerMethodInfo是放在插件内部,属于编译时的的产物,如果没有打进到包体里面,则会报错类找不到的问题,所以要主动将这两个类打进到包体中
  3. 把字节码打进包里面(分文件夹和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()
                        val classPath = file.parentFile.absolutePath + File.separator + name
                        val fos = FileOutputStream(classPath)
                        fos.write(code)
                        fos.close()
                    }
                }

                //5、将class文件返回transform打包路径
                val dest = transformInvocation.outputProvider?.getContentLocation(
                    directoryInput.name,
                    directoryInput.contentTypes,
                    directoryInput.scopes,
                    Format.DIRECTORY
                )
                println("----src = ${directoryInput.file}")
                println("----dest = $dest")
                FileUtils.copyDirectoryToDirectory(directoryInput.file, dest)
            }
        }

        //1、遍历输入的jar路径
        input.jarInputs.forEach { jarInput ->
            println("--jarInput = $jarInput")

            val src = jarInput.file
            val dest = transformInvocation.outputProvider?.getContentLocation(
                jarInput.name,
                jarInput.contentTypes,
                jarInput.scopes,
                Format.JAR
            )

            //2、创建新的空jar包,用来装改完后的jar包
            val temp = src.absolutePath.substring(0, src.absolutePath.length - 4) + "_cat.jar"
            val tempFile = File(temp)
            if (tempFile.exists()) {
                tempFile.delete()
            }
            val outputStream = JarOutputStream(FileOutputStream(tempFile))

            //3、遍历输入的jar文件
            val jarFile = JarFile(src)
            val entries = jarFile.entries()
            while (entries.hasMoreElements()) {
                //4、对jar文件加入到临时新的jar包中
                val jarEntry = entries.nextElement()
                val className = jarEntry.name
                val inputStream = jarFile.getInputStream(jarEntry)
                val classEntry = ZipEntry(className)
                outputStream.putNextEntry(classEntry)

                if (className.contains("glide")
                    && className.endsWith(".class") && !className.contains("R$")
                    && !className.contains("R.class") && !className.contains("BuildConfig.class")
                ) {
                    //5、通过ClassReader->ClassVisitor->ClassWriter(读取字节码->访问并修改字节码->输出字节码)
                    val reader = ClassReader(inputStream)
                    val writer = ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
                    val visitor = CatClassVisitor(project, writer)
                    reader.accept(visitor, ClassReader.EXPAND_FRAMES)
                    val code = writer.toByteArray()
                    outputStream.write(code)
                } else {
                    //6、不符合条件的直接源文件读取后放入到临时新的jar包中
                    var len = inputStream.read()
                    while (len != -1) {
                        outputStream.write(len)
                        len = inputStream.read()
                    }
                }
                inputStream.close()
            }
            //7、刷新后,改造后的字节码jar包会返回到transform打包路径
            outputStream.flush()
            outputStream.close()
            FileUtils.copyFile(tempFile, dest)
            //8、删除临时新的jar包
            tempFile.delete()

            println("----tempFile = $tempFile")
            println("----dest = $dest")
        }
    }
}

/**
 * 把MethodManager和MethodInfo的class文件直接打进进包里面
 * /
private fun addClass(transformInvocation: TransformInvocation?) {
    var dir = File(
        System.getProperties()
            .getProperty("user.dir")
                + File.separator + "buildSrc"
                + File.separator + "build"
                + File.separator + "classes" + File.separator + "kotlin" + File.separator + "main"
                + File.separator + MethodManager.javaClass.`package`.name.replace(
            ".",
            File.separator
        )
    )
    val dest = transformInvocation?.outputProvider?.getContentLocation(
        dir.name,
        setOf(QualifiedContent.DefaultContentType.CLASSES),
        mutableSetOf(QualifiedContent.Scope.PROJECT),
        Format.DIRECTORY
    )
    println("----src = $dir")
    println("----dest = $dest")
    FileUtils.copyDirectoryToDirectory(dir, dest)
}

可以发现的是,transform是直接通过一些列的文件操作,将class替换掉,最终将文件复制到transformInvocation?.outputProvider?.getContentLocation提供的输出位置,交给下一个transform去处理。它就犹如是一间工厂,加工完成后,将加工品传送到下一个工厂继续加工。

4、CatClassVisitor

CatClassVisitor里面就是开始对字节码操作的内容,我们通过visit获取类名信息,通过visitMethod获取方法,接着交给CatMethodVisitor开始篡改class字节码,也就是说我们的汇编程序

class CatClassVisitor(private val project: Project, classVisitor: ClassVisitor) :
    ClassVisitor(Opcodes.ASM6, classVisitor) {

    private lateinit var mClassName: String

    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)
        mClassName = name
        println("------visit = 【className】$mClassName")
    }

    override fun visitMethod(
        access: Int,
        name: String?,
        desc: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        println("------visitMethod = 【name】$name 【desc】$desc")
        var methodVisitor = super.visitMethod(access, name, desc, signature, exceptions)
        methodVisitor =
            CatMethodVisitor(Opcodes.ASM6, methodVisitor, access, mClassName, name, desc)
        return methodVisitor
    }
}

5、CatMethodVisitor

class字节码的篡改依靠MethodVisitor对程序写入指令,按照汇编的执行顺序,依次写入对应的汇编指令

  • onMethodEnter:在方法开头调用start方法和当前时间戳的方法
  • onMethodExit:在方法结束调用end方法
class CatMethodVisitor(
    api: Int, mv: MethodVisitor?, access: Int,
    private val className: String?,
    private val methodName: String?,
    private val desc: String?
) : AdviceAdapter(api, mv, access, methodName, desc) {

    private var startTimeId = 0
    private var methodId = 0
    private val isStaticMethod: Boolean = access and Opcodes.ACC_STATIC != 0
    private val argumentArrays: Array<Type> = Type.getArgumentTypes(desc)

    override fun onMethodEnter() {
        println("--------onMethodEnter")

        //1、插入Start()
        methodId = newLocal(Type.INT_TYPE)
        mv.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            Constants.method_manager,
            "start",
            "()I",
            false
        )
        mv.visitIntInsn(Opcodes.ISTORE, methodId)

        //3、插入时间戳
        startTimeId = newLocal(Type.LONG_TYPE)
        mv.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            "java/lang/System",
            "nanoTime",
            "()J",
            false
        )
        mv.visitIntInsn(Opcodes.LSTORE, startTimeId)
    }

    override fun onMethodExit(opcode: Int) {
        println("--------onMethodExit")

        //4、插入end()
        if (opcode == Opcodes.RETURN) {
            visitInsn(Opcodes.ACONST_NULL)
        } else if (opcode == Opcodes.ARETURN || opcode == Opcodes.ATHROW) {
            dup()
        } else {
            if (opcode == Opcodes.LRETURN || opcode == Opcodes.DRETURN) {
                dup2()
            } else {
                dup()
            }
            box(Type.getReturnType(methodDesc))
        }
        mv.visitLdcInsn(className)
        mv.visitLdcInsn(methodName)
        mv.visitVarInsn(Opcodes.LLOAD, startTimeId)
        mv.visitVarInsn(Opcodes.ILOAD, methodId)
        mv.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            Constants.method_manager,
            "end",
            "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;JI)V",
            false
        )
    }
}

通过图片对比,我们也可以大概知道语法所表示的意思

在这里插入图片描述

6、结果

我们通过构建启动app查看下我们的插桩情况

--directoryInput = ImmutableDirectoryInput{name=0f05f04ac88e24be2f45d8d4dbf2f736e46af0f7, file=G:\yy\UIDemo\app\build\tmp\kotlin-classes\debug, contentTypes=CLASSES, scopes=PROJECT, changedFiles={}}
----file = G:\yy\UIDemo\app\build\tmp\kotlin-classes\debug\com\dreamer\uidemo\MainActivity.class
------visit = 【className】com/dreamer/uidemo/MainActivity
------visitMethod = 【name】onCreate 【desc】(Landroid/os/Bundle;)V
--------onMethodEnter
--------onMethodExit
------visitMethod = 【name】initBanner 【desc】()V
--------onMethodEnter
--------onMethodExit
------visitMethod = 【name】initList 【desc】()V
--------onMethodEnter
--------onMethodExit
------visitMethod = 【name】initView 【desc】()V
--------onMethodEnter
--------onMethodExit
------visitMethod = 【name】testLog 【desc】()V
--------onMethodEnter
--------onMethodExit
------visitMethod = 【name】<init> 【desc】()V
--------onMethodEnter
--------onMethodExit
----src = G:\yy\UIDemo\app\build\tmp\kotlin-classes\debug
----dest = G:\yy\UIDemo\app\build\intermediates\transforms\cat\debug\48

通过日志可以看到我们的MainActivity遍历了所有方法,然后对所有方法进行耗时埋点,最终将src篡改过后的源文件位置复制到dest下transform指定的目标目录

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: └───────────────────────────────────------───────────────────────────────────------

优缺点

无痕插入统计耗时虽然做到了对开发者无感知并且无痕插入统计耗时,但其存在也有缺点,由于过多的插入代码,会带来包体积的增大的隐患,对编译速度也有性能上的损耗,建议可以在此基础上做针对性的埋点,或者重要业务上的埋点,而不是做到全量埋点。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/26229.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python实现LDA和KNN人脸识别模型(LinearDiscriminantAnalysis和KNeighborsClassifier算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 人脸识别&#xff0c;是基于人的脸部特征信息进行身份识别的一种生物识别技术。用摄像机或摄像头采集含…

【2021-TITS】Deep Learning in Lane Marking Detection: A Survey

概述 回顾了针对路面标线的深度学习算法&#xff0c;主要分析了他们的网络架构及优化目标&#xff1b;此外还总结了现有车道标线相关的数据集&#xff0c;评价基准及常见的数据处理技术 PaperCode 总结 创新点&#xff1a; 1.总结深度学习网络架构、优化目标2.总结了相关现有…

使用Portman、Postman和Newman自动化API测试

几个月前我换了工作和公司&#xff0c;很喜欢这样的变化&#xff0c;但多少会有些不知所措。现在事情已经安排好了一些&#xff0c;我可以回来做一些我喜欢的事情。 现在正在工作的团队是一个新团队&#xff0c;我们正在努力弄清楚我们的流程&#xff0c;并试图尽可能地实现自…

编译原理实验一:源程序的预处理及词法分析程序的设计与实现(python)

实验目的 设计并实现一个包含预处理功能的词法分析程序&#xff0c;加深对编译中词法分析过程的理解。 实验要求 1、实现预处理功能 源程序中可能包含有对程序执行无意义的符号&#xff0c;要求将其剔除。 首先编制一个源程序的输入过程&#xff0c;从键盘、文件或文本框输…

[附源码]java毕业设计校园拓展活动管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

论文阅读之Dynamic Routing Between Capsules(2017)

文章目录前言capsule network参考前言 capsule network 可以简单理解为将神经网络的标量计算&#xff0c;赋予了方向&#xff0c;变换成了向量进行计算。 标量只有大小之分。 向量不仅有大小&#xff0c;还有方向之分。 应该可以略微感受到这种思路的魔力了。 capsule 主要是…

IB历史备考,如何拿高分?

邻近期末考&#xff0c;大伙儿是不是十分焦虑不安呢&#xff1f;大家的老师经常发觉学生们本来知识要点都把握得挺不错的&#xff0c;但每到考試便是拿不上满意的考试成绩&#xff0c;让人十分郁闷&#xff01;因此应对那样的难点&#xff0c;大家的IB历史时间评委为大伙儿筹备…

行业分析| 智慧消防对讲

消防工程关系国计民生&#xff0c;关系到每个公民的人身和财产安全&#xff0c;在每个场所的建设中&#xff0c;消防工程建设是必不可少的一个板块&#xff0c;包括消防应急通道建设和应急设备建设两个方面。其中消防系统设备建设主要分为九大系统的建设&#xff1a;消防水系统…

VectorCAST测试工具环境搭建

**目的&#xff1a;**本文提供VectorCAST使用的实际操作方法&#xff0c;以便项目可以很好的使用VectorCAST工具。 适用范围&#xff1a;VectorCAST是一个专门针对嵌入式软件的C/C代码进行单元测试的工具&#xff0c;它可以自动生成测试用例、评估测试结果并生成测试报告。 **工…

十年来高薪专业变成了它——信息安全

“什么专业好就业&#xff1f;” “什么专业以后赚的钱多&#xff1f;” 确实&#xff0c;专业对应的行业薪资各有不同&#xff0c;选对了专业赢在起跑线也不是没有可能&#xff01; 麦可思机构分析了近十年大学生就业数据情况&#xff0c;发现从2011年到2020年的大学毕业生高…

Windows简单安装redis

1.去git下载 或者去搜点资源&#xff0c;windows下载是压缩包&#xff0c;找个目录解压进去。 Github下载地址&#xff1a;https://github.com/MicrosoftArchive/redis/releases 2.打开cmd 尝试启动 比如我放在d盘soft下面&#xff0c;windows你直接打开cmd&#xff0c;输入d…

Baklib知识分享|文档生命周期:确保您的文档产出效率

一个多世纪以来&#xff0c;文档一直是任何产品或服务的支柱。它是用户在使用您的产品、应用程序、网站或其他软件服务时所引用的内容。但在现代&#xff0c;随着大多数帮助或支持文档从传统形式转移到数字形式&#xff0c;对效率的需求比以往任何时候都更加明显。此外&#xf…

[附源码]SSM计算机毕业设计基于javaweb电影购票系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【Shell 脚本速成】01、编程语言与 Shell 脚本介绍

目录 一、编程语言介绍 二、shell介绍 1、shell介绍 2、bash shell基本特性 2.1 命令和文件自动补全 2.2 常见的快捷键 3、shell脚本介绍 准确来说&#xff1a; 3.1 案例演示 3.2 什么时候用到脚本&#xff1f; 4、shell脚本语法 4.1 shell脚本组成 4.2 脚本文件命…

Spring事务配置(案例:转账业务追加日志,事务传播行为)

目录 Spring事务配置 案例 事务传播行为 代码实现&#xff1a; 1.文件大致格式&#xff1a; 2.ServiceAdvise&#xff08;AOP功能包&#xff09; 3.JdbcConfig&#xff08;jdbc配置文件&#xff0c;包含了事务管理器&#xff09; 4.MybatisConfig&#xff08;mybatis配置…

Unity技术手册-UGUI零基础详细教程-ScrollBar和ScrollView

往期文章分享点击跳转>《导航贴》- Unity手册&#xff0c;系统实战学习点击跳转>《导航贴》- Android手册&#xff0c;重温移动开发 本文约8千字&#xff0c;新手阅读需要20分钟&#xff0c;复习需要12分钟 【收藏随时查阅不再迷路】 &#x1f449;关于作者 众所周知&…

2022/11/22 [easyx]关于字符和一些背景

#include<stdio.h> #include<graphics.h> //包含图形库头文件 int main() {//创建一个窗口//(show console 显示控制台)&#xff0c;640&#xff0c;480为窗口大小*initgraph(640,480,SHOWCONSOLE);//showconsole可以换成NOCLOSE:程序不能关闭// NOMINIMIZE不能最小…

洛谷 T292115 [传智杯 #5 练习赛] 树的变迁#并查集+逆向思维

一道质量挺高的题&#xff08;个人感觉&#xff09;&#xff0c;题目说了每次要删除一条边&#xff0c;分成两棵树&#xff0c;那么很容易想到用并查集去维护。但是问题在于如果要将原来那棵树分成新的两个树必然不能使用路径压缩&#xff0c;如图所示 如果使用了路径压缩那就…

数据安全法vs国家标准,40+标准助力数据安全保驾护航~(附整理文档及pdf下载)

2021年9月1日&#xff0c;《中华人民共和国数据安全法》生效施行&#xff0c;如何助力数据安全法的贯彻落实&#xff0c;做到数据合法合规的使用需要建设一系列的数据安全标准体系。也就是说&#xff0c;有了数据安全相关标准&#xff0c;就是有了如何践行数据安全法落地实施的…

Oracle LiveLabs实验:Manage and Monitor Autonomous Database

概述 本研讨会中的实验将引导您完成开始使用 Oracle 自治数据库的所有步骤。 首先&#xff0c;您将创建一个 Oracle 自治数据库实例。 然后&#xff0c;您将练习使用自治数据库工具和 API 从不同位置以不同格式加载数据的几种方法。 您将使用 SQL 分析数据并使用 Oracle Analy…