Android Gradle3-自定義Plugin實踐

因為要做一個無埋點收集數(shù)據(jù)的功能,需要自定義一個Plugin扬蕊,搜到的方法大部分都是打印一個HelloWorld,沒有任何的參考價值丹擎,所以詳細(xì)記錄一下過程尾抑。
如果想對編譯的class文件進(jìn)行字節(jié)碼注入,hook是一種方式蒂培,但是gradle1.5之后android gradle插件也可以通過自定義一個Plugin再愈,調(diào)用這段代碼來注冊一個Transform。

 class GatherPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        def android = project.extensions.findByType(AppExtension)
        android.registerTransform(new GatherTransform(project))
    }
}

Transform是一個抽象類护戳,通過繼承這個類可以對字節(jié)碼進(jìn)行修改翎冲。為了弄這個,經(jīng)過有些麻煩媳荒,踩了一些gradle的坑抗悍,特意記錄一下。
整個過程分為下面幾步
創(chuàng)建一個Groovy模塊
創(chuàng)建一個GatherPlugin
創(chuàng)建一個GatherTransform
利用ASM掃描所有的類文件钳枕,然后在指定地方插入代碼

這個是Gradle的API,方便查看

創(chuàng)建一個Groovy模塊
  • 創(chuàng)建一個Groovy項目
    可以通過創(chuàng)建一個lib項目把里面的文件都刪了缴渊,處理build.gradle和放源碼的目錄。
    這里的如果創(chuàng)建本工程自己用的插件文件的目錄名字必須是buildSrc鱼炒,先以本工程用的插件為例衔沼。


    Snip20170801_3.png
  • 修改build.gradle文件腳本代碼

apply plugin: 'groovy'

//上傳插件到倉庫需要 非必要
apply plugin: 'maven'

dependencies {
    compile gradleApi()//gradle sdk
    compile localGroovy()//groovy sdk

    compile 'com.android.tools.build:gradle:2.3.1'

    compile 'org.ow2.asm:asm:5.0.3'
    compile 'org.ow2.asm:asm-commons:5.0.3'

}

repositories {
    jcenter()
    mavenCentral()
}
有個坑
  • jackOptions 為true 會導(dǎo)致自定義的Transform 不能執(zhí)行
  • 創(chuàng)建的文件必須要以.groovy 為后綴,否則在其他文件中引用會語法錯誤
創(chuàng)建GatherPlugin和GatherTransform

這個很簡單

GatherPlugin.groovy文件田柔,文件后綴一定要有g(shù)roovy

class GatherPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        def android = project.extensions.findByType(AppExtension)
        android.registerTransform(new GatherTransform(project))
    }
}

在項目的gradle.build文件里引用插件

apply plugin: 'com.android.application'
apply plugin: com.cyy.gather.GatherPlugin
.....

GatherTransform.groovy文件

public class GatherTransform extends Transform{

   Project project

   // 構(gòu)造函數(shù)俐巴,我們將Project保存下來備用
   public GatherTransform(Project project) {
       this.project = project
   }

   // 設(shè)置我們自定義的Transform對應(yīng)的Task名稱
   @Override
   String getName() {
       return "GatherTransform"
   }

   // 指定輸入的類型,通過這里的設(shè)定硬爆,可以指定我們要處理的文件類型
   //這樣確保其他類型的文件不會傳入
   @Override
   Set<QualifiedContent.ContentType> getInputTypes() {
       return TransformManager.CONTENT_CLASS
   }

   // 指定Transform的作用范圍
   @Override
   Set<QualifiedContent.Scope> getScopes() {
       return TransformManager.SCOPE_FULL_PROJECT
   }

   @Override
   boolean isIncremental() {
       return false
   }

   @Override
   void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
       super.transform(transformInvocation)
       println(" transform transform ")
   }

   @Override
   void transform(Context context, Collection<TransformInput> inputs,
                  Collection<TransformInput> referencedInputs,
                  TransformOutputProvider outputProvider, boolean isIncremental)
           throws IOException, TransformException, InterruptedException {

       /**
        * Transform的inputs有兩種類型,
        *  一種是目錄擎鸠, DirectoryInput
        *  一種是jar包缀磕,JarInput
        *  要分開遍歷
        */
       inputs.each { TransformInput input ->
           /**
            * 對類型為“文件夾”的input進(jìn)行遍歷
            */
           input.directoryInputs.each {
               /**
                * 文件夾里面包含的是
                *  我們手寫的類
                *  R.class、
                *  BuildConfig.class
                *  R$XXX.class
                *  等
                *  根據(jù)自己的需要對應(yīng)處理
                */
               println("it == ${it}")

               //注入代碼
               Inject.injectOnClick(it.file.absolutePath)
               // 獲取output目錄
               def dest = outputProvider.getContentLocation(it.name,
                       it.contentTypes, it.scopes,
                       Format.DIRECTORY)

               // 將input的目錄復(fù)制到output指定目錄
               FileUtils.copyDirectory(it.file, dest)
           }
           //對類型為jar文件的input進(jìn)行遍歷
           input.jarInputs.each { JarInput jarInput ->

               //jar文件一般是第三方依賴庫jar文件

               // 重命名輸出文件(同目錄copyFile會沖突)
               def jarName = jarInput.name
               def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
               if (jarName.endsWith(".jar")) {
                   jarName = jarName.substring(0, jarName.length() - 4)
               }
               //生成輸出路徑
               def dest = outputProvider.getContentLocation(jarName + md5Name,
                       jarInput.contentTypes, jarInput.scopes, Format.JAR)
               //將輸入內(nèi)容復(fù)制到輸出
               FileUtils.copyFile(jarInput.file, dest)
           }
       }

   }
}

這樣整個插件就可以運行了劣光。

利用ASM掃描所有的類文件袜蚕,然后在指定地方插入代碼

在制定的代碼區(qū)域注入指定代碼主要在Inject,groovy中完成的,這個代碼主要就是怎么用Groovy绢涡,所以沒有貼牲剃。

我是第一次用ASM,對ASM的語法一點不懂雄可,出了很多問題凿傅〔看了很多的例子代碼,基本上都是注入一個輸出HelloWorld聪舒,屬于沒有一點參考價值的辨液。

當(dāng)然我們只是做一個插件沒有必要去花時間去學(xué)習(xí)ASM,這個東西要學(xué)習(xí)也不是一天兩天的事箱残,踩很多坑之后找到一個工具滔迈,非常好用。一個Studio插件 ASM Bytecode Outline , 下載后解壓被辑,將復(fù)制Studio的圖片中的目錄燎悍,然后重啟Studo

Snip20170801_5.png

這個插件使用很簡單,重啟后Studio左邊會出現(xiàn)如圖所示
Snip20170801_6.png

鼠標(biāo)右擊你的某一個類盼理。


Snip20170801_8.png

然后就會把你這個類的代碼全部轉(zhuǎn)化成ASM語法格式的谈山。66666。如果不會寫ASM的語法榜揖,把你的代碼在一個測試類中先寫好勾哩,然后利用ASM生成出對應(yīng)的ASM語法,在把代碼copy到Inject.groovy中即可举哟。
例如GatherClassVisitor.groovy文件中這些代碼都是通過這個工具生產(chǎn)的

methodList.each {
                if (it == "onResume" || it == "onPause"){
                    MethodVisitor mv = cv.visitMethod(ACC_PUBLIC , it, "()V", null, null)
                    mv.visitVarInsn(ALOAD, 0)
                    mv.visitMethodInsn(INVOKESPECIAL, superName, it, "()V", false)
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitInsn(it == "onResume" ? ICONST_1 : ICONST_0);
                    mv.visitMethodInsn(INVOKESTATIC, INJECT_OWNER, "onFragmentResumeOrPause", "(Landroid/support/v4/app/Fragment;Z)V", false);
                    mv.visitInsn(RETURN)
                    mv.visitMaxs(1, 1)
                    mv.visitEnd()
                } else if (it == "onHiddenChanged"){
                    MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "onHiddenChanged", "(Z)V", null, null)
                    mv.visitCode()
                    mv.visitVarInsn(ALOAD, 0)
                    mv.visitVarInsn(ILOAD, 1)
                    mv.visitMethodInsn(INVOKESPECIAL, superName, "onHiddenChanged", "(Z)V", false)
                    mv.visitVarInsn(ALOAD, 0)
                    mv.visitVarInsn(ILOAD, 1);
                    mv.visitMethodInsn(INVOKESTATIC, INJECT_OWNER, "onHiddenChanged", "(Landroid/support/v4/app/Fragment;Z)V", false);
                    mv.visitInsn(RETURN)
                    mv.visitMaxs(2, 2)
                    mv.visitEnd()
                }else if (it == "onViewCreated"){
                    MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "onViewCreated", "(Landroid/view/View;Landroid/os/Bundle;)V", null, null);
                    mv.visitCode();
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitVarInsn(ALOAD, 1);
                    mv.visitVarInsn(ALOAD, 2);
                    mv.visitMethodInsn(INVOKESPECIAL, superName, "onViewCreated", "(Landroid/view/View;Landroid/os/Bundle;)V", false);
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitVarInsn(ALOAD, 1);
                    mv.visitMethodInsn(INVOKESTATIC, INJECT_OWNER, "onFragmentCreatedView", "(Landroid/support/v4/app/Fragment;Landroid/view/View;)V", false);
                    mv.visitInsn(RETURN);
                    mv.visitMaxs(3, 3);
                    mv.visitEnd();
                }else if (it == ""){
                    MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "setUserVisibleHint", "(Z)V", null, null);
                    mv.visitCode();
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitVarInsn(ILOAD, 1);
                    mv.visitMethodInsn(INVOKESPECIAL, superName, "setUserVisibleHint", "(Z)V", false);
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitVarInsn(ILOAD, 1);
                    mv.visitMethodInsn(INVOKESTATIC, INJECT_OWNER, "onFragmentSettUserVisibleHint", "(Landroid/support/v4/app/Fragment;Z)V", false);

                    mv.visitInsn(RETURN);
                    mv.visitMaxs(2, 2);
                    mv.visitEnd();
                }
            }

源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末思劳,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子妨猩,更是在濱河造成了極大的恐慌潜叛,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件壶硅,死亡現(xiàn)場離奇詭異威兜,居然都是意外死亡,警方通過查閱死者的電腦和手機庐椒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門椒舵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人约谈,你說我怎么就攤上這事笔宿。” “怎么了棱诱?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵泼橘,是天一觀的道長。 經(jīng)常有香客問我迈勋,道長炬灭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任靡菇,我火速辦了婚禮重归,結(jié)果婚禮上米愿,老公的妹妹穿的比我還像新娘。我一直安慰自己提前,他們只是感情好吗货,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著狈网,像睡著了一般宙搬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拓哺,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天勇垛,我揣著相機與錄音,去河邊找鬼士鸥。 笑死闲孤,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的烤礁。 我是一名探鬼主播讼积,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼脚仔!你這毒婦竟也來了勤众?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤鲤脏,失蹤者是張志新(化名)和其女友劉穎们颜,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體猎醇,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡窥突,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了硫嘶。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阻问。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖沦疾,靈堂內(nèi)的尸體忽然破棺而出则拷,到底是詐尸還是另有隱情,我是刑警寧澤曹鸠,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站斥铺,受9級特大地震影響彻桃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜晾蜘,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一邻眷、第九天 我趴在偏房一處隱蔽的房頂上張望眠屎。 院中可真熱鬧,春花似錦肆饶、人聲如沸改衩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽葫督。三九已至,卻和暖如春板惑,著一層夾襖步出監(jiān)牢的瞬間橄镜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工冯乘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留洽胶,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓裆馒,卻偏偏與公主長得像姊氓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子喷好,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344