在 Android 应用开发中,热修复技术被越来越多的开发者所使用,也出现了很多热修复框架, 比如 AndFix、Tinker、Dexposed 和 Nuwa 等。如果只是会这些热修复框架的使用意义并不大 ,我们还需要了解它们的原理 ,这样不管热修复框架如何变化,只要基本原理不变,我们就可以很快地掌握它们。这一章不会对某些热修复框架源码进行解析,而是讲解热修复框架的通用原理。建议阅读本文之前最好先阅读“解析ClassLoader”这篇文章的内容。
在开发过程中我们有可能遇到如下的情况。
1)刚发布的版本出现了严重的 Bug ,这就需要去解决 Bug 、测试并打包在各个应用市场上重新发布,这会耗费大量的人力和物力,代价会比较大。
2)已经改正了此前发布版本的Bug,如果下一个版本是一个大版本, 那么两个版本的间隔时间会很长,这样要等到下个大版本发布再修复 Bug,此前版本的 Bug 会长期地影响用户。
3)版本升级率不高,并且需要很长时间来完成版本覆盖,此前版本的 Bug 就会一直影响不升级版本的用户。
4) 有一个小而重要的功能,需要短时间内完成版本覆盖,比如节日活动。
为了解决上面的问题,热修复框架就产生了,对于 Bug 的处理,开发人员不要过于依赖热修复框架,在开发的过程中还是要按照标准的流程做好自测,配合测试人员完成测试流程。
热修复框架的种类繁多 ,按照公司团队划分主要有如表1 所示的几种。
类别 | 成员 |
阿里系 | AndFix、Dexposed、阿里百川、Sophix |
腾讯系 | 微信的Tinker、QQ空间的超级补丁、手机QQ的QFix |
知名公司 | 美团的Robust、饿了么的Amigo、美丽说蘑菇街的Aceso |
其他 | RocooFix、Nuwa、AnoleFix |
特性 | AndFix | Tinker/Amigo | QQ空间 | Robust/Aceso |
即时生效 | 是 | 否 | 否 | 是 |
方法替换 | 是 | 是 | 是 | 是 |
类替换 | 否 | 是 | 是 | 否 |
类结构修改 | 否 | 是 | 否 | 否 |
资源替换 | 否 | 是 | 是 | 否 |
so替换 | 否 | 是 | 否 | 否 |
支持gradle | 否 | 是 | 否 | 否 |
支持ART | 是 | 是 | 是 | 是 |
支持Android7.0 | 是 | 是 | 是 | 是 |
从表 2中也可以发现 Tinker 和 Amigo 拥有的特性最多,是不是就选它们呢?也不尽然,拥有的特性多也意味着框架的代码量庞大,我们需要根据业务来选择最合适的,假设我们只是要用到方法替换,那么使用 Tinker 和 Amigo 显然是大材小用了。另外如果项目需要即时生效,那么使用 Tinker 和 Amigo 是无法满足需求的。对于即时生效, AndFix 、Robust 和 Aceso 都满足这 点,这是因为 AndFix 的代码修复采用了底层替换方案,而 Robust 和 Aceso 的代码修复借鉴了 Instant Run 原理 。
很多热修复的框架的资源修复参考了Instant Run 的资源修复的原理,因此我们首先分析Instant Run是什么东西。
3.1 Instant Run 概述
Instant Run 是 Android Studio 2.0 以后新增的一个运行机制,能够显著减少开发人员第二次及以后的构建和部署时间。在没有使用 Instant Run 前,我们编译部署应用程序的流程如图 1 所示。
从图 2 可以看出 Instant Run 的构建和部署都是基于更改的部分的。 Instant Run有三种方式, Instant Run 会根据代码的情况来决定采用哪种部署方式 ,无论哪种方式都不需要重新安装 App ,这一点就已经提高了不少的效率。
1)Hot swap : Hot Swap 是效率最高的部署方式, 代码的增量改变不需要重启 App ,甚至不需要重启当前的 Activity 。修改一个现有方法中的代码时会采用 Hot Swap。
2)Warm Swap: App 不需重启,但是 Activity 需要重启。修改或删除一个现有的资源文件时会采用 Warm Swap 。
3)Cold Swap: App 需要重启, 但是不需要重新安装。采用 Cold Swap 的情况很多,比如添加、删除或修改一个字段和方法、添加一个类等。
com/android/tools/fd/runtime/MonkeyPatcher.java
public static void monkeyPatchExistingResources(Context context,
String externalResourceFile, Collection activities) {
if (externalResourceFile == null) {
return;
}
try {
// 创建一个新的AssetManager
AssetManager newAssetManager = (AssetManager) AssetManager.class
.getConstructor(new Class[0]).newInstance(new Object[0]); // ... 1
Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
"addAssetPath", new Class[] { String.class }); // ... 2
mAddAssetPath.setAccessible(true);
// 通过反射调用addAssetPath方法加载外部的资源(SD卡资源)
if (((Integer) mAddAssetPath.invoke(newAssetManager,
new Object[] { externalResourceFile })).intValue() == 0) { // ... 3
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(); // ... 4
try {
// 反射得到Resources的AssetManager类型的mAssets字段
Field mAssets = Resources.class
.getDeclaredField("mAssets"); // ... 5
mAssets.setAccessible(true);
// 将mAssets字段的引用替换为新创建的newAssetManager
mAssets.set(resources, newAssetManager); // ... 6
} catch (Throwable ignore) {
...
}
// 得到Activity的Resources.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); // ... 7
} catch (NoSuchFieldException ignore) {
...
}
...
} catch (Throwable e) {
Log.e("InstantRun",
"Failed to update existing theme for activity "
+ activity, e);
}
pruneResourceCaches(resources);
}
}
/**
* 根据SDK版本的不同,用不同的方式得到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, WeakReference> 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, WeakReference> map = (HashMap) fMActiveResources
.get(thread);
references = map.values();
}
//遍历并得到弱引用集合中的 Resources ,将 Resources mAssets 字段引用替换成新的 AssetManager
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) {
...
}
resources.updateConfiguration(resources.getConfiguration(),
resources.getDisplayMetrics());
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
代码修复主要有3个方案,分别是底层替换、类加载和Instant Run方案。
com.android.dex.DexindexOverflowException: method ID not in[ O, Oxffff]: 65536
public Class> findClass(String name, List suppressed) {
for (Element element : dexElements) { // ... 1
Class> clazz = element.findClass(name, definingContext, suppressed); // ... 2
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
Test.class.getDeclaredMethod("test").invoke(Test.class.newInstance());
Android 8.1.0 的 invoke 方法,代码如下所示:
@FastNative
public native Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
static jobject Method_invoke(JNIEnv* env, jobject javaMethod, jobject javaReceiver,
jobject javaArgs) {
ScopedFastNativeObjectAccess soa(env);
return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs);
}
在Method_invoke 函数中又调用了 InvokeMethod 函数,代码如下所示:
jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
jobject javaReceiver, jobject javaArgs, size_t num_frames) {
...
ObjPtr executable = soa.Decode(javaMethod);
const bool accessible = executable->IsAccessible();
ArtMethod* m = executable->GetArtMethod(); // ... 1
...
}
在注释1处获取传入的javaMethod(Test 的 test 方法)在ART 虚拟机中对应的一个ArtMethod指针,ArtMethod 类中包含了 Java 方法的信息,包括执行入口、访问权限、所属类和代码执行地址等,ArtMethod 结构如下所示:
class ArtMethod FINAL {
...
protected:
GcRoot declaring_class_;
std::atomic access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint16_t method_index_;
uint16_t hotness_count_;
struct PtrSizedFields {
mirror::MethodDexCacheType* dex_cache_resolved_methods_; // ... 1
void* data_;
void* entry_point_from_quick_compiled_code_; // ... 2
} ptr_sized_fields_;
...
}
在ArtMethod结构中比较重要的字段是注释1处的 dex_cache_resolved_methods_ 和注释2处的 entry_point_from_quick_compiled_code_ ,它们是方法的执行入口,当我们调用某一个方法时(比如 Test 的 test 方法),就会取得 test 方法的执行入口,通过执行入口就可以跳过去执行 test 方法。替换 ArtMethod 类中的字段或者替换整个 ArtMethod 类, 这就是底层替换方案。 AndFix 采用的是替换 ArtMethod 类中的字段,这样会有兼容问题,因为厂商可能会修改 ArtMethod 类,导致方法替换失败。 Sophix 采用的是替换整个 ArtMethod 类 ,这样不会存在兼容问题。底层替换方案直接替换了方法,可以立即生效不需要重启。采用底层替换方案主要是阿里系为主,包括 AndFix、Dexposed 、阿里百 川、 Sophix。
IncrementalChange localIncrementalChange = $change; // ... 1
if (localIncrementalChange != null) { // ... 2
localIncrementalChange.access$dispatch(
"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
... });
return;
}
@CallerSensitive
public static void load(String filename) {
Runtime.getRuntime().load0(VMStack.getStackClass1(), filename); // ... 1
}
@CallerSensitive
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname); // ... 2
}
synchronized void load0(Class> fromClass, String filename) {
if (!(new File(filename).isAbsolute())) {
throw new UnsatisfiedLinkError(
"Expecting an absolute path of the library: " + filename);
}
if (filename == null) {
throw new NullPointerException("filename == null");
}
String error = doLoad(filename, fromClass.getClassLoader()); // ... 1
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
}
private String doLoad(String name, ClassLoader loader) {
String librarySearchPath = null;
if (loader != null && loader instanceof BaseDexClassLoader) {
BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
librarySearchPath = dexClassLoader.getLdLibraryPath();
}
synchronized (this) {
return nativeLoad(name, loader, librarySearchPath);
}
}
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) {
String filename = loader.findLibrary(libraryName); // ... 1
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); // ... 2
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
String filename = System.mapLibraryName(libraryName);
List candidates = new ArrayList();
String lastError = null;
for (String directory : getLibPaths()) { // ... 3
String candidate = directory + filename; // ... 4
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
String error = doLoad(candidate, loader); // ... 5
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);
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
for (NativeLibraryElement element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName); // ... 1
if (path != null) {
return path;
}
}
return null;
}
上面的代码结合 3.1 节的类加载方案,就可以得到 so 修复的一种方案,就是将 so 补丁插入到 NativeLibraryElement 数组的前部,让 so 补丁的路径先被返回,并调用 Runtime 的 doLoad 方法进行加载,doLoad 方法中会调用 native 方法 nativeLoad 。
其实 System 的 load 方法和 loadLibrary 方法在 Java Framework 层最终调用的都是 nativeLoad 方法。
nativeLoad 方法对应的 JNI 层函数如下所示:
libcore/ojluni/src/main/native/Runtime.c
JNIEXPORT jstring JNICALL
Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename,
jobject javaLoader, jstring javaLibrarySearchPath)
{
return JVM_NativeLoad(env, javaFilename, javaLoader, javaLibrarySearchPath);
}
在 Runtime_nativeLoad 函数中调用了 JVM_NativeLoad 函数,代码如下所示:
JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env,
jstring javaFilename,
jobject javaLoader,
jstring javaLibrarySearchPath) {
// 将so的文件名转化为ScopedUtfChars 类型
ScopedUtfChars filename(env, javaFilename);
if (filename.c_str() == NULL) {
return NULL;
}
std::string error_msg;
{
// 获取当前运行的虚拟机
art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM(); // ... 1
// 虚拟机加载so
bool success = vm->LoadNativeLibrary(env,
filename.c_str(),
javaLoader,
javaLibrarySearchPath,
&error_msg);
if (success) {
return nullptr;
}
}
// Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF.
env->ExceptionClear();
return env->NewStringUTF(error_msg.c_str());
}
bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
const std::string& path,
jobject class_loader,
jstring library_path,
std::string* error_msg) {
error_msg->clear();
SharedLibrary* library;
Thread* self = Thread::Current();
{
// TODO: move the locking (and more of this logic) into Libraries.
MutexLock mu(self, *Locks::jni_libraries_lock_);
library = libraries_->Get(path); // ... 1
}
...
if (library != nullptr) { // ... 2
// Use the allocator pointers for class loader equality to avoid unnecessary weak root decode.
if (library->GetClassLoaderAllocator() != class_loader_allocator) { // ... 3
// The library will be associated with class_loader. The JNI
// spec says we can't load the same library into more than one
// class loader.
StringAppendF(error_msg, "Shared library \"%s\" already opened by "
"ClassLoader %p; can't open in ClassLoader %p",
path.c_str(), library->GetClassLoader(), class_loader);
LOG(WARNING) << error_msg;
return false;
}
VLOG(jni) << "[Shared library \"" << path << "\" already loaded in "
<< " ClassLoader " << class_loader << "]";
if (!library->CheckOnLoadResult()) { // ... 4
StringAppendF(error_msg, "JNI_OnLoad failed on a previous attempt "
"to load \"%s\"", path.c_str());
return false;
}
return true;
}
Locks::mutator_lock_->AssertNotHeld(self);
const char* path_str = path.empty() ? nullptr : path.c_str();
bool needs_native_bridge = false;
// 打开路径path_str的so库,得到so句柄handle
void* handle = android::OpenNativeLibrary(env,
runtime_->GetTargetSdkVersion(),
path_str,
class_loader,
library_path,
&needs_native_bridge,
error_msg); // ... 5
VLOG(jni) << "[Call to dlopen(\"" << path << "\", RTLD_NOW) returned " << handle << "]";
if (handle == nullptr) { // ... 6
VLOG(jni) << "dlopen(\"" << path << "\", RTLD_NOW) failed: " << *error_msg;
return false;
}
if (env->ExceptionCheck() == JNI_TRUE) {
LOG(ERROR) << "Unexpected exception:";
env->ExceptionDescribe();
env->ExceptionClear();
}
// Create a new entry.
// TODO: move the locking (and more of this logic) into Libraries.
bool created_library = false;
{
// Create SharedLibrary ahead of taking the libraries lock to maintain lock ordering.
// 创建ShareLibrary
std::unique_ptr new_library(
new SharedLibrary(env,
self,
path,
handle,
needs_native_bridge,
class_loader,
class_loader_allocator)); // ... 7
MutexLock mu(self, *Locks::jni_libraries_lock_);
library = libraries_->Get(path); // ... 8
if (library == nullptr) { // We won race to get libraries_lock.
library = new_library.release();
libraries_->Put(path, library);
created_library = true;
}
}
if (!created_library) {
LOG(INFO) << "WOW: we lost a race to add shared library: "
<< "\"" << path << "\" ClassLoader=" << class_loader;
return library->CheckOnLoadResult();
}
VLOG(jni) << "[Added shared library \"" << path << "\" for ClassLoader " << class_loader << "]";
bool was_successful = false;
void* sym = library->FindSymbol("JNI_OnLoad", nullptr); // ... 9
if (sym == nullptr) { // ... 10
VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
was_successful = true;
} else {
ScopedLocalRef old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
self->SetClassLoaderOverride(class_loader);
VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]";
typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
JNI_OnLoadFn jni_on_load = reinterpret_cast(sym);
int version = (*jni_on_load)(this, nullptr); // ... 11
if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
// Make sure that sigchain owns SIGSEGV.
EnsureFrontOfChain(SIGSEGV);
}
self->SetClassLoaderOverride(old_class_loader.get());
if (version == JNI_ERR) {
StringAppendF(error_msg, "JNI_ERR returned from JNI_OnLoad in \"%s\"", path.c_str());
} else if (JavaVMExt::IsBadJniVersion(version)) {
StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"%s\": %d",
path.c_str(), version);
} else {
was_successful = true; // ... 12
}
VLOG(jni) << "[Returned " << (was_successful ? "successfully" : "failure")
<< " from JNI_OnLoad in \"" << path << "\"]";
}
library->SetResult(was_successful);
return was_successful;
}
在注释1处根据 so 的名称从 libraries_ 中获取对应的 SharedLibrary 类型指针 library ,如果满足注释2处的条件就说明此前加载过该 so 。在注释3处如果此前加载用的 ClassLoader 和当前传入的 ClassLoader 不相同的话,就会返回 false ,在注释4处判断上次加载 so 的结果, 如果有异就会返回 false,中断 so 加载。如果满足了注释2 、注释3 、注释4处的条件就会返回 true ,不再重复加载 so。
在注释5处根据 so 的路径 path_str 来打开该 so ,并返回得到 so 句柄,在注释6处如果获取 so 句柄失败就会返回 false,中断 so 载。在注释7处新创建 SharedLibrary ,并将 so 句柄作为参数传入进去。在注释8处获取传入 path 对应的 library ,如果 library 为空指针, 就将新创建的 SharedLibrary 赋值给 library ,并将 library 存储到 libraries_中。
在注释 9 处查找 JNI_OnLoad 函数的指针并赋值给空指针 sym,JNI_OnLoad 函数用于 native 方法的动态注册。在注释 10 处如果没有找到 JNI_OnLoad 函数就将 was_successful 赋值为 true,说明已经加载成功,没有找到 JNI_OnLoad 函数也加载 成功,这是因为并不是所有 so 都定义了 JNI_OnLoad 函数,因为 native 方法除了动态注册, 还有静态注册。如果找到了 JNI_OnLoad 函数,就在注释 11 处执行 JNI_OnLoad 函数并将结果赋值给 version ,如果 version 为 JNI_ERR 或者 BadJniVersion ,说明没有执行成功, was_successful 的值仍旧为默认的 false ,否则就将 was_successful 贼值为 true ,最终返回 was_successful 。