1. 程式人生 > >基於微信Tinker的熱更新詳細說明

基於微信Tinker的熱更新詳細說明

  先來吐槽一下,這個更新方法簡直6的沒話說,經我詳細的測試,可以更新類及新增類,刪除類什麼的,以及對XML資原始檔的更新,好像還能更新library,但是我還沒測試過,總之可以可以很強勢,但是在整合的過程中也很多坑,集了我一天多,報錯太多了,網上資料還不怎麼詳細,找了好久,然後各種方法集入,終於OK了,相對 Andfix(只能對方法進行更新)和Nuwa(只對類更新不能新增類)及HotFix(這個我已經無力吐槽,一旦混淆就報未知的錯,混淆檔案我都有配置的還出錯,我也很絕望就果斷放棄了)來說就複雜了一點,在這期間遇到了非常多的bug,然後各種資料API的查閱,終於給搞定了!

  注:演示的話我就不弄了,視訊肯定會很大,弄成圖片也傳不上來,(到時我直接放DEMO)這裡先說一個小提醒,在執行更新成功後,會自己殺死程序,然後我們要自己手動開啟才能看到效果,到時釋出給客戶用了,建議大家弄個框提示一下,不然以為發生了啥,怎麼閃退了。。  

先來說一個官方集入指南點:

1、點選開啟Tinker官方SDK文件
2、點選開啟SDK接入指南
3、點選下載官方提供的DEMO
 現在熱修復已經很熱門了,比較著名的有阿里巴巴的AndFix、Dexposed,騰訊QQ空間的超級補丁和微信最近開源的Tinker。
  Tinker是一個android的熱修復庫,在不重新安裝apk的情況就可以更新dex,library和resource。Tinker區別於AndFix和QQ空間超級補丁採用了更好的dexdiff演算法。想要了解詳細介紹參考下面微信負責人張紹文的部落格連結。
參考部落格:
微信 Tinker 負責人張紹文關於 Android 熱修復直播分享記錄


 想要快速學習Tinker的使用,可以只檢視Tinker GitHub和配置參考部落格。這裡我也會具體寫一下配置步驟還有自己遇到的問題。Tinker在github上的接入指南(wiki)看起來確實有點難的啊,搞了半天都沒搞明白為什麼有兩個Application,有明白了的給留個言啊。先不管這個問題了,說下具體配置。
 

 配置build.gradle


這裡新增javaVersion最好不要改成VERSION_1_8,改成8可能需要新增其他的支援。sigingConfig裡面的debug的配置可以註釋掉,否則會報關於debug找不到的錯。
這裡寫圖片描述

  下面這個是簽名和混淆的配置,這裡比較重要,到時候配置Tinker中會有一個變數 useSign = true如果為ture就必須要有簽名否則打不了release包,我這用了OkHttp,所以混淆檔案也要注意一下,要放上去,不然會報非常多稀奇古怪的錯
這裡寫圖片描述

  然後新增依賴,圖例中顯示的版本號我放置在了gradle.properties檔案內
app Build
這裡寫圖片描述

project Build
這裡寫圖片描述

gradle.properties
這裡寫圖片描述

  根據官方提供的DEMO來進行下面圖片中的配置,複製進去即可,然後在將裡面的Application包名換成自己的即可,具體可以參考DEMO,比較多程式碼就不放截圖了,我將比較重要的放上來,下面程式碼放在build.gradle(APP)的dependencies 下面就直接把剩下的配置原樣拷貝過來就可以了。

  先說一件事,如果加入下面這段程式碼,拷貝過來後,一但 Sync Now會立馬報錯,需要新增git倉庫來獲取獲取git的提交版本號,不然會報錯提示 tinkerId is not set
這裡寫圖片描述

解決方法:

1、其實可以簡單粗暴的方式解決,那就是在app/build.gradle中更改以下程式碼:

tinkerId = getTinkerIdValue()
//更改為
tinkerId = "TinkerSample"//或者其他你想要的id

2、配置git跟github並上傳一次程式碼,解決tinkerId is not set問題。

去官網下載git,並安裝,給AndroidStudio設定git,點選test
這裡寫圖片描述

配置github賬號,點選test
這裡寫圖片描述

為project設定git
這裡寫圖片描述

上傳一次專案到github就不會出tinkerId is not set問題了。
這裡寫圖片描述
此時進行sync Now或者clean project

實現自己的Application類呢,主要還是參考Tinker推薦的方式,通過註解生成Application類。

BaseApplication(manifest中新增這個application)

package czb.com.tinker.application;

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

/**
 * Created by AnmiLin on 2017/9/7.
 */
public class BaseApplication extends TinkerApplication {
    private  static final String TAG=BaseApplication.class.getSimpleName();
    public  BaseApplication(){
        super(
                //tinkerFlags, which types is supported
                //dex only, library only, all support
                ShareConstants.TINKER_ENABLE_ALL,
                // This is passed as a string so the shell application does not
                // have a binary dependency on your ApplicationLifeCycle class.
                "czb.com.tinker.application.BaseApplicationLike");
    }

}

BaseApplicationLike

package czb.com.tinker.application;

import android.annotation.TargetApi;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Build;
import android.support.multidex.MultiDex;

import com.tencent.tinker.lib.tinker.TinkerInstaller;
import com.tencent.tinker.loader.app.DefaultApplicationLike;

/**
 * Created by AnmiLin on 2017/9/7.
 */
public class BaseApplicationLike extends DefaultApplicationLike {


    public BaseApplicationLike(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)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        MultiDex.install(base);
        TinkerInstaller.install(this);

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

然後在manifest中新增這個BaseApplication及加入對於sd卡的讀寫及網路許可權
這裡寫圖片描述

MainActivity

package czb.com.tinker;

import android.content.Intent;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

import android.widget.Toast;

import com.tencent.tinker.lib.tinker.TinkerInstaller;

import java.io.File;
import java.io.IOException;

import czb.com.tinker.util.BugClass2;
import czb.com.tinker.util.HttpDownload;
import czb.com.tinker.util.LoadBugClass;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();
    private String fileState = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.v(TAG, "onCreate");

        Log.e(TAG, "i am on patch log");

        Toast.makeText(MainActivity.this, new BugClass2().getStr()+"-"+new LoadBugClass().getStr(), Toast.LENGTH_SHORT).show();
        findViewById(R.id.btn_load_patch).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                //準備補丁,從assert裡拷貝到dex裡
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        String url = "http://xxx.com/patch_signed_7zip.apk";
                        final HttpDownload httpDownload = new HttpDownload();
                        try {
                            String path = Environment.getExternalStorageDirectory() + File.separator + "TinkerHotFix";
                            int flag = httpDownload.downToFile(url, path);
                            if (flag == 1) {
                                fileState = "下載完成";
                                final String pathUrl = path + "/" + HttpDownload.getFileName(url);
                                runOnUiThread(new Runnable() {
                                    @Override
                                    public void run() {

                                        try{
                                            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), pathUrl);

                                        }catch (Exception e){
                                            Log.e(TAG,e.getMessage());
                                        }
                                        Log.e(TAG, "安裝完成");
                                        Toast.makeText(getApplicationContext(), "安裝完成", Toast.LENGTH_LONG).show();

                                    }
                                });
                            } else if (flag == -1) {
                                fileState = "下載錯誤";
                            }
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    Toast.makeText(MainActivity.this, fileState, Toast.LENGTH_SHORT).show();

                                }
                            });
                        } catch (IOException e) {
                            e.printStackTrace();
                        }


                    }
                }).start();

            }
        });

    }

    public void onRestart(View view) {
        //重啟應用
        Intent i = getPackageManager()
                .getLaunchIntentForPackage(getPackageName());
        i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        startActivity(i);
        finish();
        android.os.Process.killProcess(android.os.Process.myPid());
    }
}

執行程式碼並在終端執行命令列,編譯apk

  程式碼編寫完成後,執行自己的程式碼,如果是release的,則會自動在app/build/bakApk目錄下會有生成好的apk檔案一份以及對應的資原始檔txt檔案一份比如app-release-0907-15-19-19.apk和app-release-0907-15-19-19-R.txt及app-release-0907-15-19-19-mapping.txt,如果是debug的,則會生成app-debug-0907-15-19-19.apk和app-debug-0907-15-19-19-R.txtdebug 此時app應該已經部署到手機或者模擬器上。
 
在As的terminal終端使用命令列生成基礎APP資訊檔案
1、如果是debug就執行下面命令
gradlew assemblehDebug
2、如果是release則執行
gradlew assembleRelease

  如果以前沒有使用過命令列,可能會下載一些東西,不知道是不是我家裡網路的原因,我開的翻牆才下載成功的。如果以前使用過命令就可以直接編譯了,編譯完成之後可以在你的目錄下面看到新生成的apk。
截圖示例:
這裡寫圖片描述

 注:這是舊的APP釋出後生成的檔案資訊,不是新的APP生成的,在生成成功後,將目錄中的檔名對應下面配置提取出來,放置所需程式碼塊中,在不安裝新APP的情況下,這個要備份出來,以後疊加修復需要

//基準APP資訊檔案路徑(用於存基準的應用(這個應用一定要保留下來,後期修復都需要這個裡的檔案))
def bakPath = file("${buildDir}/bakApk/")

ext {
    //debug build?是否開啟tinker的功能
    tinkerEnabled = true

    //for normal build
    //開啟上面bakApk資料夾,在提取出舊的APK檔名
    tinkerOldApkPath = "${bakPath}/app-release-0907-15-19-19.apk"

    //proguard mapping file to build patch apk
   //開啟上面bakApk資料夾,在提取出舊的mapping檔名,如果是debug就沒有mapping檔案,則路徑可以這麼寫:
  // tinkerApplyMappingPath = "${bakPath}/"
  //relase下就要同上方法提取出來
    tinkerApplyMappingPath = "${bakPath}/app-release-0907-15-19-19-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    //如果更新了資原始檔就需要把生成好的放進來
    tinkerApplyResourcePath = "${bakPath}/app-release-0907-15-19-19-R.txt"

    //only use for build all flavor, if not, just ignore this field
    //tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}

配置好後然後在命令列中執行
1、如果是debug就執行下面命令
gradlew tinkerPatchDebug
2、如果是release則執行
gradlew tinkerPatchRelease

這裡寫圖片描述

編譯完成之後可以在你的目錄下面看到新生成的(打補丁的)apk。
這裡寫圖片描述

將生成的patch_signed_7zip.apk下載放到程式碼中編寫的路徑下面,執行app點選更新按鈕就可以了。
注:此時如果載入補丁成功,應用會閃退,Android Studio 日誌控制檯會打印出如下日誌輸出:
這裡寫圖片描述
再次執行app,可以發現之前開啟的註釋程式碼已經被差異化修復到app中,並且開啟的日誌已經輸出到控制檯,

注意:

如果apk執行之後點選按鈕沒有反應不能進行熱修復的話注意檢查自己的apk的版本號跟BaseApk是否一致(也就是你build.gradle中修改的版本號一致) configField(“patchVersion”, “1.1”)這個第一次更新都需要改變

混淆檔案我就不放上來了,大家看程式碼複製就行

以上就是我的分享,希望能對大家有幫助