概述
关联文章
假如刚发布的版本出现了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 进阶解密》