Android熱修復(fù)方案_第1頁
Android熱修復(fù)方案_第2頁
Android熱修復(fù)方案_第3頁
Android熱修復(fù)方案_第4頁
已閱讀5頁,還剩5頁未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡(jiǎn)介

1、Android 熱修復(fù)方案分析絕大部分的 APP 項(xiàng)目其實(shí)都需要一個(gè)動(dòng)態(tài)化方案,來應(yīng)對(duì)線上緊急bug 修復(fù)發(fā)新版本的高成本.之前有利用加殼,分拆兩個(gè)dex 結(jié)合 DexClassLoader 實(shí)現(xiàn)了一套全量更新的熱更方案.實(shí)現(xiàn)原理在這篇博客中有分解.因?yàn)檫@套方案是在Java端實(shí)現(xiàn) ,并且是全量更新所以兼容性較好,成功率較高 .但是在線上跑了幾個(gè)月之后就碰到了瓶頸,因?yàn)殡S著業(yè)務(wù)的增長(zhǎng)分拆過之后的dex 文件方法數(shù)也超過 65535個(gè),更換拆包方案的話維護(hù)成本太高.同時(shí)由于沒有做差異diff,就帶來了 patch 包過大 ,冗余多等缺點(diǎn) .正好微信的動(dòng)態(tài)化方案Tinker 也開源了,就趁這個(gè)機(jī)會(huì)先

2、把市面上主流的熱更方案匯總分析下,再選一個(gè)方向深入研究一個(gè)盡量兼并兼容性擴(kuò)展性及時(shí)性的方案 .Github 相關(guān)數(shù)據(jù)分析先統(tǒng)計(jì)下 github 上幾個(gè) star 比較多的開源熱更方案,數(shù)據(jù)為 2016年 11 月 3 號(hào)采集的 ,僅供參考.從非技術(shù)的角度來分析下表的數(shù)據(jù),根據(jù)開源時(shí)間到最近c(diǎn)ommit 時(shí)間、commit 數(shù)量、issues的關(guān)閉率和 Release 版本數(shù)都可以看出這幾個(gè)項(xiàng)目目前的維護(hù)情況.還有 Wiki 相關(guān)文檔的支持.怎么看 Tinker 現(xiàn)在都是一副很生猛的架勢(shì).而阿里百川的商業(yè)化Hotfix 現(xiàn)在還在公測(cè) ,方式用的是 Andfix ,把熱更做成一個(gè)商業(yè)化的功能,就不

3、清楚Andfix以后在 github 上的維護(hù)情況了 ,但是同時(shí)也證明了 Andfix的價(jià)值 .而 Dexposed 一直沒有兼容 ART, 這里就先不詳細(xì)分析了.實(shí)現(xiàn)原理AndfixAndfix 實(shí)現(xiàn)熱更的核心方法是在而替換的方法則是由源apkJNI 中動(dòng)態(tài)文件和修改過的hook 替換目標(biāo)方法 ,來達(dá)到即時(shí)修復(fù)bug 的目的 .apk 文件的dex 做 diff, 反編譯補(bǔ)丁包工具apkpatch 可以看到兩個(gè)dex遍歷做diff的過程.public DiffInfo diff(File newFile, File oldFile) throws IOException DexBackedD

4、exFile newDexFile = DexFileFactory.loadDexFile(newFile, 19, true);DexBackedDexFile oldDexFile = DexFileFactory.loadDexFile(oldFile, 19, true);DiffInfo info = DiffInfo.getInstance();boolean contains = false;for(Iterator iterator = newDexFile.getClasses().iterator(); iterator.hasNext();)DexBackedClass

5、Def newClazz = (DexBackedClassDef)iterator.next();Set oldclasses = oldDexFile.getClasses();for(Iterator iterator1 = oldclasses.iterator(); iterator1.hasNext();)DexBackedClassDef oldClazz = (DexBackedClassDef)iterator1.next(); if(newClazz.equals(oldClazz)compareField(newClazz, oldClazz, info);compare

6、Method(newClazz, oldClazz, info);contains = true;break;if(!contains)info.addAddedClasses(newClazz);return info;遍歷出修改過的方法加上一個(gè)MethodReplace 的注解 (包含要替換的目標(biāo)類和目標(biāo)方法),生成一個(gè) diffdex,再簽上名更名為.apatch 的補(bǔ)丁包通過更新的方式分發(fā)的各個(gè)終端處.通過反編譯中間 diff dex 可以看到補(bǔ)丁文件中對(duì)fix method 的描述 .public static String getBuildId() return "6f3

7、d1afc-d890-47c2-8ebe-76dc6c53050c"終端在效驗(yàn)過補(bǔ)丁包的合法性后據(jù)注解中的目標(biāo)方法配置,將method.,則把補(bǔ)丁包中帶有MethodReplaceold method 利用 classloader 加載進(jìn)內(nèi)存注解的方法遍歷出來,然后交給 JNI 去替換,根oldprivate void fixClass(Class<?> clazz, ClassLoader classLoader) Method methods = clazz.getDeclaredMethods(); MethodReplace methodReplace;Strin

8、g clz;String meth;for (Method method : methods) methodReplace = method.getAnnotation(MethodReplace.class);if (methodReplace = null)continue;clz = methodReplace.clazz();meth = methodReplace.method();if (!isEmpty(clz) && !isEmpty(meth) replaceMethod(classLoader, clz, meth, method);private void

9、 replaceMethod(ClassLoader classLoader, String clz, String meth, Method method) try String key = clz + "" + classLoader.toString();Class<?> clazz = mFixedClass.get(key);if (clazz = null) / class not loadClass<?> clzz = classLoader.loadClass(clz);/ initialize target classclazz =

10、 AndFix.initTargetClass(clzz);if (clazz != null) / initialize class OKmFixedClass.put(key, clazz);Method src = clazz.getDeclaredMethod(meth,method.getParameterTypes();AndFix.addReplaceMethod(src, method); catch (Exception e) Log.e(TAG , "replaceMethod", e);在 Andfix.app 中可以看到 JNI 中 replaceM

11、ethod 方法 ,由于從 Lolipop 開始 Android 放棄使用dalvik 轉(zhuǎn)向 android runtime, 所以 Andfix 也要區(qū)分不同的平臺(tái)進(jìn)行替換 .像 Dexposed 到目前為止都沒有做 ART 的兼容 .static void replaceMethod(JNIEnv* env, jclass clazz, jobject src, jobject dest) if (isArt) art_replaceMethod(env, src, dest); else dalvik_replaceMethod(env, src, dest);extern void _

12、attribute_ (visibility ("hidden") dalvik_replaceMethod( JNIEnv* env, jobject src, jobject dest) jobject clazz = env->CallObjectMethod(dest, jClassMethod); ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(dvmThreadSelf_fnPtr(), clazz);clz->status = CLASS_INITIALIZED;Method

13、* meth = (Method*) env->FromReflectedMethod(src); Method* target = (Method*) env->FromReflectedMethod(dest);LOGD("dalvikMethod: %s", meth->name);meth->accessFlags |= ACC_PUBLIC;meth->methodIndex = target->methodIndex;meth->jniArgInfo = target->jniArgInfo;meth->re

14、gistersSize = target->registersSize;meth->outsSize = target->outsSize;meth->insSize = target->insSize;meth->prototype = target->prototype;meth->insns = target->insns;meth->nativeFunc = target->nativeFunc;由于兼容問題在ART 的 replaceMethod 方法中對(duì)每一個(gè)不同的系統(tǒng)版本進(jìn)行區(qū)分extern void _attri

15、bute_ (visibility ("hidden") art_replaceMethod( JNIEnv* env, jobject src, jobject dest) if (apilevel > 23) replace_7_0(env, src, dest); else if (apilevel > 22) replace_6_0(env, src, dest); else if (apilevel > 21) replace_5_1(env, src, dest); else if (apilevel > 19) replace_5_0(

16、env, src, dest);elsereplace_4_4(env, src, dest);,分別實(shí)現(xiàn) .因?yàn)?Andfix的方案是在native 替換方法 ,所以穩(wěn)定性和兼容性就是差一些.就 Andfix 開源項(xiàng)目來說在實(shí)際接入的過程中發(fā)現(xiàn)對(duì)multidex 支持不友好,還需要修改補(bǔ)丁包生成工具apkpatch,并且 apkpatch 開源得也不友好,修復(fù)靜態(tài)方法有問題.Nuwa由于 Qzone 只是分享了實(shí)現(xiàn)原理,并沒有開源出來.而 Nuwa 是參考 Qzone 的實(shí)現(xiàn)方式開源的一套方案 ,這里就主要分析Nuwa 了 .Nuwa 的修復(fù)流程并不復(fù)雜,不像Andfix 需要在 JNI 中

17、進(jìn)行方法替換 .在 Application中的 attachBaseContext 方法中對(duì)Nuwa 進(jìn)行初始化 ,先將 asset路徑下的hack.apk 復(fù)制到指定位置,然后以加載補(bǔ)丁的方式加載hack.apk 至于這個(gè)hack.apk的作用下面會(huì)講.public static void init(Context context) File dexDir = new File(context.getFilesDir(), DEX_DIR);dexDir.mkdir();String dexPath = null;try dexPath = AssetUtils.copyAsset(cont

18、ext, HACK_DEX, dexDir); catch (IOException e) Log.e(TAG , "copy " + HACK_DEX + " failed");e.printStackTrace();loadPatch(context, dexPath);加載補(bǔ)丁的方法主要的作用是把補(bǔ)丁dex 通過反射加載到dexElements數(shù)組的最前端。因?yàn)镃lassloader 在 findClass 的時(shí)候是按順序遍歷dexElements(dex 數(shù)組 ),只要 dexElement 中有該class 就加載并停止遍歷.所以利用Class

19、loader 的這種特性把補(bǔ)丁包插入dexElements 的首位系統(tǒng)在 findClass 的時(shí)候就優(yōu)先拿到補(bǔ)丁包中的class,達(dá)到修復(fù)bug 的目的 .,public static void loadPatch(Context context, String dexPath) if (context = null) Log.e(TAG , "context is null");return;if (!new File(dexPath).exists() Log.e(TAG , dexPath + " is null");return;File de

20、xOptDir = new File(context.getFilesDir(), DEX_OPT_DIR);dexOptDir.mkdir();try DexUtils.injectDexAtFirst(dexPath, dexOptDir.getAbsolutePath(); catch (Exception e) Log.e(TAG , "inject " + dexPath + " failed");e.printStackTrace();public static void injectDexAtFirst(String dexPath, St

21、ring defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException Object baseDexElements = getDexElements(getPathList(getPathClassLoader();Object newDexElements = getDexElements(getPathList(dexClassLoader);Object allDexElements = combineArray(newDexElements, baseDex

22、Elements);Object pathList = getPathList(getPathClassLoader();ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);如 果 只 是 把 補(bǔ) 丁 包 插 入dexElements的 首 位 然 后 運(yùn) 行 就 會(huì) 有 一 個(gè) 異 常造成這個(gè)異常的原因是因?yàn)檠a(bǔ)丁包中的類和與其有關(guān)聯(lián)的類不在同一個(gè)dex 文件中 .跟蹤這個(gè)異常 ,定位到 Android 源碼中的Resolve.cpp 中的 dvmRes

23、olveClass 方法,可以看到只要滿足最外層(!fromUnverifiedConstant&&IS_CLASS_FLAG_SET(referrer,CLASS_ISPREVERIFIED)CLASS_ISPREVERIFIED的 條 件 就 會(huì) 拋 出pre-verified的 異 常 .Qzone就 是 從標(biāo)記入手 , 想辦法讓 Class 不打上 CLASS_ISPREVERIFIED標(biāo)簽 .ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx, bool fromUnverifie

24、dConstant).if (!fromUnverifiedConstant &&IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED)ClassObject* resClassCheck = resClass;if (dvmIsArrayClass(resClassCheck)resClassCheck = resClassCheck->elementClass;if (referrer->pDvmDex != resClassCheck->pDvmDex &&resClassCheck->cl

25、assLoader != NULL)ALOGW("Class resolved by unexpected DEX:"" %s(%p):%p ref %s %s(%p):%p", referrer->descriptor, referrer->classLoader, referrer->pDvmDex,resClass->descriptor, resClassCheck->descriptor, resClassCheck->classLoader, resClassCheck->pDvmDex);ALOGW

26、("(%s had used a different %s during pre-verification)",referrer->descriptor, resClass->descriptor);dvmThrowIllegalAccessError("Class ref in pre-verified class resolved to unexpected ""implementation");return NULL;.return resClass;Qzone 根據(jù) dexopt 的過程中 (DexPrepare.

27、cpp -> verifyAndOptimizeClass) 如果 dvmVerifyClass 返回 true 了 ,就會(huì)給 class 標(biāo)記上 CLASS_ISPREVERIFIED. 所以我們要確保 dvmVerifyClass 返回 false, 只要不被打上 CLASS_ISPREVERIFIED 標(biāo)記 ,就不會(huì)觸發(fā)上述的異常 ./* Verify and/or optimize a specific class.*/static void verifyAndOptimizeClass(DexFile* pDexFile, ClassObject* clazz, const D

28、exClassDef* pClassDef, bool doV erify, bool doOpt)./* First, try to verify it.*/if (doVerify) if (dvmVerifyClass(clazz) /* Set the "is preverified" flag in the DexClassDef.* do it here, rather than in the ClassObject structure,* because the DexClassDef is part of the odex file.We*/assert(c

29、lazz->accessFlags & JAVA_FLAGS_MASK) =pClassDef->accessFlags);(DexClassDef*)pClassDef)->accessFlags |= CLASS_ISPREVERIFIED;verified = true; else / TODO: log when in verbose modeALOGV("DexOpt: '%s' failed verification", classDescriptor);.為了能讓 dvmVerifyClass 返回 false,我們繼

30、續(xù)跟蹤這個(gè)方法(DexVerify.app -> dvmVerifyClass).首先是過濾重復(fù)驗(yàn)證,由于補(bǔ)丁包加載之前是沒有做過驗(yàn)證的,所以這個(gè)條件可以直接忽略.接下來是遍歷clazz 的 directMethods( 包含構(gòu)造 ,靜態(tài) ,私有方法 )和 virtualMethods, 只要這兩個(gè)數(shù)組中的方法存在有關(guān)聯(lián)的對(duì)象跨dex 文件的情況就可以讓dvmVerifyClass 返回 false./* Verify a class.* By the time we get here, the value of gDvm.classVerifyMode should already*

31、have been factored in. If you want to call into the verifier even* though verification is disabled, that's your business.* Returns "true" on success.*/bool dvmVerifyClass(ClassObject* clazz)int i;if (dvmIsClassVerified(clazz) ALOGD("Ignoring duplicate verify attempt on %s", c

32、lazz->descriptor); return true;for (i = 0; i < clazz->directMethodCount; i+) if (!verifyMethod(&clazz->directMethodsi) LOG_VFY("Verifier rejected class %s", clazz->descriptor);return false;for (i = 0; i < clazz->virtualMethodCount; i+) if (!verifyMethod(&clazz->virtualMethodsi) LOG_VFY("Verifier rejected class %s", clazz->descriptor);return false;return true;Qzone 給出的方案是在gradle 插件中對(duì)除了Application子類之外的所有類(包含 Jar 包中的 )的構(gòu)造方法里面通過ASM動(dòng)態(tài)注入一個(gè)獨(dú)立dex 中Class 的引用,這樣這些類就不會(huì)被打上CLASS_ISPREVERIFIED,就可以對(duì)其進(jìn)行熱更.把 App

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

最新文檔

評(píng)論

0/150

提交評(píng)論