Thinker 使用詳解


二次補充,直接用 bugly吧,基於 tinker,使用簡單,方便集成

Tinker基本介紹

Tinker 是微信官方的 Andriod 熱補丁解決方案,它支持動態下發代碼,so庫以及資源,讓應用在不需要安裝的情況下實現更新,當然,你也可以使用 Thinker 來更新你的插件。

它主要包含以下幾部分:

​ 1,gradle 編譯插件 tinker-patch-gradle-plugin:主要用於在 as 中直接完成 patch 文件的生成

​ 2,核心 sdk 庫 tinker-android-lib :核心庫,爲應用層提供的 api

​ 3,非 gradle 編譯用戶的命令行版本,tinker-path-cli.jar :爲 eclipse 做的一個工具

爲什麼使用 Tinker

​ 當前市面上 熱修復的解決方案很多,但是他們都有一些無法解決的問題,但是 Tinker 的功能是比較全面的。

Tinker QZone AndFix Robust
類替換 yes yes no no
So 替換 yes no no no
資源替換 yes yes no no
全平臺支持 yes yes yes yes
即時生效 no no yes yes
性能損耗 較小 較大 較小 較小
補丁包大小 較小 較大 一般 一般
開發透明 yes yes no no
複雜度 較低 較低 複雜 複雜
gradle 支持 yes no no no
Rom 體積 較大 較小 較小 較小
成功率 較高 較高 一般 最高

Tinker 執行原理及流程

​ 基於 android 原生的 ClassLoader ,開發了自己的 ClassLoader,通過自定義的 ClassLoader去加載 patch 文件中的字節碼

​ 基於 android 原生的 aapt,開發了自己的 aapt ,加載 資源文件

​ 基於 Dex 文件的格式,研發了 DexDiff 算法,通過 DexDiff 算法比較兩個 apk 文件中的差異

簡單的使用 Tinker

1,在項目的gradle.properties 中添加

# tinker版本號 ,控制版本,以下版本已經兼容 9.0
TINKER_VERSION=1.9.14
TINKERPATCH_VERSION=1.2.14

2,在項目的 gradle中添加:

classpath("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}")

3,在 app 中的 gradle 中添加:

   compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    // tinker 的核心 sdk 庫
    implementation("com.tinkerpatch.sdk:tinkerpatch-android-sdk:${TINKERPATCH_VERSION}") { changing = true }

4,接着進行初始化,新建一個類用於管理 tinker 的初始化

/**
 * 對 TinkerManager api 做一層封裝
 */
public class TinkerManager {

    /**
     * 是否初始化
     */
    private static boolean isInstalled = false;

    private static ApplicationLike mApplike;

    /**
     * 完成 Tinker初始化
     *
     * @param applicationLike
     */
    public static void installTinker(ApplicationLike applicationLike) {
        mApplike = applicationLike;
        if (isInstalled) {
            return;
        }
        //完成 tinker 初始化
        TinkerInstaller.install(mApplike);
        isInstalled = true;
    }

    public static void loadPatch(String path) {
        if (Tinker.isTinkerInstalled()) {
            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);
        }
    }

    /**
     * 通過 ApplicationLike 獲取 Context
     */
    private static Context getApplicationContext() {
        if (mApplike != null) {
            return mApplike.getApplication().getApplicationContext();
        }
        return null;
    }

}

5,自定義 application 繼承自 ApplicationLike

//通過 DefaultLifeCycle 註解來生成我們程序中需要用到的 Application
@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);
        //初始化
        TinkerManager.installTinker(this);
    }
}

爲什麼需要繼承 ApplicationLike,而不直接在 Application 中初始化呢?

​ 因爲 ApplicationLike 需要對 Application 的生命週期進行監聽, 所以他通過 ApplicationLike 進行代理。通過這個代理可以完成對 Application 的生命週期監聽,然後在不同的生命週期做一些別的工作,因爲Tinker 初始化非常複雜,所用用 ApplicationLike 進行了代理,這樣使用就非常簡單了!

​ 注意上面的註解:通過這個註解可以生成 需要在程序中進行添加的 application

public class MyTinkerApplication extends TinkerApplication {

    public MyTinkerApplication() {
        super(15, "com.testdemo.www.tinker.CustomTinkerLike", "com.tencent.tinker.loader.TinkerLoader", false);
    }
}

​ 上面這個就是通過註解生成的。我們需要將他添加的 AndroidManifest 中。

6,配置 tinker

//buildDir 代表的是 app 目錄下 build 文件夾,
// 如果創建成果,他會在 build 文件夾中創建 bakApk文件夾,存放 old.apk
def bakPath = file("${buildDir}/bakApk") //指定基準文件存放位置

ext {
    tinkerEnable = true
    tinkerID = "1.0"
    tinkerOldApkPath = "${bakPath}/"
    tinkerApplyMappingPath = "${bakPath}/"
    tinkerApplyResourcePath = "${bakPath}/"
}

//是否啓用 tinker
def buildWithTinker() {
    return ext.tinkerEnable
}
// old 路徑
def getOldApkPath() {
    return ext.tinkerOldApkPath
}
// 混淆文件路徑
def getApplyMappingPath() {
    return ext.tinkerApplyMappingPath
}
// 資源文件路徑
def getApplyResourceMappingPath() {
    return ext.tinkerApplyResourcePath
}
// id
def getTinkerIdValue() {
    return ext.tinkerID
}

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

    //所有 tinker 相關的參數配置
    tinkerPatch() {
        oldApk = getOldApkPath() // old.apk 路徑
        ignoreWarning = false //不忽略警告,如果警告取消生成patch
        useSign = true  // 強制 patch 文件使用簽名
        tinkerEnable = buildWithTinker() //指示是否啓用 tinker
        buildConfig() {
            applyMapping = getApplyMappingPath() // 指定old.apk 打包時所使用的的混淆文件
            applyResourceMapping = getApplyResourceMappingPath() // 指定 old.apk 資源文件
            tinkerId = getTinkerIdValue() //指定 TinkerId
            keepDexApply = false //一般置爲 false,true:生成patch 的時候會根據 dex 文件的分包去動態的編譯 patch 文件
        }

        dex() {
            dexMode = "jar"  //jar 是配到14以下,會將dex壓縮爲jar文件,然後進行處理,體積小row只能在14以上使用,直接對 dex 文件處理
            pattern = ["classes*.dex", "assets/secondary-dex-?.jar"]//指定 dex 文件位於哪些牡蠣
            loader = ["com.testdemo.www.tinker.MyTinkerApplication"] //指定加載patch文件時所用到的類
        }

        //工程中的 jar 和 so
        lib {
            pattern = ["libs/*/*.so"]
        }

        res { //指定 tinker 可以修改的資源文件路徑
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            ignoreChange = ["assets/sampple_meta.txt"] //制定不受影響的資源路徑,即時修改了這個資源文件,也不會被打入 patch
            largeModSize = 100 //資源修改大小默認值
        }
//-----------------必須項配置完成-------------------------------------
        //patch的介紹
        packageConfig {
            //補丁加載完成後通過 key 可以拿到這些 value
            configField("patchMessage", "fix 1.0 version's bugs")
            configField("patchVersion", "1.0")
        }

        //使用壓縮
        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
   

    /**
     * 複製基準包和其它必須文件到指定目錄
     */
    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[0].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")
                        }
                    }
                }
            }
        }
    }
    
}

注意 ext 中:

tinkerEnable = true //是否啓用 tinker
tinkerID = “1.0” // id ,線上的版本 id 和 補丁包的 tinkerID 必須相等

詳細的說明

7,進行測試,打包

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private static final String FILE_END = ".apk";
    private String mPatchDir;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
		//創建路徑
        mPatchDir = getExternalCacheDir().getAbsolutePath() + "/tpatch/";
        File file = new File(mPatchDir);
        file.mkdir();
        findViewById(R.id.btn).setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
    	//加載補丁包
        TinkerManager.loadPatch(getPatchName());
    }
	//拼裝一個路徑
    private String getPatchName() {
        return mPatchDir.concat("tinker").concat(FILE_END);
    }

}

看一下佈局

<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginTop="20dp"
        android:text="加載 PATCH 包"
        android:textSize="20sp"
        tools:ignore="HardcodedText" />

</androidx.appcompat.widget.LinearLayoutCompat>

接着我們就可以進行打包了,注意不能是 debug 。是需要簽名的。打包完成後會在 build 文件下生成 bakApk 文件夾,裏面就是打包的 apk。
在這裏插入圖片描述

然後把這個apk安裝到手機上即可。

8,創建補丁文件

創建補丁文件的時候需要線上的 apk。所以在這裏 我們將剛纔打包的 apk 名字複製下來,然後放在build.gradle 中的 ext 中,如下所示:
在這裏插入圖片描述
這裏的路徑就是 build 中 bakApk 中的文件,第一個是 apk,第二個是混淆路徑,因爲我沒有使用混淆,所以在上面打包後就沒有混淆文件。第三個對應資源文件。注意這裏一定要填正確,否則會導致不能生成補丁文件。弄完之後同步一下。

接着就可以修復bug了。這裏我們進行模擬一下。

<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginTop="20dp"
        android:text="加載 PATCH 包"
        android:textSize="20sp"
        tools:ignore="HardcodedText" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:gravity="center"
        android:text="我是被修復的bug"
        android:textSize="25sp" />

</androidx.appcompat.widget.LinearLayoutCompat>

修改了一下佈局,添加了一個 TextView。

接着就需要使用 tinker 插件生成補丁了。如下

在這裏插入圖片描述

點擊 tinkerPatchRelease 即可生成 補丁包,如果出現 IO 異常,可以刪除 build 文件夾中除過 bakApk 文件以外的所有文件,可能是有衝突。

或者是 出現 config 的錯誤:則在 gradle 的 android 包下添加如下:

 signingConfigs {
        config {
            storeFile file('C:\\Users\\Administrator\\Desktop\\345\\Project\\Tinker\\keystore.jks')
            storePassword '123456789'
            keyAlias = 'key0'
            keyPassword '123456789'
        }
    }

因爲使用 tinker 插件打包的時候也需要 key 和 密碼等。

最後生成的結果如下

在這裏插入圖片描述

9,加載補丁文件,修復bug

其中 patch_signed.apk 就是我們需要的補丁包。我們需要將補丁包複製到的我們程序中定義的路徑中:

在這裏插入圖片描述

這裏我用的電腦直接複製到手機中了

然後點擊加載 加載 PATCH 包 接着程序會直接退出。當你再次打開的時候就會發現你添加的 TextView 已經顯示出來了,最終的結果如下:

在這裏插入圖片描述

​ 經過上面幾步,我們就已經完成了一個從本地加載的熱修復

在項目中使用 Tinker

​ 當然了,我們不可能一直從本地加載補丁文件。所以我們需要對加載 補丁文件進行修改一下。

​ 新建一個 服務。在服務中我們會進行請求服務器是否有新的補丁,如果有補丁就下載到指定的目錄中,然後進行加載補丁文件。

public class TinkerService extends Service {

    /**
     * 文件後綴名
     */
    private static final String FILE_END = ".apk";
    /**
     * 下載 patch 文件信息
     */
    private static final int DOWNLOAD_PATCH = 0x01;
    /**
     * 檢查是否有 patch 更新
     */
    private static final int UPDATE_PATCH = 0x02;


    /**
     * patch 要保存的文件夾
     */
    private String mPatchFileDir;
    /**
     * patch 文件保存路徑
     */
    private String mFilePath;

    /**
     * 服務器 patch 的信息
     */
    private BasePatch mBasePatchInfo;

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case UPDATE_PATCH:
                    checkPatchInfo();
                    break;
                case DOWNLOAD_PATCH:
                    downloadPatch();
                    break;
            }
        }
    };

    private void downloadPatch() {
		//下載補丁文件
		mFilePath = mPatchFileDir.concat("tinker")
                        .concat(FILE_END);
		//.....下載完成,進行加載
         TinkerManager.loadPatch(mFilePath);
         //加載完成後終止服務
         stopSelf();
    }


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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //檢查是否有 patch 更新
        mHandler.sendEmptyMessage(UPDATE_PATCH);
        return START_NOT_STICKY;
    }

    private void init() {
        mPatchFileDir = getExternalCacheDir().getAbsolutePath() + "/tPatch/";
        File patchFileDir = new File(mPatchFileDir);
        try {
            if (!patchFileDir.exists()) {
                //文件夾不存在則創建
                patchFileDir.mkdir();
            }
        } catch (Exception e) {
            e.printStackTrace();
            //無法創建文件,終止服務
            stopSelf();
        }

    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    /**
     * 檢查是否有更新
     */
    private void checkPatchInfo() {
		//.....網絡請求獲取是否更新補丁文件,不更新就終止服務
		//下載補丁文件
        mHandler.sendEmptyMessage(DOWNLOAD_PATCH);
    }
}

Tinker 高級用法

  • Tinker 如何支持多渠道打包

    首先在項目中集成多渠道,我使用的是 walle,集成的方式非常簡單:

    添加插件

     classpath 'com.meituan.android.walle:plugin:1.1.6'
    

    在 app 的 build.gradle 中引入插件,設置依賴,進行配置

  apply plugin: 'walle'
  
  android{
  	......
  	 walle {
          // 指定渠道包的輸出路徑
        apkOutputFolder = new File("${project.buildDir}/outputs/channels");
          // 定製渠道包的APK的文件名稱
        apkFileNameFormat = '${appName}-${channel}-${buildType}-v${versionName}-${versionCode}.apk';
          // 渠道配置文件
          channelFile = new File("${project.getProjectDir()}/channel")
      }
  }
  
  dependencies {
  	implementation 'com.meituan.android.walle:library:1.1.6'
  }

然後在 app 中新建一個channel 文件,不要指定任何後綴,裏面寫入渠道名稱即可
在這裏插入圖片描述

這樣多渠道就配置完了。

使用多渠道後打包需要用命令行或者插件來執行,這裏使用插件的方式,如下:
在這裏插入圖片描述
一種是 debug,一種是 release。點擊 replace就會生成相應的渠道文件,如下:

在這裏插入圖片描述

可以看到渠道已經生成了。在bakApk 中也生成了 基準包,這個 apk 不是用來發布的,而是用來生成補丁用的。生成補丁的方式就和上面講的一樣了。

將渠道文件中的 apk 發佈後,出現 bug 。使用基準包生成 補丁文件,然後放在服務器中,進行下發即可。

如果使用 android 自帶的渠道,需要每個渠道apk 都要生成補丁文件,而 使用 walle 只需要一個補丁即可,而且 使用 walle 生成渠道apk 的速度非常快。

我們可以app中獲取渠道的信息,並且傳給 友盟統計,這樣非常方便,如下:

//當前渠道
String channel = WalleChannelReader.getChannel(getApplication());
UMConfigure.init(getApplication(), "*********", channel, UMConfigure.DEVICE_TYPE_PHONE, "");
  • 如何自定義 Tinker 行爲

​ 1,自定義TinkerResultService 改變 patch 安裝成功後行爲

​ 一般情況下 patch 安裝後 tinker 會殺掉進程,所以我們纔會看到 patch 加載完成後程序閃退的問題,我們可以通過 重寫 DefaultTinkerResultService 來自定義這個行爲,如下:

​ 在 DefaultTinkerResultService 的 onPatchResult 方法中 判斷了 patch 安裝成功後的行爲。如下:

 @Override
    public void onPatchResult(PatchResult result) {
     	......
        // 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!");
            }
        }
    }

​ 從源碼可以很清楚地看到殺掉進程。我們可以通過繼承的方式來解決如下:

public class CustomResultService extends DefaultTinkerResultService {

    private static final String TAG = "CustomResultService";

    //返回 patch 文件的安裝結果
    @Override
    public void onPatchResult(PatchResult result) {
		......
        // 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) {
            deleteRawPatchFile(new File(result.rawPatchFilePath));
            if (checkIfNeedKill(result)) {
                TinkerLog.e(TAG, "patch加載成功,重啓生效");
            } else {
                TinkerLog.i(TAG, "I have already install the newly patch version!");
            }
        }
    }
}

​ 其實和源碼中差不多,只是改了一句而已。加載成功後打印 log 即可。

​ 弄完以後,我們需要修改一下初始化方法。我們是在 TinkerMananger 的 installTinker 方法中初始化的,下面我們進行修改,如下:

 LoadReporter loadReporter = new DefaultLoadReporter(applicationLike.getApplication());
        PatchReporter patchReporter = new DefaultPatchReporter(applicationLike.getApplication());
        PatchListener patchListener = new DefaultPatchListener(applicationLike.getApplication());
        AbstractPatch upgradePatchProcessor = new UpgradePatch();

        //完成 tinker 初始化
        TinkerInstaller.install(applicationLike,
                loadReporter,
                patchReporter,
                patchListener,
                CustomResultService.class,
                upgradePatchProcessor);

​ CustomResultService 就是我們自定義的。將他傳入即可,注意這是一個服務,必須在 AndroidManifest.xml 中註冊

​ 結果如下:

2019-11-21 16:59:42.335 6337-6404/? E/CustomResultService: patch加載成功,重啓生效

2,自定義 PatchListener 監聽 patch receiver 事件

​ 也就是上面代碼中的 DefaultPatchListener ,繼承他即可,使用它可以完成 patch 的校驗

​ 例如,從服務器拉取補丁時服務器返回一個 md5, 我們可以在這個裏面進行判斷。

/**
 * 1,校驗 patch 文件是否合法,2,啓動 Service 去安裝 patch
 */
public class CustomPatchListener extends DefaultPatchListener {

    private String currentMD5;

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

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

    @Override
    protected int patchCheck(String path, String patchMd5) {
        if (Utils.isFileMD5Matched(path,currentMD5)){
            return ShareConstants.ERROR_PATCH_DISABLE;
        }
        return super.patchCheck(path, patchMd5);
    }
}

​ 同樣的要將初始化方法中第四個參數改爲這個類對象

3,其他的自定義

 //加載補丁文件加載時的異常監聽
        LoadReporter loadReporter = new DefaultLoadReporter(applicationLike.getApplication());
        //補丁文件合成階段的異常監聽,
        PatchReporter patchReporter = new DefaultPatchReporter(applicationLike.getApplication());
        PatchListener patchListener = new DefaultPatchListener(applicationLike.getApplication());
        AbstractPatch upgradePatchProcessor = new UpgradePatch();
        //完成 tinker 初始化
        TinkerInstaller.install(applicationLike,
                loadReporter,
                patchReporter,
                patchListener,
                CustomResultService.class,
                upgradePatchProcessor);

​ 通過繼承 loadReporter 和 patchReporter 可以實現異常的監聽。當然還有一下其他的,可以去官網查看。


總結一下

熱修復這塊基本的用法基本都已經掌握了。總結如下:

​ 在學習任何一個東西時一定要先看官方的文檔,不然喫大虧,就和我一樣,搞了好多天。。如果項目中需要用到熱修復,不要着急的直接選使用哪個,要從需求和一下客觀因素來覺得要使用那種,在滿足需求的條件下哪個學習的成本低,就學哪個。學習成本差不多的建議選擇大公司的解決方案。

​ 關於tinker的使用,例如上面這種就比較麻煩。但是有了 bugly 以後感覺挺簡單的。畢竟也是免費的。tinker 使用的是冷啓動,下發一次補丁需要十多分鐘,撤回的話沒怎麼測。但是配合 walle 一起使用效果還是挺好的。

​ 在使用 bugly 的時候發現在有些手機上使用失敗的問題。例如我的小米6。。。在 bugly 後臺上傳 補丁包一直顯示 版本對應不上。我還以爲是代碼問題,結果改代碼改了一兩天。。最後用了一個模擬器居然成功了。。無奈啊

​ 注意熱修復的作用不是替代版本的迭代。他只是爲了在線上的 app 出現問題後的一種解決方式。按照一般流程重新進行版本的迭代就會浪費大量的時間,而且還指不定用戶會不會更新。因爲如此,所以纔有了熱修復。但是不能將他和迭代混爲一談。

​ 關於上面 tinker 的用法我寫了一個demo,demo中也集成了walle和友盟統計。bugly就不傳了,官方文檔非常詳細。但是注意文檔上的依賴版本不是最新的。這個虧我已經吃了。。。

​ 其實學完以後才發現這種用法已經比較老了。所以我還是覺定用 bugly。他內部也是用的 tinker ,使用起來比較簡單,當前一定要認真看文檔,比如說我。。。吃了不少虧


參考:慕課網視頻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章