熱修復——Tinker的集成與使用

1、簡述

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

上面是Tinker官方Wiki的原話,意思嘛相信你們都看得明白,但注意啦,它並無說Tinker可讓補丁實時生效(也叫無感知更新),它必須在打上補丁後重啓App(重啓進程),補丁纔會發揮做用,這跟阿里的熱修復方案有着本質的區別。在開始集成Tinker以前,咱們有必要了解清楚,Tinker有那些不足,下面是Tinker的已知問題:android

  1. Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大組件(1.9.0支持新增非export的Activity);
  2. 因爲Google Play的開發者條款限制,不建議在GP渠道動態更新代碼;
  3. 在Android N上,補丁對應用啓動時間有輕微的影響;
  4. 不支持部分三星android-21機型,加載補丁時會主動拋出"TinkerRuntimeException:checkDexInstall failed";
  5. 對於資源替換,不支持修改remoteView。例如transition動畫,notification icon以及桌面圖標。

上述不足是因爲原理與系統限制,咱們在編程中要清楚這些,儘可能避免以上問題的出現。git

儘管Tinker有着這些「小缺點」,但也絲絕不影響Tinker在國內衆多熱修復方案中的地位,一方面Tinker是開源的(這意味着Tinker自己免費),另外一方面則是Tinker已運行在微信的數億Android設備上(說明該方案至關穩定)。下面開始進行對Tinker的集成與使用。github

2、Tinker組件依賴

一、在項目的build.gradle中:

添加tinker-patch-gradle-plugin的依賴算法

buildscript {
	dependencies {
		classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1')
	}
}
複製代碼

二、在app的gradle文件(app/build.gradle)中:

須要注意一點,Tinker須要使用到MulitDex,原話在Bugly文檔的熱更新API接口部分編程

1)添加tinker的庫依賴

Gradle版本小於2.3的這麼寫:api

dependencies {
	compile "com.android.support:multidex:1.0.1"
	//可選,用於生成application類 
	provided('com.tencent.tinker:tinker-android-anno:1.9.1')
	//tinker的核心庫
	compile('com.tencent.tinker:tinker-android-lib:1.9.1') 
}
複製代碼

Gradle版本大等於2.3的這麼寫:安全

dependencies {
    implementation "com.android.support:multidex:1.0.1"
    //tinker的核心庫
    implementation("com.tencent.tinker:tinker-android-lib:1.9.1") { changing = true }
    //可選,用於生成application類
    annotationProcessor("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }
    compileOnly("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }
}
複製代碼

2)開啓multiDex

defaultConfig {
		...
        multiDexEnabled true
}
複製代碼

3)應用tinker的gradle插件

這部分可先無論,在第三部分《Tinker的配置及任務》的第2節《配置Tinker與任務》中會添加。可跳過這部分繼續往下看。服務器

//apply tinker插件
apply plugin: 'com.tencent.tinker.patch
複製代碼

3、Tinker的配置及任務

一、開啓支持大工程模式

Tinker文檔中推薦將jumboMode設置爲true。微信

android {
    dexOptions {
        // 支持大工程模式
        jumboMode = true
    }
	...
}
複製代碼

二、配置Tinker與任務

將下面的配置所有複製粘貼到app的gradle文件(app/build.gradle)末尾,內容不少,但如今只須要看懂bakPath與ext括號內的東東就行了。

// Tinker配置與任務
def bakPath = file("${buildDir}/bakApk/")
ext {
    // 是否使用Tinker(當你的項目處於開發調試階段時,能夠改成false)
    tinkerEnabled = true
    // 基礎包文件路徑(名字這裏寫死爲old-app.apk。用於比較新舊app以生成補丁包,無論是debug仍是release編譯)
    tinkerOldApkPath = "${bakPath}/old-app.apk"
    // 基礎包的mapping.txt文件路徑(用於輔助混淆補丁包的生成,通常在生成release版app時會使用到混淆,因此這個mapping.txt文件通常只是用於release安裝包補丁的生成)
    tinkerApplyMappingPath = "${bakPath}/old-app-mapping.txt"
    // 基礎包的R.txt文件路徑(若是你的安裝包中資源文件有改動,則須要使用該R.txt文件來輔助生成補丁包)
    tinkerApplyResourcePath = "${bakPath}/old-app-R.txt"
    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/flavor"
}

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 : android.defaultConfig.versionName
}

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

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

if (buildWithTinker()) {
    //apply tinker插件
    apply plugin: 'com.tencent.tinker.patch'

    // 全局信息相關的配置項
    tinkerPatch {
        tinkerEnable = buildWithTinker()// 是否打開tinker的功能。
        oldApk = getOldApkPath()        // 基準apk包的路徑,必須輸入,不然會報錯。
        ignoreWarning = false           // 是否忽略有風險的補丁包。這裏選擇不忽略,當補丁包風險時會中斷編譯。
        useSign = true                  // 在運行過程當中,咱們須要驗證基準apk包與補丁包的簽名是否一致,咱們是否須要爲你簽名。
        // 編譯相關的配置項
        buildConfig {
            applyMapping = getApplyMappingPath()
            // 可選參數;在編譯新的apk時候,咱們但願經過保持舊apk的proguard混淆方式,從而減小補丁包的大小。這個只是推薦設置,不設置applyMapping也不會影響任何的assemble編譯。
            applyResourceMapping = getApplyResourceMappingPath()
            // 可選參數;在編譯新的apk時候,咱們但願經過舊apk的R.txt文件保持ResId的分配,這樣不只能夠減小補丁包的大小,同時也避免因爲ResId改變致使remote view異常。
            tinkerId = getTinkerIdValue()
            // 在運行過程當中,咱們須要驗證基準apk包的tinkerId是否等於補丁包的tinkerId。這個是決定補丁包能運行在哪些基準包上面,通常來講咱們可使用git版本號、versionName等等。
            keepDexApply = false
            // 若是咱們有多個dex,編譯補丁時可能會因爲類的移動致使變動增多。若打開keepDexApply模式,補丁包將根據基準包的類分佈來編譯。
            isProtectedApp = false // 是否使用加固模式,僅僅將變動的類合成補丁。注意,這種模式僅僅能夠用於加固應用中。
            supportHotplugComponent = false // 是否支持新增非export的Activity(1.9.0版本開始纔有的新功能)
        }
        // dex相關的配置項
        dex {
            dexMode = "jar"
// 只能是'raw'或者'jar'。 對於'raw'模式,咱們將會保持輸入dex的格式。對於'jar'模式,咱們將會把輸入dex從新壓縮封裝到jar。若是你的minSdkVersion小於14,你必須選擇‘jar’模式,並且它更省存儲空間,可是驗證md5時比'raw'模式耗時。默認咱們並不會去校驗md5,通常狀況下選擇jar模式便可。
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            // 須要處理dex路徑,支持*、?通配符,必須使用'/'分割。路徑是相對安裝包的,例如assets/...
            loader = [
                    // 定義哪些類在加載補丁包的時候會用到。這些類是經過Tinker沒法修改的類,也是必定要放在main dex的類。
                    // 若是你自定義了TinkerLoader,須要將它以及它引用的全部類也加入loader中;
                    // 其餘一些你不但願被更改的類,例如Sample中的BaseBuildInfo類。這裏須要注意的是,這些類的直接引用類也須要加入到loader中。或者你須要將這個類變成非preverify。
            ]
        }
        // 	lib相關的配置項
        lib {
            pattern = ["lib/*/*.so","src/main/jniLibs/*/*.so"]
            // 須要處理lib路徑,支持*、?通配符,必須使用'/'分割。與dex.pattern一致, 路徑是相對安裝包的,例如assets/...
        }
        // res相關的配置項
        res {
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            // 須要處理res路徑,支持*、?通配符,必須使用'/'分割。與dex.pattern一致, 路徑是相對安裝包的,例如assets/...,務必注意的是,只有知足pattern的資源纔會放到合成後的資源包。
            ignoreChange = [
                    // 支持*、?通配符,必須使用'/'分割。若知足ignoreChange的pattern,在編譯時會忽略該文件的新增、刪除與修改。 最極端的狀況,ignoreChange與上面的pattern一致,即會徹底忽略全部資源的修改。
                    "assets/sample_meta.txt"
            ]
            largeModSize = 100
            // 對於修改的資源,若是大於largeModSize,咱們將使用bsdiff算法。這能夠下降補丁包的大小,可是會增長合成時的複雜度。默認大小爲100kb
        }
        // 用於生成補丁包中的'package_meta.txt'文件
        packageConfig {
            // configField("key", "value"), 默認咱們自動從基準安裝包與新安裝包的Manifest中讀取tinkerId,並自動寫入configField。
            // 在這裏,你能夠定義其餘的信息,在運行時能夠經過TinkerLoadResult.getPackageConfigByName獲得相應的數值。
            // 可是建議直接經過修改代碼來實現,例如BuildConfig。
            configField("platform", "all")
            configField("patchVersion", "1.0")
//            configField("patchMessage", "tinker is sample to use")
        }
        // 7zip路徑配置項,執行前提是useSign爲true
        sevenZip {
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        }
    }
    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0
    def date = new Date().format("MMdd-HH-mm-ss")

    /**
     * bak apk and mapping
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name

        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.first().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"
                    }

                }
            }
        }
    }
}
複製代碼

其中,有幾點配置在這裏說明一下,方便理解後續的操做(當tinkerEnabled = true的狀況下):

  • app的生成目錄是:主Module(通常是名爲app)/build/bakApk文件夾。
  • 補丁包的生成路徑:主Module(通常是名爲app)/build/outputs/apk/tinkerPatch/debug/patch_signed_7zip.apk。
  • 基礎包的名字:old-app.apk,放於bakApk文件夾下。
  • 基礎包的mapping.txt和R.txt文件通常在編譯release簽名的apk時纔會用到。
  • 在用到mapping.txt文件時,須要重命名爲old-app-mapping.txt,放於bakApk文件夾下。
  • 在用到R.txt文件時,須要重命名爲old-app-R.txt,放於bakApk文件夾下。

對於mapping.txt和R.txt文件,在配置中有說明,請回配置中仔細看。
上面只是我項目中的配置,這些其實都是能夠自定義的,建議在搞清楚配置內容以後再去自定義修改。

什麼是基礎包??

基礎包就是已經上架的apk文件(假設是1.0版本)。這其實很好理解,在新版本的App上架以前(假設是2.0版本),咱們會用到Tinker來修復1.0版App中存在的bug,這時就須要用到Tinker來產生補丁包文件,而補丁包文件的本質,就是修復好Bug的App與1.0版本App之間的文件差別。在2.0版本上架以前,咱們可能會屢次產生新的補丁包,用於修復在用戶手機上的1.0版App,因此補丁包必須以1.0版App做爲參考標準,也就是說用戶手機上的app就是基礎包,即當前應用市場上的apk文件(前面說的1.0版本)。

4、Tinker封裝與拓展

一、拷貝文件

將Demo中提供的tinker包下的全部文件及文件夾都拷貝到本身項目中。

這些文件其實就是Tinker官方Demo中的文件徹底複製過來的,只是多加了一些註釋。

簡單說明下,這幾個文件的做用:

  • SampleUncaughtExceptionHandler:Tinker的全局異常捕獲器。
  • MyLogImp:Tinker的日誌輸出實現類。
  • SampleLoadReporter:加載補丁時的一些回調。
  • SamplePatchListener:過濾Tinker收到的補丁包的修復、升級請求。
  • SamplePatchReporter:修復或者升級補丁時的一些回調。
  • SampleTinkerReport:修復結果(成功、衝突、失敗等)。
  • SampleResultService::patch補丁合成進程將合成結果返回給主進程的類。
  • TinkerManager:Tinker管理器(安裝、初始化Tinker)。
  • TinkerUtils:拓展補丁條件斷定、鎖屏或後臺時應用重啓功能的工具類。

這些只是對Tinker功能的拓展和封裝罷了,都是可選的,但這些文件對項目的功能完善會有所幫助,建議加入到本身的項目中。
若是你僅僅只是爲了修復bug,而不作過多的工做(如:上傳打補丁信息到服務器等),則無須理會這些文件的做用,固然你也能夠本身封裝。

對於這些自定義類及錯誤碼的詳細說明,請參考:「Tinker官方Wiki:可選的自定義類」

二、清單文件中添加服務

前面添加的文件中,有一個SampleResultService文件,是四大組件之一,因此必須在清單文件中聲明。

<service
    android:name="com.lqr.tinker.service.SampleResultService"
    android:exported="false"/>
複製代碼

5、編寫Application的代理類

Tinker表示,Application沒法動態修復,因此有兩種選擇:

  1. 使用「繼承TinkerApplication + DefaultApplicationLike」。
  2. 使用「DefaultLifeCycle註解 + DefaultApplicationLike」。

固然,若是你以爲你自定義的Application不會用到熱修復,可無視這部分;
但下方代碼中的initTinker()方法記得要拷貝到你項目中,用於初始化Tinker。

第1種方式感受比較雞肋,這裏使用第2種(Tinker官方推薦的方式):「DefaultLifeCycle註解 + TinkerApplicationLike」,DefaultLifeCycle註解生成Application,下面就來編寫Application的代理類:

一、編寫TinkerApplicationLike

將下方的代碼拷貝到項目中,註釋簡單明瞭,很少解釋:

@SuppressWarnings("unused")
@DefaultLifeCycle(application = "com.lqr.tinker.MyApplication",// application類名。只能用字符串,這個MyApplication文件是不存在的,但能夠在AndroidManifest.xml的application標籤上使用(name)
        flags = ShareConstants.TINKER_ENABLE_ALL,// tinkerFlags
        loaderClass = "com.tencent.tinker.loader.TinkerLoader",//loaderClassName, 咱們這裏使用默認便可!(可不寫)
        loadVerifyFlag = false)//tinkerLoadVerifyFlag
public class TinkerApplicationLike extends DefaultApplicationLike {

    private Application mApplication;
    private Context mContext;
    private Tinker mTinker;

    // 固定寫法
    public TinkerApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    // 固定寫法
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
        getApplication().registerActivityLifecycleCallbacks(callback);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        mApplication = getApplication();
        mContext = getApplication();
        initTinker(base);
        // 能夠將以前自定義的Application中onCreate()方法所執行的操做搬到這裏...
    }

    private void initTinker(Context base) {
        // tinker須要你開啓MultiDex
        MultiDex.install(base);

        TinkerManager.setTinkerApplicationLike(this);
        // 設置全局異常捕獲
        TinkerManager.initFastCrashProtect();
        //開啓升級重試功能(在安裝Tinker以前設置)
        TinkerManager.setUpgradeRetryEnable(true);
        //設置Tinker日誌輸出類
        TinkerInstaller.setLogIml(new MyLogImp());
        //安裝Tinker(在加載完multiDex以後,不然你須要將com.tencent.tinker.**手動放到main dex中)
        TinkerManager.installTinker(this);
        mTinker = Tinker.with(getApplication());
    }

}
複製代碼

二、搬運自定義Application中的操做

把項目中在自定義Application的操做移到TinkerApplicationLike的onCreate()或onBaseContextAttached()方法中。

public class TinkerApplicationLike extends DefaultApplicationLike {
	...
    @Override
    public void onCreate() {
        super.onCreate();
		// 將以前自定義的Application中onCreate()方法所執行的操做搬到這裏...
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        mApplication = getApplication();
        mContext = getApplication();
        initTinker(base);
        // 或搬到這裏...
    }
}
複製代碼

三、清單文件中註冊

將@DefaultLifeCycle中application對應的值,即"com.lqr.tinker.MyApplication",賦值給清單文件的application標籤的name屬性,以下:

<application
    android:name="com.lqr.tinker.MyApplication"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    ...
</application>
複製代碼

注意:
此時name屬性會報紅,由於項目源碼中根本不存在MyApplication.java文件,但沒必要擔憂,由於它是動態生成的,Build一下項目就行了,無論它也無所謂。

對於Application代理類的詳細說明,請參考:「Tinker官方Wiki:Application代理類」

到這裏就已經集成好Tinker了,但只是本地集成而已,服務端下發補丁包到app的文章以後會陸續發佈更新。

6、經常使用API

如今來了解下代碼中會用到的幾個Tinker的重要API。

一、請求打補丁

TinkerInstaller.onReceiveUpgradePatch(context, 補丁包的本地路徑);
複製代碼

二、卸載補丁

Tinker.with(getApplicationContext()).cleanPatch();// 卸載全部的補丁
Tinker.with(getApplicationContext()).cleanPatchByVersion(版本號)// 卸載指定版本的補丁
複製代碼

三、殺死應用的其餘進程

ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
複製代碼

四、Hack方式修復so

TinkerLoadLibrary.installNavitveLibraryABI(this, abi);
複製代碼

abi:cpu架構類型

五、非Hack方式修復so

TinkerLoadLibrary.loadLibraryFromTinker(getApplicationContext(), "lib/" + abi, so庫的模塊名); // 加載任意abi庫
TinkerLoadLibrary.loadArmLibrary(getApplicationContext(), so庫的模塊名); // 只適用於加載armeabi庫
TinkerLoadLibrary.loadArmV7Library(getApplicationContext(), so庫的模塊名); // 只適用於加載armeabi-v7a庫 
複製代碼

loadArmLibrary()與loadArmV7Library()本質是調用了loadLibraryFromTinker(),有興趣的能夠查看下源碼。

對於Tinker全部API的詳細說明,請參考:「Tinker官方Wiki:Tinker-API概覽」

7、測試

由於佈局簡單且不是重點,這裏就給出一張Demo的運行圖片,剩下的就靠想像了。

一、編譯基礎包

沒有基礎包,那要補丁有什麼用?因此,第一步就是打包一個apk。

在Terminal中使用命令行./gradlew assembleDebug。不會命令行無所謂,Android Studio爲咱們提供了圖形化操做,根據下圖操做便可:

若是你是要release簽名的打包,則雙擊assembleRelease,不過還要配置簽名文件,這個後面再說。

編譯完成後,能夠在build目錄下會自動建立一個bakApk文件夾,裏面就有打包好的apk文件,由於以後的全部生成的補丁包都以這個apk會標準,因此這就是那個基礎包文件(至關於應用市場上的app)。

若是這個apk文件是release簽名且是要放到應用市場上的,那麼你必須將apk與R.txt(若是有使用混淆的話,還會有一個mapping.txt)這幾個文件保存好,切記。

如今就把這個tinker-local-debug-1206-11-48-42.apk安裝到手機上(至關因而用戶手機上的app)。

  1. 點擊"say someting"按鈕吐司"Hello"。
  2. 點擊"get string from .so"按鈕吐司"hello LQR"。
  3. 點擊"show info"按鈕顯示"patch is not loaded",說明當前沒有加載補丁。

一、修復java代碼

下面是"say someting"按鈕點擊時,調用的方法,使用Toast顯示Hello字符串:

public void say(View view) {
    Toast.makeText(getApplicationContext(), "Hello", Toast.LENGTH_SHORT).show();
}
複製代碼

1)修復代碼

如今我想讓它吐司Hello World,因此代碼修改成:

public void say(View view) {
    Toast.makeText(getApplicationContext(), "Hello World", Toast.LENGTH_SHORT).show();
}
複製代碼

2)製做補丁包

先將基礎包(前面那個tinker-local-debug-1206-11-48-42.apk文件)重命名爲old-app.apk,而後雙擊tinkerPatchDebug,操做以下圖所示:

編譯完成後,build/outputs/apk/tinkerPatch會產生3個補丁包,咱們要的就是patch_signed_7zip.apk。

對於build/outputs/apk/tinkerPatch目錄下文件及文件夾的詳細說明,請參考:「Tinker官方Wiki:輸出文件詳解」

3)下發補丁包

將patch_signed_7zip.apk放到手機的SD卡目錄下:

不必定是SD卡目錄,位置由咱們開發者決定,Demo中調用TinkerInstaller.onReceiveUpgradePatch(context, 補丁包的本地路徑)方法時,第二個參數指定了是SD卡,故如此操做。

4)打補丁

能夠看到,在install patch以前,點擊"say someting"按鈕時,仍是吐司"Hello"。 點擊"install patch"按鈕後,會提示"patch success,please restart process",說明Tinker已經打上補丁了。 這時點擊"show info",能夠看到"patch is not loaded",說明當前補丁尚未生效。 最後,點擊"kill myself"按鈕,殺死當前app(進程)。

在從新打開app,再點擊"say someting"按鈕,吐司"Hello World"。 再點擊"show info",能夠看到"patch is loaded",說明app重啓後補丁生效了。

小結:
Tinker熱修復沒法讓補丁實時生效,在重啓進程後,補丁纔會生效。
Tinker會在app重啓後自動應用補丁。

二、修復so庫

在一開始製做基礎包時,工程中就已經加入了一些so文件,存放在src/main/jniLibs目錄下,由於Android Studio默認的庫目錄是libs(與src同級),因此這裏須要在app的build.gradle文件中進行配置,指定so庫所在文件夾。

下面是"get string from .so"按鈕點擊時調用的方法:

public void string_from_so(View view) {
    String string = JniUtil.hello();
    Toast.makeText(getApplicationContext(), string, Toast.LENGTH_SHORT).show();
}
複製代碼

這個JniUtil的代碼以下:

public class JniUtil {
    public static String LIB_NAME = "LQRJni";
    public JniUtil() {}
    static {
        System.loadLibrary(LIB_NAME);
    }
    public static native String hello();
}
複製代碼

加載so庫有2點須要注意:

  1. System.loadLibrary(libname)加載固定目錄下的庫文件,而System.load(filename)加載指定目錄下的庫文件。
  2. System.loadLibrary(libname)的參數libname指的是庫的模塊名,不是so文件的名字,如libLQRJni.so文件的模塊名其實是LQRJni。

so文件的製做代碼包含在Demo中,有興趣的朋友能夠嘗試本身製做。

1)替換so文件

迴歸正題,如今so庫中獲得的文字是"Hello LQR",如今變一下,我須要獲得的文字是"Hello CSDN_LQR",將新的so文件替換掉舊的so文件便可。

2)檢查Tinker的lib匹配規則

在app的build.gradle文件中,咱們前面在第三部分《Tinker的配置及任務》的第2節《配置Tinker與任務》中,有以下一段配置:

lib {
    pattern = ["lib/*/*.so", "src/main/jniLibs/*/*.so"]
}
複製代碼

這就是Tinker的lib匹配規則,在生成補丁的過程當中,它會去把符合這個規則的庫文件拿出來與基礎包中的庫文件進行匹配,從而將有差別的庫文件放入到補丁包中。而Tinker官方Demo的配置中是沒有"src/main/jniLibs/*/*.so"這一段的,這將致使Tinker在產生補丁包時不會去檢查src/main/jniLibs目錄下的文件變化,進而補丁包中不會包含修復好的so文件,這很重要,切記。

3)生成補丁與下發補丁包

生成補丁與下發補丁包的過程與以前的操做一致,這裏再也不重覆,不過咱們來看看tinkerPatch跟以前有什麼區別吧:

最後記得將patch_signed_7zip.apk放到手機的SD卡目錄下。

4)卸載補丁

補丁是能夠打多個的,用補丁的版本號作區分,在卸載的時候,能夠根據補丁的版本號來卸載,也能夠把以前全部的補丁卸載掉,實際開發中,看項目需求來解決用哪一種方式來卸載補丁,這裏我選擇清理以前全部的補丁,下面是"uninstall patch"按鈕的點擊事件:

public void uninstall_patch(View view) {
    ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
    Tinker.with(getApplicationContext()).cleanPatch();
}
複製代碼

卸載補丁以前,要先殺死當前App的其餘進程。
卸載補丁以後,App是不安全的(由於此時Tinker已經初始化完成),最好,須要重啓一下App。

5)打補丁

如今咱們再來打一次補丁,操做以下圖所示:

能夠看到,在install patch以前,點擊"get string from .so"按鈕時,仍是吐司"Hello LQR"。 點擊"install patch"按鈕後,會提示"patch success,please restart process",說明Tinker已經打上補丁了。 這時點擊"show info",能夠看到"patch is not loaded",說明當前補丁尚未生效。 最後,點擊"kill myself"按鈕,殺死當前app(進程)。

如今重啓app,理想狀態下,當咱們再次點擊"get string from .so"按鈕時,會吐司"Hello CSDN_LQR"。

然而,吐司仍是"Hello LQR",並無變化,並且點擊"show info"按鈕後,能夠看到"patch is loaded",說明補丁已經加載了,這是爲啥?

再來看看下面的操做:

重啓app後,先點擊"load library(hack)"按鈕,再點擊"get string from .so"按鈕,出現了,吐司變成了"Hello CSDN_LQR"。

上圖是Tinker官網Wiki的文檔部分截圖,從紅線部分能夠知道,由於部分手機判斷abi並不許確(可能由於Android碎片化比較嚴重吧),Tinker沒有區分abi,天然也不會在app啓動時,自動加載對應的so庫,這須要開發者本身判斷。

下面是"load library(hack)"按鈕點擊調用的方法:

public void load_library_hack(View view) {
    String CPU_ABI = android.os.Build.CPU_ABI;
    // 將tinker library中的 CPU_ABI架構的so 註冊到系統的library path中。
    TinkerLoadLibrary.installNavitveLibraryABI(this, CPU_ABI);
}
複製代碼

這是Tinker提供的使用Hack方式加載補丁中的so庫,只是一個方法調用而已,並無什麼特別。對於非Hack方式加載補丁的方式,我本人是沒有測試成功的,很奇怪,搞不明白問題的緣由,官方的文檔也寫得不清不楚的,有知道本Demo加載不成功的緣由的朋友請不吝賜教一下哈,thx。

小結:
Tinker雖然會在app重啓後自動加載補丁,但不會自動加載補丁中的so文件,開發者需本身斷定好abi來加載so文件。

三、修復資源文件

這部分跟前面的重合度極高,故不作演示了,你能夠在補丁包中對本demo中的頭像進行替換試試,與修復java文件的操做基本一致,這部分須要提醒的是,app的build.gradle文件中Tinker配置有以下這一段:

res {
    pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
    ...
}
複製代碼

不難理解,這就是Tinker對資源文件的匹配規則,平常開發夠用,若是你的項目中把資源文件放到了這裏沒有的目錄下,須要修改這部分的配置。

8、細節

一、打包步驟

1)debug打包

  1. 調用assembleDebug編譯獲得一個debug簽名的apk(old apk),這是基礎apk。
  2. 修改代碼、更新res文件、so等。
  3. 將old apk按gradle中的參數規則,重命名爲指定名字,仍是放在bakApk目錄下(該目錄可更改)。
  4. 調用tinkerPatchDebug生成補丁包於/build/outputs/tinkerPatch/目錄(默認是patch_signed_7zip.apk)。
  5. 將補丁包複製到SD卡目錄下(目錄可更改),在程序中調用打補丁方法,重啓app便可實現熱修復。

2)release打包步驟

  1. 調用assembleRelease編譯獲得一個release簽名的apk(old apk),這是基礎apk,還有一個mapping文件。
  2. 修改代碼、更新res文件、so等。
  3. 將old apk與mapping文件按gradle中的參數規則,分別重命名爲指定名字,仍是放在bakApk目錄下(該目錄可更改)。
  4. 調用tinkerPatchRelease生成補丁包於/build/outputs/tinkerPatch/目錄(默認是patch_signed_7zip.apk)。
  5. 將補丁包複製到SD卡目錄下(目錄可更改),在程序中調用打補丁方法,重啓app便可實現熱修復。

由於調用tinker的release打包須要用到簽名文件的信息,因此還必須在app的build.gradle中配置好籤名文件。

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

        debug {
            storeFile file("./keystore/debug.keystore")
        }
    }
	...
    buildTypes {
        release {
            minifyEnabled true
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), project.file('proguard-rules.pro')
        }
        debug {
            debuggable true
            minifyEnabled false
            signingConfig signingConfigs.debug
        }
    }
}
複製代碼

其實說白了,debug與release打包的差異,除了執行的命令不同以外,release打包比debug打包多用到2個文件(mapping.txt、R.txt)。

二、使用tinker的注意事項與發現

  • tinker編譯時須要禁用instant run。
  • tinker須要MultiDex。
  • 上架前用assembleRelease編譯獲得的apk、mapping.txt、R.txt這3個文件要備份好,製做補丁時會用到。
  • 多個補丁包的版本同樣時,不影響打補丁(如:第一次補丁版本是1.0,第二次補丁仍是1.0版本,是能夠成功打上第二次補丁的)。
  • 成功打上補丁後,補丁原文件會被刪除,故項目中沒必要擔憂補丁原文件清理的問題。

三、可能會遇到的錯誤

1)onLoadPatchListenerReceiveFail code爲-2

報錯原文以下:

receive a patch file: /storage/emulated/0/patch_signed_7zip.apk, file size:3604
patch loadReporter onLoadPatchListenerReceiveFail: patch receive fail: /storage/emulated/0/patch_signed_7zip.apk, code: -2
複製代碼

出現這種狀況,請按以下兩步進行排查:

  1. 查看文件路徑是否正常。
  2. 查看清單文件中是否有添加SD卡訪問權限。

若是你的手機是Android7.0請要考慮FileProvider(Android7.0不支持直接訪問sd卡)。

2)onLoadPatchListenerReceiveFail code爲-24

報錯原文以下:

receive a patch file: /storage/emulated/0/patch_signed_7zip.apk, file size:3665
get platform:null
patch loadReporter onLoadPatchListenerReceiveFail: patch receive fail: /storage/emulated/0/patch_signed_7zip.apk, code: -24
複製代碼

提示很明顯,Tinker獲取不到platform的值,請檢查在app的build.gradle文件中是否有以下配置,這部分配置了Tinker補丁包支持的平臺與版本號:

packageConfig {
	configField("platform", "all")
	configField("patchVersion", "1.0")
}
複製代碼

9、其餘

對於多渠道打包的補丁文件,暫時沒有研究,請自行參考Tinker的官方Wiki。

本Demo基於Tinker官方Demo及文檔製做,如下是Tinker的官方文檔連接:

最後貼下Demo連接

github.com/GitLqr/HotF…

Demo中的Module說明:

  1. app:熱修復原理Demo
  2. tinker-local:本地集成Tinker熱修復Demo
  3. jnitest:生成簡單so文件的Demo

歡迎關注微信公衆號:全棧行動
相關文章
相關標籤/搜索