基於微信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”)這個第一次更新都需要改變
混淆檔案我就不放上來了,大家看程式碼複製就行
以上就是我的分享,希望能對大家有幫助