Android ASM自定義函數(shù)耗時插件(一)

[TOC]
ASM自定義函數(shù)耗時插件(一)
ASM自定義函數(shù)耗時插件(二)

簡介

本插件源碼地址~

使用ASM技術,在android transform過程中完成對Java或者kotlin方法的函數(shù)耗時代碼插樁内贮,用于解決性能問題做函數(shù)耗時計算查看代碼優(yōu)化成果的輔助工具

技術前置了解能力

寫這個小插件主要需要了解以下幾個技術點:

  • Java字節(jié)碼
  1. 輕松看懂Java字節(jié)碼
  2. 字節(jié)碼增強技術探索
  • Android打包流程(這里主要知道Android的transform調(diào)用時機以及部分源碼即可)
  1. Android APK文件結構 完整打包編譯的流程
  2. Android Gradle Transform 詳解
  3. Gradle 學習之 Android 插件的 Transform API
  • ASM使用
  1. ASM 系列詳細教程
  2. 深入理解Transform
    以上需要是在寫之前需要了解的知識點饮怯,不用太糾結細節(jié),了解清楚每個流程即可陪竿,下面直接上寫法

Coding

  • 創(chuàng)建一個JavaLib迄汛,一定要命名為buildSrc


    buildSrc目錄.png
  • 刪除所有文件寂恬,保留一個build.gradle,完成相關配置

apply plugin: 'groovy'
apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'kotlin-android-extensions'

ext {
    kt_v = "1.3.50"
}

buildscript {
    ext.kt_v = "1.3.50"
    repositories {
        maven {
            url 'https://maven.aliyun.com/repository/jcenter'
        }
        maven {
            url 'https://maven.aliyun.com/nexus/content/repositories/google'
        }
        google()
        jcenter()
        mavenCentral() //必須
    }

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${this.ext.kt_v}"
    }
}

sourceSets {
    main {
        groovy {
            srcDir '../buildSrc/src/main/groovy'
        }
        java {
            srcDir '../buildSrc/src/main/java'
        }
        kotlin {
            srcDir '../buildSrc/src/main/kotlin'
        }
        resources {
            srcDir '../buildSrc/src/main/resources'
        }
    }
}

repositories {
    maven {
        url 'https://maven.aliyun.com/repository/jcenter'
    }
    maven {
        url 'https://maven.aliyun.com/nexus/content/repositories/google'
    }
    google()
    jcenter()
    mavenCentral() //必須
}

dependencies {
    compile gradleApi() //必須
    compile localGroovy() //必須
    implementation 'com.android.tools.build:gradle:4.0.0'
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kt_v"

    // ASM 相關
    implementation 'org.ow2.asm:asm:7.2'
    implementation 'org.ow2.asm:asm-util:7.2'
    implementation 'org.ow2.asm:asm-commons:7.2'
}

sourceCompatibility = "8"
targetCompatibility = "8"
  • 創(chuàng)建插件資源目錄,用于標識插件使用


    resource目錄.png

    implementation-class內(nèi)容寫您的插件名字即可

implementation-class=com.done.plugin.PagePlugin

對應插件類代碼

package com.done.plugin

import com.android.build.gradle.BaseExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

public class PagePlugin implements Plugin<Project> {

    private Project mProject

    @Override
    void apply(Project project) {
        mProject = project
        project.getExtensions().add(InsectExtension.CONFIG_NAME, InsectExtension)
        def android = project.extensions.findByType(BaseExtension)
        android.registerTransform(new CostTransform(project))
        LogUtilsGv.log("register PagePlugin")
    }
}

在插件被調(diào)用的時候扛吞,gradle會調(diào)用apply方法呻惕,在這里注冊插樁的transform-CostTransform,可以看下transform的配置

    /**
     * 執(zhí)行transform時候task的名字
     * @return
     */
    @Override
    String getName() {
        return this.class.getName()
    }

    /**
     * 只需要class文件輸入
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 如果是app使用插件滥比,則傳遞所有的class進來亚脆,如果是lib使用的話,僅傳遞對應lib project的class過來即可
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        if (mType == PROJECT_TYPE.LIB) {
            return TransformManager.PROJECT_ONLY
        } else {
            return TransformManager.SCOPE_FULL_PROJECT
        }
    }

    /**
     * TODO 暫時不支持增量編譯
     * @return
     */
    @Override
    boolean isIncremental() {
        return false
    }

上面需要重寫的方法主要是表明僅需要class的流輸入進來盲泛,接下來就是核心的transform方法

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        mInsectExtension = mProject.getExtensions().getByName(InsectExtension.CONFIG_NAME)
        if (mInsectExtension == null || mInsectExtension.annotationNames == null || mInsectExtension.annotationNames.length < 1 || !mInsectExtension.isDebug) {
            LogUtilsGv.log("do not execute insert byte code")
            return
        }
        LogUtilsGv.log("startTransform anno:${mInsectExtension.annotationNames}")
        long startTime = System.currentTimeMillis()
        Collection<TransformInput> inputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        if (outputProvider != null) {
            outputProvider.deleteAll()
        }
        //處理目錄中的文件
        inputs.each {
            TransformInput input ->
                input.directoryInputs.each {
                    DirectoryInput directoryInput ->
                        File dest = outputProvider.getContentLocation(directoryInput.getFile().getAbsolutePath(),
                                directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY)
                        handleDirectoryFile(directoryInput.getFile(), dest, mInsectExtension)
                }
        }
        //處理Jar中的文件
        inputs.each {
            TransformInput input ->
                input.jarInputs.each {
                    JarInput jarInput ->
                        File dest = outputProvider.getContentLocation(jarInput.getFile().getAbsolutePath(),
                                jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR)
                        handleJarFile(jarInput.getFile(), dest)
                }
        }
        LogUtilsGv.log("endTransform cost:" + (System.currentTimeMillis() - startTime))
    }

為了方便外部使用濒持,需要對自定義的插件增加一些配置,如標識的注解類和需要調(diào)用的方法寺滚,插件內(nèi)部寫死傳遞兩個參數(shù)柑营,分別是method名稱和方法執(zhí)行的起始時間

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        boolean hasMethod = true
        if (Utils.isEmptyString(mInsectExtension.methodOwner) || Utils.isEmptyString(mInsectExtension.methodName)) {
            hasMethod = false
        } else {
            mCostMethod = mInsectExtension.methodName
            mCostMethodClass = mInsectExtension.methodOwner.replaceAll("(\\.)", "/")
        }
        if (hasMethod) {
            for (String annotation : mInsectExtension.annotationNames) {
                String replaceAnno = "L" + annotation.replaceAll("(\\.)", "/") + ";"
                canPrint = canPrint || replaceAnno == desc
                LogUtilsGv.log("外部配置的注解:$replaceAnno, $mClassName.$mMethodName 插樁:$canPrint, 插入$mCostMethodClass#$mCostMethod")
            }
        } else {
            LogUtilsGv.log("does not have cost method, do not insert cost code")
        }
        return super.visitAnnotation(desc, visible)
    }

    @Override
    protected void onMethodExit(int opcode) {
        if (canPrint) {
            String newName = (mClassName + "#" + mMethodName).replaceAll("/", ".")
            mv.visitLdcInsn(newName)
            mv.visitVarInsn(LLOAD, startTime)
            mv.visitMethodInsn(INVOKESTATIC, mCostMethodClass, mCostMethod, "(Ljava/lang/String;J)V", false)
        }
    }

    @Override
    protected void onMethodEnter() {
        if (canPrint) {
            invokeStatic(Type.getType("Landroid/os/SystemClock;"), new Method("uptimeMillis", "()J"))
            startTime = newLocal(Type.LONG_TYPE)
            storeLocal(startTime)
        }
    }
  • 在對應的Lib或者App下進行使用
apply plugin: 'com.done.plugin'
insectConfig {
    annotationNames = ['com.done.asm.Cost',
                       'com.done.asm.KtCost']
    methodOwner = 'com.done.asm.Utils'
    methodName = 'printCost'
    isDebug = true
}

集成插件后的能力,可以在修飾對應注解的地方插樁函數(shù)耗時的代碼
annotationNames 注解類作為數(shù)組方法中
isDebug 負責是否進行插樁村视,用于遠程打包的時候配置使用
methodOwner 是調(diào)用的靜態(tài)方法類名
methodName 是調(diào)用的靜態(tài)方法名稱

代碼量其實很少


插件全量目錄.png
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末官套,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蚁孔,更是在濱河造成了極大的恐慌奶赔,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杠氢,死亡現(xiàn)場離奇詭異站刑,居然都是意外死亡,警方通過查閱死者的電腦和手機鼻百,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門绞旅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人温艇,你說我怎么就攤上這事玻靡。” “怎么了中贝?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長臼朗。 經(jīng)常有香客問我邻寿,道長,這世上最難降的妖魔是什么视哑? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任绣否,我火速辦了婚禮,結果婚禮上挡毅,老公的妹妹穿的比我還像新娘蒜撮。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布段磨。 她就那樣靜靜地躺著取逾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪苹支。 梳的紋絲不亂的頭發(fā)上砾隅,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機與錄音债蜜,去河邊找鬼晴埂。 笑死,一個胖子當著我的面吹牛寻定,可吹牛的內(nèi)容都是我干的儒洛。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼狼速,長吁一口氣:“原來是場噩夢啊……” “哼琅锻!你這毒婦竟也來了?” 一聲冷哼從身側響起唐含,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤浅浮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后捷枯,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體滚秩,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年淮捆,在試婚紗的時候發(fā)現(xiàn)自己被綠了郁油。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡攀痊,死狀恐怖桐腌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情苟径,我是刑警寧澤案站,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站棘街,受9級特大地震影響蟆盐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜遭殉,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一石挂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧险污,春花似錦痹愚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽窖式。三九已至,卻和暖如春疾瓮,著一層夾襖步出監(jiān)牢的瞬間脖镀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工狼电, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蜒灰,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓肩碟,卻偏偏與公主長得像强窖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子削祈,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344