Java中System.loadLibrary() 的執行過程

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這個文件中):

?
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * Loads and links the library with the specified name. The mapping of the
 * specified library name to the full path for loading the library is
 * implementation-dependent.
 *
 * @param libName
 *            the name of the library to load.
 * @throws UnsatisfiedLinkError
 *             if the library could no<span style="color:#003399;"></span>t be loaded.
 */
public static void loadLibrary(String libName) {
    Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}

由上面的那段code,可以看到,它的實現非常簡單,就只是先調用VMStack.getCallingClassLoader()獲取到ClassLoader,然後再把實際要做的事情委託給了Runtime來做而已。接下來我們再看一下Runtime.loadLibrary()的實現(在libcore/luni/src/main/java/java/lang/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
/*
 * Loads and links a library without security checks.
 */
void loadLibrary(String libraryName, ClassLoader loader) {
    if (loader != null) {
        String filename = loader.findLibrary(libraryName);
        if (filename == null) {
            throw new UnsatisfiedLinkError("Couldn't load " + libraryName
                                           + " from loader " + loader
                                           + ": findLibrary returned null");
        }
        String error = nativeLoad(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 (new File(candidate).exists()) {
            String error = nativeLoad(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);
}

由上面的那段code,我們看到,loadLibrary()可以被看作是一個2步走的過程

  1. 獲取到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。
  2. 調用nativeLoad()這個native方法來load library

這段code,又牽出幾個問題,首先,可用的library path都是哪些,這實際上也決定了,我們的so文件放在哪些folder下,纔可以被真正load起來?其次,在native層load library的過程,又實際做了什麼事情?下面會對這兩個問題,一一的作出解答。

系統的library path

我們由簡單到複雜的來看這個問題。先來看一下,在傳入的ClassLoader爲空的情況(儘管我們知道,在System.loadLibrary()這個case下不會發生),前面Runtime.loadLibrary()的實現中那個mLibPaths的初始化的過程,在Runtime的構造函數中,如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * Prevent this class from being instantiated.
 */
private Runtime(){
    String pathList = System.getProperty("java.library.path", ".");
    String pathSep = System.getProperty("path.separator", ":");
    String fileSep = System.getProperty("file.separator", "/");
 
    mLibPaths = pathList.split(pathSep);
 
    // Add a '/' to the end so we don't have to do the property lookup
    // and concatenation later.
    for (int i = 0; i < mLibPaths.length; i++) {
        if (!mLibPaths[i].endsWith(fileSep)) {
            mLibPaths[i] += fileSep;
        }
    }
}

可以看到,那個library path list實際上讀取自一個system property。那在android系統中,這個system property的實際內容又是什麼呢?dump這些內容出來,就像下面這樣:

?
1
2
3
05-11 07:51:40.974: V/QRCodeActivity(11081): pathList = /vendor/lib:/system/lib
05-11 07:51:40.974: V/QRCodeActivity(11081): pathSep = :
05-11 07:51:40.974: V/QRCodeActivity(11081): fileSep = /


然後是傳入的ClassLoader非空的情況,ClassLoaderfindLibrary()方法的執行過程。首先看一下它的實現(在libcore/luni/src/main/java/java/lang/ClassLoader.java這個文件中)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * Returns the absolute path of the native library with the specified name,
 * or {@code null}. If this method returns {@code null} then the virtual
 * machine searches the directories specified by the system property
 * "java.library.path".
 * <p>
 * This implementation always returns {@code null}.
 * </p>
 *
 * @param libName
 *            the name of the library to find.
 * @return the absolute path of the library.
 */
protected String findLibrary(String libName) {
    return null;
}

竟然是一個空函數。那系統中實際運行的ClassLoader就是這個嗎?我們可以做一個小小的實驗,打印系統中實際運行的ClassLoader的String

?
1
2
ClassLoader classLoader = getClassLoader();
Log.v(TAG, "classLoader = " + classLoader.toString());
在Galaxy Nexus上執行的結果如下:

?
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]
看到了吧,android系統中的 ClassLoader真正的實現 在dalvik的dalvik.system.PathClassLoader。打開libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java來看 PathClassLoader這個class 的實現,可以看到,就只是簡單的繼承 BaseDexClassLoader而已,沒有任何實際的內容 。接下來我們就來看一下 BaseDexClassLoader中 那個 findLibrary() 真正的實現( 在libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java這個文件中 ):
?
1
2
3
4
@Override
public String findLibrary(String name) {
    return pathList.findLibrary(name);
}

這個方法看上去倒挺簡單,不用多做解釋。然後來看那個pathList的初始化的過程,在BaseDexClassLoader的構造函數裏

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * 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.originalPath = dexPath;
    this.originalLibraryPath = libraryPath;
    this.pathList =
        new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

BaseDexClassLoader的構造函數也不用多做解釋吧。然後是DexPathList的構造函數:

?
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
/**
 * Constructs an instance.
 *
 * @param definingContext the context in which any as-yet unresolved
 * classes should be defined
 * @param dexPath list of dex/resource path elements, separated by
 * {@code File.pathSeparator}
 * @param libraryPath list of native library directory path elements,
 * separated by {@code File.pathSeparator}
 * @param optimizedDirectory directory where optimized {@code .dex} files
 * should be found and written to, or {@code null} to use the default
 * system directory for same
 */
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;
    this.dexElements =
        makeDexElements(splitDexPath(dexPath), optimizedDirectory);
    this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}

關於我們的library path的問題,可以只關注最後的那個splitLibraryPath(),這個地方,實際上即是把傳進來的libraryPath 又丟給splitLibraryPath來獲取library path 的list。可以看一下DexPathList.splitLibraryPath()的實現:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * Splits the given library directory path string into elements
 * using the path separator ({@code File.pathSeparator}, which
 * defaults to {@code ":"} on Android, appending on the elements
 * from the system library path, and pruning out any elements that
 * do not refer to existing and readable directories.
 */
private static File[] splitLibraryPath(String path) {
    /*
     * 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
     *   2. the VM's library path from the system
     *      property for system libraries
     *
     * This order was reversed prior to Gingerbread; see http://b/2933456.
     */
    ArrayList<File> result = splitPaths(
            path, System.getProperty("java.library.path", "."), true);
    return result.toArray(new File[result.size()]);
}

這個地方,是在用兩個部分的library path list來由splitPaths構造最終的那個path list,一個部分是,傳進來的library path,另外一個部分是,像我們前面看到的那個,是system property。然後再來看一下DexPathList.splitPaths()的實現:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * Splits the given path strings into file elements using the path
 * separator, combining the results and filtering out elements
 * that don't exist, aren't readable, or aren't either a regular
 * file or a directory (as specified). Either string may be empty
 * or {@code null}, in which case it is ignored. If both strings
 * are empty or {@code null}, or all elements get pruned out, then
 * this returns a zero-element list.
 */
private static ArrayList<File> splitPaths(String path1, String path2,
        boolean wantDirectories) {
    ArrayList<File> result = new ArrayList<File>();
 
    splitAndAdd(path1, wantDirectories, result);
    splitAndAdd(path2, wantDirectories, result);
    return result;
}

總結一下,ClassLoader的那個findLibrary()實際上會在兩個部分的folder中去尋找System.loadLibrary()要load的那個library,一個部分是,構造ClassLoader時,傳進來的那個library path,即是app folder,另外一個部分是system property。在android系統中,查找要load的library,實際上會在如下3個folder中進行:

  1. /vendor/lib
  2. /system/lib
  3. /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這個文件中):

?
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
/*
 * static String nativeLoad(String filename, ClassLoader loader)
 *
 * Load the specified full path as a dynamic library filled with
 * JNI-compatible methods. Returns null on success, or a failure
 * message on failure.
 */
static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args,
    JValue* pResult)
{
    StringObject* fileNameObj = (StringObject*) args[0];
    Object* classLoader = (Object*) args[1];
    char* fileName = NULL;
    StringObject* result = NULL;
    char* reason = NULL;
    bool success;
 
    assert(fileNameObj != NULL);
    fileName = dvmCreateCstrFromString(fileNameObj);
 
    success = dvmLoadNativeCode(fileName, classLoader, &reason);
    if (!success) {
        const char* msg = (reason != NULL) ? reason : "unknown failure";
        result = dvmCreateStringFromCstr(msg);
        dvmReleaseTrackedAlloc((Object*) result, NULL);
    }
 
    free(reason);
    free(fileName);
    RETURN_PTR(result);
}

可以看到,nativeLoad()實際上只是完成了兩件事情,第一,是調用dvmCreateCstrFromString()將Java 的library path String 轉換到native的String,然後將這個path傳給dvmLoadNativeCode()做load,dvmLoadNativeCode()這個函數的實現在dalvik/vm/Native.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
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
/*
 * Load native code from the specified absolute pathname.  Per the spec,
 * if we've already loaded a library with the specified pathname, we
 * return without doing anything.
 *
 * TODO? for better results we should absolutify the pathname.  For fully
 * correct results we should stat to get the inode and compare that.  The
 * existing implementation is fine so long as everybody is using
 * System.loadLibrary.
 *
 * The library will be associated with the specified class loader.  The JNI
 * spec says we can't load the same library into more than one class loader.
 *
 * Returns "true" on success. On failure, sets *detail to a
 * human-readable description of the error or NULL if no detail is
 * available; ownership of the string is transferred to the caller.
 */
bool dvmLoadNativeCode(const char* pathName, Object* classLoader,
        char** detail)
{
    SharedLib* pEntry;
    void* handle;
    bool verbose;
 
    /* reduce noise by not chattering about system libraries */
    verbose = !!strncmp(pathName, "/system", sizeof("/system")-1);
    verbose = verbose && !!strncmp(pathName, "/vendor", sizeof("/vendor")-1);
 
    if (verbose)
        ALOGD("Trying to load lib %s %p", pathName, classLoader);
 
    *detail = NULL;
 
    /*
     * See if we've already loaded it.  If we have, and the class loader
     * matches, return successfully without doing anything.
     */
    pEntry = findSharedLibEntry(pathName);
    if (pEntry != NULL) {
        if (pEntry->classLoader != classLoader) {
            ALOGW("Shared lib '%s' already opened by CL %p; can't open in %p",
                pathName, pEntry->classLoader, classLoader);
            return false;
        }
        if (verbose) {
            ALOGD("Shared lib '%s' already loaded in same CL %p",
                pathName, classLoader);
        }
        if (!checkOnLoadResult(pEntry))
            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.
     *
     * The current version of the dynamic linker prints detailed information
     * about dlopen() failures.  Some things to check if the message is
     * cryptic:
     *   - make sure the library exists on the device
     *   - verify that the right path is being opened (the debug log message
     *     above can help with that)
     *   - check to see if the library is valid (e.g. not zero bytes long)
     *   - check config/prelink-linux-arm.map to ensure that the library
     *     is listed and is not being overrun by the previous entry (if
     *     loading suddenly stops working on a prelinked library, this is
     *     a good one to check)
     *   - write a trivial app that calls sleep() then dlopen(), attach
     *     to it with "strace -p <pid>" while it sleeps, and watch for
     *     attempts to open nonexistent dependent shared libs
     *
     * This can execute slowly for a large library on a busy system, so we
     * want to switch from RUNNING to VMWAIT while it executes.  This allows
     * the GC to ignore us.
     */
    Thread* self = dvmThreadSelf();
    ThreadStatus oldStatus = dvmChangeStatus(self, THREAD_VMWAIT);
    handle = dlopen(pathName, RTLD_LAZY);
    dvmChangeStatus(self, oldStatus);
 
    if (handle == NULL) {
        *detail = strdup(dlerror());
        ALOGE("dlopen(\"%s\") failed: %s", pathName, *detail);
        return false;
    }
 
    /* create a new entry */
    SharedLib* pNewEntry;
    pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib));
    pNewEntry->pathName = strdup(pathName);
    pNewEntry->handle = handle;
    pNewEntry->classLoader = classLoader;
    dvmInitMutex(&pNewEntry->onLoadLock);
    pthread_cond_init(&pNewEntry->onLoadCond, NULL);
    pNewEntry->onLoadThreadId = self->threadId;
 
    /* try to add it to the list */
    SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);
 
    if (pNewEntry != pActualEntry) {
        ALOGI("WOW: we lost a race to add a shared lib (%s CL=%p)",
            pathName, classLoader);
        freeSharedLibEntry(pNewEntry);
        return checkOnLoadResult(pActualEntry);
    } else {
        if (verbose)
            ALOGD("Added shared lib %s %p", pathName, classLoader);
 
        bool result = true;
        void* vonLoad;
        int version;
 
        vonLoad = dlsym(handle, "JNI_OnLoad");
        if (vonLoad == NULL) {
            ALOGD("No JNI_OnLoad found in %s %p, skipping init",
                pathName, classLoader);
        } 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.)
             */
            OnLoadFunc func = (OnLoadFunc)vonLoad;
            Object* prevOverride = self->classLoaderOverride;
 
            self->classLoaderOverride = classLoader;
            oldStatus = dvmChangeStatus(self, THREAD_NATIVE);
            if (gDvm.verboseJni) {
                ALOGI("[Calling JNI_OnLoad for \"%s\"]", pathName);
            }
            version = (*func)(gDvmJni.jniVm, NULL);
            dvmChangeStatus(self, oldStatus);
            self->classLoaderOverride = prevOverride;
 
            if (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 &&
                version != JNI_VERSION_1_6)
            {
                ALOGW("JNI_OnLoad returned bad version (%d) in %s %p",
                    version, pathName, classLoader);
                /*
                 * 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.
                 */
                result = false;
            } else {
                if (gDvm.verboseJni) {
                    ALOGI("[Returned from JNI_OnLoad for \"%s\"]", pathName);
                }
            }
        }
 
        if (result)
            pNewEntry->onLoadResult = kOnLoadOkay;
        else
            pNewEntry->onLoadResult = kOnLoadFailed;
 
        pNewEntry->onLoadThreadId = 0;
 
        /*
         * Broadcast a wakeup to anybody sleeping on the condition variable.
         */
        dvmLockMutex(&pNewEntry->onLoadLock);
        pthread_cond_broadcast(&pNewEntry->onLoadCond);
        dvmUnlockMutex(&pNewEntry->onLoadLock);
        return result;
    }
}

哇塞,dvmLoadNativeCode()這個函數還真的是有點複雜,那就挑那些跟我們的JNI比較緊密相關的邏輯來看吧。可以認爲這個函數做了下面的這樣一些事情:

  1. 調用dlopen() 打開一個so文件,創建一個handle。
  2. 調用dlsym()函數,查找到so文件中的JNI_OnLoad()這個函數的函數指針。
  3. 執行上一步找到的那個JNI_OnLoad()函數。

至此,大體可以結束System.loadLibrary()的執行過程的分析。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章