Android開發之Tinker的集成和使用介紹

Android開發之Tinker的集成和使用介紹。

對於熱修復我相信很多小夥伴都已經知道它們普遍的操作套路,Tinker主要是依賴自己的gradlePlugin生成拆分包,所以其拆分包的生成就由Gradle來完成,當然也可以通過命令行的方式,這裡就不對命令行做講解,Tinker接入指南

項目結構

這裡寫圖片描述

Tinker介紹

來自Tinker官方

1、優點

這裡寫圖片描述

2、缺點

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

Tinker集成

1、在項目的build.gradle中,添加依賴

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
        // Tinker
        classpath ("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}")
    }
}

這裡的TINKER_VERSION寫在項目gradle.properties中

TINKER_VERSION=1.7.7

2、在app的build.gradle文件,添加依賴

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

添加依賴以後,我們在gradle文件中做以下配置

開啟Multidex、配置Java編譯的版本 配置簽名文件,為瞭後面打包方便調試 引入另一個gradle文件專門來對Tinker生成拆分包的配置(由於多渠道要用到gradle的參數,所以將引入放在末尾)

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.3"
    defaultConfig {
        applicationId "com.handsome.thinker"
        minSdkVersion 16
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        multiDexEnabled true
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }
    dexOptions {
        jumboMode = true
    }
    signingConfigs {
        debug {
            keyAlias 'hensen'
            keyPassword '123456'
            storeFile file("../Hensen.jks")
            storePassword '123456'
        }
        release {
            keyAlias 'hensen'
            keyPassword '123456'
            storeFile file("../Hensen.jks")
            storePassword '123456'
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
        debug {
            minifyEnabled false
            signingConfig signingConfigs.debug
        }
    }
}
// 加入Tinker生成補丁包的gradle
apply from: 'buildTinker.gradle'

3、buildTinker.gradle是專門為Tinker配置和生成拆分包而寫的,具體可以參考官方gradle

//指定生成apk文件的存放位置
def bakPath = file("${buildDir}/bakApk/")
//參數配置
ext {
    //開啟Tinker
    tinkerEnable = true
    //舊的apk位置,需要我們手動指定
    tinkerOldApkPath = "${bakPath}/"
    //舊的混淆映射位置,如果開啟瞭混淆,則需要我們手動指定
    tinkerApplyMappingPath = "${bakPath}/"
    //舊的resource位置,需要我們手動指定
    tinkerApplyResourcePath = "${bakPath}/"
    tinkerID = "1.0"
}

def buildWithTinker() {
    return ext.tinkerEnable
}

def getOldApkPath() {
    return ext.tinkerOldApkPath
}

def getApplyMappingPath() {
    return ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath(){
    return ext.tinkerApplyResourcePath
}

def getTinkerIdValue(){
    return ext.tinkerID
}

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

    tinkerPatch {
        oldApk = getOldApkPath() //指定old apk文件路徑
        ignoreWarning = false //不忽略tinker警告,出現警告則中止patch文件生成
        useSign = true //patch文件必須是簽名後的
        tinkerEnable = buildWithTinker() //指定是否啟用tinker
        buildConfig {
            applyMapping = getApplyMappingPath() //指定old apk打包時所使用的混淆文件
            applyResourceMapping = getApplyResourceMappingPath() //指定old apk的資源文件
            tinkerId = getTinkerIdValue() //指定TinkerID
            keepDexApply = false
        }
        dex {
            dexMode = "jar" //jar、raw
            pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] //指定dex文件目錄
            loader = ["com.handsome.thinker.AppLike.MyTinkerApplication"] //指定加載patch文件時用到的類
        }
        lib {
            pattern = ["libs/*/*.so"] //指定so文件目錄
        }
        res {
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] //指定資源文件目錄
            ignoreChange = ["assets/sample_meta.txt"] //指定不受影響的資源路徑
            largeModSize = 100 //資源修改大小默認值
        }
        packageConfig {
            configField("patchMessage", "fix the 1.0 version's bugs")
            configField("patchVersion", "1.0")
        }
    }

    /**
     * 是否配置瞭多渠道
     */
    List flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0

    /**
     * 復制apk包和其它必須文件到指定目錄
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name
        def date = new Date().format("yyyy-MM-dd-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")
                        }
                    }
                }
            }
        }
    }
}

4、記得開啟Manifest權限,否則生成拆分包的時候有奇怪錯誤


Tinker封裝

我們提供兩個方法來初始化Tinker

默認的方式 自定義模塊的方式

public class TinkerManager {

    private static boolean isInstalled = false;
    // 這裡的ApplicationLike可以理解為Application的載體
    private static ApplicationLike mAppLike;
    private static CustomPatchListener mPatchListener;

    /**
     * 默認初始化Tinker
     *
     * @param applicationLike
     */
    public static void installTinker(ApplicationLike applicationLike) {
        mAppLike = applicationLike;
        if (isInstalled) {
            return;
        }

        TinkerInstaller.install(mAppLike);
        isInstalled = true;
    }

    /**
     * 初始化Tinker,帶有自定義模塊
     * 

* 1、CustomPatchListener * 2、CustomResultService * * @param applicationLike * @param md5Value 服務器下發的md5 */ public static void installTinker(ApplicationLike applicationLike, String md5Value) { mAppLike = applicationLike; if (isInstalled) { return; } mPatchListener = new CustomPatchListener(getApplicationContext()); mPatchListener.setCurrentMD5(md5Value); // Load補丁包時候的監聽 LoadReporter loadReporter = new DefaultLoadReporter(getApplicationContext()); // 補丁包加載時候的監聽 PatchReporter patchReporter = new DefaultPatchReporter(getApplicationContext()); AbstractPatch upgradePatchProcessor = new UpgradePatch(); TinkerInstaller.install(applicationLike, loadReporter, patchReporter, mPatchListener, CustomResultService.class, upgradePatchProcessor); isInstalled = true; } /** * 增加補丁包 * * @param path */ public static void addPatch(String path) { if (Tinker.isTinkerInstalled()) { TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path); } } /** * 獲取上下文 * * @return */ private static Context getApplicationContext() { if (mAppLike != null) { return mAppLike.getApplication().getApplicationContext(); } return null; } }

由於Tinker默認Patch檢查是沒有對文件做Md5校驗,我們可以重寫其檢驗的方法,加上我們自己的檢驗邏輯(需要自定義模塊的方式初始化Tinker)

CustomPatchListener.java

public class CustomPatchListener extends DefaultPatchListener {

    private String currentMD5;

    public void setCurrentMD5(String md5Value) {
        this.currentMD5 = md5Value;
    }

    public CustomPatchListener(Context context) {
        super(context);
    }

    /**
     * patch的檢測
     *
     * @param path
     * @return
     */
    @Override
    protected int patchCheck(String path) {
        //MD5校驗的工具可以網上查找
        //這裡要求我們在初始化Tinker的時候加上MD5的參數
        //增加patch文件的md5較驗
        if (!MD5Utils.isFileMD5Matched(path, currentMD5)) {
            return ShareConstants.ERROR_PATCH_DISABLE;
        }
        return super.patchCheck(path);
    }
}

由於Tinker默認安裝完補丁包之後是刪除補丁包,然後殺掉進程的方式,我們可以修改殺掉進程的行為(需要自定義模塊的方式初始化Tinker)

CustomResultService.java

public class CustomResultService extends DefaultTinkerResultService {

    private static final String TAG = "Tinker.SampleResultService";

    /**
     * patch文件的最終安裝結果,默認是安裝完成後殺掉自己進程
     * 此段代碼主要是復制DefaultTinkerResultService的代碼邏輯
     */
    @Override
    public void onPatchResult(PatchResult result) {
        if (result == null) {
            TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");
            return;
        }
        TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());

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

        // if success and newPatch, it is nice to delete the raw file, and restart at once
        // only main process can load an upgrade patch!
        if (result.isSuccess) {
            //刪除patch包
            deleteRawPatchFile(new File(result.rawPatchFilePath));
            //殺掉自己進程,如果不需要則可以註釋,在這裡做自己的邏輯
            if (checkIfNeedKill(result)) {
                android.os.Process.killProcess(android.os.Process.myPid());
            } else {
                TinkerLog.i(TAG, "I have already install the newly patch version!");
            }
        }
    }
}

Tinker使用

1、Tinker的使用需要ApplicationLike來生成我們的Application,然後初始化Multidex和Tinker

@DefaultLifeCycle(application = ".MyTinkerApplication", flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false)
public class CustomTinkerLike extends ApplicationLike {

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

    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);

        MultiDex.install(base);
        TinkerManager.installTinker(this);
    }
}

2、編譯項目自動生成Application,然後在Manifest中指定我們的生成的Application

3、在主頁面按鈕的點擊事件,來加載放在緩存目錄下的補丁包

public class MainActivity extends AppCompatActivity {

    private String mPath;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mPath = getExternalCacheDir().getAbsolutePath() + File.separatorChar;
    }

    /**
     * 加載Tinker補丁
     *
     * @param view
     */
    public void Fix(View view) {
        File patchFile = new File(mPath, "patch_signed.apk");
        if (patchFile.exists()) {
            TinkerManager.addPatch(patchFile.getAbsolutePath());
            Toast.makeText(this, "File Exists,Please wait a moment ", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "File No Exists", Toast.LENGTH_SHORT).show();
        }
    }
}

Tinker測試

完成Tinker的所有準備工作後,我們通過默認的初始化Tinker方式測試我們的補丁包

1、找到gradle工具欄,點擊生成Release包,作為1.0版本的程序

這裡寫圖片描述

2、將生成的Release包Push到手機上,安裝,運行程序

生成apk的目錄在build的bakApk目錄下

這裡寫圖片描述

運行程序

這裡寫圖片描述

3、在項目中,對主界面添加加載圖片的按鈕,同時添加一個drawable文件

public class MainActivity extends AppCompatActivity {

    private String mPath;
    private ImageView iv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        iv = (ImageView) findViewById(R.id.iv);

        mPath = getExternalCacheDir().getAbsolutePath() + File.separatorChar;
    }

    /**
     * 加載Tinker補丁
     *
     * @param view
     */
    public void Fix(View view) {
        File patchFile = new File(mPath, "patch_signed.apk");
        if (patchFile.exists()) {
            TinkerManager.addPatch(patchFile.getAbsolutePath());
            Toast.makeText(this, "File Exists,Please wait a moment ", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "File No Exists", Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * 新增的按鈕點擊事件
     *
     * @param view
     */
    public void Load(View view) {
        iv.setImageResource(R.drawable.bg_content_header);
    }

}

4、同時記得修改buildTinker.gradle的old安裝包的路徑,Tinker需要比對前後安裝包然後生成補丁包

//參數配置
ext {
    //開啟Tinker
    tinkerEnable = true
    //舊的apk位置,需要我們手動指定
    tinkerOldApkPath = "${bakPath}/app-release-2017-11-19-18-34-12.apk"
    //舊的混淆映射位置,如果開啟瞭混淆,則需要我們手動指定
    tinkerApplyMappingPath = "${bakPath}/"
    //舊的resource位置,需要我們手動指定
    tinkerApplyResourcePath = "${bakPath}/app-release-2017-11-19-18-34-12-R.txt"
    tinkerID = "1.0"
}

5、找到gradle工具欄,點擊thinker生成Release補丁包,作為1.0版本的補丁

這裡寫圖片描述

6、將生成的Release補丁包Push到手機的緩存目錄上,運行程序點擊修復補丁包,稍等數秒程序會被殺掉,重啟點擊加載圖片按鈕

生成的補丁包

這裡寫圖片描述

記得將補丁放到緩存目錄下,修復補丁後的程序

這裡寫圖片描述

Tinker多渠道打包

1、Tinker支持多渠道打包,我們采用友盟的打包方式,下載友盟SDK,將jar包增加到項目上

這裡寫圖片描述

2、初始化友盟SDK(新版本的SDK似乎不用初始化瞭,找不到初始化入口)

3、Manifest增加友盟的AppKey配置和渠道配置


4、在app的build.gradle中增加多渠道打包信息

/**
 * 配置多渠道
 */
productFlavors {
    googleplayer {
        manifestPlaceholders = [UMENG_CHANNEL_VALUE: "googleplayer"]
    }
    baidu {
        manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"]
    }
    wangdoujia {
        manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wangdoujia"]
    }
    productFlavors.all {
        flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
    }
}

5、在buildTinker.gradle增加配置多渠道補丁包的生成規則

/**
 * 生成多渠道補丁包
 */
project.afterEvaluate {
    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"
                }

            }
        }
    }
}

6、找到gradle工具欄,點擊生成Release包,作為1.0版本的程序

這裡寫圖片描述

7、同時記得修改buildTinker.gradle的old安裝包的路徑,Tinker需要比對前後安裝包然後生成補丁包

//參數配置
ext {
    //開啟Tinker
    tinkerEnable = true
    //舊的apk位置,需要我們手動指定
    tinkerOldApkPath = "${bakPath}/app-2017-11-19-20-35-23"
    //舊的混淆映射位置,如果開啟瞭混淆,則需要我們手動指定
    tinkerApplyMappingPath = "${bakPath}/app-2017-11-19-20-35-23"
    //舊的resource位置,需要我們手動指定
    tinkerApplyResourcePath = "${bakPath}/app-2017-11-19-20-35-23"
    //舊的多渠道位置,需要我們手動指定
    tinkerBuildFlavorDirectory = "${bakPath}/app-2017-11-19-20-35-23"
    tinkerID = "1.0"
}

8、找到gradle工具欄,點擊thinker生成Release補丁包,作為1.0版本的補丁

這裡對程序的修改就省略瞭

這裡寫圖片描述

後面的測試更上面一樣,也就省略瞭。

You May Also Like