Android熱更新:微信Tinker框架的接入與測試

Android熱修復(fù)框架的對比(最終選擇微信Tinker)

Android熱修復(fù)框架的對比

總結(jié)對比摘自Tinker官方Wiki

  1. AndFix作為native解決方案弄砍,首先面臨的是穩(wěn)定性與兼容性問題袄简,更重要的是它無法實現(xiàn)類替換逊桦,它是需要大量額外的開發(fā)成本的谈喳;
  2. Robust兼容性與成功率較高粹舵,但是它與AndFix一樣钮孵,無法新增變量與類只能用做的bugFix方案;
  3. Qzone方案可以做到發(fā)布產(chǎn)品功能眼滤,但是它主要問題是插樁帶來Dalvik的性能問題巴席,以及為了解決Art下內(nèi)存地址問題而導(dǎo)致補(bǔ)丁包急速增大的。

特別是在Android N之后诅需,由于混合編譯的inline策略修改漾唉,對于市面上的各種方案都不太容易解決荧库。而Tinker熱補(bǔ)丁方案不僅支持類、So以及資源的替換赵刑,它還是2.X-7.X的全平臺支持分衫。利用Tinker我們不僅可以用做bugfix,甚至可以替代功能的發(fā)布。Tinker已運行在微信的數(shù)億Android設(shè)備上般此,那么為什么你不使用Tinker呢蚪战?

Tinker的已知問題

截至文章發(fā)布當(dāng)天,微信 Tinker 熱修復(fù)框架 尚存問題如下所示:

  1. Tinker不支持修改AndroidManifest.xml铐懊,Tinker不支持新增四大組件邀桑;
  2. 由于Google Play的開發(fā)者條款限制,不建議在GP渠道動態(tài)更新代碼居扒;
  3. 在Android N上概漱,補(bǔ)丁對應(yīng)用啟動時間有輕微的影響;
  4. 不支持部分三星android-21機(jī)型喜喂,加載補(bǔ)丁時會主動拋出"TinkerRuntimeException:checkDexInstall failed"瓤摧;
  5. 由于各個廠商的加固實現(xiàn)并不一致,在1.7.6以及之后的版本玉吁,tinker不再支持加固的動態(tài)更新照弥;
  6. 對于資源替換,不支持修改remoteView进副。例如transition動畫这揣,notification icon以及桌面圖標(biāo)。

一影斑、接入Tinker(文末有 Demo 的 github 鏈接)

步驟一:項目的build.gradle文件
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.0'
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7')//加入tinker

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files

    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
步驟二:app的build.gradle文件

以下這些只是基本測試通過的屬性给赞,Tinker官方github上面還有更多可選可設(shè)置的屬性,如果還需要設(shè)置更多矫户,請移步至 Tinker 官方github接入指南 查看片迅。(如果覺得官方文檔看起來有點迷惑的同學(xué),直接按照我下面的來做就好了)

apply plugin: 'com.android.application'

def javaVersion = JavaVersion.VERSION_1_7
android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    compileOptions {
        sourceCompatibility javaVersion
        targetCompatibility javaVersion
    }
    //recommend
    dexOptions {
        jumboMode = true
    }


    defaultConfig {
        applicationId "com.tinker.deeson.mytinkerdemo"
        minSdkVersion 15
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        buildConfigField "String", "MESSAGE", "\"I am the base apk\""

        buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""
        buildConfigField "String", "PLATFORM",  "\"all\""
    }

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

        debug {
            storeFile file("./keystore/TinkerDemo.keystore")
            storePassword "TinkerDemo"
            keyAlias "TinkerDemo"
            keyPassword "TinkerDemo"
        }
    }

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

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


}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile "com.android.support:appcompat-v7:23.1.1"
    testCompile 'junit:junit:4.12'

    compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
    provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    compile "com.android.support:multidex:1.0.1"
}

def gitSha() {
    try {
        // String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
        String gitRev = "1008611"
        if (gitRev == null) {
            throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
        }
        return gitRev
    } catch (Exception e) {
        throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
    }
}

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-0421-12-34-45.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-release-0421-12-34-45-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-release-0421-12-34-45-R.txt"

    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-0421-12-34-45"
}

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
}

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

    tinkerPatch {
        /**
         * 默認(rèn)為null
         * 將舊的apk和新的apk建立關(guān)聯(lián)
         * 從build / bakApk添加apk
         */
        oldApk = getOldApkPath()
        /**
         * 可選皆辽,默認(rèn)'false'
         *有些情況下我們可能會收到一些警告
         *如果ignoreWarning為true柑蛇,我們只是斷言補(bǔ)丁過程
         * case 1:minSdkVersion低于14,但是你使用dexMode與raw驱闷。
         * case 2:在AndroidManifest.xml中新添加Android組件耻台,
         * case 3:裝載器類在dex.loader {}不保留在主要的dex,
         * 它必須讓tinker不工作空另。
         * case 4:在dex.loader {}中的loader類改變盆耽,
         * 加載器類是加載補(bǔ)丁dex。改變它們是沒有用的。
         * 它不會崩潰征字,但這些更改不會影響都弹。你可以忽略它
         * case 5:resources.arsc已經(jīng)改變,但是我們不使用applyResourceMapping來構(gòu)建
         */
        ignoreWarning = false

        /**
         *可選匙姜,默認(rèn)為“true”
         * 是否簽名補(bǔ)丁文件
         * 如果沒有畅厢,你必須自己做。否則在補(bǔ)丁加載過程中無法檢查成功
         * 我們將使用sign配置與您的構(gòu)建類型
         */
        useSign = true

        /**
         可選氮昧,默認(rèn)為“true”
         是否使用tinker構(gòu)建
         */
        tinkerEnable = buildWithTinker()

        /**
         * 警告框杜,applyMapping會影響正常的android build!
         */
        buildConfig {
            /**
             *可選袖肥,默認(rèn)為'null'
             * 如果我們使用tinkerPatch構(gòu)建補(bǔ)丁apk咪辱,你最好應(yīng)用舊的
             * apk映射文件如果minifyEnabled是啟用!
             * 警告:你必須小心椎组,它會影響正常的組裝構(gòu)建油狂!
             */
            applyMapping = getApplyMappingPath()
            /**
             *可選,默認(rèn)為'null'
             * 很高興保持資源ID從R.txt文件寸癌,以減少java更改
             */
            applyResourceMapping = getApplyResourceMappingPath()

            /**
             *必需专筷,默認(rèn)'null'
             * 因為我們不想檢查基地apk與md5在運行時(它是慢)
             * tinkerId用于在試圖應(yīng)用補(bǔ)丁時標(biāo)識唯一的基本apk。
             * 我們可以使用git rev蒸苇,svn rev或者簡單的versionCode磷蛹。
             * 我們將在您的清單中自動生成tinkerId
             */
            tinkerId = getTinkerIdValue()

            /**
             *如果keepDexApply為true,則表示dex指向舊apk的類溪烤。
             * 打開這可以減少dex diff文件大小味咳。
             */
            keepDexApply = false
        }

        dex {
            /**
             *可選,默認(rèn)'jar'
             * 只能是'raw'或'jar'檬嘀。對于原始槽驶,我們將保持其原始格式
             * 對于jar,我們將使用zip格式重新包裝dexes鸳兽。
             * 如果你想支持下面14捺檬,你必須使用jar
             * 或者你想保存rom或檢查更快,你也可以使用原始模式
             */
            dexMode = "jar"

            /**
             *必需贸铜,默認(rèn)'[]'
             * apk中的dexes應(yīng)該處理tinkerPatch
             * 它支持*或?模式聂受。
             */
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /**
             *必需蒿秦,默認(rèn)'[]'
             * 警告,這是非常非常重要的蛋济,加載類不能隨補(bǔ)丁改變棍鳖。
             * 因此,它們將從補(bǔ)丁程序中刪除。
             * 你必須把下面的類放到主要的dex渡处。
             * 簡單地說镜悉,你應(yīng)該添加自己的應(yīng)用程序{@code tinker.sample.android.SampleApplication}
             * 自己的tinkerLoader,和你使用的類
             *
             */
            loader = [
                    //use sample, let BaseBuildInfo unchangeable with tinker
                    "tinker.sample.android.app.BaseBuildInfo"
            ]
        }

        lib {
            /**
             可選医瘫,默認(rèn)'[]'
             apk中的圖書館應(yīng)該處理tinkerPatch
             它支持*或侣肄?模式。
             對于資源庫拍嵌,我們只是在補(bǔ)丁目錄中恢復(fù)它們
             你可以得到他們在TinkerLoadResult與Tinker
             */
            pattern = ["lib/armeabi/*.so"]
        }

        res {
            /**
             *可選幕垦,默認(rèn)'[]'
             * apk中的什么資源應(yīng)該處理tinkerPatch
             * 它支持*或窄赋?模式。
             * 你必須包括你在這里的所有資源矩距,
             * 否則,他們不會重新包裝在新的apk資源怖竭。
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**
             *可選锥债,默認(rèn)'[]'
             *資源文件排除模式,忽略添加痊臭,刪除或修改資源更改
             * *它支持*或哮肚?模式。
             * *警告趣兄,我們只能使用文件沒有relative與resources.arsc
             */
            ignoreChange = ["assets/sample_meta.txt"]

            /**
             *默認(rèn)100kb
             * *對于修改資源绽左,如果它大于'largeModSize'
             * *我們想使用bsdiff算法來減少補(bǔ)丁文件的大小
             */
            largeModSize = 100
        }

        packageConfig {
            /**
             *可選,默認(rèn)'TINKER_ID艇潭,TINKER_ID_VALUE'拼窥,'NEW_TINKER_ID,NEW_TINKER_ID_VALUE'
             * 包元文件gen蹋凝。路徑是修補(bǔ)程序文件中的assets / package_meta.txt
             * 你可以在您自己的PackageCheck方法中使用securityCheck.getPackageProperties()
             * 或TinkerLoadResult.getPackageConfigByName
             * 我們將從舊的apk清單為您自動獲取TINKER_ID鲁纠,
             * 其他配置文件(如下面的patchMessage)不是必需的
             */
            configField("patchMessage", "tinker is sample to use")
            /**
             *只是一個例子,你可以使用如sdkVersion鳍寂,品牌改含,渠道...
             * 你可以在SamplePatchListener中解析它。
             * 然后你可以使用補(bǔ)丁條件迄汛!
             */
            configField("platform", "all")
            /**
             * 補(bǔ)丁版本通過packageConfig
             */
            configField("patchVersion", "1.0")
        }
        //或者您可以添加外部的配置文件捍壤,或從舊apk獲取元值
        //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
        //project.tinkerPatch.packageConfig.configField("test2", "sample")

        /**
         * 如果你不使用zipArtifact或者path,我們只是使用7za來試試
         */
        sevenZip {
            /**
             * 可選鞍爱,默認(rèn)'7za'
             * 7zip工件路徑鹃觉,它將使用正確的7za與您的平臺
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
            /**
             * 可選,默認(rèn)'7za'
             * 你可以自己指定7za路徑睹逃,它將覆蓋zipArtifact值
             */
//        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("MMdd-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.properties文件

將下面這行 Tinker 的版本號添加到 gradle.properties 文件中(Tinker的最新版本盗扇,請留意Tinker github
TINKER_VERSION=1.7.7

** 強(qiáng)烈建議同學(xué)們使用最新的版本,因為tinker 的wiki上面提到最新版本支持應(yīng)用加固,見下圖**

tinker 加固相關(guān)
步驟四:自己的application文件

新建一個類疗隶,名字(SampleApplicationLike )隨意起佑笋,當(dāng)然最好是意義明顯的,并繼承自DefaultApplicationLike 斑鼻,注意蒋纬,這里并不是繼承 Application,這個是 Tinker 的推薦寫法卵沉。其他的注解和重寫的方法颠锉,照著寫就好了。最后自己的 Application 邏輯就寫在 onCreate() 方法里面史汗。

package com.tinker.deeson.mytinkerdemo;

import android.annotation.TargetApi;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.multidex.MultiDex;
import android.widget.Toast;

import com.tencent.tinker.anno.DefaultLifeCycle;
import com.tencent.tinker.lib.listener.DefaultPatchListener;
import com.tencent.tinker.lib.patch.UpgradePatch;
import com.tencent.tinker.lib.reporter.DefaultLoadReporter;
import com.tencent.tinker.lib.reporter.DefaultPatchReporter;
import com.tencent.tinker.lib.tinker.Tinker;
import com.tencent.tinker.lib.tinker.TinkerInstaller;
import com.tencent.tinker.loader.app.DefaultApplicationLike;
import com.tencent.tinker.loader.shareutil.ShareConstants;


@SuppressWarnings("unused")
@DefaultLifeCycle(application = "com.tinker.deeson.mytinkerdemo.SampleApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
    private static final String TAG = "Tinker.SampleApplicationLike";

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

    /**
     * install multiDex before install tinker
     * so we don't need to put the tinker lib classes in the main dex
     *
     * @param base
     */
    @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);
        TinkerInstaller.install(this,new DefaultLoadReporter(getApplication())
                ,new DefaultPatchReporter(getApplication()),new DefaultPatchListener(getApplication()),SampleResultService.class,new UpgradePatch());
        Tinker tinker = Tinker.with(getApplication());

        Toast.makeText(getApplication(),"加載完成", Toast.LENGTH_SHORT).show();
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
        getApplication().registerActivityLifecycleCallbacks(callback);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //此處寫自己的Application邏輯
    }
}
步驟五:注冊一個處理加載補(bǔ)丁結(jié)果的service(SampleResultService)

在步驟四中的application類里琼掠,我們看到重寫的 onBaseContextAttached() 方法里出現(xiàn)了一個繼承自 DefaultTinkerResultService 的 SampleResultService 類,而這個 SampleResultService 類就是我們在加載補(bǔ)丁后供 Tinker 回調(diào)的一個類停撞。Demo的service中所做的操作是在你加載成功熱更新插件后瓷蛙,會提示你更新成功,并且這里做了鎖屏操作就會加載熱更新插件戈毒。然而艰猬,這個service里的具體邏輯是可以根據(jù)自己項目的需求,具體設(shè)計埋市。如下所示:

package com.tinker.deeson.mytinkerdemo;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;

import com.tencent.tinker.lib.service.DefaultTinkerResultService;
import com.tencent.tinker.lib.service.PatchResult;
import com.tencent.tinker.lib.util.TinkerLog;
import com.tencent.tinker.lib.util.TinkerServiceInternals;
import com.tencent.tinker.loader.shareutil.SharePatchFileUtil;

import java.io.File;


/**
 * optional, you can just use DefaultTinkerResultService
 * we can restart process when we are at background or screen off
 */
public class SampleResultService extends DefaultTinkerResultService {
    private static final String TAG = "Tinker.SampleResultService";


    @Override
    public void onPatchResult(final PatchResult result) {
        if (result == null) {
            TinkerLog.e(TAG, "SampleResultService received null result!!!!");
            return;
        }
        TinkerLog.i(TAG, "SampleResultService receive result: %s", result.toString());

        //first, we want to kill the recover process
        TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());

        Handler handler = new Handler(Looper.getMainLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (result.isSuccess) {
                    Toast.makeText(getApplicationContext(), "patch success, please restart process", Toast.LENGTH_LONG).show();
                } else {
                    Toast.makeText(getApplicationContext(), "patch fail, please check reason", Toast.LENGTH_LONG).show();
                }
            }
        });
        // is success and newPatch, it is nice to delete the raw file, and restart at once
        // for old patch, you can't delete the patch file
        if (result.isSuccess) {
            File rawFile = new File(result.rawPatchFilePath);
            if (rawFile.exists()) {
                TinkerLog.i(TAG, "save delete raw patch file");
                SharePatchFileUtil.safeDeleteFile(rawFile);
            }
            //not like TinkerResultService, I want to restart just when I am at background!
            //if you have not install tinker this moment, you can use TinkerApplicationHelper api
            if (checkIfNeedKill(result)) {
                if (Utils.isBackground()) {
                    TinkerLog.i(TAG, "it is in background, just restart process");
                    restartProcess();
                } else {
                    //we can wait process at background, such as onAppBackground
                    //or we can restart when the screen off
                    TinkerLog.i(TAG, "tinker wait screen to restart process");
                    new ScreenState(getApplicationContext(), new ScreenState.IOnScreenOff() {
                        @Override
                        public void onScreenOff() {
                            restartProcess();
                        }
                    });
                }
            } else {
                TinkerLog.i(TAG, "I have already install the newly patch version!");
            }
        }
    }

    /**
     * you can restart your process through service or broadcast
     */
    private void restartProcess() {
        TinkerLog.i(TAG, "app is background now, i can kill quietly");
        //you can send service or broadcast intent to restart your process
        android.os.Process.killProcess(android.os.Process.myPid());
    }

    static class ScreenState {
        interface IOnScreenOff {
            void onScreenOff();
        }

        ScreenState(Context context, final IOnScreenOff onScreenOffInterface) {
            IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_SCREEN_OFF);
            context.registerReceiver(new BroadcastReceiver() {

                @Override
                public void onReceive(Context context, Intent in) {
                    String action = in == null ? "" : in.getAction();
                    TinkerLog.i(TAG, "ScreenReceiver action [%s] ", action);
                    if (Intent.ACTION_SCREEN_OFF.equals(action)) {

                        context.unregisterReceiver(this);

                        if (onScreenOffInterface != null) {
                            onScreenOffInterface.onScreenOff();
                        }
                    }
                }
            }, filter);
        }
    }

}
步驟六:Utils工具類
package com.tinker.deeson.mytinkerdemo;

import android.os.Environment;
import android.os.StatFs;

import com.tencent.tinker.loader.shareutil.ShareConstants;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;

public class Utils {

    /**
     * the error code define by myself
     * should after {@code ShareConstants.ERROR_PATCH_INSERVICE
     */
    public static final int ERROR_PATCH_GOOGLEPLAY_CHANNEL      = -5;
    public static final int ERROR_PATCH_ROM_SPACE               = -6;
    public static final int ERROR_PATCH_MEMORY_LIMIT            = -7;
    public static final int ERROR_PATCH_ALREADY_APPLY           = -8;
    public static final int ERROR_PATCH_CRASH_LIMIT             = -9;
    public static final int ERROR_PATCH_RETRY_COUNT_LIMIT       = -10;
    public static final int ERROR_PATCH_CONDITION_NOT_SATISFIED = -11;

    public static final String PLATFORM = "platform";

    public static final int MIN_MEMORY_HEAP_SIZE = 45;

    private static boolean background = false;

    public static boolean isGooglePlay() {
        return false;
    }

    public static boolean isBackground() {
        return background;
    }

    public static void setBackground(boolean back) {
        background = back;
    }

    public static int checkForPatchRecover(long roomSize, int maxMemory) {
        if (Utils.isGooglePlay()) {
            return Utils.ERROR_PATCH_GOOGLEPLAY_CHANNEL;
        }
        if (maxMemory < MIN_MEMORY_HEAP_SIZE) {
            return Utils.ERROR_PATCH_MEMORY_LIMIT;
        }
        //or you can mention user to clean their rom space!
        if (!checkRomSpaceEnough(roomSize)) {
            return Utils.ERROR_PATCH_ROM_SPACE;
        }

        return ShareConstants.ERROR_PATCH_OK;
    }

    public static boolean isXposedExists(Throwable thr) {
        StackTraceElement[] stackTraces = thr.getStackTrace();
        for (StackTraceElement stackTrace : stackTraces) {
            final String clazzName = stackTrace.getClassName();
            if (clazzName != null && clazzName.contains("de.robv.android.xposed.XposedBridge")) {
                return true;
            }
        }
        return false;
    }

    @Deprecated
    public static boolean checkRomSpaceEnough(long limitSize) {
        long allSize;
        long availableSize = 0;
        try {
            File data = Environment.getDataDirectory();
            StatFs sf = new StatFs(data.getPath());
            availableSize = (long) sf.getAvailableBlocks() * (long) sf.getBlockSize();
            allSize = (long) sf.getBlockCount() * (long) sf.getBlockSize();
        } catch (Exception e) {
            allSize = 0;
        }

        if (allSize != 0 && availableSize > limitSize) {
            return true;
        }
        return false;
    }

    public static String getExceptionCauseString(final Throwable ex) {
        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        final PrintStream ps = new PrintStream(bos);

        try {
            // print directly
            Throwable t = ex;
            while (t.getCause() != null) {
                t = t.getCause();
            }
            t.printStackTrace(ps);
            return toVisualString(bos.toString());
        } finally {
            try {
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static String toVisualString(String src) {
        boolean cutFlg = false;

        if (null == src) {
            return null;
        }

        char[] chr = src.toCharArray();
        if (null == chr) {
            return null;
        }

        int i = 0;
        for (; i < chr.length; i++) {
            if (chr[i] > 127) {
                chr[i] = 0;
                cutFlg = true;
                break;
            }
        }

        if (cutFlg) {
            return new String(chr, 0, i);
        } else {
            return src;
        }
    }
}
步驟七:AndroidManifest.xml文件
  1. 在application標(biāo)簽里加入步驟四中新建的Application類 android:name=".SampleApplication"冠桃,此處的名字需要與步驟四的SampleApplicationLike 類最頂部的@DefaultLifeCycle()注解保持一致。如果你添加不進(jìn)去道宅,或者是紅色的話食听,請先build一下。如下紅色圈中:
    SampleApplicationLike
  2. 注冊SampleResultService
  3. 加入訪問sdcard權(quán)限污茵,Android6.0以上的樱报,請自行解決權(quán)限問題,很簡單泞当。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.tinker.deeson.mytinkerdemo">

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

    <application
        android:name=".SampleApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service
            android:name=".SampleResultService"
            android:exported="false" />
    </application>

</manifest>
步驟八:MainActivity 類中對 Tinker API 的調(diào)用

只有兩個按鈕迹蛤,一個是加載熱補(bǔ)丁插件;一個是殺死應(yīng)用加載補(bǔ)丁襟士。

package com.tinker.deeson.mytinkerdemo;

import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

import com.tencent.tinker.lib.tinker.TinkerInstaller;
import com.tencent.tinker.loader.shareutil.ShareTinkerInternals;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.btn_load).setOnClickListener(this);
        findViewById(R.id.btn_kill).setOnClickListener(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        Utils.setBackground(false);
    }

    @Override
    protected void onPause() {
        super.onPause();
        Utils.setBackground(true);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_load:
                loadPatch();
            break;
            case R.id.btn_kill:
                killApp();
            break;
        }
    }

    /**
     * 加載熱補(bǔ)丁插件
     */
    public void loadPatch() {
        TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
                Environment.getExternalStorageDirectory().getAbsolutePath() + "/myTinkerDemo/TinkerPatch");
    }

    /**
     * 殺死應(yīng)用加載補(bǔ)丁
     */
    public void killApp() {
        ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
        android.os.Process.killProcess(android.os.Process.myPid());
    }
}

看看主界面的樣子盗飒,很樸素。


activity_main.xml

至此陋桂,我們對 Tinker 的接入已經(jīng)完成了逆趣,剩下的就是對 Tinker 熱修復(fù)的測試。

二章喉、測試Tinker

步驟一:打基礎(chǔ)包

點開 Android Studio的Gradle 界面,如下,雙擊 assembleDebug 或者 assembleRelease:


Gradle

注意看秸脱,項目目錄build文件夾里面落包,在雙擊 assembleDebug 或者 assembleRelease 之前,是如下界面:

build文件夾

在雙擊 assembleDebug 或者 assembleRelease 之后摊唇,build文件夾下面會生成一個bakApk文件夾咐蝇,里面存放著我們的基礎(chǔ)包,里面的apk文件用于安裝到手機(jī)測試或者發(fā)布到應(yīng)用市場巷查,(這里生成的基礎(chǔ)安裝包和 R文件以及release版本的mapping文件一定要自己保存好有序,因為通過后續(xù)的步驟你會清楚地看到,每次打補(bǔ)丁包都需要用到這些文件作為基礎(chǔ)文件岛请,丟掉的話旭寿,后果就很滑稽臉了),如下:

  • **如果打包失敗崇败,請clean一下項目盅称,再雙擊 assembleDebug 或者 assembleRelease; **
  • 如果clean之后再打包還失敗后室,那就需要看具體的報錯缩膝,慢慢調(diào)試設(shè)置(首先很有可能是代碼混淆的問題,文末有混淆相關(guān)的介紹文章岸霹,寫得很全面易懂)疾层。
bakApk文件夾

安裝到手機(jī)后,發(fā)現(xiàn)贡避,有bug痛黎,如下顯示,果斷接著步驟二贸桶,使用Tinker緊急熱修復(fù)這個bug:


有bug版本
步驟二:打補(bǔ)丁包

1.將步驟一生成的 bakApk 文件夾中的 apk 文件和 R 文件的名稱舅逸,填寫到app的 build.gradle 類的 ext 這里,sync一下,如下:


build.gradle debug版

當(dāng)然皇筛,如果在步驟一打的是release的基礎(chǔ)包的話琉历,會多一個mapping文件,同樣將它的名稱填寫到app的 build.gradle 類的 ext 這里水醋,界面如下:

build.gradle release版

2.接著旗笔,我們?nèi)バ薷闹鹘缑娴腷ug,并增加一個圖片資源文件(圖片自己找)拄踪,如下:

activity_main.xml

3.接下來蝇恶,真正地打補(bǔ)丁包,點開 Android Studio的Gradle 界面惶桐,如下撮弧,雙擊 tinkerPatchDebug 或者tinkerPatchRelease 潘懊,如下:


Gradle Tinker

4.緊接著,Tinker 在build 文件夾下的 outputs 文件夾里面會生成我們需要的補(bǔ)丁文件贿衍,patch_signed_7zip.apk 就是我們所要的補(bǔ)丁包授舟,如下:

outputs tinkerPatch

當(dāng)然,如果你想了解更多關(guān)于輸出文件的情況贸辈,可以點開Tinker Wiki 的 輸出文件詳解释树。

步驟三:將補(bǔ)丁包拷貝到手機(jī)sdcard中測試

將步驟二生成的 tinkerPatch 文件夾下面的 patch_signed_7zip.apk 文件,拷貝出來擎淤,改成你的 MainActivity中加載的文件名字奢啥,demo這里叫TinkerPatch,將其拷貝到手機(jī)的sdcard中的myTinkerDemo 文件夾下嘴拢,沒有這個文件夾你就自己手動新建一個桩盲,下圖帶你回顧一下 MainActivity 的設(shè)置:

注意此處,測試和發(fā)布版本的不同:發(fā)布版本的補(bǔ)丁文件一般是通過網(wǎng)絡(luò)下載下來炊汤,存放到sdcard中正驻,再加載。

MainActivity

步驟四:加載補(bǔ)丁

點擊主頁的加載補(bǔ)丁按鈕抢腐,沒加載之前如下界面:


有bug

點擊加載補(bǔ)丁之后姑曙,鎖屏或者殺死進(jìn)程,再次進(jìn)入demo迈倍,補(bǔ)丁已經(jīng)加載出來伤靠,并且 sdcard中的補(bǔ)丁包也會被刪除掉,因為它和老apk合并了啼染。如下:

tinker fixed

OK!大功告成宴合!

問題記錄

  1. 如果有同學(xué)遇到熱修復(fù)過的app,無法正常進(jìn)行版本升級的問題的話迹鹅,可以參考這里卦洽,每次版本升級都需要更新 build.gradle 文件里的 TINKER_ID。如下圖所示:
關(guān)于TinkerId的問題

后續(xù)

感謝微信團(tuán)隊斜棚!
Tinker github

關(guān)于release版本的混淆

可以參考這篇文章 5分鐘搞定android混淆

關(guān)于release版本的多渠道打包

可以參考我的下一篇文章 (續(xù))Android熱更新:Tinker與Walle多渠道打包

關(guān)于某些平臺需要加固的問題

可以參考Tinker官方wiki Tinker是否兼容加固

Demo 源碼下載

哈哈哈哈阀蒂,Demo 的 github 地址在這里

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市弟蚀,隨后出現(xiàn)的幾起案子蚤霞,更是在濱河造成了極大的恐慌,老刑警劉巖义钉,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件昧绣,死亡現(xiàn)場離奇詭異,居然都是意外死亡捶闸,警方通過查閱死者的電腦和手機(jī)夜畴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門拖刃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贪绘,你說我怎么就攤上這事序调。” “怎么了兔簇?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長硬耍。 經(jīng)常有香客問我垄琐,道長,這世上最難降的妖魔是什么经柴? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任狸窘,我火速辦了婚禮,結(jié)果婚禮上坯认,老公的妹妹穿的比我還像新娘翻擒。我一直安慰自己,他們只是感情好牛哺,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布陋气。 她就那樣靜靜地躺著,像睡著了一般引润。 火紅的嫁衣襯著肌膚如雪巩趁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天淳附,我揣著相機(jī)與錄音议慰,去河邊找鬼。 笑死奴曙,一個胖子當(dāng)著我的面吹牛别凹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播洽糟,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼炉菲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了脊框?” 一聲冷哼從身側(cè)響起颁督,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎浇雹,沒想到半個月后沉御,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡昭灵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年吠裆,在試婚紗的時候發(fā)現(xiàn)自己被綠了伐谈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡试疙,死狀恐怖诵棵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情祝旷,我是刑警寧澤履澳,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站怀跛,受9級特大地震影響距贷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吻谋,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一忠蝗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧漓拾,春花似錦阁最、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至低千,卻和暖如春哟旗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背栋操。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工闸餐, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人矾芙。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓舍沙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親剔宪。 傳聞我的和親對象是個殘疾皇子拂铡,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容