【Android】使用Bugly快速接入Tinker熱更新功能

再見理想2017發表於2016-12-09

前言

公司專案的Bug、Crash統計一直用的的騰訊的 Bugly ,今日發現多了一個熱更新(HotFix)功能,使用的技術是微信的方案 Tinker ,再三考慮後決定接入這個功能。

Tinker介紹

【Android】使用Bugly快速接入Tinker熱更新功能

Tinker是微信官方的Android熱補丁解決方案,它支援動態下發程式碼、So庫以及資源,讓應用能夠在不需要重新安裝的情況下實現更新。當然,你也可以使用Tinker來更新你的外掛。

Tinker與其他熱更新外掛的比較:

【Android】使用Bugly快速接入Tinker熱更新功能

其他關於Tinker的介紹請看Tinker的 wiki

開始接入

我們選擇Tinker的道理很簡單:穩定、開發透明。
而我們選擇通過Bugly來接入Tinker,因為它簡單,不需要我們再提供補丁下載介面和補丁管理後臺。

1、在專案的根build.gradle中新增依賴

// tinker gradle外掛
classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.5')
// tinkersupport外掛
classpath "com.tencent.bugly:tinker-support:latest.release"複製程式碼

2、在專案主module的build.gradle中新增(可先原樣複製)

apply plugin: 'com.tencent.bugly.tinker-support'
def bakPath = file("${buildDir}/bakApk/")

ext {
    // for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true

    // for normal build
    // old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/app-release.apk"
    // proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/mapping.txt"
    // resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/R.txt"
    // only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-1124-23-03-52"
}

def getOldApkPath() {
    return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}

def getApplyMappingPath() {
    return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
    return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}

def getTinkerIdValue() {
    return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
}

def buildWithTinker() {
    return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

// 注意:必須要配置tinker-support
tinkerSupport {
    enable = true
}

// 全域性資訊相關配置項
tinkerPatch {
    oldApk = getOldApkPath()  //打補丁包時必選, 基準包路徑

    ignoreWarning = false // 可選,預設false
    useSign = true // 可選,預設true, 驗證基準apk和patch簽名是否一致

    // 編譯相關配置項
    buildConfig {
        applyMapping = getApplyMappingPath() //  可選,設定mapping檔案,建議保持舊apk的proguard混淆方式
        applyResourceMapping = getApplyResourceMappingPath()      // 可選,設定R.txt檔案,通過舊apk檔案保持ResId的分配
        tinkerId = "v1.0.0"  // 必選,當前版本的唯一標識,可以是git版本號、versionName
    }

    // dex相關配置項
    dex {
        dexMode = "jar" // 可選,預設為jar
        usePreGeneratedPatchDex = false // 可選,預設為false
        pattern = ["classes*.dex",
                   "assets/secondary-dex-?.jar"]
        loader = ["com.tencent.tinker.loader.*",  // 必選
                  "com.huidr.patient.HuiTinkerApplication",
        ]
    }

    // lib相關的配置項
    lib {
        pattern = ["lib/armeabi/*.so"]
    }

    // res相關的配置項
    res {
        pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
        ignoreChange = ["assets/sample_meta.txt"]
        largeModSize = 100
    }

    // 用於生成補丁包中的'package_meta.txt'檔案
    packageConfig {
        configField("patchMessage", "tinker is sample to use")
        configField("platform", "all")
        configField("patchVersion", "1.0")
    }

    // 7zip路徑配置項,執行前提是useSign為true
    sevenZip {
        zipArtifact = "com.tencent.mm:SevenZip:1.1.10" // optional
    }
}複製程式碼

3、建立Application的代理類(可直接複製)

我們之前的自定義Application類現在是不能直接繼承Application了,需要改成繼承Tinker提供的DefaultApplicationLike類

/**
 * Application代理類
 */
public class ApplicationDelegate extends DefaultApplicationLike {

    public ApplicationDelegate(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent, Resources[] resources, ClassLoader[] classLoader, AssetManager[] assetManager) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent, resources, classLoader, assetManager);
    }

    @Override
    public void onCreate() {
        super.onCreate();

        if (BuildConfig.RELEASE) {
            Bugly.init(getApplication(), BuildConfig.buglyAppId, false);
        } else {
            Bugly.init(getApplication(), BuildConfig.buglyAppId, true);
        }
    }


    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        // you must install multiDex whatever tinker is installed!
        MultiDex.install(base);
        // 安裝tinker
        Beta.installTinker(this);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks) {
        getApplication().registerActivityLifecycleCallbacks(callbacks);
    }
}複製程式碼

這個代理的Application類的時候方法和原來一樣,只不過它的呼叫是由Tinker來呼叫了,而這個呼叫者就是我們即將要定義的Application。

定義TinkerApplication

/**
 * TinkerApplication.
 * <p>
 * 注意:這個類整合TinkerApplication類,這裡面不做任何操作,所有Application的程式碼都會放到ApplicationLike繼承類當中<br/>
 * <pre>
 * 引數解析:
 * 引數1:int tinkerFlags 表示Tinker支援的型別 dex only、library only or all suuport,default: TINKER_ENABLE_ALL
 * 引數2:String delegateClassName Application代理類 這裡填寫你自定義的ApplicationLike
 * 引數3:String loaderClassName  Tinker的載入器,使用預設即可
 * 引數4:boolean tinkerLoadVerifyFlag  載入dex或者lib是否驗證md5,預設為false
 * </pre>
 */
public class HuiTinkerApplication extends TinkerApplication {

    public HuiTinkerApplication() {
        super(ShareConstants.TINKER_ENABLE_ALL, "com.huidr.patient.ApplicationDelegate",
                "com.tencent.tinker.loader.TinkerLoader", false);
    }

}複製程式碼

這裡構造方法裡的"com.huidr.patient.ApplicationDelegate"改成剛剛定義的Application代理類。
同時在AndroidMenifest.xml中把Application改成新定義的Appcation。
到這裡,配置已經完成了。

打包基線版本

回到專案主module的build.gradle中,找到:

// dex相關配置項
    dex {
        dexMode = "jar" // 可選,預設為jar
        usePreGeneratedPatchDex = false // 可選,預設為false
        pattern = ["classes*.dex",
                   "assets/secondary-dex-?.jar"]
        loader = ["com.tencent.tinker.loader.*",  // 必選
                  "com.huidr.patient.HuiTinkerApplication",
        ]
    }複製程式碼

將這裡配置的Applicaiton改成你剛剛建立的。

現在可以打包出ralease版本,得到app-release.apk檔案,將其執行起來。

生成熱更新補丁

現在主要關注這部分配置就可以了

 // 編譯相關配置項
    buildConfig {
        applyMapping = getApplyMappingPath() //  可選,設定mapping檔案,建議保持舊apk的proguard混淆方式
        applyResourceMapping = getApplyResourceMappingPath()      // 可選,設定R.txt檔案,通過舊apk檔案保持ResId的分配
        tinkerId = "v1.0.0"  // 必選,當前版本的唯一標識,可以是git版本號、versionName
    }複製程式碼

【Android】使用Bugly快速接入Tinker熱更新功能

在build資料夾下建立bakApk資料夾(資料夾的名稱在配置中可以修改),並把app-release.apk、mapping.txt檔案、R.txt檔案複製進來。
Mapping檔案位置:app\build\outputs\mapping\release\mapping.txt
R.txt檔案位置:app\build\intermediates\symbols\release\R.txt

最後修改tinkerId(當前版本的唯一標識,可以是git版本號、versionName)

【Android】使用Bugly快速接入Tinker熱更新功能

執行後得到補丁app\build\outputs\patch\release\patch_signed_7zip.apk

上傳補丁並下發

這裡最好把patch_signed_7zip.apk副檔名改成.zip,防止有些手機廠商對apk檔案劫持。
開啟Bugly熱更新後臺介面,選擇釋出補丁,並下發

【Android】使用Bugly快速接入Tinker熱更新功能

現在將App退出重新開啟2次,就可以看到更新後的結果了
第一次開啟會監測補丁版本;
第二次開啟會安裝新補丁。

ps:如果發現補丁有問題,還可以使用撤回功能,讓App回覆補丁前的樣子。

結尾

感謝在一線上為熱更新奮戰的程式設計師,這是一條跪著走完的道路,正是有了他們背後的默默付出,我們才能這麼方便地用上熱更新功能,致敬!

以上整合過程主要參考以下文件:
Bugly Android熱更新使用指南
Tinker 接入指南

相關文章