欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

引言

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

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

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

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

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

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

1 插件發(fā)布

初始化之后,我一般會(huì)先建2個(gè)文件夾

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

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

如果需要發(fā)布插件到maven倉(cāng)庫(kù),或者放在本地,可以通過(guò)maven-publish(gradle 7.0+)插件來(lái)實(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倉(cāng)庫(kù)的地址,這里就是寫在本地一個(gè)文件夾

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

接下來(lái)我們嘗試用下這個(gè)插件

buildscript {
    repositories {
        google()
        mavenCentral()
        jcenter()
        //這里配置了我們的插件依賴的本地倉(cāng)庫(kù)地址
        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'

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

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

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

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

2 Javassist

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

2.1 準(zhǔn)備工作

引入Javassist,插件工程引入Javassist

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

2.2 Transform

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

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

在build文件夾中,我們可以看到這些文件夾,像merged_assets、merged_java_res等,這是Gradle的Transform,用于打包資源文件到apk文件中,執(zhí)行的順序?yàn)榇袌?zhí)行,一個(gè)任務(wù)的輸出為下一個(gè)任務(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
    }
    /**
     * 作用域:整個(gè)項(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生效,需要在插件中注冊(cè)這個(gè)Transform

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

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

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

Task :app:transformClassesWithMyRobustForDebug

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

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

 @Override
void transform(TransformInvocation transformInvocation)  {
    super.transform(transformInvocation)
    println "transform start"
    transformInvocation.inputs.each { input ->
        //對(duì)于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)
        }
        //對(duì)jar包不處理,直接扔給下一個(gè)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ù)中有一個(gè)參數(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包處理

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

//對(duì)jar包不處理,直接扔給下一個(gè)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é)碼處理

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

通過(guò)日志打印就能得知,只從這個(gè)位置取class文件

//對(duì)于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文件夾根目錄之后,只需要遞歸遍歷這個(gè)文件夾,然后拿到全部的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é)開(kāi)始的那個(gè)圖,如果拿到了BuildConfig.class文件,那么就需要獲取當(dāng)前字節(jié)碼文件的全類名,然后從字節(jié)碼池子中獲取這個(gè)字節(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)前這個(gè)包名下的類,才會(huì)執(zhí)行插樁操作
        insertCode(ctClass, fileName)
    }
}

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

2.4 Javassist織入代碼

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

2.4.1 ClassPool

ClassPool可以看做是一個(gè)字節(jié)碼池,在ClassPool中維護(hù)了一個(gè)Hashtable,key為類的名字也就是全類名,通過(guò)全類名能夠獲取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();
}

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

classPool.appendClassPath(jar.file.absolutePath)

2.4.2 CtClass

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

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

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

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

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

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

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

相關(guān)文章

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

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

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

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

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

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

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

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

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

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

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

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

    協(xié)程上下文是一個(gè)有索引的Element實(shí)例集合,每個(gè)element在這個(gè)集合里有一個(gè)唯一的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)意時(shí)鐘的示例代碼

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

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

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

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

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

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

最新評(píng)論