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

深入理解 System.loadLibrary

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

    原文链接: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(String 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   ·   实用小工具   ·   1099 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 18:57 · PVG 02:57 · LAX 10:57 · JFK 13:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.