在做so动态加载时遇到了后加载的JNI方法失效的问题


背景描述

在进行动态加载3期分享时,使用so文件的动态加载作为示例,发现一个有趣的现象——在某些情况下,通过System.load("foo.so")加载进来的so并没有生效。

先看作为示例的动态加载Demo代码。

声明的native接口

1
public native String say();

C文件中实现方法很简单,返回一个预定义好的字符串Keep Quiet!

jni.c

1
2
3
4
5
6
7
#include "com_leili_season1_jni_activity_JNIActivity.h"

JNIEXPORT jstring JNICALL Java_com_leili_season1_jni_activity_JNIActivity_say
(JNIEnv *env, jobject obj) {

return (*env)->NewStringUTF(env, "Keep Quiet!");
}

初始化时,加载返回Keep Quiet!的so文件

1
2
3
4
5
6
@Override
protected void onCreate(Bundle savedInstanceState) {
System.loadLibrary("Jni"); // 写法A,通过System.loadLibrary()首次加载
System.load(getFilesDir().getParent() + "/lib/libJni.so"); // 写法B,通过System.load()首次加载
... // 以下无关内容省略
}

点击按钮后加载新的so文件,其中修改了say()方法的返回值为Hello World!

1
2
soFilePath = getFilesDir() + "/libJni2.so"; // /data/data/com.leili.season1/lib/libJni.so
System.load(soFilePath);

此时再次调用say()后,根据预期,应该返回的是修改后的Hello World!。然而,如代码中的注释所写,使用写法A进行首次加载的so库,无法通过System.load(soFilePath)被覆盖;使用写法B进行首次加载的so库,却可以通过System.load(soFilePath)被覆盖。


原因探究

第一个想法是,System.loadSystem.loadLibrary内部实现有差异,通过System.loadLibrary加载进来的so库,其方法无法被覆盖。

先来看一下SDK源码中这两个方法的细节,首先是System.load,使用SDK版本为23

System.load 过程

System.java

1
2
3
4
5
6
/**
* See {@link Runtime#load}.
*/
public static void load(String pathName) {
Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader());
}

调用了Runtime中的load方法

Runtime.java

1
2
3
4
5
6
7
8
9
10
11
12
/*
* Loads the given shared library using the given ClassLoader.
*/
void load(String absolutePath, ClassLoader loader) {
if (absolutePath == null) {
throw new NullPointerException("absolutePath == null");
}
String error = doLoad(absolutePath, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
}

跟进 doLoad 方法查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private String doLoad(String name, ClassLoader loader) {
// Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH,
// which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH.

// The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load
// libraries with no dependencies just fine, but an app that has multiple libraries that
// depend on each other needed to load them in most-dependent-first order.

// We added API to Android's dynamic linker so we can update the library path used for
// the currently-running process. We pull the desired path out of the ClassLoader here
// and pass it to nativeLoad so that it can call the private dynamic linker API.

// We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the
// beginning because multiple apks can run in the same process and third party code can
// use its own BaseDexClassLoader.

// We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any
// dlopen(3) calls made from a .so's JNI_OnLoad to work too.

// So, find out what the native library search path is for the ClassLoader in question...
String ldLibraryPath = null;
String dexPath = null;
if (loader == null) {
// We use the given library path for the boot class loader. This is the path
// also used in loadLibraryName if loader is null.
ldLibraryPath = System.getProperty("java.library.path");
} else if (loader instanceof BaseDexClassLoader) {
BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
ldLibraryPath = dexClassLoader.getLdLibraryPath();
}
// nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless
// of how many ClassLoaders are in the system, but dalvik doesn't support synchronized
// internal natives.
synchronized (this) {
return nativeLoad(name, loader, ldLibraryPath);
}
}

// TODO: should be synchronized, but dalvik doesn't support synchronized internal natives.
private static native String nativeLoad(String filename, ClassLoader loader,
String ldLibraryPath);

可以看到最终通过native调用nativeLoad加载了lib文件。

System.loadLibrary 过程

System.loadLibrary 的写法是

1
System.loadLibrary("Jni");

真实加载的so文件是libJni.so,可以猜想内部进行了一个拼接文件名的过程。

System.java

1
2
3
4
5
6
/**
* See {@link Runtime#loadLibrary}.
*/
public static void loadLibrary(String libName) {
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}

Runtime.java中的loadLibrary方法

Runtime.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
* Searches for and loads the given shared library using the given ClassLoader.
*/
void loadLibrary(String libraryName, ClassLoader loader) {
if (loader != null) {
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) + "\"");
}
String error = doLoad(filename, loader);
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 : mLibPaths) {
String candidate = directory + filename;
candidates.add(candidate);

if (IoUtils.canOpenReadOnly(candidate)) {
String error = doLoad(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);
}

注意到这里参数中的libraryName仍然是形如Jni的不包含前缀lib与后缀.so的文件名。真正的文件名补全是在这一步进行的。

1
String filename = loader.findLibrary(libraryName);

BaseDexClassLoader.java

1
2
3
4
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}

DexPathList.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Finds the named native code library on any of the library
* directories pointed at by this instance. This will find the
* one in the earliest listed directory, ignoring any that are not
* readable regular files.
*
* @return the complete path to the library or {@code null} if no
* library was found
*/
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
for (File directory : nativeLibraryDirectories) {
String path = new File(directory, fileName).getPath();
if (IoUtils.canOpenReadOnly(path)) {
return path;
}
}
return null;
}

绕了一圈,又回到System这个类中,看到下面这儿一目了然。

1
2
3
4
5
6
7
8
9
10
11
/**
* Returns the platform specific file name format for the shared library
* named by the argument. On Android, this would turn {@code "MyLibrary"} into
* {@code "libMyLibrary.so"}.
*/
public static String mapLibraryName(String nickname) {
if (nickname == null) {
throw new NullPointerException("nickname == null");
}
return "lib" + nickname + ".so";
}

追踪过了补全文件名的过程,我们回到最初System.loadLibrary的代码中,可以看到与System.load一样,也是调用了同样的doLoad方法!!!

到这里可能会有些困惑了,既然两者殊途同归,为什么在应用时会有差别呢?先别急,既然方法是同样的,会不会是参数有区别?


System.load & System.loadLibrary 加载so库文件

通过断点,分别查看以下两个语句的执行情况,果然发现了一丝区别

1
System.loadLibrary("Jni"); // 写法A,通过System.loadLibrary()首次加载

写法A加载的lib文件,真实路径是/data/app/com.leili.season1-1/lib/x86/libJni.so,这是用户app在安装时的路径(系统app安装目录为/system/app

1
System.load(getFilesDir().getParent() + "/lib/libJni.so"); // 写法B,通过System.load()首次加载

写法B加载的lib文件,真实路径是/data/data/com.leili.season1/lib/libJni.so。与我们的预期一致。

等等,我们通过adb shell看一下/data/data/com.leili.season1/lib/这个目录

1
2
3
4
root@vbox86p:/data/data/com.leili.season1 # ll
drwxrwx--x u0_a174 u0_a174 2016-06-16 19:34 cache
drwx------ u0_a174 u0_a174 2016-06-16 19:34 files
lrwxrwxrwx install install 2016-06-17 10:28 lib -> /data/app/com.leili.season1-1/lib/x86

发现了什么?目录/data/data/com.leili.season1/lib/竟然是指向/data/app/com.leili.season1-1/lib/x86的软链接。天哪!这说明,通过System.load(filePath)加载的so文件,与通过System.loadLibrary(libName)加载的so文件,压根就是同一个!

我的天哪。。。明明是调用同样的方法加载同一个文件,为啥会出现两种截然不同的结果?!!!

最新发现:初次加载时不用System.loadLibrary("Jni"),而用System.load("/data/app/com.leili.season1-2/lib/x86/libJni.so")来直接加载/data/app的库文件时,一样会阻止后续的加载!

到这里被BLOCK住了,HELP~