一文了解 Java 中 so 文件的加载原理
Posted TechMerger
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文了解 Java 中 so 文件的加载原理相关的知识,希望对你有一定的参考价值。
前言
无论是 android 开发者还是 Java 工程师应该都有使用过 JNI 开发,但对于 JVM 如何加载 so、Android 系统如何加载 so,可能鲜有时间了解。
本文通过代码、流程解释,带大家快速了解其加载原理,扫清困惑。
1. System#load() + loadLibrary()
1.1 load()
System
提供的 load()
用于指定 so 的完整的路径名且带文件后缀并加载,等同于调用 Runtime
类提供的 load()。
If the filename argument, when stripped of any platform-specific library prefix, path, and file extension, indicates a library whose name is, for example, L, and a native library called L is statically linked with the VM, then the JNI_OnLoad_L function exported by the library is invoked rather than attempting to load a dynamic library.
Eg.
System.load("/sdcard/path/libA.so")
步骤简述:
-
通过 Reflection 获取调用来源的 Class 实例
-
接着调用 Runtime 的 load0() 实现
-
load0() 首先获取系统的 SecurityManager
-
当 SecurityManager 存在的话检查目标 so 文件的访问权限:权限不足的话打印拒绝信息、抛出
SecurityException
,如果 name 参数为空,抛出NullPointerException
-
如果 so 文件名非绝对路径的话,并不支持,并抛出
UnsatisfiedLinkError
,message 为:Expecting an absolute path of the library: xxx
-
针对 so 文件的权限检查和名称检查均通过的话,继续调用 ClassLoader 的 loadLibrary() 实现,需要留意的是绝对路径参数为 true
-
// java/lang/System.java
public static void load(String filename)
Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
// java/lang/Runtime.java
synchronized void load0(Class<?> fromClass, String filename)
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkLink(filename);
if (!(new File(filename).isAbsolute()))
throw new UnsatisfiedLinkError(
"Expecting an absolute path of the library: " + filename);
ClassLoader.loadLibrary(fromClass, filename, true);
1.2 loadLibrary()
System
类提供的 loadLibrary()
用于指定 so 的名称并加载,等同于调用 Runtime
类提供的 loadLibrary()。在 Android 平台系统会自动去系统目录(/system/lib64/)、应用 lib 目录(/data/app/xxx/lib64/)下去找 libname 参数拼接了 lib 前缀的库文件。
The libname argument must not contain any platform specific prefix, file extension or path.
If a native library called libname is statically linked with the VM, then the JNI_OnLoad_libname function exported by the library is invoked.
Eg.
System.loadLibrary("A")
步骤简述:
-
同样通过 Reflection 获取调用来源的 Class 实例
-
接着调用 Runtime 的 loadLibrary0() 实现
-
loadLibrary0() 首先获取系统的 SecurityManager,并检查目标 so 文件的访问权限:权限不足或文件名为空的话和上面一样抛出 Exception
-
确保 so 名称不包含 /,反之,抛出
UnsatisfiedLinkError
,message 为:Directory separator should not appear in library name: xxx
-
检查通过后,同样调用 ClassLoader 的 loadLibrary() 实现继续下一步,只不过绝对路径参数为 false
-
// java/lang/System.java
public static void loadLibrary(String libname)
Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
// java/lang/Runtime.java
synchronized void loadLibrary0(Class<?> fromClass, String libname)
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkLink(libname);
if (libname.indexOf((int)File.separatorChar) != -1)
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
ClassLoader.loadLibrary(fromClass, libname, false);
2. ClassLoader#loadLibrary()
上面的调用栈可以看到无论是 load()
还是 loadLibrary()
最终都是调用 ClassLoader
的 loadLibrary()
,主要区别在于 name 参数是 lib 完整路径、还是 lib 名称,以及是否是绝对路径参数。
-
首先通过
getClassLoader()
获得加载源所属的 ClassLoader 实例 -
确保存放 libraries 路径的字符串数组
sys_paths
不为空- 尚且为空的话,调用 initializePath(“java.library.path”) 先初始化
usr
路径字符串数组,再调用 initializePath(“sun.boot.library.path”) 初始化system
路径字符串数组。initializePath()
具体见下章节
- 尚且为空的话,调用 initializePath(“java.library.path”) 先初始化
-
依据是否
isAbsolute
决定是否直接加载 library-
name 是绝对路径的话,直接创建 File 实例,调用
loadLibrary0()
,继续加载该文件。具体见下章节-
检查 loadLibrary0 的结果:true:即表示加载成功,结束;false:即表示加载失败,抛出 UnsatisfiedLinkError
Can’t load xxx
-
-
name 非绝对路径并且获取的 ClassLoader 存在的话,通过
findLibrary()
,根据 so 名称获得 lib 绝对路径,并创建指向该路径的 File 实例libfile
-
并确保该文件的路径是绝对路径。反之,抛出 UnsatisfiedLinkError
ClassLoader.findLibrary failed to return an absolute path: xxx
-
此后也是调用
loadLibrary0()
继续加载该文件,并检查 loadLibrary0 的结果,处理同上
-
-
-
假使 ClassLoader 不存在:遍历 system 路径字符串数组的元素,
-
通过
mapLibraryName()
分别将 lib name 映射到平台关联的 lib 完整名称并返回,具体见下章节 -
创建当前遍历的 path 下 libfile 实例
-
调用 loadLibrary0() 继续加载该文件,并检查结果:
-
true 则直接结束
-
false 的话,通过 mapAlternativeName() 获取该 lib 可能存在的替代文件名,比如将后缀替换为
jnilib
- 如果再度 map 后的 libfile 不为空,调用 loadLibrary0() 再度加载该文件并检查结果,true 则直接结束;反之,进入下一次循环
-
-
-
至此,如果仍未成功找到 library 文件,则在 ClassLoader 存在的情况下,到 usr 路径字符串数组中查找
- 遍历 usr 路径字符串数组的元素
- 后续逻辑和上述一致,只是 map 时候的前缀不同,是 usr_paths 的元素
- 遍历 usr 路径字符串数组的元素
-
最终进行默认处理,即抛出
UnsatisfiedLinkError
,提示在 java.library.path propery 代表的路径下也未找到 so 文件
no xx in java.library.path
// java/lang/ClassLoader.java
static void loadLibrary(Class<?> fromClass, String name,
boolean isAbsolute)
ClassLoader loader =
(fromClass == null) ? null : fromClass.getClassLoader();
if (sys_paths == null)
usr_paths = initializePath("java.library.path");
sys_paths = initializePath("sun.boot.library.path");
if (isAbsolute)
if (loadLibrary0(fromClass, new File(name)))
return;
throw new UnsatisfiedLinkError("Can't load library: " + name);
if (loader != null)
String libfilename = loader.findLibrary(name);
if (libfilename != null)
File libfile = new File(libfilename);
if (!libfile.isAbsolute())
throw new UnsatisfiedLinkError(...);
if (loadLibrary0(fromClass, libfile))
return;
throw new UnsatisfiedLinkError("Can't load " + libfilename);
for (int i = 0 ; i < sys_paths.length ; i++)
File libfile = new File(sys_paths[i], System.mapLibraryName(name));
if (loadLibrary0(fromClass, libfile))
return;
libfile = ClassLoaderHelper.mapAlternativeName(libfile);
if (libfile != null && loadLibrary0(fromClass, libfile))
return;
if (loader != null)
for (int i = 0 ; i < usr_paths.length ; i++)
File libfile = new File(usr_paths[i],
System.mapLibraryName(name));
if (loadLibrary0(fromClass, libfile))
return;
libfile = ClassLoaderHelper.mapAlternativeName(libfile);
if (libfile != null && loadLibrary0(fromClass, libfile))
return;
// Oops, it failed
throw new UnsatisfiedLinkError("no " + name + " in java.library.path");
3. ClassLoader#initializePath()
从 System
中获取对应 property 代表的 path 到数组中。
-
先调用
getProperty()
从 JVM 中取出配置的路径,默认的是 “”-
其中的
checkKey()
将检查 key 名称是否合法,null
的话抛出NullPointerException
key can’t be null
如果为
""
,抛出IllegalArgumentException
key can’t be empty
-
后面通过
getSecurityManager()
获取SecurityManager
实例,检查是否存在该 property 的访问权限
-
-
如果允许引用路径元素并且 \\ 存在的话,将路径字符串的 char 取出进行拼接、计算得到路径字符串数组
-
反之通过 indexOf(/) 统计 / 出现的次数,并创建一个 / 次数 + 1 的数组
-
遍历该路径字符串,通过 substring() 将各 / 的中间 path 内容提取到上述数组中
-
最后返回得到的 path 数组
// java/lang/ClassLoader.java
private static String[] initializePath(String propname)
String ldpath = System.getProperty(propname, "");
String ps = File.pathSeparator;
...
i = ldpath.indexOf(ps);
n = 0;
while (i >= 0)
n++;
i = ldpath.indexOf(ps, i + 1);
String[] paths = new String[n + 1];
n = i = 0;
j = ldpath.indexOf(ps);
while (j >= 0)
if (j - i > 0)
paths[n++] = ldpath.substring(i, j);
else if (j - i == 0)
paths[n++] = ".";
i = j + 1;
j = ldpath.indexOf(ps, i);
paths[n] = ldpath.substring(i, ldlen);
return paths;
4. ClassLoader#findLibrary()
findLibrary() 将到 ClassLoader
中查找 lib,取决于各 JVM 的具体实现。比如可以看看 Android 上的实现。
- 到
DexPathList
的具体实现中调用 - 首先通过 System 类的
mapLibraryName()
中获得 mapping 后的 lib 全名,细节见下章节 - 遍历存放 native lib 路径元素数组
nativeLibraryPathElements
- 逐个调用各元素的
findNativeLibrary()
实现去寻找 - 一经找到立即返回,遍历结束仍未发现的话返回 null
// android/libcore/dalvik/src/main/java/dalvik/system/
// BaseDexClassLoader.java
public String findLibrary(String name)
return pathList.findLibrary(name);
// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
public String findLibrary(String libraryName)
// 到 System 中获得 mapping 后的 lib 全名
String fileName = System.mapLibraryName(libraryName);
// 到存放 native lib 路径数组中遍历
for (NativeLibraryElement element : nativeLibraryPathElements)
String path = element.findNativeLibrary(fileName);
// 一旦找到立即返回并结束,反之进入下一次循环
if (path != null)
return path;
// 路径中全找遍了,仍未找到则返回 null
return null;
4.1 System#mapLibraryName()
mapLibraryName()
的作用很简单,即将 lib 名称 mapping 到完整格式的名称,比如输入 opencv 得到的是 libopencv.so。如果遇到名称为空或者长度超上限 240 的话,将抛出相应 Exception。
// java/lang/System.java
public static native String mapLibraryName(String libname);
其是 native 方法,具体实现位于 JDK Native Source Code 中,可在如下网站中看到:
// native/java/lang/System.c
#define JNI_LIB_PREFIX "lib"
#define JNI_LIB_SUFFIX ".so"
Java_java_lang_System_mapLibraryName(JNIEnv *env, jclass ign, jstring libname)
// 定义最后名称的 Sring 长度变量
int len;
// 并获取 lib 前缀、后缀的字符串常量的长度
int prefix_len = (int) strlen(JNI_LIB_PREFIX);
int suffix_len = (int) strlen(JNI_LIB_SUFFIX);
// 定义临时的存放最后名称的 char 数组
jchar chars[256];
// 如果 libname 参数为空,抛出 NPE
if (libname == NULL)
JNU_ThrowNullPointerException(env, 0);
return NULL;
// 获取 libname 长度
len = (*env)->GetStringLength(env, libname);
// 如果大于 240 的话抛出 IllegalArgumentException
if (len > 240)
JNU_ThrowIllegalArgumentException(env, "name too long");
return NULL;
// 将前缀 ”lib“ 的字符拷贝到临时的 char 数组头部
cpchars(chars, JNI_LIB_PREFIX, prefix_len);
// 将 lib 名称从字符串里拷贝到 char 数组的 “lib” 后面
(*env)->GetStringRegion(env, libname, 0, len, chars + prefix_len);
// 更新名称长度为:前缀+ lib 名称
len += prefix_len;
// 将后缀 ”.so“ 的字符拷贝到临时的 char 数组里的 lib 名称后
cpchars(chars + len, JNI_LIB_SUFFIX, suffix_len);
// 再次更新名称长度为:前缀+ lib 名称 + 后缀
len += suffix_len;
// 从 char 数组里提取当前长度的复数 char 成员到新创建的 String 对象中返回
return (*env)->NewString(env, chars, len);
static void cpchars(jchar *dst, char *src, int n)
int i;
for (i = 0; i < n; i++)
dst[i] = src[i];
逻辑很清晰,检查 lib 名称参数是否合法,之后便是将名称分别加上前后缀到临时字符数组中,最后转为字符串返回。
4.2 nativeLibraryPathElements()
nativeLibraryPathElements 数组来源于获取到的所有 native Library 目录后转换而来。
// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
public DexPathList(ClassLoader definingContext, String librarySearchPath)
...
this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories());
所有 native Library 目录除了包含应用自身的 library 目录列表以外,还包括了系统的列表部分。
// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
private List<File> getAllNativeLibraryDirectories()
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
return allNativeLibraryDirectories;
/** List of application native library directories. */
private final List<File> nativeLibraryDirectories;
/** List of system native library directories. */
private final List<File> systemNativeLibraryDirectories;
应用自身的 library 目录列表来自于 DexPathList 初始化时传入的 librarySearchPath 参数,splitPaths() 负责去该 path 下遍历各级目录得到对应数组。
// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
public DexPathList(ClassLoader definingContext, String librarySearchPath)
...
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
private static List<File> splitPaths(String searchPath, boolean directoriesOnly)
List<File> result = new ArrayList<>();
if (searchPath != null)
for (String path : searchPath.split(File.pathSeparator))
if (directoriesOnly)
try
StructStat sb = Libcore.os.stat(path);
if (!S_ISDIR(sb.st_mode))
continue;
catch (ErrnoException ignored)
continue;
result.add(new File(path));
return result;
系统列表则来自于系统的 path 路径,调用 splitPaths() 的第二个参数不同,促使其在分割的时候只处理目录类型的部分,纯文件的话跳过。
// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
public DexPathList(ClassLoader definingContext, String librarySearchPath)
...
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
...
拿到 path 文件列表之后就是调用 makePathElements
转成对应元素数组。
- 按照列表长度创建等长的
Element
数组 - 遍历 path 列表
- 如果 path 包含 “!/” 的话,将其拆分为 path 和 zipDir 两部分,并创建
NativeLibraryElement
实例 - 反之,如果是目录的话,直接用 path 创建 NativeLibraryElement 实例,zipDir 参数则为空
// android/libcore/dalvik/src/main/java/dalvik/system/
// DexPathList.java
private static NativeLibraryElement[] makePathElements(List<File> files)
NativeLibraryElement[] elements = new NativeLibraryElement[files.size()];
int elementsPos = 0;
for (File file : files)
String path = file.getPath();
if (path08-SO加载解析过程