Android热修复基本原理

热修复基本原理

       在 Android 应用开发中,热修复技术被越来越多的开发者所使用,也出现了很多热修复框架, 比如 AndFix、Tinker、Dexposed 和 Nuwa 等。如果只是会这些热修复框架的使用意义并不大 ,我们还需要了解它们的原理 ,这样不管热修复框架如何变化,只要基本原理不变,我们就可以很快地掌握它们。这一章不会对某些热修复框架源码进行解析,而是讲解热修复框架的通用原理。建议阅读本文之前最好先阅读“解析ClassLoader这篇文章的内容

    1. 热修复的产生

       在开发过程中我们有可能遇到如下的情况。

       1)刚发布的版本出现了严重的 Bug ,这就需要去解决 Bug 、测试并打包在各个应用市场上重新发布,这会耗费大量的人力和物力,代价会比较大。

       2)已经改正了此前发布版本的Bug,如果下一个版本是一个大版本, 那么两个版本的间隔时间会很长,这样要等到下个大版本发布再修复 Bug,此前版本的 Bug 会长期地影响用户。

       3)版本升级率不高,并且需要很长时间来完成版本覆盖,此前版本的 Bug 就会一直影响不升级版本的用户。

       4) 有一个小而重要的功能,需要短时间内完成版本覆盖,比如节日活动。

       为了解决上面的问题,热修复框架就产生了,对于 Bug 的处理,开发人员不要过于依赖热修复框架,在开发的过程中还是要按照标准的流程做好自测,配合测试人员完成测试流程。

     2. 热修复框架的种类和对比

       热修复框架的种类繁多 ,按照公司团队划分主要有如表所示的几种。

表1 按照公司团队划分的热修复框架
类别 成员
阿里系 AndFix、Dexposed、阿里百川、Sophix
腾讯系 微信的Tinker、QQ空间的超级补丁、手机QQ的QFix
知名公司

美团的Robust、饿了么的Amigo、美丽说蘑菇街的Aceso

其他 RocooFix、Nuwa、AnoleFix
       虽然热修复 框架很多 ,但热修复 框架的核心技术主 要有 三类, 分别是代码修复、资源修复和动态链接库修复,其中每个核心技术又有很多 不同的技术方案,每个技术方案又有不同的实现,另外这些热修复框架仍在不断地更新迭代中, 可见热修复框架的技术实现是繁多可变的。作为开发者需要了解这些技术方案的基本原理,这样就可以以不变应万变。
 
       部分热修复框架的对比如表2所示。
表2 部分热修复框架的对比
特性 AndFix Tinker/Amigo QQ空间 Robust/Aceso
即时生效
方法替换
类替换 是 
类结构修改
资源替换
so替换
支持gradle
支持ART
支持Android7.0
       我们可以根据表 2 和具体业务来选择合适的热修复框架,当然表 2 所示的信息很难做到完全准确,因为部分的热修复植架还在不断更新迭代。

        从表 2中也可以发现 Tinker 和 Amigo 拥有的特性最多,是不是就选它们呢?也不尽然,拥有的特性多也意味着框架的代码量庞大,我们需要根据业务来选择最合适的,假设我们只是要用到方法替换,那么使用 Tinker 和 Amigo 显然是大材小用了。另外如果项目需要即时生效,那么使用 Tinker 和 Amigo 是无法满足需求的。对于即时生效, AndFix 、Robust 和 Aceso 都满足这 点,这是因为 AndFix 的代码修复采用了底层替换方案,而 Robust 和 Aceso 的代码修复借鉴了 Instant Run 原理

    3. 资源修复

       很多热修复的框架的资源修复参考了Instant Run 的资源修复的原理,因此我们首先分析Instant Run是什么东西。

       3.1 Instant Run 概述

       Instant Run 是 Android Studio 2.0 以后新增的一个运行机制,能够显著减少开发人员第二次及以后的构建和部署时间。在没有使用 Instant Run 前,我们编译部署应用程序的流程如图 1 所示。

图1  传统编译部署
       从图 可以看出,传统的编译部署需要重新安装 App 和重启 App ,这显然会很耗时, Instant Run 会避免这种 情况,如图 2 所示。
图2  Instant Run 编译部署

       从图 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 的情况很多,比如添加、删除或修改一个字段和方法、添加一个类等。

     3.2 Instant Run 的资源修复

       既然很多热修复的框架资源修 复参考 Instant Run 的资源修 复原理 ,那么我们了解 Instant Run 的资源修 复原理就可 以了。 Instant Run 并不是 Android 的源码,需要通过反编译获取(Instant Run 源码下载链接
       Instant Run 资源修复的核心逻辑在 Monkey Patcher 的  monkeyPatchExistingResources 方法 中,如下所示:
 

 com/android/tools/fd/runtime/MonkeyPatcher.java

	public static void monkeyPatchExistingResources(Context context,
			String externalResourceFile, Collection<Activity> 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<WeakReference<Resources>> 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<Resources>> 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<Resources>> map = (HashMap) fMActiveResources
						.get(thread);

				references = map.values();
			}
            //遍历并得到弱引用集合中的 Resources ,将 Resources mAssets 字段引用替换成新的 AssetManager
			for (WeakReference<Resources> 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);
		}
	}
       在注释1处创建一 个新的 AssetManager ,在注释2 和注释3 处通过反射调用 addAssetPath 方法加载外部( SD 卡)的资源。在注释4 处遍历 Activity  列表,得到每个 Activity 的 Resources ,在注释5 处通过反射得到 Resources 的  AssetManager 类型的 rnAssets 字段 ,并在注释6处改写 m Assets 字段的引用为新的 AssetManager 。采用同样的方式,在注释7处将 Resources. Theme 的 m Assets 字段 的引用替换为新创建的 AssetManager 。 紧接着 根据 SDK 版本的不同,用不同的方式得到 Resources 的弱引用集合,再遍历这个弱引用集合, 将弱引用集合中的 Resources 的 m Assets 字段引用都替换成新创建的 AssetManager 。
       可以看出 Instant Run 中的资源热修复可以简单地总结为两个步骤:
       1) 创建新的 AssetManager ,通过反射调用 addAssetPath 方法加载外部的资源,这样 新创建的 AssetManager 就含有了外部资源。
 
       2)将 AssetManager 类型的 rnAssets 字段的引用全部替换为新创建的 AssetManager。
 
 

    4. 代码修复

       代码修复主要有3个方案,分别是底层替换、类加载和Instant Run方案。

     4.1 类加载方案

       类加载方案基于Dex分包方案, 什么是 Dex 分包方案呢?这个得先从 65536 限制和 LinearAlloc 限制说起。
 
       1. 65536 限制
       
        随着应用功能越来越复杂,代码量不断地增大,引入的库也越来越多,可能会在编译 时提示如下异常:
 
com.android.dex.DexindexOverflowException: method ID not in[ O, Oxffff]: 65536
       这说明应用中引用的方法数超过了 最大 65536 个。 产生这一 问题的原因就是系统的 65536 限制, 65536 限制的 主要原 因是 DVM Bytecode 的限制, DVM 指令集的方法调用指令  invoke-kind 索引为 1 6bits ,最多 能引用 65536 个方法。
 
       2. LinearAlloc 限制
 
       在安装应用时可能会提示 INSTALL _FAILED_ DEXOPT ,产生的原因就是 LinearAlloc 限制, DVM 中的 LinearAlloc 是一 个固定的缓存区, 方法数超出了缓存区的大小时会报错。
 
 
       为了解决 65536 限制和 LinearAlloc 限制,从而产生了 Dex 分包方案。 Dex 分包方案主 要做的是在打包时将应用代码分成多个 Dex ,将应用启动时必须用到的类和这些类的直接 引用类放到主 Dex 中,其他代码放到次 Dex 中。当应用启动时先加载主 Dex ,等到应用启动后再动态地加载次 Dex,从而缓解了主  Dex 的  65536 限制和 LinearAlloc 限制。
 
       Dex 分包方案主要有两种,分别是 Google 官方方案、 Dex 自动拆包和动态加载方案。 因为 Dex 分包方案不是本文的重 点,这里就不再过多 的介绍,接着来看类加载方案。  在 ClassLoader 的加载过程,其中一个环节就是调用 DexPathList 的  findClass 的方法,代码如下所示:
 
libcore/dalvik/src/main/java/dalvik/system/DexPathlist.java
 
    public Class<?> findClass(String name, List<Throwable> 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;
    }
       Element 内部封装了 DexFile,  DexFile 用于加载  dex 文 件,因此每个 dex 文件对应一个  Element 。多个 Element 组成了有序的 Element 数组 dexElements 。当要查找类时,会在注释1处 遍历 E lement 数组 d exE lements (相当于遍历 dex 文件数组),注释2 处调用 Element 的  findClass 方法 其方法内部会调 DexFile 的  loadClassBinaryName 方法 查找类。如果在 Element 中( dex 文件)找到了该类就返回,如果没有找到就接着在下一个 Element 中进行 查找。
 
       根据 上面的 查找流程 ,我们将有  Bug 的类  Key .class 进行修改,再将 Key.class 打包 补丁包 Patch.dex  ,放在 Element 数组 dexElements 的第一 个元素,这样 会首先 找到 Patch.dex 中的 Key.class 去替 换之前存在 Bug 的  Key.class ,排在数组后面的 dex 文件 中存在 Bug 的  Key. class 根据 ClassLoader 的双 亲委 托模式就不 会被 加载,这就是类 加载方案,如图3所示。
 
 
图3  类加载方案
       类加载方案 需要重启 App 后让 C lassLoader 重新加载新的类 ,为什么需 要重启呢?这 是因为类是无法被卸载的,要想重新加载新的类就需要重启 App ,因此采用类加载方案的热修复框架是不能即时生效的。虽然很多热修复框架采用了类加载方案,但具体的实现细节和步骤还是有一些区别的,比如 QQ 空间的超级补丁和 Nuwa 是按照上面说的将补丁包放在 Element 数组的第一 个元 素得到 优先加载 。微信 Tinker 将新旧 APK 做了 di ff ,得到 patch.dex ,再将 patch.dex 与手机中 APK 的  classes.dex 合并,生成新 classes.dex ,然后 在运行 时通过反射将 classes.dex 放在E lement 数组的第一个元素。饿了么的 A migo 则是将补丁包中 每个 dex 对 应的 E lement 取出 来,之后组成新的 E lement 数组,在运行时 通过反射 用新的 Element 数组替换掉现有的 Element 数组。
 
       采用类加载方案的主 要是以腾讯系为主 ,包括微信的 Tinker 、 QQ 空间的超级补丁、手 QQ 的 QFix 、饿了 么的 A migo 和  Nuwa 等。
 
 
      4.2 底层替换方案
 
       与类加载方案不同的是,底层替换方案不会再次加载新类,而是直接在 Nat ive 层 修改原有类,由于在原有类进行修改限制 比较 多,且 不能增减原有类的方法和字段,如果我 们增加了方法数,那 么方法索 引数也会 增加,这样访问方法时会无法通过索引找到正确的方法,同样的字段也是类似的情况 。底层替 换方案和反射的原理有些关联,就拿方法替换来说,方法反射我们可以调用 java .l ang.Class.getDeclaredMethod ,  假设我们要反射 Test 的  test 方法, 会调 用如下所示的代码:
 
Test.class.getDeclaredMethod("test").invoke(Test.class.newInstance());

       Android 8.1.0 的 invoke 方法,代码如下所示:

libcore/ojluni/src/main/java/java/lang/reflect/Method.java
 
    @FastNative
    public native Object invoke(Object obj, Object... args)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
       invoke 方法 是一个  native 方法 ,对应  Jni 层的代码为:
 
art/runtime/native/java_lang_reflect_Method.cc
 
static jobject Method_invoke(JNIEnv* env, jobject javaMethod, jobject javaReceiver,
                             jobject javaArgs) {
  ScopedFastNativeObjectAccess soa(env);
  return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs);
}

       在Method_invoke 函数中又调用了 InvokeMethod 函数,代码如下所示:

art/runtime/reflection.cc
 
jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
                     jobject javaReceiver, jobject javaArgs, size_t num_frames) {
  ...

  ObjPtr<mirror::Executable> executable = soa.Decode<mirror::Executable>(javaMethod);
  const bool accessible = executable->IsAccessible();
  ArtMethod* m = executable->GetArtMethod(); // ... 1

  ...
  }

       在注释1处获取传入的javaMethod(Test 的 test 方法)在ART 虚拟机中对应的一个ArtMethod指针,ArtMethod 类中包含了 Java 方法的信息,包括执行入口、访问权限、所属类和代码执行地址等,ArtMethod 结构如下所示:

art/runtime/art_method.h
 
 
class ArtMethod FINAL {
 ...
 protected:
  GcRoot<mirror::Class> declaring_class_;
  std::atomic<std::uint32_t> 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。

      4.3 Instant Run 方案

        除了资 原修复,代码修复同样也可以借鉴 Instant Run 的原理,可以说 Instant Run 的出 现推动了热修复框架的发展。 Instant Run 第一 次构建 APK 时,在每一个类中注入了一个$change的成员变量,它实现了IncrementalChange接口,并在每一个方法中,插入了一段类似的逻辑。
 
IncrementalChange localIncrementalChange = $change; // ... 1
if (localIncrementalChange != null) { // ... 2
localIncrementalChange.access$dispatch(
"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
... });
return;
}
       其中注释1 处是一 个成员变量 localIncrementalChange ,它的值为 $change,  $change 实 现了 Incremental Change 这个抽象接口。当我们点击 InstantRun 时,如果方法没有变化则 $c hange 为  null ,就直接return 不做任何处理。如果方法有变化,就生成替换类,这里我 们假设 MainActivity 的  on Create 方法 做了修改,就会生成替换类 MainActivity$override  ,这个类实现了 Incremental Change 抽象 接口,同时也会生成 一个 A ppPatche sL oaderlmpl 类,这个类的  getPatchedClasses 方法会返回被修改的类的 表(里面包含了 Ma inActivity ), 根据列表会将 MainAc tivity 的  $c han ge 设置为 Ma inActivity $override   ,因此满足了注释2的条件,会 执行 MainActivity $override 的  ac ce ss$ dispatch 方法,在 access$di spatch 方法中会根据参数 "onCreate.(Landroid/os/Bundle;)V" 执行  Ma inActi vity$override  的 on Create 方法 ,从而实现了 onCreate 方法 的修改。 借鉴 Instant Run 的原理的热修复框架有 Robust 和  Ace so 。
 
 

    5. 动态链接库的修复

       Android 平台的动态链接库主要指的是 so 库,为了更好地理解,本文 动态链接库简称为 so。  热修 复框架的 so 的修复主要是更新 so ,换句话说就是重新加载 so ,因此  so 的修复的基本原 理就是加载 so。

     5.1 System 的 load 和 loadLibrary 方法

       加载so主要用到了System类的load和loadLibrary方法,代码如下所示:
 
libcore/ojluni/src/main/java/java/lang/System.java
 
    @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
    }
       System 的  load 方法 传入的参数是 so 在磁盘的完整路径,用于加载指定路径的 so。  System 的  loadLibrary 方法传入的参数是 so 的名称,用于加载 App 安装后自动从 apk 包中复制到 / data/data/packagename/lib 下的 so 。目前 so 的修复都是基于这两个方法,这里分别对这两个方法进行讲解。
 
 
       1. System 的 load 方法
 
       注释1 处的 R untim e.getRu ntime() 会 得到当前  Java 应用程序的运行环境 Runtime, Runtime 的  load0 方法 如下所示:
 
libcore/ojluni/src/main/java/java/lang/Runtime.java
 
    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);
        }
    }
       在注释1 处调用了 do Load 方法,并将加载该 类的类 加载器作为参 数传入进去,doLoad方法的代码如下所示:
 
libcore/ojluni/src/main/java/java/lang/Runtime.java
 
    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);
        }
    }
       do Load 方法会调用   native 方法  nativeLoad ,关于  native Load 方法我们后面再分析。
 
 
        2. System 的 loadLibrary 方法
  
       System 的 loadLibrary 方法会调用Runtime 的 loadLibrary0方法,代码如下所示:
 
libcore/ojluni/src/main/java/java/lang/Runtime.java
       
    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<String> candidates = new ArrayList<String>();
        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);
    }
       loadLibrary0 方法 分为两个部分, 一 个是传入的 ClassLoader  不为 null 的部分,另一个是 ClassLoader 为  null 的部分,我们先来看  ClassLoader 为  null 的部分。在注释 3  处遍历 getLibPaths 方法,这个方法会返回 java.library .path 选项配置 的路径数组。 在 注释4处 拼接 so 路径并传入注释5 处调用的 do Load 方也中。  ClassLoader 不为 null 时,在注释2处同样调用了 do Load 方法,其中第一 个参数是通过注释1 处  ClassLoader 的  findLibrary 方法来得到的, findLibrary 方法在 ClassLoader 的实 现类 BaseDexClassLoader 中实现,代码如下所示:
 
libcore/dalvik/src/main/java/dalvik/system/BaseDexClassloader.java
    @Override
    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }
       在findLibrary 方法中调用了 DexPathList 的 findLibrary 方法,代码如下所示:
 
libcore/dalvik/src/main/java/dalvik/system/DexPathlist.java
    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 节讲到的 DexPathList 的  findClass 方法 类似,在 Nati veLibrary Element 数组中的每一个  NativeLibraryElement 对应一个  so 库,在注释1 处调用 N ativeLibrary Element 的  findNativeLibrary 方法就可以返回 so 的路径。
   

       上面的代码结合 3.1 节的类加载方案,就可以得到 so 修复的一种方案,就是将 so 补丁插入到 NativeLibraryElement 数组的前部,让 so 补丁的路径先被返回,并调Runtime 的 doLoad 方法进行加载,doLoad 方法中会调用 native 方法 nativeLoad

       其实 System 的 load 方法loadLibrary 方法Java Framework 层最终调用的都是 nativeLoad 方法。

     5.2 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 函数,代码如下所示:

art/runtime/openjdkjvm/OpenjdkJvm.cc
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());
}
       在注释1 处获取当前运行时的 JavaVMExt 类型指针, JavaVMExt 用于代表一 个虚拟机实例,然后调用 JavaVMExt 的 LoadNativeLibrarγ 函数来加载 so。 LoadNativeLibrarγ函数的代码如下所示:
 
art/Runtime/java_vm_ext.cc
 
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<SharedLibrary> 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<jobject> 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<JNI_OnLoadFn>(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 函数的指针并赋值给空指针 symJNI_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 。

       LoadNativeLibrary 函数总结:
       1) 判断 so 是否被加载过,两次 ClassLoader 是否是同 个,避免 so 重复加载。
       2) 打开 so 并得到 so 句柄,如果 so 句柄获取失败,就返回 false 。 创建新的 SharedLibrary, 如果传入 path 对应的 library 为空指针,就将新创建的 SharedLibrary 赋值给 library ,并将 library 存储到 libraries_ 中。
       3) 查找 JNI_ OnLoad 的函数指针,根据不同情况设置  was_ successful 的值,最终返回该  was_ successful。
 
       由以上分析,总结一下so修复的两个方案:
       1)将  so 补丁插入到 Nati veLi braryElement 数组的前部,让 so 补丁的路径先被返回和 加载。
 
       2) 调用 System 的 l oad 方法来接管 so 的加载入口。
 
 
 

猜你喜欢

转载自blog.csdn.net/lixiong0713/article/details/107616098