【Android】熱修復——Tinker(入門)

前言

不知你是否遇到這樣的情況?千辛萬苦上開發(fā)了一個版本,好不容易上線了,突然發(fā)現(xiàn)了一個嚴重bug需要進行緊急修復,怎么辦?難道又要重新打包App、測試,發(fā)布新個版本?就為了修改一兩行的代碼?
莫慌,這種問題其實可以分分鐘解決。如果你學會了這項黑科技——熱修復。
在用戶使用App的時候,不知不覺,這個Bug就被修復了。


莫慌

熱修復:熱修復(也稱熱補丁、熱修復補丁,英語:hotfix)是一種包含信息的獨立的累積更新包,通常表現(xiàn)為一個或多個文件。這被用來解決軟件產(chǎn)品的問題(例如一個程序錯誤)?!S基百科

本文介紹了Tinker的接入方式,更加詳細的內(nèi)容可以查閱官方文檔

介紹

Tinker是微信官方的Android熱補丁解決方案,它支持動態(tài)下發(fā)代碼、So庫以及資源,讓應用能夠在不需要重新安裝的情況下實現(xiàn)更新。當然,你也可以使用Tinker來更新你的插件。
Tinker所支持的功能如下


來自官方Github

Tinker熱補丁方案·不僅支持類、So以及資源的替換,它還是2.X-7.X的全平臺支持。

接入

進入正題吧

額,我是說進入正題

在項目的build.gradle

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        // TinkerPatch 插件
        classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:1.1.4"
    }
}

然后在appgradle文件app/build.gradle

dependencies {
    // 若使用annotation需要單獨引用,對于tinker的其他庫都無需再引用
    provided("com.tencent.tinker:tinker-android-anno:1.7.7")
    compile("com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.1.4")
}

在app目錄下,創(chuàng)建tinkerpatch.gradle(可以去后面的鏈接下載源碼,把這個文件拷進去)

tinkerpatch.gradle

將 TinkerPatch 相關的配置都放于tinkerpatch.gradle中,然后在app的gradle文件app/build.gradle中還添加

apply from: 'tinkerpatch.gradle'

完整的app/buidl.gradle:

apply plugin: 'com.android.application'
apply from: 'tinkerpatch.gradle'
android {
    ...
}
dependencies {
    .....
    // 若使用annotation需要單獨引用,對于tinker的其他庫都無需再引用
    provided("com.tencent.tinker:tinker-android-anno:1.7.7")
    compile("com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.1.4")
}

配置參數(shù)

打開之前創(chuàng)建的tinkerpatch.gradle添加

apply plugin: 'tinkerpatch-support'

/**
 * TODO: 請按自己的需求修改為適應自己工程的參數(shù)
 */
def bakPath = file("${buildDir}/bakApk/")
def baseInfo = "app-1.0.0-0330-20-37-31"
def variantName = "release"

/**
 * 對于插件各參數(shù)的詳細解析請參考
 * http://tinkerpatch.com/Docs/SDK
 */
tinkerpatchSupport {
    /** 可以在debug的時候關閉 tinkerPatch **/
    tinkerEnable = true
    reflectApplication = true

    autoBackupApkPath = "${bakPath}"

    appKey = "你的appkey"http:// 注意?。?!  需要修改成你的appkey

    /** 注意: 若發(fā)布新的全量包, appVersion一定要更新 **/
    appVersion = "1.0.0"

    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}"
    }
}
/**
 * 一般來說,我們無需對下面的參數(shù)做任何的修改
 * 對于各參數(shù)的詳細介紹請參考:
 * 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
    }
}
  • 參數(shù)說明
  • bakPath:基包路徑
  • baseInfo:基包文件夾名(打補丁包的時候,需要修改)
  • appKey:進入官網(wǎng)注冊一個賬號,新增APP,得到對應的appKey。
    官方參數(shù)說明

其中appKey在新增的APP中可以看到

appKey

使用

創(chuàng)建FetchPatchHandler用于檢測是否更新(剛打開時會檢測一次)

public class FetchPatchHandler extends Handler {
    public static final long HOUR_INTERVAL = 3600 * 1000;
    private long checkInterval;

    /**
     * 通過handler, 達到按照時間間隔輪訓的效果
     * @param hour
     */
    public void fetchPatchWithInterval(int hour) {
        //設置TinkerPatch的時間間隔
        TinkerPatch.with().setFetchPatchIntervalByHours(hour);
        checkInterval = hour * HOUR_INTERVAL;
        //立刻嘗試去訪問,檢查是否有更新
        sendEmptyMessage(0);
    }
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);

        //這里使用false即可
        TinkerPatch.with().fetchPatchUpdate(false);
        //每隔一段時間都去訪問后臺, 增加10分鐘的buffer時間
        sendEmptyMessageDelayed(0, checkInterval + 10 * 60 * 1000);
    }
}

創(chuàng)建MyApplication初始化(reflectApplication = true 時)

public class MyApplication extends Application{
    private ApplicationLike tinkerApplicationLike;
    @Override
    public void onCreate() {
        super.onCreate();
        if (BuildConfig.TINKER_ENABLE) {

            // 我們可以從這里獲得Tinker加載過程的信息
            tinkerApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike();

            // 初始化TinkerPatch SDK, 更多配置可參照API章節(jié)中的,初始化SDK
            TinkerPatch.init(tinkerApplicationLike)
                    .reflectPatchLibrary()
                    .setPatchRollbackOnScreenOff(true)
                    .setPatchRestartOnSrceenOff(true);

            // 每隔3個小時去訪問后臺時候有更新,通過handler實現(xiàn)輪訓的效果
            new FetchPatchHandler().fetchPatchWithInterval(3);
        }
    }
}

最后在記得在Manifest.xml中配置Application,還要給SD卡讀寫權限

<manifest >
    .....
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <application
       ...
       android:name=".MyApplication">
</manifest>

注意:初始化的代碼建議緊跟 super.onCreate(),并且所有進程都需要初始化,已達到所有進程都可以被 patch 的目的
如果你確定只想在主進程中初始化 tinkerPatch,那也請至少在 :patch 進程中初始化,否則會有嚴重的 crash 問題

打生產(chǎn)包

注意:打包前記得配置簽名??蓞⒖荚创a

每次開發(fā)完成后,開始打包。
打開Studio右側的Gradle,選擇assemableRelease打正式包

Gradle

完成后可以在文件夾build中找到生成的文件(這里稱為基包)

結果

打開build -> bakApk -> app-1.0.0-0330-21-40-52 (根據(jù)時間命名)
release文件夾中會出現(xiàn)我們剛打完的包。一個apk,對應一個txt文件。
將apk安裝到手機上,這是一個剛創(chuàng)建的項目,里面只有Hello World

app-1.0.0-0330-21-40-52備份,打補丁包的時候需要用到。
多渠道打包時會根據(jù)渠道名分包,目錄結構相似。

生成補丁包

這里模擬一個修補bug的場景,發(fā)一個熱補丁,彈一個Toast。
注意:在打生產(chǎn)包的代碼上做修改

Toast.makeText(this, "這是個補丁", Toast.LENGTH_SHORT).show();
  • 將之前的備份好的基包復制到build/bakApk下,打開tinkerpatch.gradle修改baseInfo成對應的文件名

    修改baseInfo

  • 修改tinkerpatch.gradle中的tinkerpatchSupport -> appVersion

    appVersion

  • 完成后打開Gradle,如下選擇tinkerPatchRelease

    Gradle

    補丁包將位于 build/outputs/tinkerPatch 中,這里只需要用到patch_signed_7zip.apk
    補丁包

發(fā)布

最后,只需要將剛生成的補丁包發(fā)布,然后靜靜等待即可。

  • 選擇對應的app,添加APP版本
    添加APP版本

    添加版本

    版本號對應tinkerpatch.gradle中的appVersion
  • 選擇patch_signed_7zip.apk文件,提交即可(更多下發(fā)選項,參考官方文檔)
    發(fā)布補丁
  • 提交后,查看補丁的下載數(shù)量以及成功應用數(shù)


    補丁詳情

這時候重新打開app,等待補丁下載。下載完成后關閉app,再次打開,查看結果。就這樣,整個熱修復的流程就完成了。
注意:一定要關閉后打開,熱修復才會生效。

結果

關于兼容多渠道包

關于渠道包的問題,若使用flavor編譯渠道包,會導致不同的渠道包由于BuildConfig變化導致classes.dex差異。這里建議的方式有:

  • 將渠道信息寫在AndroidManifest.xml或文件中,例如channel.ini;
  • 將渠道信息寫在apk文件的zip comment中,這種是建議方式,例如可以使用項目packer-ng-plugin或者可使用V2 Scheme的walle
  • 若不同渠道存在功能上的差異,建議將差異部分放于單獨的dex或采用相同代碼不同配置方式實現(xiàn);

已通過Walle實現(xiàn)【Android】Walle多渠道打包&Tinker熱修復

Tinker已知的問題:

  1. Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大組件;
  2. 由于Google Play的開發(fā)者條款限制,不建議在GP渠道動態(tài)更新代碼;
  3. Android N上,補丁對應用啟動時間有輕微的影響;
  4. 不支持部分三星android-21機型,加載補丁時會主動拋出TinkerRuntimeException:checkDexInstall failed;
  5. 由于各個廠商的加固實現(xiàn)并不一致,在1.7.6以及之后的版本,tinker不再支持加固的動態(tài)更新;
    (在官方——對加固的支持看到相關的內(nèi)容,說是支持多種加固,不過我沒試過 2017.7.3)
  6. 對于資源替換,不支持修改remoteView。例如transition動畫,notification icon以及桌面圖標。

Tips:

  • dubug模式下,tinkerpatch.gradle —> tinkerpatchSupport—>tinkerEnable需要改為false
  • 添加SD卡權限,
  • 下載補丁后,殺掉進程重新打開,補丁才會生效
  • 補丁非即時生效,需要等一會兒

源碼地址

Github

參考

官方文檔
接入指南

以上有錯誤之處,感謝指出

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容