亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Android進(jìn)階從字節(jié)碼插樁技術(shù)了解美團(tuán)熱修復(fù)實(shí)例詳解

 更新時間:2023年01月29日 10:51:03   作者:layz4android  
這篇文章主要為大家介紹了Android進(jìn)階從字節(jié)碼插樁技術(shù)了解美團(tuán)熱修復(fù)實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

引言

熱修復(fù)技術(shù)如今已經(jīng)不是一個新穎的技術(shù),很多公司都在用,而且像阿里、騰訊等互聯(lián)網(wǎng)巨頭都有自己的熱修復(fù)框架,像阿里的AndFix采用的是hook native底層修改代碼指令集的方式;騰訊的Tinker采用類加載的方式修改dexElement;而美團(tuán)則是采用字節(jié)碼插樁的方式,也就是本文將介紹的一種技術(shù)手段。

我們知道,如果上線出現(xiàn)bug,通常是發(fā)生在方法的調(diào)用階段,某個方法異常導(dǎo)致崩潰;字節(jié)碼插樁,就是在編譯階段將一段代碼插入該方法中,如果線上崩潰,需要發(fā)布補(bǔ)丁包,同時在執(zhí)行該方法時,如果檢測到補(bǔ)丁包的存在,將會走插樁插入的邏輯,而不是原邏輯。

如果想要知道美團(tuán)實(shí)現(xiàn)的熱修復(fù)框架原理,那么首先需要知道,robust該怎么用

對于每個模塊,如果想要插樁需要引入robust插件,所以如果自己實(shí)現(xiàn)一個簡單的robust的功能,就需要創(chuàng)建一個插件,然后在插件中處理邏輯,我個人喜歡在buildSrc里寫插件然后發(fā)布,當(dāng)然也可以自己創(chuàng)建一個java工程改造成groovy工程

plugins {
    id 'groovy'
    id 'maven-publish'
}
dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:3.1.2'
}

如果創(chuàng)建一個java模塊,如果要【改裝】成一個groovy工程,就需要做上述的配置??

1 插件發(fā)布

初始化之后,我一般會先建2個文件夾

plugin用于自定義插件,定義輸入輸出; task用于任務(wù)執(zhí)行。

class MyRobustPlugin implements Plugin<Project>{
    @Override
    void apply(Project project) {
        //項(xiàng)目配置階段執(zhí)行,配置完成之后,
        project.afterEvaluate {
            println '插件開始執(zhí)行了'
        }
    }
}

如果需要發(fā)布插件到maven倉庫,或者放在本地,可以通過maven-publish(gradle 7.0+)插件來實(shí)現(xiàn)

afterEvaluate {
    publishing {
        publications{
            releaseType(MavenPublication){
                from components.java
                groupId  'com.demo'
                artifactId  'robust'
                version  '0.0.1'
            }
        }
        repositories {
            maven {
                url uri('../repo')
            }
        }
    }
}

publications:這里可以添加你要發(fā)布的maven版本配置 repositories:maven倉庫的地址,這里就是寫在本地一個文件夾

重新編譯之后,在publish文件夾下會生成很多任務(wù),執(zhí)行發(fā)布到maven倉庫的任務(wù),就會在本地的repo文件夾下生成對應(yīng)的jar包

接下來我們嘗試用下這個插件

buildscript {
    repositories {
        google()
        mavenCentral()
        jcenter()
        //這里配置了我們的插件依賴的本地倉庫地址
        maven {
            url uri('repo')
        }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:7.0.3"
        classpath "com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10"
        classpath "com.demo:robust:0.0.1"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

配置完成后,在app模塊添加插件依賴

apply plugin:'com.demo'

這里會報(bào)錯,com.demo這個插件id找不到,原因就是,其實(shí)插件是一個jar包,然后我們只是創(chuàng)建了這個插件,并沒有聲明入口,在編譯jar包時找不到清單文件,因此需要在資源文件夾下聲明清單文件

implementation-class=com.tal.robust.plugin.MyRobustPlugin

創(chuàng)建插件名字的屬性文件,聲明插件的入口,就是我們自己定義的插件,再次編譯運(yùn)行

這也意味著,我們的插件執(zhí)行成功了,所以準(zhǔn)備工作已完成,如果需要插樁的模塊,那么就需要依賴這個插件

2 Javassist

Javassist號稱字節(jié)碼手術(shù)刀,能夠在class文件生成之后,打包成dex文件之前就將我們自定義的代碼插入某個位置,例如在getClassId方法第62行代碼的位置,插入邏輯判斷代碼

2.1 準(zhǔn)備工作

引入Javassist,插件工程引入Javassist

implementation 'org.javassist:javassist:3.20.0-GA'

2.2 Transform

Javassist作用于class文件生成之后,在dex文件生成之前,所以如果想要對字節(jié)碼做處理,就需要在這個階段執(zhí)行代碼插入,這里就涉及到了一個概念 --- transform;

Android官方對于transform做出的定義就是:Transform用于在class打包成dex這個中間過程,對字節(jié)碼做修改

在build文件夾中,我們可以看到這些文件夾,像merged_assets、merged_java_res等,這是Gradle的Transform,用于打包資源文件到apk文件中,執(zhí)行的順序?yàn)榇袌?zhí)行,一個任務(wù)的輸出為下一個任務(wù)的輸入,而在transforms文件夾下就是我們自己定義的transform

implementation 'com.android.tools.build:transform-api:1.5.0'

導(dǎo)入Transform依賴????

class MyRobustTransform extends Transform{
    /**
     * 在transforms文件夾下的文件夾名字
     * @return
     */
    @Override
    String getName() {
        return "MyRobust"
    }
    /**
     * Transform要處理的輸入文件類型 : 字節(jié)碼
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }
    /**
     * 作用域:整個項(xiàng)目
     * @return
     */
    @Override
    Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }
    /**
     * 是否為增量編譯
     * @return
     */
    @Override
    boolean isIncremental() {
        return false
    }
    @Override
    void transform(TransformInvocation transformInvocation) throws IOException, TransformException, InterruptedException {
    }
}

如何讓自定義的Transform生效,需要在插件中注冊這個Transform

@Override
void apply(Project project) {
    println '插件開始執(zhí)行了'
    //注冊Transform
    def ext = project.extensions.getByType(AppExtension)
    if(ext != null){
       ext.registerTransform(new MyRobustTransform(project));
    }
}

對于每個模塊,Gradle編譯時都是創(chuàng)建一個Project對象,這里就是拿到了當(dāng)前模塊gradle中的android擴(kuò)展,然后調(diào)用了registerTransform函數(shù)注冊Transform,MyRobustTransform中的transform函數(shù)會被調(diào)用,將class、jar、resource等文件做處理

把一開始的流程圖細(xì)分一下,其實(shí)class字節(jié)碼在處理的時候是經(jīng)歷了多個transform,這里可以把transform看做是任務(wù),每個任務(wù)執(zhí)行完成之后,都將輸出交由下一個task作為輸入,我們自定義的transform是被放在transform鏈的頭部

Task :app:transformClassesWithMyRobustForDebug

2.3 transform函數(shù)注入代碼

OK,我們注冊完成之后,這個Transform任務(wù)就能夠執(zhí)行了,執(zhí)行的時候,會執(zhí)行transform函數(shù)中的代碼,我們注入代碼也是在這個函數(shù)中進(jìn)行

 @Override
void transform(TransformInvocation transformInvocation)  {
    super.transform(transformInvocation)
    println "transform start"
    transformInvocation.inputs.each { input ->
        //對于class字節(jié)碼,需要處理
        input.directoryInputs.each { dic ->
            println "dic路徑 $dic.file.absolutePath"
            classPool.appendClassPath(dic.file.absolutePath)
            //插入代碼 -- javassist
            //找到class在哪,需要遍歷class
            findTargetClass(dic.file, dic.file.absolutePath)
            def nextTransform = transformInvocation.outputProvider.getContentLocation(dic.name, dic.contentTypes, dic.scopes, Format.DIRECTORY)
            FileUtils.copyDirectory(dic.file, nextTransform)
        }
        //對jar包不處理,直接扔給下一個Transform
        input.jarInputs.each { jar ->
            println "jar包路徑  $jar.file.absolutePath"
            classPool.appendClassPath(jar.file.absolutePath)
            def nextTransform = transformInvocation.outputProvider.getContentLocation(jar.name, jar.contentTypes,jar.scopes, Format.JAR)
            FileUtils.copyFile(jar.file, nextTransform)
        }
    }
    println "transform end"
}

在transform函數(shù)中有一個參數(shù)TransformInvocation,能夠獲取輸入,因?yàn)樽远xtransform是放在頭部,所以能夠獲取到的就是jar包、class字節(jié)碼等資源,如下:

public interface TransformInput {
    /**
     * Returns a collection of {@link JarInput}.
     */
    @NonNull
    Collection<JarInput> getJarInputs();
    /**
     * Returns a collection of {@link DirectoryInput}.
     */
    @NonNull
    Collection<DirectoryInput> getDirectoryInputs();
}

2.3.1 Jar包處理

對于jar包,我們不需要處理,直接作為輸出扔給下一級的transform處理,那么如何獲取到輸出,就是通過TransformInvocation獲取TransformOutputProvider,獲取輸出文件的位置,將jar包拷貝進(jìn)去即可

//對jar包不處理,直接扔給下一個Transform
input.jarInputs.each { jar ->
    println "jar包路徑  $jar.file.absolutePath"
    classPool.appendClassPath(jar.file.absolutePath)
    def nextTransform = transformInvocation.outputProvider.getContentLocation(jar.name, jar.contentTypes,jar.scopes, Format.JAR)
    FileUtils.copyFile(jar.file, nextTransform)
}

2.3.2 字節(jié)碼處理

對于字節(jié)碼處理,transform拿到的就是javac文件夾下的全部class文件

通過日志打印就能得知,只從這個位置取class文件

//對于class字節(jié)碼,需要處理
input.directoryInputs.each { dic ->
    println "dic路徑 $dic.file.absolutePath"
    classPool.appendClassPath(dic.file.absolutePath)
    //插入代碼 -- javassist
    //找到class在哪,需要遍歷class
    findTargetClass(dic.file, dic.file.absolutePath)
    def nextTransform = transformInvocation.outputProvider.getContentLocation(dic.name, dic.contentTypes, dic.scopes, Format.DIRECTORY)
    FileUtils.copyDirectory(dic.file, nextTransform)
}

在拿到classes文件夾根目錄之后,只需要遞歸遍歷這個文件夾,然后拿到全部的class文件,執(zhí)行代碼插入

/**
 * 遞歸查找class文件
 * @param file classes文件夾
 * @param fileName  ../build/javac/debug/classes 路徑名
 */
private void findTargetClass(File file, String fileName) {
    //遞歸查找
    if (file.isDirectory()) {
        file.listFiles().each {
            findTargetClass(it, fileName)
        }
    } else {
        //如果是文件
        modify(file, fileName)
    }
}

遞歸查找,我們拿本小節(jié)開始的那個圖,如果拿到了BuildConfig.class文件,那么就需要獲取當(dāng)前字節(jié)碼文件的全類名,然后從字節(jié)碼池子中獲取這個字節(jié)碼信息

/**
 * 獲取字節(jié)碼文件全類名
 * @param file   BuildConfig.class
 * @param fileName  ../build/javac/debug/classes 路徑名
 */
private void modify(File file, String fileName) {
    def fullName = file.absolutePath
    if (!fullName.endsWith(SdkConstants.DOT_CLASS)) {
        return
    }
    if (fileName.contains("BuildConfig.class") || fileName.contains("R")) {
        return
    }
    //獲取當(dāng)前class的全類名 com.tal.demo02.MainActivity.class
    def temp = fullName.replace(fileName, "").replace("/", ".")
    def className = temp.replace(SdkConstants.DOT_CLASS, "").substring(1)
    println "className $className"
    //從字節(jié)碼池中找到ctClass
    def ctClass = classPool.get(className)
    if (className.contains("com.tal.demo02")) {
        //如果是在當(dāng)前這個包名下的類,才會執(zhí)行插樁操作
        insertCode(ctClass, fileName)
    }
}

怎么獲取字節(jié)碼文件的全類名,其實(shí)這里是用了一個取巧的方式,因此我們能拿到字節(jié)碼文件所在的絕對路徑,然后把classes文件夾路徑去掉,將 / 替換為 . ,然后再把.class后綴去掉,就拿到了全類名。

2.4 Javassist織入代碼

前面我們已經(jīng)拿到了字節(jié)碼的全類名,那么就可以從Javassist提供的ClassPool字節(jié)碼池中,通過全類名獲取CtClass,CtClass包含了當(dāng)前字節(jié)碼的全部信息,可以通過類似反射的方式,來獲取方法、參數(shù)等屬性,加以構(gòu)造

2.4.1 ClassPool

ClassPool可以看做是一個字節(jié)碼池,在ClassPool中維護(hù)了一個Hashtable,key為類的名字也就是全類名,通過全類名能夠獲取CtClass

public ClassPool(ClassPool parent) {
    this.classes = new Hashtable(INIT_HASH_SIZE);
    this.source = new ClassPoolTail();
    this.parent = parent;
    if (parent == null) {
        CtClass[] pt = CtClass.primitiveTypes;
        for (int i = 0; i < pt.length; ++i)
            classes.put(pt[i].getName(), pt[i]);
    }
    this.cflow = null;
    this.compressCount = 0;
    clearImportedPackages();
}

在遍歷輸入文件的時候,我們把字節(jié)碼的路徑添加到ClassPool中,那么在查找的時候(調(diào)用get方法),其實(shí)就是從這個路徑下查找字節(jié)碼文件,如果查找到了就返回CtClass

classPool.appendClassPath(jar.file.absolutePath)

2.4.2 CtClass

通過CtClass能夠像使用反射的方式那樣獲取方法CtMethod

private void insertCode(CtClass ctClass, String fileName) {
    //拿到了這個類,需要反射獲取方法,在某些方法下面加
    try {
        def method = ctClass.getDeclaredMethod("getClassId")
        if(method != null){
            //在這個方法之前插入
            method.insertBefore("if(a &gt; 0){\n" +
                    "            \n" +
                    "            return \"\";\n" +
                    "        }")
            ctClass.writeFile(fileName)
        }
    }catch(Exception e){
    }finally{
        ctClass.detach()
    }
}

通過CtMethod可以設(shè)置,在方法之前、方法之后、或者方法中某個行號中插入代碼,最終通過CtClass的writeFile方法,將字節(jié)碼重新規(guī)整,最終像處理Jar文件一樣,將處理的文件交給下一級的transform處理。

最終可以看一下效果,在MainActivity中一個getClassId方法,一開始只是返回了id_0009989799,我們將一部分代碼織入后,字節(jié)碼變成下面的樣子。

 public String getClassId() {
    return this.a > 0 ? "" : "id_0009989799";
 }

所以,美團(tuán)Robust在熱修復(fù)時,是以同樣的方式(美團(tuán)采用的是ASM字節(jié)碼插樁,本文使用的是Javassist),在每個方法中織入了一段判斷邏輯代碼,當(dāng)線上出現(xiàn)問題之后,通過某種方式使得代碼執(zhí)行這個判斷邏輯,實(shí)現(xiàn)了即時修復(fù)

以上就是Android進(jìn)階從字節(jié)碼插樁技術(shù)了解美團(tuán)熱修復(fù)實(shí)例詳解的詳細(xì)內(nèi)容,更多關(guān)于Android 美團(tuán)熱修復(fù)的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 學(xué)習(xí)使用Android Chronometer計(jì)時器

    學(xué)習(xí)使用Android Chronometer計(jì)時器

    Chronometer是一個簡單的計(jì)時器,你可以給它一個開始時間,并以計(jì)時,或者如果你不給它一個開始時間,它將會使用你的時間通話開始,這篇文章主要幫助大家學(xué)習(xí)掌握使用Android Chronometer計(jì)時器,感興趣的小伙伴們可以參考一下
    2016-04-04
  • Android textview 實(shí)現(xiàn)長按自由選擇復(fù)制功能的方法

    Android textview 實(shí)現(xiàn)長按自由選擇復(fù)制功能的方法

    下面小編就為大家?guī)硪黄狝ndroid textview 實(shí)現(xiàn)長按自由選擇復(fù)制功能的方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-04-04
  • android自定義環(huán)形對比圖效果

    android自定義環(huán)形對比圖效果

    這篇文章主要為大家詳細(xì)介紹了android自定義環(huán)形對比圖,外環(huán)有類似進(jìn)度條的旋轉(zhuǎn)動畫,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-10-10
  • rxjava+retrofit實(shí)現(xiàn)多圖上傳實(shí)例代碼

    rxjava+retrofit實(shí)現(xiàn)多圖上傳實(shí)例代碼

    本篇文章主要介紹了rxjava+retrofit實(shí)現(xiàn)多圖上傳實(shí)例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-06-06
  • Android自動獲取輸入短信驗(yàn)證碼庫AutoVerifyCode詳解

    Android自動獲取輸入短信驗(yàn)證碼庫AutoVerifyCode詳解

    這篇文章主要為大家詳細(xì)介紹了Android自動獲取輸入短信驗(yàn)證碼庫,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-07-07
  • Kotlin協(xié)程上下文與上下文元素深入理解

    Kotlin協(xié)程上下文與上下文元素深入理解

    協(xié)程上下文是一個有索引的Element實(shí)例集合,每個element在這個集合里有一個唯一的key;協(xié)程上下文包含用戶定義的一些數(shù)據(jù)集合,這些數(shù)據(jù)與協(xié)程密切相關(guān);協(xié)程上下文用于控制線程行為、協(xié)程的生命周期、異常以及調(diào)試
    2022-08-08
  • Android獲取SHA1的方法

    Android獲取SHA1的方法

    這篇文章主要介紹了Android獲取SHA1的方法,需要的朋友可以參考下
    2017-12-12
  • Android?Flutter實(shí)現(xiàn)創(chuàng)意時鐘的示例代碼

    Android?Flutter實(shí)現(xiàn)創(chuàng)意時鐘的示例代碼

    時鐘這個東西很奇妙,總能當(dāng)做創(chuàng)意實(shí)現(xiàn)的入口。這篇文章主要介紹了如何通過Android?Flutter實(shí)現(xiàn)一個創(chuàng)意時鐘,感興趣的小伙伴可以了解一下
    2023-03-03
  • RecyclerView 源碼淺析測量 布局 繪制 預(yù)布局

    RecyclerView 源碼淺析測量 布局 繪制 預(yù)布局

    這篇文章主要介紹了RecyclerView 源碼淺析測量 布局 繪制 預(yù)布局,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12
  • Android 百分比布局詳解及實(shí)例代碼

    Android 百分比布局詳解及實(shí)例代碼

    這篇文章主要介紹了Android 百分比布局詳解及實(shí)例代碼的相關(guān)資料,這里附有代碼實(shí)例幫助大家學(xué)習(xí)參考,如何實(shí)現(xiàn)百分比布局,需要的朋友可以參考下
    2016-11-11

最新評論