概述
關聯文章
假如剛發佈的版本出現了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 進階解密》