对网络上热修复方案和原理的文章和三方框架进行了二次整理,让读者对热修复方案和原理有个整体的认知。总的来说热修复不是简单的一项技术,更贴切的说是一种解决方案,不仅涉及到APP端的补丁生成和生效技术,还涉及系统兼容性、开发过程代码管理、补丁管理系统等。除非有足够的人力物力支持,否则在生产环境中引入热修复还是推荐使用阿里、腾讯等大厂的现成方案,不推荐自己造轮子。

热修复框架

阿里系

框架 简介 官网 相关文章推荐
HotFix 阿里百川未开源免费、实时生效 官网 阿里百川HotFix快速集成
AndFix
开源免费,基于native替换,实时生效,有兼容性问题,官方已不再维护
github
Dexposed
开源免费,劫持Java method实现AOP、插桩、热补丁、SDK hook 等功能,不支持art平台,官方已不再维护
github 阿里 Dexposed 热修复原理
Sophix
阿里云未开源收费,实时生效/冷启动修复,图形界面一键打包、加密传输、签名校验和服务端控制发布与灰度功能,必须继承SophixApplication,但支持保留原Application
官网 阿里Sophix热修复接入指南
Amigo
饿了么出品,开源,冷启动修复,补丁管理平台已关闭
github -

腾讯系

框架 简介 官网 相关文章推荐
Tinker 微信部分开源收费,tinker出补丁包,burgly分发管理补丁 github wiki
Qzone超级补丁 QQ空间未开源,冷启动修复 - Qzone 超级补丁热修复方案原理
QFix 手Q开源免费,冷启动修复,项目不再维护 github QFix探索之路——手Q热补丁轻量级方案
Shadow 开源免费,无反射全动态 github wiki

国内知名公司

框架 简介 官网 相关文章推荐
Robust 美团开源免费,实时修复 github wiki
Aceso 美丽说蘑菇街开源免费,实时修复,不再维护 github wiki
Nuwa 大众点评开源免费,冷启动修复,不再维护 github -

其他组织或个人

框架 简介 官网 相关文章推荐
RocooFix 开源免费,不再维护 github -
AnoleFix 开源免费,基于InstantRun,不再维护 github -

核心技术

代码修复

multidex方案

由于Android不能直接执行class文件,而是执行的dex文件。所以加载dex就需要一些特殊的类加载器。Android中常见的类加载器有BootClassLoader、BaseDexClassLoader、PathClassLoader、DexClassLoader。

  • BootClassLoader是加载Android系统源码,例如Activity,AMS等。
  • PathClassLoader和DexClassLoader都是继承于BaseDexClassLoader,两者的区别在于构造方法参数不同。默认情况下,PathClassLoader是用于加载三方库,比如AppCompatActivity等这些代码。DexClassLoader是加载外部的dex文件,其实使用PathClassLoader去加载外部的dex文件也是没问题的。

双亲委托机制

类加载过程可以描述为:先检查已加载的类,找不到则优先从父类加载器查找,否则从BootstrapClassLoader查找,还是没有则调用当前类加载器的findClass方法进行加载。

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类加载机制

ClassLoader#findClass是抽象方法,Android的BaseDexClassLoader实现了此方法:

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中查看,介绍两个可以在线看framework源码的网站:

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

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

// 加载名字为name的class对象
public Class findClass(String name, List<Throwable> suppressed) {// 遍历从dexPath查询到的dex和资源Elementfor (Element element : dexElements) {DexFile dex = element.dexFile;// 如果当前的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文件转换成dexFile对象,存入Element[]数组,findclass顺序遍历Element数组获取DexFile,然后执行DexFile的loadClassBinaryName。Android这种类加载机制的目的是防止类的重复加载和实现就近加载原则,而这也为我们实现类的动态加载和替换提供了可能。

核心代码

通过上面类加载过程的分析,我们只需要hook ClassLoader.pathList.dexElements[],将补丁的dex插入到数组的首位即可实现Class替换。

以下是Nuwa的关键实现源码:

public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {//新建一个ClassLoader加载补丁DexDexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());//反射获取旧DexElements数组Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));//反射获取补丁DexElements数组Object newDexElements = getDexElements(getPathList(dexClassLoader));//合并,将新数组的Element插入到最前面Object allDexElements = combineArray(newDexElements, baseDexElements);Object pathList = getPathList(getPathClassLoader());//更新旧ClassLoader中的Element数组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. 补丁class生成全量patch.dex

通过技术手段筛选出需要替换的类生成的class文件,将这些class文件生成一个单独patch.dex。

这种方式比较直观,但是容易遭遇CLASS_ISPREVERIFIED标志问题:例如MainAcivity和Utils类存在于同一个dex中,这个时候MainActivity会被打上CLASS_ISPREVERIFIED标志,大概意思就是当MainActivity使用Utils类的时候,会直接从该dex中加载,而不会从其他dex中加载,补丁失效。

《安卓App热补丁动态修复技术介绍》给出了一种解决方案,采取对抗策略:为了避免类被加上CLASS_ISPREVERIFIED,使用插桩,单独放一个帮助类在独立的dex中让其他类调用。

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

为了避免dex插桩带来的性能损耗,dex替换采取另外的方式:使用diff工具生成patch.dex差量包,在运行时使用patch工具将patch.dex与应用的classes.dex合并成一个完整的dex,插入到ClassLoader.pathList.dexElements[]头部。

这也是微信Tinker采用的方案,并且Tinker自研了DexDiff/DexMerge算法。这个方案具有补丁小,兼容性好的优点,但是无法做到实时生效,需要在下次启动才能生效,并且Dex合并内存消耗大,容易OOM导致合并失败,应该要另起一个进程做这个事情。

特点总结

优点 缺点
  • 不需要考虑对dalvik虚拟机和art虚拟机做适配
  • 代码是非侵入式的,对apk体积影响不大
  • 使用了反射,需要考虑版本兼容问题
  • Android N混合编译机制问题:Android N混合编译与对热补丁影响解析

Java Hook(InstantRun)方案

在打基础包时插桩,在每个方法前插入一段补丁发现和应用代码,实现有补丁时补丁生效,没补丁时执行原来的代码。

核心代码

以美团的Robust为例,打基础包时插桩,在每个方法前插入一段类型为 ChangeQuickRedirect 静态变量的逻辑:

public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {//为每个方法自动插入修复逻辑代码,如果ChangeQuickRedirect为空则不执行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);...
}

然后是补丁发现和加载的方法:

public class PatchExecutor extends Thread {@Overridepublic void run() {...applyPatchList(patches);...}/*** 应用补丁列表*/protected void applyPatchList(List<Patch> patches) {...for (Patch p : patches) {...currentPatchResult = patch(context, p);...}}/*** 核心修复源码*/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) {...}...//通过遍历其中的类信息进而反射修改其中 ChangeQuickRedirect 对象的值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;}
}

特点总结

优点 缺点
  • 高兼容性、高稳定性,修复成功率高达99.9%
  • 补丁实时生效,不需要重新启动
  • 支持方法级别的修复,包括静态方法
  • 支持增加方法和类
  • 支持ProGuard的混淆、内联、优化等操作
  • 代码是侵入式的,会在原有的类中加入相关代码,从而增大apk的体积,平均一个函数会比原来增加17.47个字节,10万个函数会增加1.67M
  • so和资源的替换暂时不支持

native替换方案

每一个Java方法在art中都对应一个ArtMethod,ArtMethod记录了这个Java方法的所有信息,包括访问权限及代码执行地址等。通过env->FromReflectedMethod得到方法对应的ArtMethod的真正开始地址,然后强转为ArtMethod指针,从而对其所有成员进行修改。这样以后调用这个方法时就会直接走到新方法的实现中,达到热修复的效果。

核心代码

示例:AndFix安卓6.0 ArtMethod替换代码片段

void replace_6_0(JNIEnv* env, jobject src, jobject dest) {// 通过Method对象得到底层Java函数对应ArtMethod的真实地址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;//把旧函数的所有成员变量都替换为新函数的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_);
}

特点总结

优点 缺点
  • 即时生效
  • 没有性能开销,不需要任何编辑器的插桩或代码改写
  • 存在稳定及兼容性问题。ArtMethod的结构基本参考Google开源的代码,各大厂商的ROM都可能有所改动,可能导致结构不一致,修复失败。
  • 无法增加变量及类,只能修复方法级别的Bug,无法做到新功能的发布

资源替换

原理概述:

  1. 构建一个新的AssetManager,并通过反射调用addAssertPath,把这个完整的新资源包加入到AssetManager中,这样就得到一个含有所有新资源的AssetManager;
  2. 找到所有值钱引用到原有AssetManager的地方,通过反射,把引用处替换为AssetManager;

核心代码

 public static void monkeyPatchExistingResources(Context context, String externalResourceFile, Collection activities) {if (externalResourceFile == null) {return;}try {//反射一个新的   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的引用处,全部换成刚新构建的AssetManager对象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);}}

动态链接库修复

so加载入口替换

APP中所有加载so文件的地方统一调用sdk提供的方法:

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

SOPatchManger.loadLibrary接口加载so库的时候优先尝试去加载APP指定目录下补丁的so,若不存在,则再去加载安装apk目录下的so库。

关于System.loadLibrary(String libName)和System.load(String filename):

  • load(String filename):从指定的绝对路径加载so文件,因此可以加载外部so文件;
  • loadLibrary(String libName):加载指定文件名的so文件,从系统默认路径加载(Runtime#mLibPaths,String[]类型,从System.getProperty("java.library.path")读取)。若手动将外部路径添加到系统默认路径,同样可以实现外部so文件加载;

特点总结

优点 缺点
  • 静态代码,无性能和兼容性问题
  • 需要侵入业务代码,替换掉System默认加载so库的接口,建议采用ASM插装实现

反射注入

采取类似类修复反射注入方式,把补丁so库的路径插入到DexPathList#nativeLibraryDirectories数组的最前面,就能够达到加载so库的时候是补丁so库而不是原来so库的目录,从而达到修复。

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) {// 我们调用System#loadLibrary时loader从VMStack.getCallingClassLoader()获取,// 通常不为空,因此走这个分支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时,走这个逻辑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默认实现为空,对于Android真正的实现在BaseDexClassLoader#findLibrary:

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

调用了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;
}

关于如何注入自定义lib路径,由于需要考虑兼容性,还是比较麻烦的,可以参考这篇:Android 系统so文件路径修改

特点总结

优点 缺点
  • 不需侵入用户接口调用
  • 反射方案的共性问题,需要做版本兼容控制,这是个持久战

参考资料

  • Android热修复原理(一)热修复框架对比和代码修复
  • Android热修复技术选型——三大流派解析
  • 进阶高工必备技能:Android热修复技术全解析!
  • 【腾讯Bugly干货分享】Android Patch 方案与持续交付
  • 2020 Android 大厂面试(五)插件化、模块化、组件化、热修复、增量更新、Gradle

Android热修复核心原理介绍相关推荐

  1. Android 热修复核心原理

    目录 Android 热修复核心原理,  ClassLoader类加载 ART 和 Dalvik dexopt与dexaot ClassLoader介绍 双亲委托机制 热修复 在线源码 Android ...

  2. Android 获取ROOT权限原理介绍和签名验证原理及反编译学习

     Root 的介绍 1.       Root 的目的 可以让我们拥有掌控手机系统的权限,比如删除一些system/app下面的无用软件,更换开关机铃声和动画,拦截状态栏弹出的广告等. 2.      ...

  3. Android Animation学习(一) Property Animation原理介绍和API简介

    Android Animation学习(一) Property Animation介绍 Android Animation Android framework提供了两种动画系统: property a ...

  4. Android热修复升级探索——代码修复冷启动方案

    前言 前面一篇文档, 我们提到热部署修复方案有诸多特点(有关热部署修复方案实现, Android热修复升级探索--追寻极致的代码热替换).其根本原理是基于native层方法的替换, 所以当类结构变化时 ...

  5. 【Android架构师java原理详解】二;反射原理及动态代理模式

    前言: 本篇为Android架构师java原理专题二:反射原理及动态代理模式 大公司面试都要求我们有扎实的Java语言基础.而很多Android开发朋友这一块并不是很熟练,甚至半路初级底子很薄,这给我 ...

  6. Android热修复升级探索——SO库修复方案

    摘要: 通常情况下,大多数人希望android下热补丁方案能够做到补丁的全方位修复,包括类修复/资源修复/so库的修复. 这里主要介绍热补丁之so库修复思路. 一.前言 通常情况下,大多数人希望and ...

  7. Android热修复升级探索——SO库修复方案 1

    一.前言 通常情况下,大多数人希望android下热补丁方案能够做到补丁的全方位修复,包括类修复/资源修复/so库的修复. 这里主要介绍热补丁之so库修复思路. 二.so库加载原理 Java Api提 ...

  8. Android 热修复之DexPatch 介绍

    简介:Android 热修复之DexPatch 介绍 1. 方案介绍 为了解决Native模块上线后的问题,mPaas[1] 提供了热修复功能,实现不发布客户端apk场景下的热修复.目前Android ...

  9. android长截屏代码,android长截屏原理及实现代码

    android长截屏原理及实现代码 发布时间:2020-08-31 06:55:16 来源:脚本之家 阅读:158 作者:Android笔记 小米系统自带的长截屏应该很多人都用过,效果不错.当长截屏时 ...

  10. Android 获取ROOT权限原理解析

    一. 概述 本文介绍了android中获取root权限的方法以及原理,让大家对android玩家中常说的"越狱"有一个更深层次的认识. 二. Root的介绍 1.       Ro ...

最新文章

  1. 深度学习经典教程:深度学习+动手学深度学习
  2. WR:Tetrasphaera PAO 代谢中的储能物质与微生物多样性及除磷效能之间的关系
  3. 前端笔记(7)css属性书写顺序,布局流程,chrome调试工具
  4. Kafka使用入门教程
  5. db2有主键时默认hash分区_不允许设置db2主键问题的解决
  6. html单选框+点击取消选中,【前端JS】radio 可单选可点击取消选中
  7. mvc做网站怎么在mvc中直接访问.html网页 [问题点数:20分]
  8. eclipse 打包 apk 文件
  9. TensorFlow基础篇(三)——tf.nn.softmax_cross_entropy_with_logits
  10. NS2仿真过程中需要的语言及基本组件
  11. python入门--斐波那契数列
  12. Java开发自学教程!java从入门到精通txt下载
  13. HBuilderX App开发环境搭建
  14. 郑厂长系列故事——排兵布阵 状态压缩DP
  15. 最近非常火的电子木鱼流量主小程序源码
  16. 手机QQ浏览器的HTML管理器,手机qq浏览器中文件管理器有哪些功能
  17. 如何使用ContentProvider打造自己的本地图片库
  18. 设计模式读书笔记汇总
  19. revit二次开发——如何选取元素(revit2016)
  20. 选择适合的Node js授权认证策略

热门文章

  1. 13我无所不能——无线网络里面的秘密
  2. OA发票管理 发票验真 费用报销流程对接
  3. 焊接工时简便计算工具_2020年新版机械加工工时费用计算(17页)-原创力文档...
  4. 视觉培训4 完成手写识别项目
  5. 微信小程序 实现城市名称拼音搜索框 汉字/拼音(城市区县定位模块)
  6. 【登陆设计】-【技术上】你会做WEB上的用户登录功能吗?
  7. 童诗白先生诞辰100周年纪念会暨“天立-童诗白中国自动化教育奖”启动仪式
  8. Drool规则引擎详解(一)
  9. 3dmax无法显示缩略图 或者 缩略图显示为黑色 -解决方法
  10. 怎样做计算机系统的镜像文件,Windows7 镜像制作过程 图文说明