System.loadLibrary()是我們在使用Java的JNI機制時,會用到的一個非常重要的函數,它的作用即是把實現了我們在Java code中聲明的native方法的那個libraryload進來,或者load其他什麼動態連接庫。
算是處於好奇吧,我們可以看一下這個方法它的實現,即執行流程。(下面分析的那些code,來自於android 4.2.2 aosp版。)先看一下這個方法的code(在libcore/luni/src/main/java/java/lang/System.java這個文件中):
01 |
/** |
02 |
*
Loads and links the library with the specified name. The mapping of the |
03 |
*
specified library name to the full path for loading the library is |
04 |
*
implementation-dependent. |
05 |
* |
06 |
*
@param libName |
07 |
*
the name of the library to load. |
08 |
*
@throws UnsatisfiedLinkError |
09 |
*
if the library could no<span style="color:#003399;"></span>t be loaded. |
10 |
*/ |
11 |
public static void loadLibrary(String
libName) { |
12 |
Runtime.getRuntime().loadLibrary(libName,
VMStack.getCallingClassLoader()); |
13 |
} |
由上面的那段code,可以看到,它的實現非常簡單,就只是先調用VMStack.getCallingClassLoader()獲取到ClassLoader,然後再把實際要做的事情委託給了Runtime來做而已。接下來我們再看一下Runtime.loadLibrary()的實現(在libcore/luni/src/main/java/java/lang/Runtime.java這個文件中):
01 |
/* |
02 |
*
Loads and links a library without security checks. |
03 |
*/ |
04 |
void loadLibrary(String
libraryName, ClassLoader loader) { |
05 |
if (loader
!= null )
{ |
06 |
String
filename = loader.findLibrary(libraryName); |
07 |
if (filename
== null )
{ |
08 |
throw new UnsatisfiedLinkError( "Couldn't
load " +
libraryName |
09 |
+ "
from loader " +
loader |
10 |
+ ":
findLibrary returned null" ); |
11 |
} |
12 |
String
error = nativeLoad(filename, loader); |
13 |
if (error
!= null )
{ |
14 |
throw new UnsatisfiedLinkError(error); |
15 |
} |
16 |
return ; |
17 |
} |
18 |
19 |
String
filename = System.mapLibraryName(libraryName); |
20 |
List<String>
candidates = new ArrayList<String>(); |
21 |
String
lastError = null ; |
22 |
for (String
directory : mLibPaths) { |
23 |
String
candidate = directory + filename; |
24 |
candidates.add(candidate); |
25 |
if ( new File(candidate).exists())
{ |
26 |
String
error = nativeLoad(candidate, loader); |
27 |
if (error
== null )
{ |
28 |
return ; //
We successfully loaded the library. Job done. |
29 |
} |
30 |
lastError
= error; |
31 |
} |
32 |
} |
33 |
34 |
if (lastError
!= null )
{ |
35 |
throw new UnsatisfiedLinkError(lastError); |
36 |
} |
37 |
throw new UnsatisfiedLinkError( "Library
" +
libraryName + "
not found; tried " +
candidates); |
38 |
} |
由上面的那段code,我們看到,loadLibrary()可以被看作是一個2步走的過程:
- 獲取到library path。對於這一點,上面的那個函數,依據於所傳遞的ClassLoader的不同,會有兩種不同的方法。如果ClassLoader非空,則會利用ClassLoader的findLibrary()方法來獲取library的path。而如果ClassLoader爲空,則會首先依據傳遞進來的library name,獲取到library file的name,比如傳遞“hello”進來,它的library file name,經過System.mapLibraryName(libraryName)將會是“libhello.so”;然後再在一個path list(即上面那段code中的mLibPaths)中查找到這個library file,並最終確定library 的path。
- 調用nativeLoad()這個native方法來load library
這段code,又牽出幾個問題,首先,可用的library path都是哪些,這實際上也決定了,我們的so文件放在哪些folder下,纔可以被真正load起來?其次,在native層load library的過程,又實際做了什麼事情?下面會對這兩個問題,一一的作出解答。
系統的library path
我們由簡單到複雜的來看這個問題。先來看一下,在傳入的ClassLoader爲空的情況(儘管我們知道,在System.loadLibrary()這個case下不會發生),前面Runtime.loadLibrary()的實現中那個mLibPaths的初始化的過程,在Runtime的構造函數中,如下:
01 |
/** |
02 |
*
Prevent this class from being instantiated. |
03 |
*/ |
04 |
private Runtime(){ |
05 |
String
pathList = System.getProperty( "java.library.path" , "." ); |
06 |
String
pathSep = System.getProperty( "path.separator" , ":" ); |
07 |
String
fileSep = System.getProperty( "file.separator" , "/" ); |
08 |
09 |
mLibPaths
= pathList.split(pathSep); |
10 |
11 |
//
Add a '/' to the end so we don't have to do the property lookup |
12 |
//
and concatenation later. |
13 |
for ( int i
= 0 ;
i < mLibPaths.length; i++) { |
14 |
if (!mLibPaths[i].endsWith(fileSep))
{ |
15 |
mLibPaths[i]
+= fileSep; |
16 |
} |
17 |
} |
18 |
} |
可以看到,那個library path list實際上讀取自一個system property。那在android系統中,這個system property的實際內容又是什麼呢?dump這些內容出來,就像下面這樣:
1 |
05-11
07:51:40.974: V/QRCodeActivity(11081): pathList = /vendor/lib:/system/lib |
2 |
05-11
07:51:40.974: V/QRCodeActivity(11081): pathSep = : |
3 |
05-11
07:51:40.974: V/QRCodeActivity(11081): fileSep = / |
然後是傳入的ClassLoader非空的情況,ClassLoader的findLibrary()方法的執行過程。首先看一下它的實現(在libcore/luni/src/main/java/java/lang/ClassLoader.java這個文件中):
01 |
/** |
02 |
*
Returns the absolute path of the native library with the specified name, |
03 |
*
or {@code null}. If this method returns {@code null} then the virtual |
04 |
*
machine searches the directories specified by the system property |
05 |
*
"java.library.path". |
06 |
*
<p> |
07 |
*
This implementation always returns {@code null}. |
08 |
*
</p> |
09 |
* |
10 |
*
@param libName |
11 |
*
the name of the library to find. |
12 |
*
@return the absolute path of the library. |
13 |
*/ |
14 |
protected String
findLibrary(String libName) { |
15 |
return null ; |
16 |
} |
竟然是一個空函數。那系統中實際運行的ClassLoader就是這個嗎?我們可以做一個小小的實驗,打印系統中實際運行的ClassLoader的String:
1 |
ClassLoader
classLoader = getClassLoader(); |
2 |
Log.v(TAG, "classLoader
= " +
classLoader.toString()); |
1 |
05-11
08:18:57.857: V/QRCodeActivity(11556): classLoader = dalvik.system.PathClassLoader[dexPath=/data/app/com.qrcode.qrcode-1.apk,libraryPath=/data/app-lib/com.qrcode.qrcode-1] |
1 |
@Override |
2 |
public String
findLibrary(String name) { |
3 |
return pathList.findLibrary(name); |
4 |
} |
這個方法看上去倒挺簡單,不用多做解釋。然後來看那個pathList的初始化的過程,在BaseDexClassLoader的構造函數裏:
01 |
/** |
02 |
*
Constructs an instance. |
03 |
* |
04 |
*
@param dexPath the list of jar/apk files containing classes and |
05 |
*
resources, delimited by {@code File.pathSeparator}, which |
06 |
*
defaults to {@code ":"} on Android |
07 |
*
@param optimizedDirectory directory where optimized dex files |
08 |
*
should be written; may be {@code null} |
09 |
*
@param libraryPath the list of directories containing native |
10 |
*
libraries, delimited by {@code File.pathSeparator}; may be |
11 |
*
{@code null} |
12 |
*
@param parent the parent class loader |
13 |
*/ |
14 |
public BaseDexClassLoader(String
dexPath, File optimizedDirectory, |
15 |
String
libraryPath, ClassLoader parent) { |
16 |
super (parent); |
17 |
18 |
this .originalPath
= dexPath; |
19 |
this .originalLibraryPath
= libraryPath; |
20 |
this .pathList
= |
21 |
new DexPathList( this ,
dexPath, libraryPath, optimizedDirectory); |
22 |
} |
BaseDexClassLoader的構造函數也不用多做解釋吧。然後是DexPathList的構造函數:
01 |
/** |
02 |
*
Constructs an instance. |
03 |
* |
04 |
*
@param definingContext the context in which any as-yet unresolved |
05 |
*
classes should be defined |
06 |
*
@param dexPath list of dex/resource path elements, separated by |
07 |
*
{@code File.pathSeparator} |
08 |
*
@param libraryPath list of native library directory path elements, |
09 |
*
separated by {@code File.pathSeparator} |
10 |
*
@param optimizedDirectory directory where optimized {@code .dex} files |
11 |
*
should be found and written to, or {@code null} to use the default |
12 |
*
system directory for same |
13 |
*/ |
14 |
public DexPathList(ClassLoader
definingContext, String dexPath, |
15 |
String
libraryPath, File optimizedDirectory) { |
16 |
if (definingContext
== null )
{ |
17 |
throw new NullPointerException( "definingContext
== null" ); |
18 |
} |
19 |
20 |
if (dexPath
== null )
{ |
21 |
throw new NullPointerException( "dexPath
== null" ); |
22 |
} |
23 |
24 |
if (optimizedDirectory
!= null )
{ |
25 |
if (!optimizedDirectory.exists())
{ |
26 |
throw new IllegalArgumentException( |
27 |
"optimizedDirectory
doesn't exist: " |
28 |
+
optimizedDirectory); |
29 |
} |
30 |
31 |
if (!(optimizedDirectory.canRead() |
32 |
&&
optimizedDirectory.canWrite())) { |
33 |
throw new IllegalArgumentException( |
34 |
"optimizedDirectory
not readable/writable: " |
35 |
+
optimizedDirectory); |
36 |
} |
37 |
} |
38 |
39 |
this .definingContext
= definingContext; |
40 |
this .dexElements
= |
41 |
makeDexElements(splitDexPath(dexPath),
optimizedDirectory); |
42 |
this .nativeLibraryDirectories
= splitLibraryPath(libraryPath); |
43 |
} |
關於我們的library path的問題,可以只關注最後的那個splitLibraryPath(),這個地方,實際上即是把傳進來的libraryPath 又丟給splitLibraryPath來獲取library path 的list。可以看一下DexPathList.splitLibraryPath()的實現:
01 |
/** |
02 |
*
Splits the given library directory path string into elements |
03 |
*
using the path separator ({@code File.pathSeparator}, which |
04 |
*
defaults to {@code ":"} on Android, appending on the elements |
05 |
*
from the system library path, and pruning out any elements that |
06 |
*
do not refer to existing and readable directories. |
07 |
*/ |
08 |
private static File[]
splitLibraryPath(String path) { |
09 |
/* |
10 |
*
Native libraries may exist in both the system and |
11 |
*
application library paths, and we use this search order: |
12 |
* |
13 |
*
1. this class loader's library path for application |
14 |
*
libraries |
15 |
*
2. the VM's library path from the system |
16 |
*
property for system libraries |
17 |
* |
18 |
*
This order was reversed prior to Gingerbread; see http://b/2933456. |
19 |
*/ |
20 |
ArrayList<File>
result = splitPaths( |
21 |
path,
System.getProperty( "java.library.path" , "." ), true ); |
22 |
return result.toArray( new File[result.size()]); |
23 |
} |
這個地方,是在用兩個部分的library path list來由splitPaths構造最終的那個path list,一個部分是,傳進來的library path,另外一個部分是,像我們前面看到的那個,是system property。然後再來看一下DexPathList.splitPaths()的實現:
01 |
/** |
02 |
*
Splits the given path strings into file elements using the path |
03 |
*
separator, combining the results and filtering out elements |
04 |
*
that don't exist, aren't readable, or aren't either a regular |
05 |
*
file or a directory (as specified). Either string may be empty |
06 |
*
or {@code null}, in which case it is ignored. If both strings |
07 |
*
are empty or {@code null}, or all elements get pruned out, then |
08 |
*
this returns a zero-element list. |
09 |
*/ |
10 |
private static ArrayList<File>
splitPaths(String path1, String path2, |
11 |
boolean wantDirectories)
{ |
12 |
ArrayList<File>
result = new ArrayList<File>(); |
13 |
14 |
splitAndAdd(path1,
wantDirectories, result); |
15 |
splitAndAdd(path2,
wantDirectories, result); |
16 |
return result; |
17 |
} |
總結一下,ClassLoader的那個findLibrary()實際上會在兩個部分的folder中去尋找System.loadLibrary()要load的那個library,一個部分是,構造ClassLoader時,傳進來的那個library path,即是app folder,另外一個部分是system property。在android系統中,查找要load的library,實際上會在如下3個folder中進行:
- /vendor/lib
- /system/lib
- /data/app-lib/com.qrcode.qrcode-1
上面第3個item只是一個例子,每一個app,它的那個app library path的最後一個部分都會是特定於那個app的。至於說,構造BaseDexClassLoader時的那個libraryPath 到底是怎麼來的,那可能就會牽扯到android本身更復雜的一些過程了,在此不再做更詳細的說明。
Native 層load library的過程
然後來看一下native層,把so文件load起的過程,先來一下nativeLoad()這個函數的實現(在JellyBean/dalvik/vm/native/java_lang_Runtime.cpp這個文件中):
01 |
/* |
02 |
*
static String nativeLoad(String filename, ClassLoader loader) |
03 |
* |
04 |
*
Load the specified full path as a dynamic library filled with |
05 |
*
JNI-compatible methods. Returns null on success, or a failure |
06 |
*
message on failure. |
07 |
*/ |
08 |
static void Dalvik_java_lang_Runtime_nativeLoad( const u4*
args, |
09 |
JValue*
pResult) |
10 |
{ |
11 |
StringObject*
fileNameObj = (StringObject*) args[0]; |
12 |
Object*
classLoader = (Object*) args[1]; |
13 |
char *
fileName = NULL; |
14 |
StringObject*
result = NULL; |
15 |
char *
reason = NULL; |
16 |
bool success; |
17 |
18 |
assert (fileNameObj
!= NULL); |
19 |
fileName
= dvmCreateCstrFromString(fileNameObj); |
20 |
21 |
success
= dvmLoadNativeCode(fileName, classLoader, &reason); |
22 |
if (!success)
{ |
23 |
const char *
msg = (reason != NULL) ? reason : "unknown
failure" ; |
24 |
result
= dvmCreateStringFromCstr(msg); |
25 |
dvmReleaseTrackedAlloc((Object*)
result, NULL); |
26 |
} |
27 |
28 |
free (reason); |
29 |
free (fileName); |
30 |
RETURN_PTR(result); |
31 |
} |
可以看到,nativeLoad()實際上只是完成了兩件事情,第一,是調用dvmCreateCstrFromString()將Java 的library path String 轉換到native的String,然後將這個path傳給dvmLoadNativeCode()做load,dvmLoadNativeCode()這個函數的實現在dalvik/vm/Native.cpp中,如下:
001 |
/* |
002 |
*
Load native code from the specified absolute pathname. Per the spec, |
003 |
*
if we've already loaded a library with the specified pathname, we |
004 |
*
return without doing anything. |
005 |
* |
006 |
*
TODO? for better results we should absolutify the pathname. For fully |
007 |
*
correct results we should stat to get the inode and compare that. The |
008 |
*
existing implementation is fine so long as everybody is using |
009 |
*
System.loadLibrary. |
010 |
* |
011 |
*
The library will be associated with the specified class loader. The JNI |
012 |
*
spec says we can't load the same library into more than one class loader. |
013 |
* |
014 |
*
Returns "true" on success. On failure, sets *detail to a |
015 |
*
human-readable description of the error or NULL if no detail is |
016 |
*
available; ownership of the string is transferred to the caller. |
017 |
*/ |
018 |
bool dvmLoadNativeCode( const char *
pathName, Object* classLoader, |
019 |
char **
detail) |
020 |
{ |
021 |
SharedLib*
pEntry; |
022 |
void *
handle; |
023 |
bool verbose; |
024 |
025 |
/*
reduce noise by not chattering about system libraries */ |
026 |
verbose
= !! strncmp (pathName, "/system" , sizeof ( "/system" )-1); |
027 |
verbose
= verbose && !! strncmp (pathName, "/vendor" , sizeof ( "/vendor" )-1); |
028 |
029 |
if (verbose) |
030 |
ALOGD( "Trying
to load lib %s %p" ,
pathName, classLoader); |
031 |
032 |
*detail
= NULL; |
033 |
034 |
/* |
035 |
*
See if we've already loaded it. If we have, and the class loader |
036 |
*
matches, return successfully without doing anything. |
037 |
*/ |
038 |
pEntry
= findSharedLibEntry(pathName); |
039 |
if (pEntry
!= NULL) { |
040 |
if (pEntry->classLoader
!= classLoader) { |
041 |
ALOGW( "Shared
lib '%s' already opened by CL %p; can't open in %p" , |
042 |
pathName,
pEntry->classLoader, classLoader); |
043 |
return false ; |
044 |
} |
045 |
if (verbose)
{ |
046 |
ALOGD( "Shared
lib '%s' already loaded in same CL %p" , |
047 |
pathName,
classLoader); |
048 |
} |
049 |
if (!checkOnLoadResult(pEntry)) |
050 |
return false ; |
051 |
return true ; |
052 |
} |
053 |
054 |
/* |
055 |
*
Open the shared library. Because we're using a full path, the system |
056 |
*
doesn't have to search through LD_LIBRARY_PATH. (It may do so to |
057 |
*
resolve this library's dependencies though.) |
058 |
* |
059 |
*
Failures here are expected when java.library.path has several entries |
060 |
*
and we have to hunt for the lib. |
061 |
* |
062 |
*
The current version of the dynamic linker prints detailed information |
063 |
*
about dlopen() failures. Some things to check if the message is |
064 |
*
cryptic: |
065 |
*
- make sure the library exists on the device |
066 |
*
- verify that the right path is being opened (the debug log message |
067 |
*
above can help with that) |
068 |
*
- check to see if the library is valid (e.g. not zero bytes long) |
069 |
*
- check config/prelink-linux-arm.map to ensure that the library |
070 |
*
is listed and is not being overrun by the previous entry (if |
071 |
*
loading suddenly stops working on a prelinked library, this is |
072 |
*
a good one to check) |
073 |
*
- write a trivial app that calls sleep() then dlopen(), attach |
074 |
*
to it with "strace -p <pid>" while it sleeps, and watch for |
075 |
*
attempts to open nonexistent dependent shared libs |
076 |
* |
077 |
*
This can execute slowly for a large library on a busy system, so we |
078 |
*
want to switch from RUNNING to VMWAIT while it executes. This allows |
079 |
*
the GC to ignore us. |
080 |
*/ |
081 |
Thread*
self = dvmThreadSelf(); |
082 |
ThreadStatus
oldStatus = dvmChangeStatus(self, THREAD_VMWAIT); |
083 |
handle
= dlopen(pathName, RTLD_LAZY); |
084 |
dvmChangeStatus(self,
oldStatus); |
085 |
086 |
if (handle
== NULL) { |
087 |
*detail
= strdup(dlerror()); |
088 |
ALOGE( "dlopen(\"%s\")
failed: %s" ,
pathName, *detail); |
089 |
return false ; |
090 |
} |
091 |
092 |
/*
create a new entry */ |
093 |
SharedLib*
pNewEntry; |
094 |
pNewEntry
= (SharedLib*) calloc (1, sizeof (SharedLib)); |
095 |
pNewEntry->pathName
= strdup(pathName); |
096 |
pNewEntry->handle
= handle; |
097 |
pNewEntry->classLoader
= classLoader; |
098 |
dvmInitMutex(&pNewEntry->onLoadLock); |
099 |
pthread_cond_init(&pNewEntry->onLoadCond,
NULL); |
100 |
pNewEntry->onLoadThreadId
= self->threadId; |
101 |
102 |
/*
try to add it to the list */ |
103 |
SharedLib*
pActualEntry = addSharedLibEntry(pNewEntry); |
104 |
105 |
if (pNewEntry
!= pActualEntry) { |
106 |
ALOGI( "WOW:
we lost a race to add a shared lib (%s CL=%p)" , |
107 |
pathName,
classLoader); |
108 |
freeSharedLibEntry(pNewEntry); |
109 |
return checkOnLoadResult(pActualEntry); |
110 |
} else { |
111 |
if (verbose) |
112 |
ALOGD( "Added
shared lib %s %p" ,
pathName, classLoader); |
113 |
114 |
bool result
= true ; |
115 |
void *
vonLoad; |
116 |
int version; |
117 |
118 |
vonLoad
= dlsym(handle, "JNI_OnLoad" ); |
119 |
if (vonLoad
== NULL) { |
120 |
ALOGD( "No
JNI_OnLoad found in %s %p, skipping init" , |
121 |
pathName,
classLoader); |
122 |
} else { |
123 |
/* |
124 |
*
Call JNI_OnLoad. We have to override the current class |
125 |
*
loader, which will always be "null" since the stuff at the |
126 |
*
top of the stack is around Runtime.loadLibrary(). (See |
127 |
*
the comments in the JNI FindClass function.) |
128 |
*/ |
129 |
OnLoadFunc
func = (OnLoadFunc)vonLoad; |
130 |
Object*
prevOverride = self->classLoaderOverride; |
131 |
132 |
self->classLoaderOverride
= classLoader; |
133 |
oldStatus
= dvmChangeStatus(self, THREAD_NATIVE); |
134 |
if (gDvm.verboseJni)
{ |
135 |
ALOGI( "[Calling
JNI_OnLoad for \"%s\"]" ,
pathName); |
136 |
} |
137 |
version
= (*func)(gDvmJni.jniVm, NULL); |
138 |
dvmChangeStatus(self,
oldStatus); |
139 |
self->classLoaderOverride
= prevOverride; |
140 |
141 |
if (version
!= JNI_VERSION_1_2 && version != JNI_VERSION_1_4 && |
142 |
version
!= JNI_VERSION_1_6) |
143 |
{ |
144 |
ALOGW( "JNI_OnLoad
returned bad version (%d) in %s %p" , |
145 |
version,
pathName, classLoader); |
146 |
/* |
147 |
*
It's unwise to call dlclose() here, but we can mark it |
148 |
*
as bad and ensure that future load attempts will fail. |
149 |
* |
150 |
*
We don't know how far JNI_OnLoad got, so there could |
151 |
*
be some partially-initialized stuff accessible through |
152 |
*
newly-registered native method calls. We could try to |
153 |
*
unregister them, but that doesn't seem worthwhile. |
154 |
*/ |
155 |
result
= false ; |
156 |
} else { |
157 |
if (gDvm.verboseJni)
{ |
158 |
ALOGI( "[Returned
from JNI_OnLoad for \"%s\"]" ,
pathName); |
159 |
} |
160 |
} |
161 |
} |
162 |
163 |
if (result) |
164 |
pNewEntry->onLoadResult
= kOnLoadOkay; |
165 |
else |
166 |
pNewEntry->onLoadResult
= kOnLoadFailed; |
167 |
168 |
pNewEntry->onLoadThreadId
= 0; |
169 |
170 |
/* |
171 |
*
Broadcast a wakeup to anybody sleeping on the condition variable. |
172 |
*/ |
173 |
dvmLockMutex(&pNewEntry->onLoadLock); |
174 |
pthread_cond_broadcast(&pNewEntry->onLoadCond); |
175 |
dvmUnlockMutex(&pNewEntry->onLoadLock); |
176 |
return result; |
177 |
} |
178 |
} |
哇塞,dvmLoadNativeCode()這個函數還真的是有點複雜,那就挑那些跟我們的JNI比較緊密相關的邏輯來看吧。可以認爲這個函數做了下面的這樣一些事情:
- 調用dlopen() 打開一個so文件,創建一個handle。
- 調用dlsym()函數,查找到so文件中的JNI_OnLoad()這個函數的函數指針。
- 執行上一步找到的那個JNI_OnLoad()函數。
至此,大體可以結束System.loadLibrary()的執行過程的分析。