深入理解 System.loadLibrary - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
pqpo
V2EX    程序员

深入理解 System.loadLibrary

  •  
  •   pqpo 2017-05-31 17:04:56 +08:00 8177 次点击
    这是一个创建于 3072 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文链接:https://pqpo.me/2017/05/31/system-loadlibrary/

    本文主要讲述 Android 加载动态链接库的过程及其涉及的底层原理。 会先以一个 Linux 的例子描述 native 层加载动态链接库的过程, 再从 Java 层由浅入深分析 System.loadLibrary

    如果对 JNI 技术不太熟悉,可以先看先前关于 JNI 的文章《理解 JNI 技术》 首先我们知道在 Android 中加载一个动态链接库非常简单,只需一行代码:

     System.loadLibrary("native-lib"); 

    事实上这是 Java 提供的 API,对于 Java 层实现基本一致,但是对于不同的 JVM 其底层(native)实现会有所差异。本文分析的代码基于 Android 6.0 系统。 看过《理解 JNI 技术》的应该知道上述代码执行过程中会调用 native 层的 JNI_OnLoad 方法,一般用于动态注册 native 方法。

    # Linux 系统加载动态库过程分析

    Android 是基于 Linux 系统的,那么在 Linux 系统下是如何加载动态链接库的呢? 如果对此不敢兴趣或者对 C++比较陌生的可以先跳到后面阅读 Android Java 层实现部分,但是最终还是会涉及到 native 代码。 当然你也可以直接跳到末尾看结论。

    Linux 环境下加载动态库主要包括如下方法,位于头文件#include <dlfcn.h>中:

     void *dlopen(const char *filename, int flag); //打开动态链接库 char *dlerror(void); //获取错误信息 void *dlsym(void *handle, const char *symbol); //获取方法指针 int dlclose(void *handle); //关闭动态链接库 

    在 Linux 环境下可以通过下述命令查看具体使用方法:

     man dlopen 

    下面我们来看一下如何在 Linux 环境下创建动态链接库,并加载使用动态链接库中的函数。 下面是一个简单的 C++文件,作为动态链接库包含计算相关函数: [caculate.cpp]

     extern "C" int add(int a, int b) { return a + b; } extern "C" int mul(int a, int b) { return a*b; } 

    对于 C++文件函数前的 extern “ C ” 不能省略,原因是 C++编译之后会修改函数名,之后动态加载函数的时候会找不到该函数。加上 extern “ C ”是告诉编译器以 C 的方式编译,不用修改函数名。 然后通过下述命令编译成动态链接库:

     g++ -fPIC -shared caculate.cpp -o libcaculate.so 

    这样会在同级目录生成一个动态库文件:libcaculate.so 然后编写加载动态库的代码: [main_call.cpp]

     #include <iostream> #include <dlfcn.h> using namespace std; static const char * const LIB_PATH = "./libcaculate.so"; typedef int (*CACULATE_FUNC)(int, int); int main() { void* symAdd = nullptr; void* symMul = nullptr; char* errorMsg = nullptr; dlerror(); //1.打开动态库,拿到一个动态库句柄 void* handle = dlopen(LIB_PATH, RTLD_NOW); if(handle == nullptr) { cout << "load error!" << endl; return -1; } // 查看是否有错误 if ((errorMsg = dlerror()) != nullptr) { cout << "errorMsg:" << errorMsg << endl; return -1; } cout << "load success!" << endl; //2.通过句柄和方法名获取方法指针地址 symAdd = dlsym(handle, "add"); if(symAdd == nullptr) { cout << "dlsym failed!" << endl; if ((errorMsg = dlerror()) != nullptr) { cout << "error message:" << errorMsg << endl; return -1; } } //3.将方法地址强制类型转换成方法指针 CACULATE_FUNC addFunc = reinterpret_cast(symAdd); //4.调用动态库中的方法 cout << "1 + 2 = " << addFunc(1, 2) << endl; //5.通过句柄关闭动态库 dlclose(handle); return 0; } 

    还是比较容易理解的,主要就用了上面提到的 4 个函数,过程如下:

    1. 打开动态库,拿到一个动态库句柄
    2. 通过句柄和方法名获取方法指针地址
    3. 将方法地址强制类型转换成方法指针
    4. 调用动态库中的方法
    5. 通过句柄关闭动态库

    中间会使用 dlerror 检测是否有错误。 有必要解释一下的是方法指针地址到方法指针的转换,为了方便这里定义了一个方法指针的别名:

     typedef int (*CACULATE_FUNC)(int, int); 

    指明该方法接受两个 int 类型参数返回一个 int 值。 拿到地址之后强制类型转换成方法指针用于调用:

     CACULATE_FUNC addFunc = reinterpret_cast(symAdd); 

    最后只要编译运行即可:

     g++ -std=c++11 -ldl main_call.cpp -o main .main 

    上面就是 Linux 环境下创建动态库,加载并使用动态库的全部过程。由于 Android 基于 Linux 系统,所以我们有理由猜测 Android 系统底层也是通过这种方式加载并使用动态库的。下面开始从 Android 上层 Java 代码开始分析。

    # System.loadLibrary

    [System.java]

     public static void loadLibrary(String libName) { Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader()); } 

    此处 VMStack.getCallingClassLoader()拿到的是调用者的 ClassLoader,一般情况下是 PathClassLoader。 [Runtime.java]

     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 candidates = new ArrayList(); 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); } 

    这里根据 ClassLoader 是否存在分了两种情况,当 ClasssLoader 存在的时候通过 loader 的 findLibrary()查看目标库所在路径,当 ClassLoader 不存在的时候通过 mLibPaths 加载路径。最终都会调用 doLoad 加载动态库。 下面只讲 ClassLoader 存在的情况,不存在的情况更加简单。findLibrary 位于 PathClassLoader 的父类 BaseDexClassLoader 中: [BaseDexClassLoader.java]

     @Override public String findLibrary(String name) { return pathList.findLibrary(name); } 

    其中 pathList 的类型为 DexPathList,它的构造方法如下: [DexPathList.java]

     public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) { // 省略其他代码 this.nativeLibraryDirectories = splitPaths(libraryPath, false); this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true); List 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; } } 

    这里收集了 Apk 的 so 目录,一般位于:/data/app/${package-name}/lib/arm/ 还有系统的 so 目录:System.getProperty(“ java.library.path ”),可以打印看一下它的值:/vendor/lib:/system/lib,其实就是前后两个目录,事实上 64 位系统是 /vendor/lib64:/system/lib64。 最终查找 so 文件的时候就会在这三个路径中查找,优先查找 APK 目录。 [DexPathList.java]

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

    String fileName = System.mapLibraryName(libraryName)的实现很简单: [System.java]

     public static String mapLibraryName(Sting nickname) { if (nickname == null) { throw new NullPointerException("nickname == null"); } return "lib" + nickname + ".so"; } 

    也就是为什么动态库的命名必须以 lib 开头了。 然后会遍历 nativeLibraryPathElements 查找某个目录下是否有改文件,有的话就返回: [DexPathList.java]

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

    回到 Runtime 的 loadLibrary 方法,通过 ClassLoader 找到目标文件之后会调用 doLoad 方法: [Runtime.java]

     private String doLoad(String name, ClassLoader loader) { String ldLibraryPath = null; String dexPath = null; if (loader == null) { ldLibraryPath = System.getProperty("java.library.path"); } else if (loader instanceof BaseDexClassLoader) { BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader; ldLibraryPath = dexClassLoader.getLdLibraryPath(); } synchronized (this) { return nativeLoad(name, loader, ldLibraryPath); } } 

    这里的 ldLibraryPath 和之前所述类似,loader 为空时使用系统目录,否则使用 ClassLoader 提供的目录,ClassLoader 提供的目录中包括 apk 目录和系统目录。 最后调用 native 代码: [java_lang_Runtime.cc]

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

    继续调用 JavaVMExt 对象的 LoadNativeLibrary 方法: [java_vm_ext.cc]

     bool JavaVMExt::LoadNativeLibrary(JNIEnv* env, const std::string& path, jobject class_loader,std::string* error_msg) { error_msg->clear(); SharedLibrary* library; Thread* self = Thread::Current(); { MutexLock mu(self, *Locks::jni_libraries_lock_); library = libraries_->Get(path); } if (library != nullptr) { if (env->IsSameObject(library->GetClassLoader(), class_loader) == JNI_FALSE) { 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; } if (!library->CheckOnLoadResult()) { StringAppendF(error_msg, "JNI_OnLoad failed on a previous attempt " "to load \"%s\"", path.c_str()); return false; } return true; } Locks::mutator_lock_->AssertNotHeld(self); const char* path_str = path.empty() ? nullptr : path.c_str(); //1.打开动态链接库 void* handle = dlopen(path_str, RTLD_NOW); bool needs_native_bridge = false; if (handle == nullptr) { if (android::NativeBridgeIsSupported(path_str)) { handle = android::NativeBridgeLoadLibrary(path_str, RTLD_NOW); needs_native_bridge = true; } } 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(); } bool created_library = false; { std::unique_ptr 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) { return library->CheckOnLoadResult(); } bool was_successful = false; void* sym; if (needs_native_bridge) { library->SetNeedsNativeBridge(); sym = library->FindSymbolWithNativeBridge("JNI_OnLoad", nullptr); } else { //2.获取方法地址 sym = dlsym(handle, "JNI_OnLoad"); } if (sym == nullptr) { VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]"; was_successful = true; } else { ScopedLocalRef old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride())); self->SetClassLoaderOverride(class_loader); typedef int (*JNI_OnLoadFn)(JavaVM*, void*); //3.强制类型转换成函数指针 JNI_OnLoadFn jni_on_load = reinterpret_cast(sym); //4.调用函数 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); } else { was_successful = true; } return was_successful; } 

    这个函数有点长,主要看注释的地方。开始的时候会查看动态库是否已经加载过,之后会通过 dlopen 打开动态共享库。然后会获取动态库中的 JNI_OnLoad 方法,如果有的话调用之。最后会通过 JNI_OnLoad 的返回值确定是否加载成功:

     static bool IsBadJniVersion(int version) { // We don't support JNI_VERSION_1_1\. These are the only other valid versions. return version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 && version != JNI_VERSION_1_6; } 

    这也是为什么在 JNI_OnLoad 函数中必须正确返回的原因。 可以看到最终没有调用 dlclose,当然也不能调用,这里只是加载,真正的函数调用还没有开始,之后就会使用 dlopen 拿到的句柄来访问动态库中的方法了。 看完这篇文章我们明确了几点:

    1. System.loadLibrary 会优先查找 apk 中的 so 目录,再查找系统目录,系统目录包括:/vendor/lib(64),/system/lib(64)
    2. System.loadLibrary 加载过程中会调用目标库的 JNI_OnLoad 方法,我们可以在动态库中加一个 JNI_OnLoad 方法用于动态注册
    3. 如果加了 JNI_OnLoad 方法,其的返回值为 JNI_VERSION_1_2,JNI_VERSION_1_4,JNI_VERSION_1_6 其一。我们一般使用 JNI_VERSION_1_4 即可
    4. Android 动态库的加载与 Linux 一致使用 dlopen 系列函数,通过动态库的句柄和函数名称来调用动态库的函数
    5 条回复    2021-02-19 15:02:05 +08:00
    thinkloki
        1
    thinkloki  
       2017-05-31 17:21:42 +08:00
    不错可以。给个赞
    pqpo
        2
    pqpo  
    OP
       2017-05-31 17:31:56 +08:00
    @thinkloki:) 谢谢
    lrannn
        3
    lrannn  
       2017-06-02 17:52:18 +08:00
    学习了,一直进行 native 开发,竟然不知道这些,太惭愧了我
    pqpo
        4
    pqpo  
    OP
       2017-06-03 11:35:23 +08:00
    @lrannn android native 开发吗?
    flintlovesam
        5
    flintlovesam  
       2021-02-19 15:02:05 +08:00
    不错 so 加载讲的不错 后面 java 代码有点看不明白(不太会 java ) 前面 Linux 加载很精彩 收藏
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5604 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 01:35 PVG 09:35 LAX 18:35 JFK 21:35
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86