在Java中加载一个jni库时通常会使用下面的代码加载
static {
System.loadLibrary("native-lib");
}
具体分析一下Android中System.loadLibrary("native-lib");
的加载流程。
JVM如何识别本地方法
带有native
关键字的方法在生成class文件的时候,方法的flags会带有ACC_NATIVE标志。
为了测试,我们新建一个JniMain.java文件:
public class JniMain{
public static native void jniMethod();
}
然后使用javac JniMain.java
生成JniMain.class文件,利用javap命令查看class文件结构javap -s -p -v JniMain.class
Classfile /D:/JniMain.class
Last modified 2017-8-13; size 208 bytes
MD5 checksum 3e8c842f3fef9dce094959cc49b058c7
Compiled from "JniMain.java"
public class JniMain
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#11 // java/lang/Object."<init>":()V
#2 = Class #12 // JniMain
#3 = Class #13 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 jniMethod
#9 = Utf8 SourceFile
#10 = Utf8 JniMain.java
#11 = NameAndType #4:#5 // "<init>":()V
#12 = Utf8 JniMain
#13 = Utf8 java/lang/Object
{
public JniMain();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0
public static native void jniMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
}
上面输出的是一个class文件结构,可以看到jniMethod的flags中包含了ACC_NATIVE标志。
System.loadLibrary源码分析
public static void loadLibrary(String libName) {
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}
Runtime.java的loadLibrary方法
/*
* 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);
}
1. 找so库的全路径
可以看到在ClassLoader不为空的前提下,会先调用String filename = loader.findLibrary(libraryName)
得到filename,然后根据filename调用Runtime.java的doLoad(filename, loader)
进行加载。
在Android中此处loader即为PathClassLoader,PathClassLoader继承自BaseDexClassLoader,并且所有实现都在BaseDexClassLoader中。所以我们先看一下BaseDexClassLoader的findLibrary方法。
BaseDexClassLoader.java
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
findLibrary直接返回的是pathList的findLibrary方法,pathList是BaseDexClassLoader的成员变量。
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
/** List of native library path elements. */
private final Element[] nativeLibraryPathElements;
...
/**
* Constructs an instance.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; may be {@code null}
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
...
}
pathList在构造函数中被初始化,我们看一下DexPathList的构造方法:
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
// 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.
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);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
}
其中比较重要的一句
/**
* Makes an array of dex/resource path elements, one per element of the given array.
*/
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null, suppressedExceptions);
现在我们看一下pathList.findLibrary(name);
到底做了啥
/**
* 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;
}
首先会调 System.mapLibraryName(libraryName)根据平台相关的特殊命名,如注释所示。(7.0已经转为native 方法)
/**
* 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";
}
然后nativeLibraryPathElements中遍历查找element.findNativeLibrary(fileName);
。
Element是DexPathList的静态内部类,每一个
/**
* 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;
public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
this.dir = dir;
this.isDirectory = isDirectory;
this.zip = zip;
this.dexFile = dexFile;
}
...
}
nativeLibraryPathElements中每一个Element中都保存着dex/resource file path信息。
//DexPathList$Element
public String findNativeLibrary(String name) {
maybeInit();
if (isDirectory) {
String path = new File(dir, name).getPath();
if (IoUtils.canOpenReadOnly(path)) {
return path;
}
} else if (zipFile != null) {
String entryName = new File(dir, name).getPath();
if (isZipEntryExistsAndStored(zipFile, entryName)) {
return zip.getPath() + zipSeparator + entryName;
}
}
return null;
}
至此 获取到本地库的全路径。
2. 加载so库
Runtime.java 的doLoad方法如下
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);
}
}
最后会调用本地方法nativeLoad方法进行加载:
private static native String nativeLoad(String filename, ClassLoader loader,
String ldLibraryPath);
Runtime.c中会通过动态注册的方式调用Runtime_nativeLoad方法进行加载。