Android热修复核心原理介绍
对网络上热修复方案和原理的文章和三方框架进行了二次整理,让读者对热修复方案和原理有个整体的认知。总的来说热修复不是简单的一项技术,更贴切的说是一种解决方案,不仅涉及到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导致合并失败,应该要另起一个进程做这个事情。
特点总结
优点 | 缺点 |
---|---|
|
|
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;}
}
特点总结
优点 | 缺点 |
---|---|
|
|
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_);
}
特点总结
优点 | 缺点 |
---|---|
|
|
资源替换
原理概述:
- 构建一个新的AssetManager,并通过反射调用addAssertPath,把这个完整的新资源包加入到AssetManager中,这样就得到一个含有所有新资源的AssetManager;
- 找到所有值钱引用到原有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文件加载;
特点总结
优点 | 缺点 |
---|---|
|
|
反射注入
采取类似类修复反射注入方式,把补丁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热修复核心原理介绍相关推荐
- Android 热修复核心原理
目录 Android 热修复核心原理, ClassLoader类加载 ART 和 Dalvik dexopt与dexaot ClassLoader介绍 双亲委托机制 热修复 在线源码 Android ...
- Android 获取ROOT权限原理介绍和签名验证原理及反编译学习
Root 的介绍 1. Root 的目的 可以让我们拥有掌控手机系统的权限,比如删除一些system/app下面的无用软件,更换开关机铃声和动画,拦截状态栏弹出的广告等. 2. ...
- Android Animation学习(一) Property Animation原理介绍和API简介
Android Animation学习(一) Property Animation介绍 Android Animation Android framework提供了两种动画系统: property a ...
- Android热修复升级探索——代码修复冷启动方案
前言 前面一篇文档, 我们提到热部署修复方案有诸多特点(有关热部署修复方案实现, Android热修复升级探索--追寻极致的代码热替换).其根本原理是基于native层方法的替换, 所以当类结构变化时 ...
- 【Android架构师java原理详解】二;反射原理及动态代理模式
前言: 本篇为Android架构师java原理专题二:反射原理及动态代理模式 大公司面试都要求我们有扎实的Java语言基础.而很多Android开发朋友这一块并不是很熟练,甚至半路初级底子很薄,这给我 ...
- Android热修复升级探索——SO库修复方案
摘要: 通常情况下,大多数人希望android下热补丁方案能够做到补丁的全方位修复,包括类修复/资源修复/so库的修复. 这里主要介绍热补丁之so库修复思路. 一.前言 通常情况下,大多数人希望and ...
- Android热修复升级探索——SO库修复方案 1
一.前言 通常情况下,大多数人希望android下热补丁方案能够做到补丁的全方位修复,包括类修复/资源修复/so库的修复. 这里主要介绍热补丁之so库修复思路. 二.so库加载原理 Java Api提 ...
- Android 热修复之DexPatch 介绍
简介:Android 热修复之DexPatch 介绍 1. 方案介绍 为了解决Native模块上线后的问题,mPaas[1] 提供了热修复功能,实现不发布客户端apk场景下的热修复.目前Android ...
- android长截屏代码,android长截屏原理及实现代码
android长截屏原理及实现代码 发布时间:2020-08-31 06:55:16 来源:脚本之家 阅读:158 作者:Android笔记 小米系统自带的长截屏应该很多人都用过,效果不错.当长截屏时 ...
- Android 获取ROOT权限原理解析
一. 概述 本文介绍了android中获取root权限的方法以及原理,让大家对android玩家中常说的"越狱"有一个更深层次的认识. 二. Root的介绍 1. Ro ...
最新文章
- 深度学习经典教程:深度学习+动手学深度学习
- WR:Tetrasphaera PAO 代谢中的储能物质与微生物多样性及除磷效能之间的关系
- 前端笔记(7)css属性书写顺序,布局流程,chrome调试工具
- Kafka使用入门教程
- db2有主键时默认hash分区_不允许设置db2主键问题的解决
- html单选框+点击取消选中,【前端JS】radio 可单选可点击取消选中
- mvc做网站怎么在mvc中直接访问.html网页 [问题点数:20分]
- eclipse 打包 apk 文件
- TensorFlow基础篇(三)——tf.nn.softmax_cross_entropy_with_logits
- NS2仿真过程中需要的语言及基本组件
- python入门--斐波那契数列
- Java开发自学教程!java从入门到精通txt下载
- HBuilderX App开发环境搭建
- 郑厂长系列故事——排兵布阵 状态压缩DP
- 最近非常火的电子木鱼流量主小程序源码
- 手机QQ浏览器的HTML管理器,手机qq浏览器中文件管理器有哪些功能
- 如何使用ContentProvider打造自己的本地图片库
- 设计模式读书笔记汇总
- revit二次开发——如何选取元素(revit2016)
- 选择适合的Node js授权认证策略
热门文章
- 13我无所不能——无线网络里面的秘密
- OA发票管理 发票验真 费用报销流程对接
- 焊接工时简便计算工具_2020年新版机械加工工时费用计算(17页)-原创力文档...
- 视觉培训4 完成手写识别项目
- 微信小程序 实现城市名称拼音搜索框 汉字/拼音(城市区县定位模块)
- 【登陆设计】-【技术上】你会做WEB上的用户登录功能吗?
- 童诗白先生诞辰100周年纪念会暨“天立-童诗白中国自动化教育奖”启动仪式
- Drool规则引擎详解(一)
- 3dmax无法显示缩略图 或者 缩略图显示为黑色 -解决方法
- 怎样做计算机系统的镜像文件,Windows7 镜像制作过程 图文说明