ASM自定义函数耗时(二)

ASM自定义函数耗时

[TOC]

ASM自定义函数耗时插件(一)
ASM自定义函数耗时插件(二)

简介

本插件源码地址~

使用ASM技术,在android transform过程中完成对Java或者kotlin方法的函数耗时代码插桩,用于解决性能问题做函数耗时计算查看代码优化成果的辅助工具

之所以写这篇二是在上一篇基础上用kotlin完善和修复了一些bug,实现的更加优雅,欢迎大家下载下来体验

技术前置了解能力

惯例需要了解这些前提知识,一定要仔细研读,不然就会出现老板上次一期的问题(手动笑哭)~

写这个小插件主要需要了解以下几个技术点:

  • Java字节码
  1. 轻松看懂Java字节码
  2. 字节码增强技术探索
  • Android打包流程(这里主要知道Android的transform调用时机以及部分源码即可)
  1. Android APK文件结构 完整打包编译的流程
  2. Android Gradle Transform 详解
  3. Gradle 学习之 Android 插件的 Transform API
  • ASM使用
  1. ASM 系列详细教程
  2. 深入理解Transform
    以上需要是在写之前需要了解的知识点,不用太纠结细节,了解清楚每个流程即可,下面直接上写法

Coding

首先就是按照规范实现android的Plugin并注册

class InsectPlugin : Plugin<Project> {

    private lateinit var mProject: Project

    override fun apply(project: Project) {
        configProj(project)
        when {
            //在app中依赖
            project.plugins.hasPlugin("com.android.application") || project.plugins.hasPlugin("com.android.dynamic-feature") -> {
                project.getAndroid<AppExtension>().let { androidExt ->
                    PLogger.log("application register plugin")
                    androidExt.registerTransform(InsectTransformer(project))
                }
            }
            //在lib中依赖
            project.plugins.hasPlugin("com.android.library") -> {
                project.getAndroid<LibraryExtension>().let { libExt ->
                    PLogger.log("lib register plugin")
                    libExt.registerTransform(InsectTransformer(project))
                }
            }
        }
    }

    private fun configProj(project: Project) {
        mProject = project
                .also {
                    it.extensions.add(InsectPluginConstants.INSECT_CONFIG, InsectExtension())
                }
    }
}

然后注册android的transformer

class InsectTransformer(project: Project) : BaseTransformer(project) {

    private lateinit var mHelper: InsectTransformHelper

    override fun transform(transformInvocation: TransformInvocation?) {
        val insectExtension = mProject.extensions.findByName(InsectPluginConstants.INSECT_CONFIG) as InsectExtension
        if (insectExtension.isDebug) {
            mHelper = InsectTransformHelper(insectExtension)
            //TODO 以下增加了增量编译后再进行编写
//            if (isIncremental) {
//                PLogger.log("增量编译,走父类的transform流程")
//                super.transform(transformInvocation)
//            } else {
//                if (transformInvocation != null) {
//                    transformInvocation.outputProvider?.deleteAll()
//                    PLogger.log("命中transform, 走transform逻辑")
//                    doFullTransform(transformInvocation)
//                } else {
//                    PLogger.log("transformInvocation为空,走父类的transform流程")
//                    super.transform(transformInvocation)
//                }
//            }
            if (transformInvocation != null) {
                transformInvocation.outputProvider?.deleteAll()
                doFullTransform(transformInvocation)
            } else {
                PLogger.e("transformInvocation为空,走父类的transform流程")
                super.transform(transformInvocation)
            }
        } else {
            PLogger.e("release下编译,不进行字节码插桩,走父类的transform")
            super.transform(transformInvocation)
        }
    }

    private fun doFullTransform(transformInvocation: TransformInvocation) {
        transformInvocation.inputs?.let { it ->
            it.map {
                it.jarInputs + it.directoryInputs
            }.flatten().forEach { input ->
                mHelper.doTransform(transformInvocation, input)
            }
        }
        super.transform(transformInvocation)
    }
}

最核心的实现其实是咱们的transformerHelper这个类,主要实现以下的逻辑完成插桩

  • 遍历所有input输出,判断是文件还是目录
  • 如果是目录,则继续遍历到文件为止
  • 判断文件是jar还是class文件,然后分别处理jar和class
  • 如果是jar则复制文件即可
  • 如果是class,则进行插桩

接下来我们看一下对文件判断处理的类InsectTransformHelper

class InsectTransformHelper(val insectExtension: InsectExtension) : ITransform {

    override fun doTransform(transformInvocation: TransformInvocation, input: QualifiedContent) {
        val format = if (input is DirectoryInput) Format.DIRECTORY else Format.JAR
        when {
            format == Format.DIRECTORY && input is DirectoryInput -> transformDirectory(transformInvocation, input)
            format == Format.JAR && input is JarInput -> transformJar(transformInvocation, input)
        }
    }

    private fun transformJar(transformInvocation: TransformInvocation, input: JarInput) {
        transformInvocation.outputProvider?.let { provider ->
            PLogger.log("transform jar ${input.file.absolutePath}")
            val contentLocation = provider.getContentLocation(input.file.absolutePath, input.contentTypes, input.scopes, Format.JAR)
            _transformJar(input.file, contentLocation)
        }
    }

    private fun transformDirectory(transformInvocation: TransformInvocation, input: DirectoryInput) {
        transformInvocation.outputProvider.let { provider ->
            provider.getContentLocation(input.file.absolutePath, input.contentTypes, input.scopes, Format.DIRECTORY)?.let { dest ->
                _transformDirectory(input.file, dest)
            }
        }
    }

    private fun _transformDirectory(src: File, dest: File) {
        if (dest.exists()) {
            FileUtils.forceDelete(dest)
        }
        FileUtils.forceMkdir(dest)
        src.listFiles()?.forEach {
            val destPath = it.absolutePath.replace(src.absolutePath, dest.absolutePath)
            val destFile = File(destPath)
            when {
                it.isDirectory -> _transformDirectory(it, destFile)
                it.isFile -> when (it.extension) {
                    "class" -> _transformClass(it, destFile)
                    "jar" -> _transformJar(it, destFile)
                    else -> PLogger.log("啥也不是,啥也不干 src:$it, dest:$destFile")
                }
            }
        }

    }

    private fun _transformJar(src: File?, dest: File?) {
        if (src != null && dest != null) {
            FileUtils.copyFile(src, dest)
        }
    }

    private fun _transformClass(src: File, dest: File) {
        FileUtils.touch(dest)
        var fos = FileOutputStream(dest)
        var fis = FileInputStream(src)
        try {
            fos.also { os ->
                ClassReader(fis).also { reader ->
                    val classNode = InsectClassVisitor(insectExtension)
                    reader.accept(classNode, ClassReader.EXPAND_FRAMES)
                    val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
                    classNode.accept(classWriter)
                    os.write(classWriter.toByteArray())
                    os.flush()
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            IOUtils.closeQuietly(fos)
            IOUtils.closeQuietly(fis)
        }
    }

}

然后是具体插桩代码的逻辑:

  • 判断是否有标记过的注解
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
  var hasMethod = !insectExtension.annotationNames.isNullOrEmpty()
  if (hasMethod) {
    insectExtension.annotationNames?.forEach { annotation ->
                                              val relpaceAnno = "L${annotation.replace(".", "/")};"
                                              mDoCost = mDoCost || relpaceAnno == descriptor ?: "nonono"
                                             }
  }
  if (mDoCost) {
    PLogger.i("${this@InsectClassVisitor.name}.${this.mMethodName} 可以开始进行插桩")
  }
  return super.visitAnnotation(descriptor, visible)
}
  • 分别在方法开始和结束的地方插入对应的代码
override fun onMethodEnter() {
  super.onMethodEnter()
  if (mDoCost) {
    invokeStatic(Type.getType("Landroid/os/SystemClock;"), Method("uptimeMillis", "()J"))
    mStartVar = newLocal(Type.LONG_TYPE)
    storeLocal(mStartVar!!)
  }
}

override fun onMethodExit(opcode: Int) {
  super.onMethodExit(opcode)
  if (mDoCost) {
    val name = "${this@InsectClassVisitor.name}.${this.mMethodName} => cost ".replace("/", ".")
    super.mv.visitLdcInsn(name)
    super.mv.visitVarInsn(LLOAD, mStartVar!!)
    super.mv.visitMethodInsn(INVOKESTATIC, insectExtension.methodOwner.replace(".", "/"),
                             insectExtension.methodName, "(Ljava/lang/String;J)V", false)
  }
}

本来还有一种极其简单的方式,就是通过classNode的methods.find函数来进行插桩,结果在选择存储临时变量即方法开始的时间戳的时候,进行LLOAD操作,变量的index因为不熟悉的原因调试了很久都没成功,才改用现在的手写MethodVisitor来实现,希望有知道这里知识的大佬能指导下。。。关于ASM的知识网上大多都很零散,特别的ASM不同版本的方法都有一定的差异。。。这里用的是gradle4自带的ASM7版本


2020年11月12日01:29:31 更新日历

  • 增加判断抽象方法的耗时不予插桩
  • 对象成员方法的类传递使用object.getClass.getName方式获取类名,原有的实现方式可能会传递基类的类名,不是很友好
  • 进一步封装methodVisitor插桩,支持横向扩展插桩能力

新增一个支持横向扩展能力的methodVisitor接口,抽取抽来通用的方法,在使用时候一定要注意,adviceAdpater提供给实现避免开发者自行维护索引的情况

interface IVisitMethod {

    fun onVisitAnnotation(annotationVisitor: AnnotationVisitor?, descriptor: String?, visible: Boolean)

    fun onMethodEnter(adviceAdapter: AdviceAdapter?, methodVisitor: MethodVisitor?)

    fun onMethodExit(adviceAdapter: AdviceAdapter?, methodVisitor: MethodVisitor?, opcode: Int)

    fun getMaxStack(): Int

    fun getMaxLocals(): Int
}

而原有的内部类改为静态内部类实现,仅仅是个"广义责任链"的分发者,利用小控制反转的方式方便开发者专注于插桩的实现

class InsectMethodVisitorProxy(api: Int, methodVisitor: MethodVisitor, access: Int, name: String, descriptor: String,
                                   val ownerClassName: String = "class", val insectExtension: InsectExtension)
        : AdviceAdapter(api, methodVisitor, access, name, descriptor) {

        private val mMethodImpls by lazy { ConcurrentHashMap<Int, BaseVisitMethodImpl>() }

        fun appendMethodImpl(methodVisitor: BaseVisitMethodImpl) {
            mMethodImpls[methodVisitor.hashCode()] = methodVisitor
        }

        override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
            val visitAnnotation = super.visitAnnotation(descriptor, visible)
            for (methodImpl in mMethodImpls) {
                methodImpl.value.onVisitAnnotation(visitAnnotation, descriptor, visible)
            }
            return visitAnnotation
        }

        override fun onMethodEnter() {
            super.onMethodEnter()
            for (methodImpl in mMethodImpls) {
                methodImpl.value.onMethodEnter(this, super.mv)
            }
        }

        override fun onMethodExit(opcode: Int) {
            super.onMethodExit(opcode)
            for (methodImpl in mMethodImpls) {
                methodImpl.value.onMethodExit(this, super.mv, opcode)
            }
        }

        /**
         * Visits the maximum stack size and the maximum number of local variables of the method.
         *
         * @param maxStack maximum stack size of the method.
         * @param maxLocals maximum number of local variables for the method.
         */
        override fun visitMaxs(maxStack: Int, maxLocals: Int) {
            var maxStackCount = 0
            var maxLocalsCount = 0
            for (methodImpl in mMethodImpls) {
                maxStackCount += methodImpl.value.getMaxStack()
                maxLocalsCount += methodImpl.value.getMaxLocals()
            }
            super.visitMaxs(maxStack + maxStackCount, maxLocals + maxLocalsCount)
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343