微信Tinker在Android中集成以及使用

Tinker項目(點擊進入)

Tinker是微信官方的Android熱補丁解決方案,它支持動態下發代碼。.so庫以及資源庫,讓應用可以在不須要重複安裝的狀況下實現更新,固然也可使用Tinker來更新你的插件。css

Tinker原理圖

在接入Tinker以前咱們先對Tinker的結構瞭解一下java

Tinker主要包括一下幾個部分:

1.gradle編譯插件:tinker-patch-gradle-plugin。
2.核心SDK庫:tinker-android-lib。
3.非gradle編譯用戶的命令行版本:tinker-patch-cil.jar。android

Tinker的已知問題:

1.Tinker不支持修改AndroidMainfest.xml,Tinker不支持新增四大組件。
2.因爲Google Pay的開發者條款限制,不建議在GP渠道動態更新代碼。
3.在Android N上,補丁對應用啓動時有輕微的影響。
4.不支持部分三星android-21機型,加載補丁時會主動拋出「TinkerRuntimeException:checkDexInstall failed」異常。
5.因爲各個廠商加固實現並不一致,在1.7.6之後的版本,Tinker不在支持加固的動態更新。
6.對於資源替換,不支持修改remoteView,例如transition動畫,notification icon以及桌面圖標。git

Tinker的修復方案跟Hotfix的修復方案大同小異,都是在兩個apk包上做比較而後生成patch。下面對Tinker進行接入。github

在工程目錄下的build.gradle中添加依賴庫
buildscript { repositories { jcenter() }
        dependencies { classpath 'com.android.tools.build:gradle:2.2.3' // 編譯插件tinker-patch-gradle-plugin classpath 'com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7' }
    }
    allprojects { repositories { jcenter() }
    }
    task clean(type: Delete) { delete rootProject.buildDir }
在工程app目錄下的build.gradle中添加依賴庫
dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        ......
        //可選,用於生成application類
        provided('com.tencent.tinker:tinker-android-anno:1.7.7')
        //tinker的核心庫
        compile('com.tencent.tinker:tinker-android-lib:1.7.7')
    }
API引入

通常咱們都是在Application中onCreate()中作初始化和加載patch,不過Tinker推薦以下寫法。由於程序啓動時會加載默認的Application類,這致使咱們補丁包是沒法對它作修改了。爲了規避這個問題Tinker經過代碼框架的方式來避免,這也是爲了儘可能少的去反射,提高框架的兼容性。web

@DefaultLifeCycle(
            application = ".AppContext", flags = ShareConstants.TINKER_ENABLE_ALL
    )
    public class AppContextLike extends ApplicationLike { 
 
   


        public AppContextLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
            super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
        }

        @Override
        public void onCreate() {
            super.onCreate();
            TinkerInstaller.install(this);
        }
    }

代碼中AppContextLike繼承了ApplicationLike,而ApplicationLike並不是集成Application,而是相似於Application的一個類。Tinker建議編寫一個ApplicationLike的子類,能夠當作Application使用,注意頂部的註解:@DefaultLifeCycle,其application屬性,會在編譯期生成一個.AppContext類。因此咱們在AndroidManifest.xml中的application標籤下這樣寫:算法

<application
        android:name=".AppContext"
        ......
    </application>

寫完後會報紅,此時只須要Build下便可解決報紅。Application配置就到此結束。接下來生成patch文件。由於patch文件是寫入到SDCrad的,因此咱們須要在AndroidManifest中添加以下權限(注: 6.0及已上系統請動態設置權限或者手動在設置中爲項目設置):api

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

Tinker須要在AndroidManifest.xml中指定TINKER_ID微信

<meta-data
        android:name="TINKER_ID"
        android:value="tinker_id_100" />
Patch生成

patch生成官方提供了兩種接入方式:app

1.基於命令行的方式。
2.gradle編譯的方式。

1.gradle編譯生成patch
微信Tinker的gradle配置也很簡單,先來瀏覽一下Tinker接入指南,點擊進入查看,對使用gradle配置的參數瞭解一下,接下來附上一個相對比較完整的gradle配置。

apply plugin: 'com.android.application'

android {

    compileSdkVersion 25
    buildToolsVersion "25.0.2"

    compileOptions {
        sourceCompatibility getJavaVersion()
        targetCompatibility getJavaVersion()
    }

    // Tinker推薦模式
    dexOptions {
        jumboMode = true
    }

    // 關閉aapt對png優化
    aaptOptions {
        cruncherEnabled false
    }

    signingConfigs {
        try {
            config {
                keyAlias 'testres'
                keyPassword 'testres'
                storeFile file('./keystore/release.keystore')
                storePassword 'testres'
            }
        } catch (ex) {
            throw new InvalidUserDataException(ex.toString())
        }
    }

    defaultConfig {
        applicationId "com.tinker.app"
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0.0"
        // 使用multiDex庫
        multiDexEnabled true
        // 設置簽名
        signingConfig signingConfigs.config
        manifestPlaceholders = [TINKER_ID: "${getTinkerIdValue()}"]
        buildConfigField "String", "MESSAGE", "\"I am the base apk\""
        buildConfigField "String", "CLIENTVERSION", "\"${getTinkerIdValue()}\""
        buildConfigField "String", "PLATFORM",  "\"all\""
    }

    buildTypes {
        release {
            minifyEnabled false
            signingConfig signingConfigs.config
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            minifyEnabled false
            signingConfig signingConfigs.config
        }
    }

    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'com.android.support:appcompat-v7:25.1.0'
    // 依賴multiDex庫
    compile 'com.android.support:multidex:1.0.1'
    //可選,用於生成application類
    provided ("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}"){changing = true}
    //Tinker的核心庫
    compile ("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}"){changing = true}
}

// 指定JDK版本
def getJavaVersion() {
    return JavaVersion.VERSION_1_7
}

def bakPath = file("${buildDir}/bakApk/")

/** * ext相關配置 */
ext {
    tinkerEnabled = true
    // 基礎版本apk
    tinkerOldApkPath = "${bakPath}/app-debug-20170217-18-51-30.apk"
    // 未開啓混淆的話mapping能夠忽略,若是開啓混淆mapping要保持一致。 
    tinkerApplyMappingPath = "${bakPath}/"
    // 與基礎版本一塊兒生成的R.text文件
    tinkerApplyResourcePath = "${bakPath}/app-debug-20170217-18-51-30-R.txt"
    // 只用於構建全部的Build,若是不是,此處可忽略。
    tinkerBuildFlavorDirectory = "${bakPath}/"
}

// 基礎APK的位置
def getOldApkPath() {
    return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}

// Mapping的位置
def getApplyMappingPath() {
    return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}

// ResourceMapping的位置
def getApplyResourceMappingPath() {
    return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}

// 用來獲取TinkerId(當前版本號就是TinkerId)
def getTinkerIdValue() {
    return android.defaultConfig.versionName
}

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

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

if (buildWithTinker()) {
    // Tinker插件
    apply plugin: 'com.tencent.tinker.patch'
    /** * 全局信息相關配置 */
    tinkerPatch {
        // 基準apk包的路徑,必須輸入,不然會報錯。
        oldApk = getOldApkPath()
        /** * 若是出現如下的狀況,而且ignoreWarning爲false,咱們將中斷編譯。 * 由於這些狀況可能會致使編譯出來的patch包帶來風險: * case 1: minSdkVersion小於14,可是dexMode的值爲"raw"; * case 2: 新編譯的安裝包出現新增的四大組件(Activity, BroadcastReceiver...); * case 3: 定義在dex.loader用於加載補丁的類不在main dex中; * case 4: 定義在dex.loader用於加載補丁的類出現修改; * case 5: resources.arsc改變,但沒有使用applyResourceMapping編譯。 */
        ignoreWarning = false

        /** * 運行過程當中須要驗證基準apk包與補丁包的簽名是否一致,是否須要簽名。 */
        useSign = true

        /** * optional,default 'true' * whether use tinker to build */
        tinkerEnable = buildWithTinker()

        /** * 編譯相關的配置項 */
        buildConfig {
            /** * 可選參數;在編譯新的apk時候,咱們但願經過保持舊apk的proguard混淆方式,從而減小補丁包的大小。 * 這個只是推薦設置,不設置applyMapping也不會影響任何的assemble編譯。 */
            applyMapping = getApplyMappingPath()
            /** * 可選參數;在編譯新的apk時候,咱們但願經過舊apk的R.txt文件保持ResId的分配。 * 這樣不只能夠減小補丁包的大小,同時也避免因爲ResId改變致使remote view異常。 */
            applyResourceMapping = getApplyResourceMappingPath()

            /** * 在運行過程當中,咱們須要驗證基準apk包的tinkerId是否等於補丁包的tinkerId。 * 這個是決定補丁包能運行在哪些基準包上面,通常來講咱們可使用git版本號、versionName等等。 */
            tinkerId = getTinkerIdValue()

            /** * 若是咱們有多個dex,編譯補丁時可能會因爲類的移動致使變動增多。 * 若打開keepDexApply模式,補丁包將根據基準包的類分佈來編譯。 */
            keepDexApply = false
        }
        /** * dex相關的配置項 */
        dex {
            /** * 只能是'raw'或者'jar'。 * 對於'raw'模式,咱們將會保持輸入dex的格式。 * 對於'jar'模式,咱們將會把輸入dex從新壓縮封裝到jar。 * 若是你的minSdkVersion小於14,你必須選擇‘jar’模式,並且它更省存儲空間,可是驗證md5時比'raw'模式耗時。 * 默認咱們並不會去校驗md5,通常狀況下選擇jar模式便可。 */
            dexMode = "jar"

            /** * 須要處理dex路徑,支持*、?通配符,必須使用'/'分割。路徑是相對安裝包的,例如assets/... */
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /** * 這一項很是重要,它定義了哪些類在加載補丁包的時候會用到。 * 這些類是經過Tinker沒法修改的類,也是必定要放在main dex的類。 * 這裏須要定義的類有: * 1. 你本身定義的Application類; * 2. Tinker庫中用於加載補丁包的部分類,即com.tencent.tinker.loader.*; * 3. 若是你自定義了TinkerLoader,須要將它以及它引用的全部類也加入loader中; * 4. 其餘一些你不但願被更改的類,例如Sample中的BaseBuildInfo類。 * 這裏須要注意的是,這些類的直接引用類也須要加入到loader中。 * 或者你須要將這個類變成非preverify。 * 5. 使用1.7.6版本以後版本,參數一、2會自動填寫。 * */
            loader = [
                    // Tinker庫中用於加載補丁包的部分類
                    "com.tencent.tinker.loader.*",
                    // 本身定義的Application類;
                    "com.tinker.app.AppContext",
                    //use sample, let BaseBuildInfo unchangeable with tinker
                    "tinker.sample.android.app.BaseBuildInfo"
            ]
        }
        /** * lib相關的配置項 */
        lib {
            /** * 須要處理lib路徑,支持*、?通配符,必須使用'/'分割。 * 與dex.pattern一致, 路徑是相對安裝包的,例如assets/... */
            pattern = ["lib/*/*.so"]
        }
        /** * res相關的配置項 */
        res {
            /** * 須要處理res路徑,支持*、?通配符,必須使用'/'分割。 * 與dex.pattern一致, 路徑是相對安裝包的,例如assets/..., * 務必注意的是,只有知足pattern的資源纔會放到合成後的資源包。 */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /** * 支持*、?通配符,必須使用'/'分割。若知足ignoreChange的pattern,在編譯時會忽略該文件的新增、刪除與修改。 * 最極端的狀況,ignoreChange與上面的pattern一致,即會徹底忽略全部資源的修改。 */
            ignoreChange = ["assets/sample_meta.txt"]

            /** * 對於修改的資源,若是大於largeModSize,咱們將使用bsdiff算法。 * 這能夠下降補丁包的大小,可是會增長合成時的複雜度。默認大小爲100kb */
            largeModSize = 100
        }
        /** * 用於生成補丁包中的'package_meta.txt'文件 */
        packageConfig {
            /** * configField("key", "value"), 默認咱們自動從基準安裝包與新安裝包的Manifest中讀取tinkerId,並自動寫入configField。 * 在這裏,你能夠定義其餘的信息,在運行時能夠經過TinkerLoadResult.getPackageConfigByName獲得相應的數值。 * 可是建議直接經過修改代碼來實現,例如BuildConfig。 */
            configField("patchMessage", "tinker is sample to use")
            /** * just a sample case, you can use such as sdkVersion, brand, channel... * you can parse it in the SamplePatchListener. * Then you can use patch conditional! */
            configField("platform", "all")
            /** * 配置patch補丁版本 */
            configField("patchVersion", "1.0.0")
        }
        /** * 7zip路徑配置項,執行前提是useSign爲true */
        sevenZip {
            /** * 例如"com.tencent.mm:SevenZip:1.1.10",將自動根據機器屬性得到對應的7za運行文件,推薦使用。 */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
            /** * 系統中的7za路徑,例如"/usr/local/bin/7za"。path設置會覆蓋zipArtifact,若都不設置,將直接使用7za去嘗試。 */
            // path = "/usr/local/bin/7za"
        }
    }

    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0
    /** * bak apk and mapping */
    android.applicationVariants.all { variant ->
        /** * task type, you want to bak */
        def taskName = variant.name
        def date = new Date().format("yyyyMMdd-HH-mm-ss")

        tasks.all {
            if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {

                it.doLast {
                    copy {
                        def fileNamePrefix = "${project.name}-${variant.baseName}"
                        def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"

                        def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
                        from variant.outputs.outputFile
                        into destPath
                        rename { String fileName ->
                            fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
                        }

                        from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
                        }

                        from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
                        }
                    }
                }
            }
        }
    }
    project.afterEvaluate {
        //sample use for build all flavor for one time
        if (hasFlavors) {
            task(tinkerPatchAllFlavorRelease) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"
                    }
                }
            }

            task(tinkerPatchAllFlavorDebug) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
                    }
                }
            }
        }
    }
}

gradle配置就到此結束了,要注意的地方有如下幾點:

1.ext相關配置,示例中有完整描述。
2.Tinker插件apply plugin: 'com.tencent.tinker.patch'
3.全局信息相關配置tinkerPatch

配置完這些東西之後就能夠調用tinkerPatch命令生成patch補丁文件。tinkerPatch有Debug和Release兩種模式,由於是案例,因此就使用tinkerPatchDebug命令。
tinkerPatch命令
調用tinkerPatchDebug命令後生成7patch文件
注意:調用tinkerPatchDebug命令以前須要修改ext相關配置,ext相關配置已基準apk包爲準。
ext相關配置

7zip文件

加載補丁前
加載補丁後會提示「加載patch成功,請重啓進程」
重啓進程後,就會顯示出該改好的內容
到此微信Tinker熱修復gradle配置結束。
案例地址:https://github.com/mengjingbo/TinkerApp

本文分享 CSDN - 秦川小將。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索