概述
关联文章
假如刚发布的版本出现了bug,我们就需要解决bug,并且重新发布新的版本,这样会浪费很多的人力物力,有没有一种可以不重新发布App,不需要用户覆盖安装,就可以解决bug。
热修复就是为了解决上方的问题出现的,热修复主要分为三种修复,分别是
- 代码修复
- 资源修复
- 动态链接库的修复(so修复)
我们依次说一下他们的原理
代码修复
代码修复主要有三个方案
- 底层替换方案
- 类加载方案
- Instant Run方案
我们今天主要讲类加载方案
类加载方案
类加载方案基于dex分包,由于应用的功能越来越复杂,代码不断的增大,可能会导致65536限制异常,这说明应用中的方法数超过了65536个,产生这个问题的原因就是DVM Bytecode的限制,DVM指令集方法调用指令invoke-kind索引为16bits,最多能引用65536个方法
为了解决65536限制,从而产生了dex分包方案,dex分包方案主要做的是,在打包的时候把代码分成多个dex,将启动时必须用到的类直接放到主dex中,其他代码放到次dex中,当应用启动时先加载主dex,然后再动态加载次dex,从而缓解了65536限制
在上篇文章Android中的ClassLoader,中讲到DexPathList的findClass方法
public Class<?> findClass(String name, List<Throwable> suppressed) { //注释1
for (Element element : dexElements) {
//注释2
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
Element内部封装了DexFile,DexFile用于加载dex文件,每一个dex文件对应于一个Element,多个Element组成了有序数组dexElements,当我们在查找类时,会在注释1处遍历dexElements数组,注释2处调用Element的findClass查找类,如果在dex找到了就返回该类,如果没有找到就在下一个dex查找
根据上方的流程我们把有bug的key.class类进行修改,然后把修改后的Key.class打包成含dex的补丁包patch.jar,放在dexElements数组的第一个元素,这样会首先找到patch.jar的key.class来替换有bug的key.class
类加载方案需要重启App让ClassLoader重新加载类,所以采用此方案的不能即时生效
资源修复
资源修复并没有代码修复这么复杂,基本上就是对AssetManager进行修改,很多热修复参考了instant run的原理,我们直接分析一下instant run原理就行
public static void monkeyPatchExistingResources(@Nullable Context context,
@Nullable String externalResourceFile,
@Nullable Collection<Activity> activities) {
if (externalResourceFile == null) {
return;
}
try {
//利用反射创建一个新的AssetManager
AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
//利用反射获取addAssetPath方法
Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
mAddAssetPath.setAccessible(true);
//利用反射调用addAssetPath方法加载外部的资源(SD卡)
if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
mEnsureStringBlocks.setAccessible(true);
mEnsureStringBlocks.invoke(newAssetManager);
if (activities != null) {
//遍历activities
for (Activity activity : activities) {
//拿到Activity的Resources
Resources resources = activity.getResources();
try {
//获取Resources的成员变量mAssets
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
//给成员变量mAssets重新赋值为自己创建的newAssetManager
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);
}
//获取activity的theme
Resources.Theme theme = activity.getTheme();
try {
try {
//反射得到Resources.Theme的mAssets变量
Field ma = Resources.Theme.class.getDeclaredField("mAssets");
ma.setAccessible(true);
//将Resources.Theme的mAssets替换成newAssetManager
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");
mtm.setAccessible(true);
mtm.invoke(activity);
Method mCreateTheme = AssetManager.class.getDeclaredMethod("createTheme");
mCreateTheme.setAccessible(true);
Object internalTheme = mCreateTheme.invoke(newAssetManager);
Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");
mTheme.setAccessible(true);
mTheme.set(theme, internalTheme);
} catch (Throwable e) {
Log.e(LOG_TAG, "Failed to update existing theme for activity " + activity,
e);
}
pruneResourceCaches(resources);
}
}
// 根据sdk版本的不同,用不同的方式获取Resources的弱引用集合
Collection<WeakReference<Resources>> references;
if (SDK_INT >= KITKAT) {
// Find the singleton instance of ResourcesManager
Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
mGetInstance.setAccessible(true);
Object resourcesManager = mGetInstance.invoke(null);
try {
Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
@SuppressWarnings("unchecked")
ArrayMap<?, WeakReference<Resources>> arrayMap =
(ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager);
references = arrayMap.values();
} catch (NoSuchFieldException ignore) {
Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
mResourceReferences.setAccessible(true);
//noinspection unchecked
references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager);
}
} else {
Class<?> activityThread = Class.forName("android.app.ActivityThread");
Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
Object thread = getActivityThread(context, activityThread);
@SuppressWarnings("unchecked")
HashMap<?, WeakReference<Resources>> map =
(HashMap<?, WeakReference<Resources>>) fMActiveResources.get(thread);
references = map.values();
}
//将的到的弱引用集合遍历得到Resources,将Resources中的mAssets字段替换为newAssetManager
for (WeakReference<Resources> wr : references) {
Resources resources = wr.get();
if (resources != null) {
// Set the AssetManager of the Resources instance to our brand new one
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);
}
}
可以看出instance run热修复可以简单的总结为俩个步骤
- 创建新的
AssetManager,并通过反射调用addAssetPath方法加载外部资源,这样新建的AssetManager就包含了外部资源 - 将
AssetManager类型的mAsset字段的引用全部替换为新创建的AssetManager
动态链接库的修复(so修复)
so修复有俩种方式可以达到目的
- 加载so方法的替换
- 反射注入so路径
加载so方法的替换
Android平台加载so库主要用到了2个方法
System.load:可以加载自定义路径下的so
System.loadLibaray:用来加载已经安装APK中的so
通过上面俩个方法我们可以想到,如果有补丁so下发,就调用System.load去加载,如果没有补丁下发就用System.loadLibaray去加载,原理比较简单
反射注入so路径
这个需要我们分析一下System.loadLibaray的源码,他会调用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) {
//注释1
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) + "\"");
}
//注释2
String error = nativeLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
//注释3
for (String directory : getLibPaths()) {
//注释4
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
//注释5
String error = nativeLoad(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为null的时候,注释3 遍历getLibPaths方法,这个方法会返回java.library.path选项配置的路径数组,在注释4拼接出so路径并传入注释5处nativeLoad方法
当ClassLoader不为null的时候,在注释2处也调用了nativeLoad方法,不过他的参数是通过注释1处findLibrary方法获取的,我们看下这个方法
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
for (NativeLibraryElement element : nativeLibraryPathElements) {
//注释1
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
这个和上面讲的findClass方法类似,nativeLibraryPathElements中的每一个NativeLibraryElement元素都对应一个so库,在注释1处调用findNativeLibrary,就会返回so的路径,这个就可以根据类加载方案一样,插入nativeLibraryPathElements数组前部,让补丁的so的路径先返回
参考:《Android 进阶解密》