一、簡介
之前項目分包dex加載出現了問題,解決之後,為了加深了解,網上找了些有關内容學習了, 而熱修複技術很重要,現在剛好有時間,研究學習下,熱修複涉及的技術比較多,能更深層的剖析Android app啟動加載底層的原理;目前市面熱修複方案比較多,對比了下,微信官方的Tinker熱更新檔方案比較強大,(下圖取自Tinker官網)今天在公衆号上看到阿裡又推出個新的熱修複方案Sophix,看了文章介紹,很屌,現在先學習Tinker;

詳情對比可閱讀微信Android熱更新檔實踐演進之路
1、Tinker是什麼
Tinker是微信官方的Android熱更新檔解決方案,它支援動态下發代碼、So庫以及資源,讓應用能夠在不需要重新安裝的情況下實作更新。當然,你也可以使用Tinker來更新你的插件。
2、Tinker原理
簡單來說,在編譯時通過新舊兩個Dex生成差異path.dex。在運作時,将差異patch.dex重新跟原始安裝包的舊Dex還原為新的Dex。這個過程可能比較耗費時間與記憶體,是以我們是單獨放在一個背景程序:patch中。為了更新檔包盡量的小,微信自研了DexDiff算法,它深度利用Dex的格式來減少差異的大小。它的粒度是Dex格式的每一項,可以充分利用原本Dex的資訊,而BsDiff的粒度是檔案,AndFix/QZone的粒度為class。
apk解壓後,就可以看到dex檔案,app在安裝啟動時也是要解壓apk,加載各個資源;開發時,我上線釋出後,使用者手機上的就是舊apk,或叫基準包,當我們修改代碼後,重新編譯生成的新包時,通過tinker提供了patch的生成工具,會對基準包和新包做對比處理,生成一個更新檔包apk,然後把更新檔包下載下傳都手機本地,和手機上的基準包進行合并,生成新的apk,來完成代碼的更新;
3、Tinker的已知問題
由于原理與系統限制,Tinker有以下已知問題:
1>Tinker不支援修改AndroidManifest.xml,Tinker不支援新增四大元件;
2>由于Google Play的開發者條款限制,不建議在GP管道動态更新代碼;
3>在Android N上,更新檔對應用啟動時間有輕微的影響;
4>不支援部分三星android-21機型,加載更新檔時會主動抛出"TinkerRuntimeException:checkDexInstall failed";
5>對于資源替換,不支援修改remoteView。例如transition動畫,notification icon以及桌面圖示。
二、Tinker接入
tinker官方推薦采用gradle接入,同時也給出了使用例子tinker-sample-android,根據它把相關類和配置引入我們的項目
1、在項目的build.gradle中,添加tinker-patch-gradle-plugin的依賴
dependencies {
//省略
classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.11')
}
2、然後在app的gradle檔案app/build.gradle,我們需要添加tinker的庫依賴以及apply tinker的gradle插件
dependencies {
//可選,用于生成application類
provided('com.tencent.tinker:tinker-android-anno:1.7.11')
//tinker的核心庫
compile('com.tencent.tinker:tinker-android-lib:1.7.11')
}
3、此外還需設定jumboMode=true,防止由于字元串增多導緻force-jumbol,導緻更多的變更
dexOptions {
jumboMode = true
}
4、還有gradle相關參數配置,下面隻是不完整的一小部分,具體可以參考例子中的gradle
if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'
tinkerPatch {
oldApk = getOldApkPath()
//其他相關
5、對Application進行改造,因為程式預設啟動時會加載Application,導緻更新檔無法對其實進行修改,Tinker通過代碼架構的方式來避免,将原來的Application類隔離起來,即其他任何類都不能再引用我們自己的Application,官方推薦
使用tinker-android-anno在運作時生成你的Application類。這樣保證你無法修改你的Application類,不會因為錯誤操作導緻引入更多無法修改的類。
@DefaultLifeCycle(
application = "com.daitu_liang.study.mytest.app.GetFightApplication",//application類名
flags = TINKER_ENABLE_ALL, //tinkerFlags
loadVerifyFlag = false)
public class GetFightApplicationTinker extends DefaultApplicationLike {
public static Context CONTEXT;
public static PreferencesManager preferenceManager;
public static RefWatcher getRefWatcher(Context context) {
return refWatcher;
}
private static final String TAG = "GetFightApplicationTinker";
public GetFightApplicationTinker(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);
SampleApplicationContext.application = getApplication();
SampleApplicationContext.context = getApplication();
TinkerManager.setTinkerApplicationLike(this);
TinkerManager.initFastCrashProtect();
//should set before tinker is installed
TinkerManager.setUpgradeRetryEnable(true);
//optional set logIml, or you can use default debug log
TinkerInstaller.setLogIml(new MyLogImp());
//installTinker after load multiDex
//or you can put com.tencent.tinker.** to main dex
TinkerManager.installTinker(this);
Tinker tinker = Tinker.with(getApplication());
setContext(this.getApplication());
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}
private static RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
init();
}
private void init() {
preferenceManager = PreferencesManager.getInstance(this.getApplication());
refWatcher = LeakCanary.install(this.getApplication());
Fresco.initialize(this.getApplication());
LitePal.initialize(this.getApplication());
}
private static void setContext(Context mContext) {
CONTEXT = mContext;
}
public static Context getContext() {
return CONTEXT;
}
public static PreferencesManager getPreferenceManager() {
return preferenceManager;
}
}
清單裡application我們需要設定為DefaultLifeCycle裡application的值,
<application
android:name=".app.GetFightApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
我們發現這個裡的GetFightApplication沒有報錯,因為通過Annotation自動生成了我們的application,點選可以檢視
/**
*
* Generated application for tinker life cycle
*
*/
public class GetFightApplication extends TinkerApplication {
public GetFightApplication() {
super(7, "com.daitu_liang.study.mytest.app.GetFightApplicationTinker", "com.tencent.tinker.loader.TinkerLoader", false);
}
}
若采用Annotation生成Application,需要将原來的Application類删掉。還有以下要求
1>Application的attachBaseContext方法實作要單獨移動到onBaseContextAttached中;
2>對GetFightApplicationTinker 中,引用application的地方改成getApplication();
3>對其他引用GetFightApplicationTinker 或者它的靜态對象與方法的地方,改成引用GetFightApplicationTinker 的靜态對象與方法;
此外,不想用Annotation生成application的話,也可以,
1>建立一個類SampleApplicationLike繼承DefaultApplicationLike,将原自己的Application的初始化等需要的業務放在onBaseContextAttached裡
2>原自己的Application改為繼承TinkerApplication,必須有個構造,且第二個參數必須為1>裡的SampleApplicationLike
public GetFightApplication() {
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.
"com.daitu_liang.study.mytest.app.SampleApplicationLike");
}
3>清單裡application name為原自己的Application,添加讀寫權限;
接入到此結束,
6、添加加載更新檔代碼
//合成path.dex
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
Environment.getExternalStorageDirectory().getAbsolutePath() + "/tinker.zip");
7.示範下看下效果,主要将背景顔色由紅色改為藍色,setText一句我終于熱修複成功了;
1>打開as右側的gradle找到自己module,點選build,再點選assembleDebug編譯,我們會将編譯過的包儲存在build/bakApk中,如何上傳左側。然後我們将它安裝到手機,這也就是我們的基準包,點選show_tip按鈕,可以看到更新檔并沒有加載.
2>先修改代碼,把紅色變為藍色,然後修改build.gradle中的參數,将步驟一編譯儲存的安裝包路徑拷貝到tinkerPatch中的oldApk參數中,以及txt檔案,如上圖。
3>在as右側gradle找到自己module,點選tinker,再點選tinkerPatchDebug編譯, 更新檔包與相關日志會儲存在/build/outputs/tinkerPatch/。如上圖左側,然後我們将patch_signed_7zip.apk,它就是更新檔包,推送到手機的sdcard中,它最終在手機上會合成一個全量的資源apk,這個過程是比較耗時的。patch_signed_7zip.apk是已簽名并且經過7z壓縮的更新檔包,但是最好重命名一下,不要讓它以.apk結尾,這是因為有些營運商會挾持以.apk結尾的資源。
4>點選加載更新檔包,如果看到patch success, please restart process的toast,即可鎖屏或者重新開機,可看到修改後的效果,即熱修複成功;或者點選show_tip檢視日志,判斷是否成功修複;
還有比較重要的東西,就是dex有關的,檢視Tinker的源碼,在内部dex的合成處理,需要了解dex檔案的格式以及DexDiff算法,
Dex檔案格式 http://blog.csdn.net/feglass/article/details/51761902
對dex檔案格式分析,可以借助工具010Editor進行分析,如下圖,左邊是dex檔案16進制的展現方式,右下角是dex各個區,具體看上面Dex檔案格式連結内容,
DexDiff算法解析
傳回的一些錯誤碼可以在檢視Tinker自定義擴充
https://github.com/Tencent/tinker
微信Android熱更新檔實踐演進之路
微信Tinker的一切都在這裡,包括源碼(一)