Android熱修復使用詳解

概述

熱修復是一種動態修復程序解決問題的思想,其特點如下:

  • 無需發版,實現高效Bug修復,用戶無感知
  • 無需下載新應用,只需下載補丁包,代價小
  • 修復成功率高,降低緊急Bug的損失

原理介紹

大致有三種方案:底層Native替換方案、Java層類加載方案

Java層類加載方案

類加載方案基於Dex分包方案,Dex分包方案則是因爲65536限制和LinearAlloc限制。

  • 65536限制就是DVM指令集的方法調用指令invoke-kind索引爲16bits,最多隻能引用65535個方法,超過則會編譯失敗。
  • LinearAlloc限制就是LinearAlloc是DVM中一個固定的緩存區,方法數超過混存取大小會在安裝時提示INSTALL_FAILED_DEXOPT錯誤。
    Dex分包方案就是打包時將應用代碼分成多個Dex文件,啓動必須用到的類放到主Dex中,其他代碼放到次Dex中,應用啓動先加載主Dex把應用啓動起來,再動聽加載次Dex,從而解決65536限制和LinearAlloc限制的問題。
    說回類加載方案,當應用在加載一個類的時候 他會利用ClassLoader機制去尋找這個類,關鍵代碼在DexPathList.findClass()函數中
 public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {//1
            Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
            if (clazz != null) {
                return clazz;
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

Element代表了一個dex文件,其內部封裝了DexFile用於加載dex文件,由於上面的分包方案一個應用有了多個dex,而dexElements則有序的存儲了所有dex。這邊findClass則會遍歷dexElements在各個dex中按順序查找所需類,找到後就返回並終止查找。類加載方案則是將要修改的.class文件打包成包含dex的patch.jar,App下載拿到後然後將這個dex放到dexElements列表的首位或者利用其它方法讓patch.jar中的class排在前面位置,則要修改的.class則會先被找到,存在bug的舊.class根據ClassLoader的雙親委派模式就不會被加載

PS:由於類加載後不會卸載,class被findClass一次後不需要findClass第二次所以需要重新啓動應用,重新findClass纔會生效。

底層Native替換方案

利用Native反射替換要修復的類的方法的信息(執行入口、訪問權限、所屬類、代碼執行地址等)
PS:即時生效,但由於基於Native層直接替換原有類,限制多,無法增減原有類的方法和字段

Android 熱修復原理篇及幾大方案比較

方案對比

特性 AndFix(阿里) Hotfix(阿里) Sophix(阿里) 超級補丁(QQ空間) Tinker(微信) Amigo(餓了麼) Robust(美團) Aceso(美麗說蘑菇街)
即時生效 同時支持即時生效和冷啓動修復
方法替換
類替換
資源替換
so替換
支持ART

Tinker使用介紹

  1. http://www.tinkerpatch.com 平臺註冊一個app並獲取appKey
  2. 根據 http://www.tinkerpatch.com/Docs/SDK 文檔做配置
  3. build.gradle.中
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {

    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.2'
        // TinkerPatch 插件
        classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:1.2.6"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
  1. app/build.gradle中,主要是簽名和tinker的包
apply plugin: 'com.android.application'
apply from: 'tinkerpatch.gradle'
android {
    signingConfigs {
        release {
            keyAlias 'key0'
            keyPassword 'tinker'
            storeFile file('../tinker.jks')
            storePassword 'tinker'
        }
    }
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.android.commonlib.tinkertest"
        minSdkVersion 19
        targetSdkVersion 26
        versionCode 1
        versionName "1.0.1"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
        debug {
            signingConfig signingConfigs.release
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    implementation "com.android.support:multidex:1.0.2"
    //若使用annotation需要單獨引用,對於tinker的其他庫都無需再引用
    annotationProcessor("com.tinkerpatch.tinker:tinker-android-anno:1.9.6") { changing = true }
    compileOnly("com.tinkerpatch.tinker:tinker-android-anno:1.9.6") { changing = true }
    implementation("com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.2.6") { changing = true }
}
  1. Application
public class SampleApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        initTinkerPatch();
    }

    private void initTinkerPatch() {
        // 我們可以從這裏獲得Tinker加載過程的信息
        ApplicationLike tinkerApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike();
        // 初始化TinkerPatch SDK
        TinkerPatch.init(
                tinkerApplicationLike
//                new TinkerPatch.Builder(tinkerApplicationLike)
//                    .requestLoader(new OkHttp3Loader())
//                    .build()
        )
                .reflectPatchLibrary()
                .setPatchRollbackOnScreenOff(true)
                .setPatchRestartOnSrceenOff(true)
                .setFetchPatchIntervalByHours(3)
        ;
        // 獲取當前的補丁版本

        // fetchPatchUpdateAndPollWithInterval 與 fetchPatchUpdate(false)
        // 不同的是,會通過handler的方式去輪詢
        TinkerPatch.with().fetchPatchUpdateAndPollWithInterval();
    }

    @Override
    public void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //you must install multiDex whatever tinker is installed!
        MultiDex.install(base);
    }
}
  1. tinkerpatch.gradle
apply plugin: 'tinkerpatch-support'

/**
 * TODO: 請按自己的需求修改爲適應自己工程的參數
 */
def bakPath = file("${buildDir}/bakApk/")
def baseInfo = "app-1.0.1-0525-15-59-41"
def variantName = "release"

/**
 * 對於插件各參數的詳細解析請參考
 * http://tinkerpatch.com/Docs/SDK
 */
tinkerpatchSupport {
    /** 可以在debug的時候關閉 tinkerPatch **/
    /** 當disable tinker的時候需要添加multiDexKeepProguard和proguardFiles,
     這些配置文件本身由tinkerPatch的插件自動添加,當你disable後需要手動添加
     你可以copy本示例中的proguardRules.pro和tinkerMultidexKeep.pro,
     需要你手動修改'tinker.sample.android.app'本示例的包名爲你自己的包名, com.xxx前綴的包名不用修改
     **/
    tinkerEnable = true
    reflectApplication = true
    /**
     * 是否開啓加固模式,只能在APK將要進行加固時使用,否則會patch失敗。
     * 如果只在某個渠道使用了加固,可使用多flavors配置
     **/
    protectedApp = false
    /**
     * 實驗功能
     * 補丁是否支持新增 Activity (新增Activity的exported屬性必須爲false)
     **/
    supportComponent = true

    autoBackupApkPath = "${bakPath}"

    appKey = "a2fc5f63bf186415"

    /** 注意: 若發佈新的全量包, appVersion一定要更新 **/
    appVersion = "1.0.1"

    def pathPrefix = "${bakPath}/${baseInfo}/${variantName}/"
    def name = "${project.name}-${variantName}"

    baseApkFile = "${pathPrefix}/${name}.apk"
    baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
    baseResourceRFile = "${pathPrefix}/${name}-R.txt"

    /**
     *  若有編譯多flavors需求, 可以參照: https://github.com/TinkerPatch/tinkerpatch-flavors-sample
     *  注意: 除非你不同的flavor代碼是不一樣的,不然建議採用zip comment或者文件方式生成渠道信息(相關工具:walle 或者 packer-ng)
     **/
}

/**
 * 用於用戶在代碼中判斷tinkerPatch是否被使能
 */
android {
    defaultConfig {
        buildConfigField "boolean", "TINKER_ENABLE", "${tinkerpatchSupport.tinkerEnable}"
    }
}

/**
 1. 一般來說,我們無需對下面的參數做任何的修改
 2. 對於各參數的詳細介紹請參考:
 3. https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
 */
tinkerPatch {
    ignoreWarning = false
    useSign = true
    dex {
        dexMode = "jar"
        pattern = ["classes*.dex"]
        loader = []
    }
    lib {
        pattern = ["lib/*/*.so"]
    }

    res {
        pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
        ignoreChange = []
        largeModSize = 100
    }

    packageConfig {
    }
    sevenZip {
        zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
//        path = "/usr/local/bin/7za"
    }
    buildConfig {
        keepDexApply = false
    }
}

這裏寫圖片描述
配置完成,測試下,先運行assembleRelease模擬打正式包,生成文件如下(即app-release.apk)安裝在手機上。
這裏寫圖片描述
每次編譯都會生成一個基準包,這邊此次生成的基準包放在app-1.0.1-0525-15-59-41目錄中,修改tinkerpatch.gradle中baseInfo的值爲【app-1.0.1-0525-15-59-41】這個目錄。
然後隨意修改下我們的代碼,表示我們要打的補丁。之後再運行tinkerPatchRelease就會基於這個基準包生成補丁包了。即tinker_result/patch_signed_7zip.apk

  1. 發佈到tinkerpatch平臺,我們剛剛的appVersion是1.0.1,在tinkerpatch中建一個1.0.1的版本,然後下發補丁即可。
  2. 建議本地先測試下,可以將補丁包發送到本地目錄下,然後調用以下代碼即可自動打補丁,重啓應用即可生效。如果有錯誤也可以從log中看出
 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView textView = findViewById(R.id.textview);
        textView.setText("...Hello World...");

        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
            }
        });
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章