对网络上热修复方案和原理的文章和三方框架进行了二次整理,让读者对热修复方案和原理有个整体的认知。总的来说热修复不是简单的一项技术,更贴切的说是一种解决方案,不仅涉及到APP端的补丁生成和生效技术,还涉及系统兼容性、开发过程代码管理、补丁管理系统等。除非有足够的人力物力支持,否则在生产环境中引入热修复还是推荐使用阿里、腾讯等大厂的现成方案,不推荐自己造轮子。
热修复框架
阿里系
腾讯系
国内知名公司
其他组织或个人
框架 | 简介 | 官网 | 相关文章推荐 |
---|
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) 核心代码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
c = findClass(name);
}
}
return c;
}
Android类加载机制
ClassLoader#findClass 是抽象方法,Android的BaseDexClassLoader实现了此方法:
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
@Override
protected 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源码的网站:
Class通过DexPathList#findClass(String, List<Throwable>) 来查找:
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
DexFile#loadClassBinaryName :
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 {
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
Object newDexElements = getDexElements(getPathList(dexClassLoader));
Object allDexElements = combineArray(newDexElements, baseDexElements);
Object pathList = getPathList(getPathClassLoader());
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体积影响不大
|
|
Java Hook方案
在打基础包时插桩,在每个方法前插入一段补丁发现和应用代码,实现有补丁时补丁生效,没补丁时执行原来的代码。
核心代码
以美团的Robust为例,打基础包时插桩,在每个方法前插入一段类型为 ChangeQuickRedirect 静态变量的逻辑:
public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {
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 {
@Override
public 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) {
...
DexClassLoader 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) {
...
}
...
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和资源的替换暂时不支持
|
InstantRun方案
native替换方案
每一个Java方法在art中都对应一个ArtMethod,ArtMethod记录了这个Java方法的所有信息,包括访问权限及代码执行地址等。通过env->FromReflectedMethod得到方法对应的ArtMethod的真正开始地址,然后强转为ArtMethod指针,从而对其所有成员进行修改。这样以后调用这个方法时就会直接走到新方法的实现中,达到热修复的效果。
核心代码
示例:AndFix安卓6.0 ArtMethod替换代码片段
void replace_6_0(JNIEnv* env, jobject src, jobject dest) {
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_;
reinterpret_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;
reinterpret_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,无法做到新功能的发布
|
资源替换
原理概述:
- 构建一个新的AssetManager,并通过反射调用addAssertPath,把这个完整的新资源包加入到AssetManager中,这样就得到一个含有所有新资源的AssetManager;
- 找到所有值钱引用到原有AssetManager的地方,通过反射,把引用处替换为AssetManager;
核心代码
public static void monkeyPatchExistingResources(Context context, String externalResourceFile, Collection activities) {
if (externalResourceFile == null) {
return;
}
try {
AssetManager newAssetManager = (AssetManager) AssetManager.class
.getConstructor(new Class[0]).newInstance(new Object[0]);
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]);
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库的时候优先尝试去加载sdk指定目录下补丁的so。若不存在,则再去加载安装apk目录下的so库。
特点总结
优点 | 缺点 |
---|
|
- 需要侵入业务代码,替换掉System默认加载so库的接口
|
反射注入
采取类似类修复反射注入方式,把补丁so库的路径插入到nativeLibraryDirectories数组的最前面,就能够达到加载so库的时候是补丁so库而不是原来so库的目录,从而达到修复。
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
for (NativeLibraryElement element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
特点总结
参考资料
|