日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > Android >内容正文

Android

Android热修复核心原理介绍

發(fā)布時(shí)間:2023/12/9 Android 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Android热修复核心原理介绍 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

對(duì)網(wǎng)絡(luò)上熱修復(fù)方案和原理的文章和三方框架進(jìn)行了二次整理,讓讀者對(duì)熱修復(fù)方案和原理有個(gè)整體的認(rèn)知。總的來說熱修復(fù)不是簡單的一項(xiàng)技術(shù),更貼切的說是一種解決方案,不僅涉及到APP端的補(bǔ)丁生成和生效技術(shù),還涉及系統(tǒng)兼容性、開發(fā)過程代碼管理、補(bǔ)丁管理系統(tǒng)等。除非有足夠的人力物力支持,否則在生產(chǎn)環(huán)境中引入熱修復(fù)還是推薦使用阿里、騰訊等大廠的現(xiàn)成方案,不推薦自己造輪子。

熱修復(fù)框架

阿里系

框架簡介官網(wǎng)相關(guān)文章推薦
HotFix阿里百川未開源免費(fèi)、實(shí)時(shí)生效官網(wǎng)阿里百川HotFix快速集成
AndFix開源免費(fèi),基于native替換,實(shí)時(shí)生效,有兼容性問題,官方已不再維護(hù)github
Dexposed開源免費(fèi),劫持Java method實(shí)現(xiàn)AOP、插樁、熱補(bǔ)丁、SDK hook 等功能,不支持art平臺(tái),官方已不再維護(hù)github阿里 Dexposed 熱修復(fù)原理
Sophix阿里云未開源收費(fèi),實(shí)時(shí)生效/冷啟動(dòng)修復(fù),圖形界面一鍵打包、加密傳輸、簽名校驗(yàn)和服務(wù)端控制發(fā)布與灰度功能,必須繼承SophixApplication,但支持保留原Application官網(wǎng)阿里Sophix熱修復(fù)接入指南
Amigo餓了么出品,開源,冷啟動(dòng)修復(fù),補(bǔ)丁管理平臺(tái)已關(guān)閉github-

騰訊系

框架簡介官網(wǎng)相關(guān)文章推薦
Tinker微信部分開源收費(fèi),tinker出補(bǔ)丁包,burgly分發(fā)管理補(bǔ)丁githubwiki
Qzone超級(jí)補(bǔ)丁QQ空間未開源,冷啟動(dòng)修復(fù)-Qzone 超級(jí)補(bǔ)丁熱修復(fù)方案原理
QFix手Q開源免費(fèi),冷啟動(dòng)修復(fù),項(xiàng)目不再維護(hù)githubQFix探索之路——手Q熱補(bǔ)丁輕量級(jí)方案
Shadow開源免費(fèi),無反射全動(dòng)態(tài)githubwiki

國內(nèi)知名公司

框架簡介官網(wǎng)相關(guān)文章推薦
Robust美團(tuán)開源免費(fèi),實(shí)時(shí)修復(fù)githubwiki
Aceso美麗說蘑菇街開源免費(fèi),實(shí)時(shí)修復(fù),不再維護(hù)githubwiki
Nuwa大眾點(diǎn)評(píng)開源免費(fèi),冷啟動(dòng)修復(fù),不再維護(hù)github-

其他組織或個(gè)人

框架簡介官網(wǎng)相關(guān)文章推薦
RocooFix開源免費(fèi),不再維護(hù)github-
AnoleFix開源免費(fèi),基于InstantRun,不再維護(hù)github-

核心技術(shù)

代碼修復(fù)

multidex方案

由于Android不能直接執(zhí)行class文件,而是執(zhí)行的dex文件。所以加載dex就需要一些特殊的類加載器。Android中常見的類加載器有BootClassLoader、BaseDexClassLoader、PathClassLoader、DexClassLoader。

  • BootClassLoader是加載Android系統(tǒng)源碼,例如Activity,AMS等。
  • PathClassLoader和DexClassLoader都是繼承于BaseDexClassLoader,兩者的區(qū)別在于構(gòu)造方法參數(shù)不同。默認(rèn)情況下,PathClassLoader是用于加載三方庫,比如AppCompatActivity等這些代碼。DexClassLoader是加載外部的dex文件,其實(shí)使用PathClassLoader去加載外部的dex文件也是沒問題的。

雙親委托機(jī)制

類加載過程可以描述為:先檢查已加載的類,找不到則優(yōu)先從父類加載器查找,否則從BootstrapClassLoader查找,還是沒有則調(diào)用當(dāng)前類加載器的findClass方法進(jìn)行加載。

java.lang.ClassLoader#loadClass(java.lang.String, boolean)核心代碼:

/*** Loads the class with the specified <a href="#name">binary name</a>. The* default implementation of this method searches for classes in the* following order:** <ol>** <li><p> Invoke {@link #findLoadedClass(String)} to check if the class* has already been loaded. </p></li>** <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method* on the parent class loader. If the parent is <tt>null</tt> the class* loader built-in to the virtual machine is used, instead. </p></li>** <li><p> Invoke the {@link #findClass(String)} method to find the* class. </p></li>** </ol>** <p> If the class was found using the above steps, and the* <tt>resolve</tt> flag is true, this method will then invoke the {@link* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.** <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link* #findClass(String)}, rather than this method. </p>*** @param name* The <a href="#name">binary name</a> of the class** @param resolve* If <tt>true</tt> then resolve the class** @return The resulting <tt>Class</tt> object** @throws ClassNotFoundException* If the class could not be found*/// Android-removed: Remove references to getClassLoadingLock// Remove perf counters.//// <p> Unless overridden, this method synchronizes on the result of// {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method// during the entire class loading process.protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.c = findClass(name);}}return c;}

Android類加載機(jī)制

ClassLoader#findClass是抽象方法,Android的BaseDexClassLoader實(shí)現(xiàn)了此方法:

public class BaseDexClassLoader extends ClassLoader {private final DexPathList pathList;@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {List<Throwable> suppressedExceptions = new ArrayList<Throwable>();Class c = pathList.findClass(name, suppressedExceptions);if (c == null) {ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);for (Throwable t : suppressedExceptions) {cnfe.addSuppressed(t);}throw cnfe;}return c;} }

下面的代碼均不能在AS中查看,介紹兩個(gè)可以在線看framework源碼的網(wǎng)站:

  • http://androidxref.com/
  • http://source.android.com/

Class通過DexPathList#findClass(String, List<Throwable>)來查找:

// 加載名字為name的class對(duì)象 public Class findClass(String name, List<Throwable> suppressed) {// 遍歷從dexPath查詢到的dex和資源Elementfor (Element element : dexElements) {DexFile dex = element.dexFile;// 如果當(dāng)前的Element是dex文件元素if (dex != null) {// 使用DexFile.loadClassBinaryName加載類Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);if (clazz != null) {return clazz;}}}if (dexElementsSuppressedExceptions != null) {suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));}return null; }

DexFile#loadClassBinaryName:

/*** See {@link #loadClass(String, ClassLoader)}.** This takes a "binary" class name to better match ClassLoader semantics.** @hide*/ public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {return defineClass(name, loader, mCookie, this, suppressed); }private static Class defineClass(String name, ClassLoader loader, Object cookie,DexFile dexFile, List<Throwable> suppressed) {Class result = null;try {result = defineClassNative(name, loader, cookie, dexFile);} catch (NoClassDefFoundError e) {if (suppressed != null) {suppressed.add(e);}} catch (ClassNotFoundException e) {if (suppressed != null) {suppressed.add(e);}}return result; }private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile) throws ClassNotFoundException, NoClassDefFoundError;

dex文件轉(zhuǎn)換成dexFile對(duì)象,存入Element[]數(shù)組,findclass順序遍歷Element數(shù)組獲取DexFile,然后執(zhí)行DexFile的loadClassBinaryName。Android這種類加載機(jī)制的目的是防止類的重復(fù)加載和實(shí)現(xiàn)就近加載原則,而這也為我們實(shí)現(xiàn)類的動(dòng)態(tài)加載和替換提供了可能。

核心代碼

通過上面類加載過程的分析,我們只需要hook ClassLoader.pathList.dexElements[],將補(bǔ)丁的dex插入到數(shù)組的首位即可實(shí)現(xiàn)Class替換。

以下是Nuwa的關(guān)鍵實(shí)現(xiàn)源碼:

public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {//新建一個(gè)ClassLoader加載補(bǔ)丁DexDexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());//反射獲取舊DexElements數(shù)組Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));//反射獲取補(bǔ)丁DexElements數(shù)組Object newDexElements = getDexElements(getPathList(dexClassLoader));//合并,將新數(shù)組的Element插入到最前面Object allDexElements = combineArray(newDexElements, baseDexElements);Object pathList = getPathList(getPathClassLoader());//更新舊ClassLoader中的Element數(shù)組ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements); }private static PathClassLoader getPathClassLoader() {PathClassLoader pathClassLoader = (PathClassLoader) DexUtils.class.getClassLoader();return pathClassLoader; }private static Object getDexElements(Object paramObject)throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {return ReflectionUtils.getField(paramObject, paramObject.getClass(), "dexElements"); }private static Object getPathList(Object baseDexClassLoader)throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {return ReflectionUtils.getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList"); }private static Object combineArray(Object firstArray, Object secondArray) {Class<?> localClass = firstArray.getClass().getComponentType();int firstArrayLength = Array.getLength(firstArray);int allLength = firstArrayLength + Array.getLength(secondArray);Object result = Array.newInstance(localClass, allLength);for (int k = 0; k < allLength; ++k) {if (k < firstArrayLength) {Array.set(result, k, Array.get(firstArray, k));} else {Array.set(result, k, Array.get(secondArray, k - firstArrayLength));}}return result; }

patch.dex生成

1. 補(bǔ)丁class生成全量patch.dex

通過技術(shù)手段篩選出需要替換的類生成的class文件,將這些class文件生成一個(gè)單獨(dú)patch.dex。

這種方式比較直觀,但是容易遭遇CLASS_ISPREVERIFIED標(biāo)志問題:例如MainAcivity和Utils類存在于同一個(gè)dex中,這個(gè)時(shí)候MainActivity會(huì)被打上CLASS_ISPREVERIFIED標(biāo)志,大概意思就是當(dāng)MainActivity使用Utils類的時(shí)候,會(huì)直接從該dex中加載,而不會(huì)從其他dex中加載,補(bǔ)丁失效。

《安卓App熱補(bǔ)丁動(dòng)態(tài)修復(fù)技術(shù)介紹》給出了一種解決方案,采取對(duì)抗策略:為了避免類被加上CLASS_ISPREVERIFIED,使用插樁,單獨(dú)放一個(gè)幫助類在獨(dú)立的dex中讓其他類調(diào)用。

2. 差量patch.dex合并替換主dex

為了避免dex插樁帶來的性能損耗,dex替換采取另外的方式:使用diff工具生成patch.dex差量包,在運(yùn)行時(shí)使用patch工具將patch.dex與應(yīng)用的classes.dex合并成一個(gè)完整的dex,插入到ClassLoader.pathList.dexElements[]頭部。

這也是微信Tinker采用的方案,并且Tinker自研了DexDiff/DexMerge算法。這個(gè)方案具有補(bǔ)丁小,兼容性好的優(yōu)點(diǎn),但是無法做到實(shí)時(shí)生效,需要在下次啟動(dòng)才能生效,并且Dex合并內(nèi)存消耗大,容易OOM導(dǎo)致合并失敗,應(yīng)該要另起一個(gè)進(jìn)程做這個(gè)事情。

特點(diǎn)總結(jié)

優(yōu)點(diǎn)缺點(diǎn)
  • 不需要考慮對(duì)dalvik虛擬機(jī)和art虛擬機(jī)做適配
  • 代碼是非侵入式的,對(duì)apk體積影響不大
  • 使用了反射,需要考慮版本兼容問題
  • Android N混合編譯機(jī)制問題:Android N混合編譯與對(duì)熱補(bǔ)丁影響解析

Java Hook(InstantRun)方案

在打基礎(chǔ)包時(shí)插樁,在每個(gè)方法前插入一段補(bǔ)丁發(fā)現(xiàn)和應(yīng)用代碼,實(shí)現(xiàn)有補(bǔ)丁時(shí)補(bǔ)丁生效,沒補(bǔ)丁時(shí)執(zhí)行原來的代碼。

核心代碼

以美團(tuán)的Robust為例,打基礎(chǔ)包時(shí)插樁,在每個(gè)方法前插入一段類型為 ChangeQuickRedirect 靜態(tài)變量的邏輯:

public static ChangeQuickRedirect u; protected void onCreate(Bundle bundle) {//為每個(gè)方法自動(dòng)插入修復(fù)邏輯代碼,如果ChangeQuickRedirect為空則不執(zhí)行if (u != null) {if (PatchProxy.isSupport(new Object[]{bundle}, this, u, false, 78)) {PatchProxy.accessDispatchVoid(new Object[]{bundle}, this, u, false, 78);return;}}super.onCreate(bundle);... }

然后是補(bǔ)丁發(fā)現(xiàn)和加載的方法:

public class PatchExecutor extends Thread {@Overridepublic void run() {...applyPatchList(patches);...}/*** 應(yīng)用補(bǔ)丁列表*/protected void applyPatchList(List<Patch> patches) {...for (Patch p : patches) {...currentPatchResult = patch(context, p);...}}/*** 核心修復(fù)源碼*/protected boolean patch(Context context, Patch patch) {...//新建ClassLoaderDexClassLoader loader= new DexClassLoader(patch.getTempPath(), context.getCacheDir().getAbsolutePath(),null, PatchExecutor.class.getClassLoader());patch.delete(patch.getTempPath());...try {patchsInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());patchesInfo = (PatchesInfo) patchsInfoClass.newInstance();} catch (Throwable t) {...}...//通過遍歷其中的類信息進(jìn)而反射修改其中 ChangeQuickRedirect 對(duì)象的值for (PatchedClassInfo patchedClassInfo : patchedClasses) {...try {oldClass = classLoader.loadClass(patchedClassName.trim());Field[] fields = oldClass.getDeclaredFields();for (Field field : fields) {if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), oldClass.getCanonicalName())) {changeQuickRedirectField = field;break;}}...try {patchClass = classLoader.loadClass(patchClassName);Object patchObject = patchClass.newInstance();changeQuickRedirectField.setAccessible(true);changeQuickRedirectField.set(null, patchObject);} catch (Throwable t) {...}} catch (Throwable t) {...}}return true;} }

特點(diǎn)總結(jié)

優(yōu)點(diǎn)缺點(diǎn)
  • 高兼容性、高穩(wěn)定性,修復(fù)成功率高達(dá)99.9%
  • 補(bǔ)丁實(shí)時(shí)生效,不需要重新啟動(dòng)
  • 支持方法級(jí)別的修復(fù),包括靜態(tài)方法
  • 支持增加方法和類
  • 支持ProGuard的混淆、內(nèi)聯(lián)、優(yōu)化等操作
  • 代碼是侵入式的,會(huì)在原有的類中加入相關(guān)代碼,從而增大apk的體積,平均一個(gè)函數(shù)會(huì)比原來增加17.47個(gè)字節(jié),10萬個(gè)函數(shù)會(huì)增加1.67M
  • so和資源的替換暫時(shí)不支持

native替換方案

每一個(gè)Java方法在art中都對(duì)應(yīng)一個(gè)ArtMethod,ArtMethod記錄了這個(gè)Java方法的所有信息,包括訪問權(quán)限及代碼執(zhí)行地址等。通過env->FromReflectedMethod得到方法對(duì)應(yīng)的ArtMethod的真正開始地址,然后強(qiáng)轉(zhuǎn)為ArtMethod指針,從而對(duì)其所有成員進(jìn)行修改。這樣以后調(diào)用這個(gè)方法時(shí)就會(huì)直接走到新方法的實(shí)現(xiàn)中,達(dá)到熱修復(fù)的效果。

核心代碼

示例:AndFix安卓6.0 ArtMethod替換代碼片段

void replace_6_0(JNIEnv* env, jobject src, jobject dest) {// 通過Method對(duì)象得到底層Java函數(shù)對(duì)應(yīng)ArtMethod的真實(shí)地址art::mirror::ArtMethod* smeth =(art::mirror::ArtMethod*) env->FromReflectedMethod(src);art::mirror::ArtMethod* dmeth =(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_ =reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_; //for plugin classloaderreinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ = reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_-1;//for reflection invokereinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;//把舊函數(shù)的所有成員變量都替換為新函數(shù)的smeth->declaring_class_ = dmeth->declaring_class_;smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;smeth->access_flags_ = dmeth->access_flags_ | 0x0001;smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;smeth->dex_method_index_ = dmeth->dex_method_index_;smeth->method_index_ = dmeth->method_index_;smeth->ptr_sized_fields_.entry_point_from_interpreter_ =dmeth->ptr_sized_fields_.entry_point_from_interpreter_;smeth->ptr_sized_fields_.entry_point_from_jni_ =dmeth->ptr_sized_fields_.entry_point_from_jni_;smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;LOGD("replace_6_0: %d , %d",smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_); }void setFieldFlag_6_0(JNIEnv* env, jobject field) {art::mirror::ArtField* artField =(art::mirror::ArtField*) env->FromReflectedField(field);artField->access_flags_ = artField->access_flags_ & (~0x0002) | 0x0001;LOGD("setFieldFlag_6_0: %d ", artField->access_flags_); }

特點(diǎn)總結(jié)

優(yōu)點(diǎn)缺點(diǎn)
  • 即時(shí)生效
  • 沒有性能開銷,不需要任何編輯器的插樁或代碼改寫
  • 存在穩(wěn)定及兼容性問題。ArtMethod的結(jié)構(gòu)基本參考Google開源的代碼,各大廠商的ROM都可能有所改動(dòng),可能導(dǎo)致結(jié)構(gòu)不一致,修復(fù)失敗。
  • 無法增加變量及類,只能修復(fù)方法級(jí)別的Bug,無法做到新功能的發(fā)布

資源替換

原理概述:

  • 構(gòu)建一個(gè)新的AssetManager,并通過反射調(diào)用addAssertPath,把這個(gè)完整的新資源包加入到AssetManager中,這樣就得到一個(gè)含有所有新資源的AssetManager;
  • 找到所有值錢引用到原有AssetManager的地方,通過反射,把引用處替換為AssetManager;
  • 核心代碼

    public static void monkeyPatchExistingResources(Context context, String externalResourceFile, Collection activities) {if (externalResourceFile == null) {return;}try {//反射一個(gè)新的 AssetManagerAssetManager newAssetManager = (AssetManager) AssetManager.class.getConstructor(new Class[0]).newInstance(new Object[0]);//反射 addAssetPath 添加新的資源包Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", new Class[]{String.class});mAddAssetPath.setAccessible(true);if (((Integer) mAddAssetPath.invoke(newAssetManager,new Object[]{externalResourceFile})).intValue() == 0) {throw new IllegalStateException("Could not create new AssetManager");}Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks", new Class[0]);mEnsureStringBlocks.setAccessible(true);mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);//反射得到Activity中AssetManager的引用處,全部換成剛新構(gòu)建的AssetManager對(duì)象if (activities != null) {for (Activity activity : activities) {Resources resources = activity.getResources();try {Field mAssets = Resources.class.getDeclaredField("mAssets");mAssets.setAccessible(true);mAssets.set(resources, newAssetManager);} catch (Throwable ignore) {Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");mResourcesImpl.setAccessible(true);Object resourceImpl = mResourcesImpl.get(resources);Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");implAssets.setAccessible(true);implAssets.set(resourceImpl, newAssetManager);}Resources.Theme theme = activity.getTheme();try {try {Field ma = Resources.Theme.class.getDeclaredField("mAssets");ma.setAccessible(true);ma.set(theme, newAssetManager);} catch (NoSuchFieldException ignore) {Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");themeField.setAccessible(true);Object impl = themeField.get(theme);Field ma = impl.getClass().getDeclaredField("mAssets");ma.setAccessible(true);ma.set(impl, newAssetManager);}Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme");mt.setAccessible(true);mt.set(activity, null);Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme", new Class[0]);mtm.setAccessible(true);mtm.invoke(activity, new Object[0]);Method mCreateTheme = AssetManager.class.getDeclaredMethod("createTheme", new Class[0]);mCreateTheme.setAccessible(true);Object internalTheme = mCreateTheme.invoke(newAssetManager, new Object[0]);Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");mTheme.setAccessible(true);mTheme.set(theme, internalTheme);} catch (Throwable e) {Log.e("InstantRun","Failed to update existing theme for activity "+ activity, e);}pruneResourceCaches(resources);}}Collection references;if (Build.VERSION.SDK_INT >= 19) {Class resourcesManagerClass = Class.forName("android.app.ResourcesManager");Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance", new Class[0]);mGetInstance.setAccessible(true);Object resourcesManager = mGetInstance.invoke(null, new Object[0]);try {Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");fMActiveResources.setAccessible(true);ArrayMap arrayMap = (ArrayMap) fMActiveResources.get(resourcesManager);references = arrayMap.values();} catch (NoSuchFieldException ignore) {Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");mResourceReferences.setAccessible(true);references = (Collection) mResourceReferences.get(resourcesManager);}} else {Class activityThread = Class.forName("android.app.ActivityThread");Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");fMActiveResources.setAccessible(true);Object thread = getActivityThread(context, activityThread);HashMap map = (HashMap) fMActiveResources.get(thread);references = map.values();}for (WeakReference wr : references) {Resources resources = (Resources) wr.get();if (resources != null) {try {Field mAssets = Resources.class.getDeclaredField("mAssets");mAssets.setAccessible(true);mAssets.set(resources, newAssetManager);} catch (Throwable ignore) {Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");mResourcesImpl.setAccessible(true);Object resourceImpl = mResourcesImpl.get(resources);Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");implAssets.setAccessible(true);implAssets.set(resourceImpl, newAssetManager);}resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());}}} catch (Throwable e) {throw new IllegalStateException(e);}}

    動(dòng)態(tài)鏈接庫修復(fù)

    so加載入口替換

    APP中所有加載so文件的地方統(tǒng)一調(diào)用sdk提供的方法:

    SOPatchManger.loadLibrary(String libName) 替換 System.loadLibrary(String libName)

    SOPatchManger.loadLibrary接口加載so庫的時(shí)候優(yōu)先嘗試去加載APP指定目錄下補(bǔ)丁的so,若不存在,則再去加載安裝apk目錄下的so庫。

    關(guān)于System.loadLibrary(String libName)System.load(String filename)

    • load(String filename):從指定的絕對(duì)路徑加載so文件,因此可以加載外部so文件;
    • loadLibrary(String libName):加載指定文件名的so文件,從系統(tǒng)默認(rèn)路徑加載(Runtime#mLibPaths,String[]類型,從System.getProperty("java.library.path")讀取)。若手動(dòng)將外部路徑添加到系統(tǒng)默認(rèn)路徑,同樣可以實(shí)現(xiàn)外部so文件加載;

    特點(diǎn)總結(jié)

    優(yōu)點(diǎn)缺點(diǎn)
    • 靜態(tài)代碼,無性能和兼容性問題
    • 需要侵入業(yè)務(wù)代碼,替換掉System默認(rèn)加載so庫的接口,建議采用ASM插裝實(shí)現(xiàn)

    反射注入

    采取類似類修復(fù)反射注入方式,把補(bǔ)丁so庫的路徑插入到DexPathList#nativeLibraryDirectories數(shù)組的最前面,就能夠達(dá)到加載so庫的時(shí)候是補(bǔ)丁so庫而不是原來so庫的目錄,從而達(dá)到修復(fù)。

    so文件加載流程

    回顧一下so文件的加載流程:System#loadLibrary(String libname) -> Runtime#loadLibrary(String libname) -> Runtime#loadLibrary0(ClassLoader loader, String libname),從這里開始看下load過程:

    // java.lang.Runtime#loadLibrary0 synchronized void loadLibrary0(ClassLoader loader, String libname) {if (libname.indexOf((int)File.separatorChar) != -1) {throw new UnsatisfiedLinkError("Directory separator should not appear in library name: " + libname);}String libraryName = libname;if (loader != null) {// 我們調(diào)用System#loadLibrary時(shí)loader從VMStack.getCallingClassLoader()獲取,// 通常不為空,因此走這個(gè)分支String filename = loader.findLibrary(libraryName);if (filename == null) {// It's not necessarily true that the ClassLoader used// System.mapLibraryName, but the default setup does, and it's// misleading to say we didn't find "libMyLibrary.so" when we// actually searched for "liblibMyLibrary.so.so".throw new UnsatisfiedLinkError(loader + " couldn't find \"" + System.mapLibraryName(libraryName) + "\"");}String error = doLoad(filename, loader);if (error != null) {throw new UnsatisfiedLinkError(error);}return;}// 未指定loader時(shí),走這個(gè)邏輯String filename = System.mapLibraryName(libraryName);List<String> candidates = new ArrayList<String>();String lastError = null;for (String directory : getLibPaths()) {String candidate = directory + filename;candidates.add(candidate);if (IoUtils.canOpenReadOnly(candidate)) {String error = doLoad(candidate, loader);if (error == null) {return; // We successfully loaded the library. Job done.}lastError = error;}}if (lastError != null) {throw new UnsatisfiedLinkError(lastError);}throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);}

    ClassLoader#findLibrary默認(rèn)實(shí)現(xiàn)為空,對(duì)于Android真正的實(shí)現(xiàn)在BaseDexClassLoader#findLibrary:

    // BaseDexClassLoader#findLibrary @Override public String findLibrary(String name) {return pathList.findLibrary(name); }

    調(diào)用了DexPathList#findLibrary:

    // DexPathList#findLibrary public String findLibrary(String libraryName) {String fileName = System.mapLibraryName(libraryName);// nativeLibraryPathElements由DexPathList#makePathElements通過傳入nativeLibraryDirectories等生成for (NativeLibraryElement element : nativeLibraryPathElements) {String path = element.findNativeLibrary(fileName);if (path != null) {return path;}}return null; }

    關(guān)于如何注入自定義lib路徑,由于需要考慮兼容性,還是比較麻煩的,可以參考這篇:Android 系統(tǒng)so文件路徑修改

    特點(diǎn)總結(jié)

    優(yōu)點(diǎn)缺點(diǎn)
    • 不需侵入用戶接口調(diào)用
    • 反射方案的共性問題,需要做版本兼容控制,這是個(gè)持久戰(zhàn)

    參考資料

    • Android熱修復(fù)原理(一)熱修復(fù)框架對(duì)比和代碼修復(fù)
    • Android熱修復(fù)技術(shù)選型——三大流派解析
    • 進(jìn)階高工必備技能:Android熱修復(fù)技術(shù)全解析!
    • 【騰訊Bugly干貨分享】Android Patch 方案與持續(xù)交付
    • 2020 Android 大廠面試(五)插件化、模塊化、組件化、熱修復(fù)、增量更新、Gradle

    總結(jié)

    以上是生活随笔為你收集整理的Android热修复核心原理介绍的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。