本文源码摘自 android-6.0.0_r24分支

主要内容为梳理Android调用 System.loadLibrary()加载so库时的调用链

总括

在能找到ClassLoader的情况下调用的顺序为:

  • System.loadLibrary
  • Runtime.getRuntime().loadLibrary
  • 构建本地文件系统和应用的库路径(在安装时,PMS会读取libs目录下的so文件,进行本地释放解压到指定目录)。拼接so的全称,匹配搜索so的全路径并返回
  • Runtime.getRuntime().doLoad():获取从当前加载的Classloader获取LD_LIBRARY_PATH路径,传入底层的nativeLoad
  • 获取虚拟机对象并调用其内部的LoadNativeLibrary方法。
  • 判断目标库是否已加载。如果已加载,判断加载的ClassLoader和本次的ClassLoader是否相同,如果不同,则警告。如果未加载,使用dlopen打开并加载目标库的本进程的虚拟地址空间中,返回handle句柄。如果打开失败(handle为null),使用nativeBridge做二次尝试加载(会有是否支持nativeBridge加载的判断)。
  • 如果两次加载都失败,直接返回。
  • 如果加载成功,开始创建一个共享库对象。因为是多线程,所以在创建完成后需要判断是否有其他线程先于我们加载完成。如果是,则释放本次的资源。否,则使用dlsym获取JNI_OnLoad的地址。
  • 如果目标库不存在JNI_OnLoad,返回false
  • 如果存在,则调用并执行JNI_OnLoad->RegisterNatives->dvmRegisterJNIMethod->dvmUseJNIBridge.也就是说上层调用RegisterNatives,传入一个gMethods方法数组,注册的具体实现由各个VM自己实现。在Dalvik中,会遍历gMethod,将每个方法都存储到DalvikBridge中,也就完成了这个方法的注册。这里的Bridge其实是处理多平台的情况。比如so是面向多个平台的,每个平台都针对so有一套解析,比如参数的处理、栈架构的虚拟机、寄存器架构的虚拟机等,所以每个平台都自行封装一个Bridge,实现必要的方法,从而与上下层链接起来。

System.loadLibrary\load

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

/**
* See {@link Runtime#loadLibrary}.
*/
public static void loadLibrary(String libName) {
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}

先分析loadLibrary.

这里会跟着调用Runtime的loadLibrary方法,传入两个值,一个是当前的so名称,一个是调用的ClassLoader。

  • 这里的so名称是不含”lib”前缀和”.so”后缀的。例如,要加载libtest.so,那么这里的libName应该为test
  • VMStack.getCallingClassLoader()这里其实就是获取可以加载该so的ClassLoader,说白了这里就是双亲委派模式。
1
2
3
4
5
6
7
8
9
10
public final class VMStack {
/**
* Returns the defining class loader of the caller's caller.
*
* @return the requested class loader, or {@code null} if this is the
* bootstrap class loader.
*/
native public static ClassLoader getCallingClassLoader();
.....
}

RunTime

这里先提一下RunTime这个类。它的作用是将Java应用与它们正在运行的环境连接起来。这是个单例类,也就是说在Android中同一时间只能存在一个Runtime,每个应用获得的都是它的实例

1
2
3
4
5
6
7
8
/**
* Allows Java applications to interface with the environment in which they are
* running. Applications can not create an instance of this class, but they can
* get a singleton instance by invoking {@link #getRuntime()}.
*
* @see System
*/
public class Runtime {

下图为它内部的函数结构

RunTime_Detail

它包含的功能其实也就是执行command、库相关的管理、系统资源信息的获取(如堆中可用内存、总共内存、堆最大可拓展到多少内存)等等。说白了就是一个桥接的作用,里面封了一堆方法用于上层快速调用或者获取系统调用、资源情况等。

RunTime.getRuntime().loadLibrary

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
43
44
/*
* Searches for and loads the given shared library using the given ClassLoader.
*/
void loadLibrary(String libraryName, ClassLoader loader) {
if (loader != null) {
// 返回的filename是目标库的全路径。
// 此时的名字已经做好拼接.如/com/example/test/libtest.so
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);
}

接着看loadLibrary。

这里有个分叉,也就是传入的ClassLoader是否为空。

双亲委派模式也就是一层一层的往父类找,让父类先尝试加载,避免出现多个本质相同但加载器不同的字节码。

先看loader != null的情况:

1
String filename = loader.findLibrary(libraryName);

首先,去获取so的文件名,也就是对so名做拼装。上面提到的libtest.so,传入test,返回就是完整的名字。

之所以会有这么一步,是因为在不同平台下,test的拼装结果不一样,所以最好的方式就是上层应用方只需传入名称,前缀跟后缀由各个平台自动补齐。

在Android中,Classloader分两种:

  • 一种是DexClassloader,用于在app的路径下加载jar包或者apk中相关的资源。
  • 一种是PathClassloader,用于加载本地文件系统路径下的资源
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
/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
*
* <p>This class loader requires an application-private, writable directory to
* cache optimized classes. Use {@code Context.getCodeCacheDir()} to create
* such a directory: <pre> {@code
* File dexOutputDir = context.getCodeCacheDir();
* }</pre>
*
* <p><strong>Do not cache optimized classes on external storage.</strong>
* External storage does not provide access controls necessary to protect your
* application from code injection attacks.
*/
public class DexClassLoader extends BaseDexClassLoader {
}



/**
* Provides a simple {@link ClassLoader} implementation that operates on a list
* of files and directories in the local file system, but does not attempt to
* load classes from the network. Android uses this class for its system class
* loader and for its application class loader(s).
*/
public class PathClassLoader extends BaseDexClassLoader {
}

它们都属于BaseDexClassLoader的子类,也就是说双亲委派加载类时都会让BaseDexClassLoader尝试加载。

而我们的findLibrary,就属于BaseDexClassLoader的内部方法。

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

这里的pathList又是什么呢?

其实就是一个用于管理dex、resource、本地库路径的类。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* A pair of lists of entries, associated with a {@code ClassLoader}.
* One of the lists is a dex/resource path &mdash; typically referred
* to as a "class path" &mdash; list, and the other names directories
* containing native code libraries. Class path entries may be any of:
* a {@code .jar} or {@code .zip} file containing an optional
* top-level {@code classes.dex} file as well as arbitrary resources,
* or a plain {@code .dex} file (with no possibility of associated
* resources).
*
* <p>This class also contains methods to use these lists to look up
* classes and resources.</p>
*/
/*package*/ final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String zipSeparator = "!/";

/** class definition context */
private final ClassLoader definingContext;

/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private final Element[] dexElements;

/** List of native library path elements. */
private final Element[] nativeLibraryPathElements;

/** List of application native library directories. */
private final List<File> nativeLibraryDirectories;

/** List of system native library directories. */
private final List<File> systemNativeLibraryDirectories;

/**
* Exceptions thrown during creation of the dexElements list.
*/
private final IOException[] dexElementsSuppressedExceptions;

........
........


/**
* Element of the dex/resource file path
*/
/*package*/ static class Element {
private final File dir;
private final boolean isDirectory;
private final File zip;
private final DexFile dexFile;

private ZipFile zipFile;
private boolean initialized;
....
....
}
}

所以说,如果我们要查找一个so的路径,就需要从dexPathList中查阅。

它的查找逻辑为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 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 (Element element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);

if (path != null) {
return path;
}
}

return null;
}

1、首先,拼接so的全称,也就是上面的System.mapLibraryName

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";
}

2、然后在本地的库路径中查找目标so,如果命中,则返回全路径

这里的nativeLibraryPathElements是在构造函数中初始化的:

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
 public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {

....
....
// Native libraries may exist in both the system and
// application library paths, and we use this search order:
//
// 1. This class loader's library path for application libraries (libraryPath):
// 1.1. Native library directories
// 1.2. Path to libraries in apk-files
// 2. The VM's library path from the system property for system libraries
// also known as java.library.path
//
// This order was reversed prior to Gingerbread; see http://b/2933456.

// 两个路径,一个是当前应用的库路径,一个是系统的库路径。
// add的方式加入list中,当前应用的库路径position先于系统的
this.nativeLibraryDirectories = splitPaths(libraryPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);

this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null,
suppressedExceptions);
......
.....
}

对于so来说,它可能存在于系统的库路径下,亦可能存在于当前应用的库路径下。

所以对于so的搜索查找来说,它的顺序为:

  • 现在当前应用的库路径下查找
  • 如果没有,再到系统的库路径下查找

在我们拼装完当前so的全称后,就会遍历nativeLibraryPathElements这个List,它里面包含的也就是应用的库路径和系统的库路径(应用的库路径position在系统的库路径前面)。

接下来,就返回到了RunTime.getRunTime().loadLibrary()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;

这里的filename也就是查找库后的全路径。

接着就会调用doLoad将这个so加载到内存当中。

LD_LIBRARY_PATH

进行下一步分析之前,需要了解一下LD_LIBRARY_PATH。

这个字段是Linux的环境变量名,主要用于指定查找共享库(动态链接库)时除了默认路径之外的其他路径。

因为Android的应用进程都fork自zygote,这也就意味着所有的应用进程都不能拥有自定义的LD_LIBRARY_PATH。也就是说一个APP的共享库路径不在LD_LIBRARY_PATH上。

对于PathClassloader来说,它本来就是用于查找本地文件系统下的资源和dex,所以它能获取目标库而不需要任何依赖。但对于一个拥有多个相互依赖的库的APP来说,load就必须遵从most-dependent-first的顺序。

所以Android的动态链接器增加了一套API,用于更新当前运行的进程的库路径。它会获取当前ClassLoader加载的库路径,在调用nativeLoad时将这个路径传入。

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
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);
}
}

这里可以看到,如果classloader为空,ldLibraryPath就指定为本地文件库的路径。

在BaseDexClassLoader中,获取ldLibraryPath的方法为:

遍历pathList,拿到当前应用下的库路径(不是系统的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @hide
*/
public String getLdLibraryPath() {
StringBuilder result = new StringBuilder();
for (File directory : pathList.getNativeLibraryDirectories()) {
if (result.length() > 0) {
result.append(':');
}
result.append(directory);
}

return result.toString();
}

nativeLoad

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
static jstring Runtime_nativeLoad(JNIEnv* env, jclass, jstring javaFilename, jobject javaLoader,
jstring javaLdLibraryPathJstr) {
ScopedUtfChars filename(env, javaFilename);
if (filename.c_str() == nullptr) {
return nullptr;
}

SetLdLibraryPath(env, javaLdLibraryPathJstr);

std::string error_msg;
{
JavaVMExt* vm = Runtime::Current()->GetJavaVM();
bool success = vm->LoadNativeLibrary(env, filename.c_str(), javaLoader, &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());
}

class ScopedUtfChars {
public:
ScopedUtfChars(JNIEnv* env, jstring s) : env_(env), string_(s) {
if (s == NULL) {
utf_chars_ = NULL;
jniThrowNullPointerException(env, NULL);
} else {
utf_chars_ = env->GetStringUTFChars(s, NULL);
}
}

~ScopedUtfChars() {
if (utf_chars_) {
env_->ReleaseStringUTFChars(string_, utf_chars_);
}
}

const char* c_str() const {
return utf_chars_;
}

size_t size() const {
return strlen(utf_chars_);
}

const char& operator[](size_t n) const {
return utf_chars_[n];
}

private:
JNIEnv* const env_;
const jstring string_;
const char* utf_chars_;

DISALLOW_COPY_AND_ASSIGN(ScopedUtfChars);
};

这里主要做的内容为将上方传入的libtest.so转为jni下的String类型,设置LdLibraryPath,拿到VM对象,告诉虚拟机加载目标库文件LoadNativeLibrary。

虚拟机加载库的逻辑为:

  • 判断是否已经加载过该so,如果已经加载,直接返回
  • 如果没有,则使用dlopen打开目标库。只要目标库的引用计数不为0,那么就不会调用dlclose
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
bool JavaVMExt::LoadNativeLibrary(JNIEnv* env, const std::string& path, jobject class_loader,
std::string* error_msg) {
error_msg->clear();

// See if we've already loaded this library. If we have, and the class loader
// matches, return successfully without doing anything.
// TODO: for better results we should canonicalize the pathname (or even compare
// inodes). This implementation is fine if everybody is using System.loadLibrary.
SharedLibrary* library;
Thread* self = Thread::Current();
{
// TODO: move the locking (and more of this logic) into Libraries.
MutexLock mu(self, *Locks::jni_libraries_lock_);

// 这里的libraries_是一个LibraryList类,里面记录的是所有被Crazy Linker加载过的动态链接库
// 在这里面查一查,如果有,那就说明已经被加载了。
library = libraries_->Get(path);
}

// LibraryList中存在目标库
if (library != nullptr) {
// 首先判断是否是由同一个Classloader加载的
// 如果不是,返回异常。因为同一个so库只能被同一个classloader加载
if (env->IsSameObject(library->GetClassLoader(), class_loader) == JNI_FALSE) {
// 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;
}

// 已经加载了这个so库,判断以前是否加载成功
// 以前加载成功,返回true
// 以前加载失败,返回false
VLOG(jni) << "[Shared library \"" << path << "\" already loaded in "
<< " ClassLoader " << class_loader << "]";
if (!library->CheckOnLoadResult()) {
StringAppendF(error_msg, "JNI_OnLoad failed on a previous attempt "
"to load \"%s\"", path.c_str());
return false;
}
return true;
}

// Open the shared library. Because we're using a full path, the system
// doesn't have to search through LD_LIBRARY_PATH. (It may do so to
// resolve this library's dependencies though.)

// Failures here are expected when java.library.path has several entries
// and we have to hunt for the lib.

// Below we dlopen but there is no paired dlclose, this would be necessary if we supported
// class unloading. Libraries will only be unloaded when the reference count (incremented by
// dlopen) becomes zero from dlclose.

Locks::mutator_lock_->AssertNotHeld(self);

// 拿到so的全路径
const char* path_str = path.empty() ? nullptr : path.c_str();

// 使用dlopen打开目标库
// dlopen的作用是将目标库加载进调用进程的虚拟地址空间并增加该库的打开引用计数
// 它在成功时会返回一个句柄,后续对它的函数调用就可以直接使用该句柄了。
// 如果发生错误,比如无法找到库,那么会返回NULL。
// 如果目标库还依赖其他共享库,那么dlopen会自动加载那些库,如果有必要的话,这一过程会递归进行。
// 加载进来的库也就呗称为这个库的依赖树。
// 可以多次调用dlopen,但是库加载进内存的操作只会发生一次,所有的调用都返回相同的句柄值。
// 但dloepn会为每次打开都维护一个引用计数,每次打开都会增加,关闭就会减少。
// 当引用计数等于0时,就从内存中删掉这个库。
void* handle = dlopen(path_str, RTLD_NOW);
bool needs_native_bridge = false;
if (handle == nullptr) {

// 如果handle为空,也就意味着加载失败可
// 这时候再判断该路径是否支持native bridge,
// 如果支持,那么就再次尝试加载。也就是到java.library.path
if (android::NativeBridgeIsSupported(path_str)) {
handle = android::NativeBridgeLoadLibrary(path_str, RTLD_NOW);
needs_native_bridge = true;
}
}

VLOG(jni) << "[Call to dlopen(\"" << path << "\", RTLD_NOW) returned " << handle << "]";

// 如果经过上面两次加载,仍然为null,抛出异常
if (handle == nullptr) {
*error_msg = dlerror();
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.
// 创建一个共享文件库Entry
// 因为是多线程的,所以在这里需要判断我们创建的时候是否已经有其他线程先于我们创建了同一个共享文件库。
// 如果有,那么这里的加载其实就是不必要的了(因为其他线程已经加载了这个库)。
// 所以直接释放当前的资源即可,并调用CheckOnLoadResult
std::unique_ptr<SharedLibrary> new_library(
new SharedLibrary(env, self, path, handle, class_loader));
MutexLock mu(self, *Locks::jni_libraries_lock_);
library = libraries_->Get(path);
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 << "]";

// 上面有两次加载so,所以这里需要通过标志位判断到底是用哪种方式加载的
// 对于正常加载的流程来说,使用dlsym获取JNI_OnLoad的地址
bool was_successful = false;
void* sym;
if (needs_native_bridge) {
library->SetNeedsNativeBridge();
sym = library->FindSymbolWithNativeBridge("JNI_OnLoad", nullptr);
} else {
sym = dlsym(handle, "JNI_OnLoad");
}


if (sym == nullptr) {
VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
was_successful = true;
} else {
// Call JNI_OnLoad. We have to override the current class
// loader, which will always be "null" since the stuff at the
// top of the stack is around Runtime.loadLibrary(). (See
// the comments in the JNI FindClass function.)
ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
self->SetClassLoaderOverride(class_loader);

// 获取JNI的版本信息
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);

if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
fault_manager.EnsureArtActionInFrontOfSignalChain();
}

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 (IsBadJniVersion(version)) {
StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"%s\": %d",
path.c_str(), version);
// It's unwise to call dlclose() here, but we can mark it
// as bad and ensure that future load attempts will fail.
// We don't know how far JNI_OnLoad got, so there could
// be some partially-initialized stuff accessible through
// newly-registered native method calls. We could try to
// unregister them, but that doesn't seem worthwhile.
} else {
was_successful = true;
}
VLOG(jni) << "[Returned " << (was_successful ? "successfully" : "failure")
<< " from JNI_OnLoad in \"" << path << "\"]";
}

library->SetResult(was_successful);
return was_successful;
}

dlopen(const char *libfilename, int flags)

dlopen的作用是将目标库加载进调用进程的虚拟地址空间并增加该库的打开引用计数,它在成功时会返回一个句柄,后续对它的函数调用就可以直接使用该句柄了。

如果发生错误,比如无法找到库,那么会返回NULL。

如果目标库还依赖其他共享库,那么dlopen会自动加载那些库,如果有必要的话,这一过程会递归进行。加载进来的库也就被称为这个库的依赖树。

可以多次调用dlopen,但是库加载进内存的操作只会发生一次,所有的调用都返回相同的句柄值。但dloepn会为每次打开都维护一个引用计数,每次打开都会增加,关闭就会减少。当引用计数等于0时,就从内存中删掉这个库(调用dlclose)。

这里的源码太长(位置在bionic/linker/dlfcn.cpp),所以这里总结一下内容:

触发dlopen时会做各种判断,包括flag是否正确、targetSDK是否匹配、so是否已加载等等。

然后根据传入的flag、name等参数构建一个soinfo对象(也就是ELF文件)。配置完基本信息后就加入到LoadTaskList中。

对于加载失败的so,例如现在需要连续加载test1和test2,但是1成功了,2失败了,也会卸载掉所有本次加载成功的so。

上述操作返回后(一个soinfo),判断so的构造方法是否已经被调用了,如果没有,则调用构造方法。递归加载所有子类的构造函数,然后再初始化本身。

1
2
3
4
5
6
7
8
9
// bionic/linker/linker.cpp 中 void soinfo::call_constructors()

get_children().for_each([] (soinfo* si) {
si->call_constructors();
});

// DT_INIT should be called before DT_INIT_ARRAY if both are present.
call_function("DT_INIT", init_func_); // 调用init_func_这个函数
call_array("DT_INIT_ARRAY", init_array_, init_array_count_, false);

然后设置其引用计数。也就是说,调用一次,引用计数就会加一。

dlsym(void handle, char symbol)

它会在handle指向的库以及该库的依赖树中搜索名为symbol的符号(函数或变量)。

如果找到了symbol,那么dlsym会返回其地址,否则返回NULL

dlclose(void *handle)

它会减少handle所引用的库的打开引用计数,如果这个引用计数变成了0并且其他库已经不再需要用到该库中的内容了,那么就会卸载这个库。对于这个库的依赖树而言,会递归执行。

JNI_OnLoad相关

以下面的JNI_OnLoad的示例举例,位置在device/sample/frameworks/PlatformLibrary/jni/PlatformLibrary.cpp。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* This is called by the VM when the shared library is first loaded.
*/
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint result = -1;

if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
ALOGE("ERROR: GetEnv failed\n");
goto bail;
}
assert(env != NULL);

if (registerMethods(env) != 0) {
ALOGE("ERROR: PlatformLibrary native registration failed\n");
goto bail;
}

/* success -- return valid version number */
result = JNI_VERSION_1_4;

bail:
return result;
}

调用registerMethods显式注册本地方法,首先查找目标class,找到之后调用RegisterNatives根据传入的gmethods对象注册本地所有方法。gMethods对象包含java层的方法签名、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
/*
* Explicitly register all methods for our class.
*
* While we're at it, cache some class references and method/field IDs.
*
* Returns 0 on success.
*/
static int registerMethods(JNIEnv* env) {
static const char* const kClassName =
"com/example/android/platform_library/PlatformLibrary";
jclass clazz;

/* look up the class */
clazz = env->FindClass(kClassName);
if (clazz == NULL) {
ALOGE("Can't find class %s\n", kClassName);
return -1;
}

/* register all the methods */
if (env->RegisterNatives(clazz, gMethods,
sizeof(gMethods) / sizeof(gMethods[0])) != JNI_OK)
{
ALOGE("Failed registering methods for %s\n", kClassName);
return -1;
}

/* fill out the rest of the ID cache */
return cacheIds(env, clazz);
}

接着就调用虚拟机中的RegisterNatives方法。遍历gMethods,依次注册。这部分代码摘自网络,Android源码里没找到,没有定位到Dalvik虚拟机那一层

1
2
3
4
5
6
7
8
9
static bool dvmRegisterJNIMethod(ClassObject* clazz, const char* methodName,
const char* signature, void* fnPtr) {
Method* method = dvmFindDirectMethodByDescriptor(clazz, methodName, signature);
if (method == NULL) {
method = dvmFindVirtualMethodByDescriptor(clazz, methodName, signature);
}

dvmUseJNIBridge(method, fnPtr);
}

通过函数名称和签名在指定的类中找到对应的Method,然后调用dvmUseJNIBridge为这个Method设置一个Bridge。

1
2
3
4
5
6
7
void dvmUseJNIBridge(Method* method, void* func)
{
DalvikBridgeFunc bridge = shouldTrace(method)
? dvmTraceCallJNIMethod
: dvmSelectJNIBridge(method);
dvmSetNativeFunc(method, bridge, func);
}

这里先获取一个DalvikBridgeFunc,然后再去调dvmSetNativeFunc。

1
2
3
4
5
6
7
8
9
10
11
12
13
void dvmSetNativeFunc(Method* method, DalvikBridgeFunc func,
const u2* insns)
{
ClassObject* clazz = method->clazz;

if (insns != NULL) {
method->insns = insns;
android_atomic_release_store((int32_t) func,
(void*) &method->nativeFunc);
} else {
method->nativeFunc = func;
}
}

真正的函数指针赋给了insns,而nativeFunc设成了Bridge,为什么要这么做呢?解释器中执行到一个native函数时,调用的是其nativeFunc,也就是Bridge了,然后再调insns执行真正的函数。

再来看被动注册。在类加载过程中,会扫描类的各个函数,调用loadMethodFromDex加载函数。当发现是native时,会将其nativeFunc设为dvmResolveNativeMethod。当没有主动注册时,调到这个native函数就会执行到dvmResolveNativeMethod,否则就会执行到Bridge。

1
2
3
4
5
6
7
8
9
static void loadMethodFromDex(ClassObject* clazz, const DexMethod* pDexMethod,
Method* meth) {
..........
if (dvmIsNativeMethod(meth)) {
meth->nativeFunc = dvmResolveNativeMethod;
meth->jniArgInfo = computeJniArgInfo(&meth->prototype);
}
..........
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void dvmResolveNativeMethod(const u4* args, JValue* pResult,
const Method* method, Thread* self)
{
ClassObject* clazz = method->clazz;
void* func;

..........

func = lookupSharedLibMethod(method);
if (func != NULL) {
dvmUseJNIBridge((Method*) method, func);
(*method->nativeFunc)(args, pResult, method, self);
return;
}
}

先找到Method对应的函数指针,然后给Method设置一个Bridge,然后执行这个Bridge。看来不管是主动注册还是被动注册,都逃不掉Bridge。我们来看看是怎么通过Method找到对应的函数指针的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void* lookupSharedLibMethod(const Method* method) {
return (void*) dvmHashForeach(gDvm.nativeLibs, findMethodInLib,
(void*) method);
}

static int findMethodInLib(void* vlib, void* vmethod) {
..........

preMangleCM =
createJniNameString(meth->clazz->descriptor, meth->name, &len);

mangleCM = mangleString(preMangleCM, len);

func = dlsym(pLib->handle, mangleCM);

..........

return (int) func;
}

dvmHashForeach就是遍历的,参数中传入了一个函数指针,就是说遍历时调这个函数指针来判断是否找到。找到的依据就是通过dlsym找到了我们要的符号。

用dlopen加载so,然后全局缓存起来。当要调so中的函数时,就用dlsym到so中找到对应的函数指针。值得注意的是,类初始化时,所有的native函数的指针都指向了dvmResolveNativeMethod,表示这个函数还有待解析。如果主动在JNI_Onload中注册了,就会将函数指针指向一个Bridge,而真正要执行的函数指针赋给了insns。如果没有主动注册过,执行native函数时就会先解析函数,找到该函数真正要执行的函数指针,然后同样要指定一个Bridge,之后的流程都一样了。

这个Bridge的作用是什么呢?为什么要中间多这么一层呢?一方面是准备参数,另一方面虚拟机调用so中的函数,而so是编译于不同平台下的,不同平台下函数调用的参数传递规则是不同的,是入栈还是放在寄存器中,如果是入栈那么内存布局是什么样的,还有函数返回值怎么传递给调用者,这些都是平台相关的,有的甚至是用汇编写的。所以为了屏蔽这些不同,Dalvik虚拟机用了libffi开源库来统一调用接口,调用时也不是直接调用的,而是统一通过dvmPlatformInvoke来调用so中的函数。感兴趣的可以参考http://sourceware.org/libffi/

完结,撒花 :)