美文网首页
android so 加载过程源码分析

android so 加载过程源码分析

作者: kotlon | 来源:发表于2021-02-02 17:21 被阅读0次

    Runtime.loadLibrary() 源码分析

    最近的用户反馈,碰到一个 loadLibrary() 失败的问题,之前对这一个流程一直没有进行细致梳理,现在趁有空,梳理一下。

    loadLibrary() 的流程

    一般情况下,通过 System.loadLibrary() 去加载你需要的 so 库,如下:

    System.loadLibrary("native-lib")
    

    System 调用的代码如下:

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

    实际上,调用的是 Runtime 的 loadLibrary0() 函数,Runtime 是每个 Java 应用的一个运行时,并且getRutime() 获得Runtime 对象是个单例:

    public class Runtime {
        private static Runtime currentRuntime = new Runtime();
      
          public static Runtime getRuntime() {
            return currentRuntime;
        }
    }
    

    最终 loadLibrary0() 的源码如下:

    synchronized void loadLibrary0(ClassLoader loader, String libname) {
        if (libname.indexOf((int)File.separatorChar) != -1) {//判断so名称是否包含文件分隔符,如果包含文件分隔符,则抛出异常
            throw new UnsatisfiedLinkError(
    "Directory separator should not appear in library name: " + libname);
        }
        String libraryName = libname;
        if (loader != null) {
            String filename = loader.findLibrary(libraryName);//通过 ClassLoader 去 findLibrary()
            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 = nativeLoad(filename, loader);//调用 jni 方法,nativeLoad() 方法
            if (error != null) {
                throw new UnsatisfiedLinkError(error);
            }
            return;
        }
    
        String filename = System.mapLibraryName(libraryName);//使用 mapLibraryName() 方法查找实际的 so 库的名称
        List<String> candidates = new ArrayList<String>();
        String lastError = null;
        for (String directory : getLibPaths()) {//遍历so 库目录,尝试加载 so
            String candidate = directory + filename;
            candidates.add(candidate);
    
            if (IoUtils.canOpenReadOnly(candidate)) {
                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);
    }
    

    首先注意到 loadLibrary0() 方法是 synchronize 的,所以如果多个线程调用,则需要等待。 loadLibrary0() 方法主要分为以下几步调用流程:

    1. 检查 so 库名称,是否包含了文件分割符,如果包含则直接抛出异常。
    2. 如果 ClassLoader 不为空,调用 ClassLoader.findLibrary() 去查找so,如果找不到则抛出 XXClassLoader couldn't find XXX.so.
    3. 如果 ClassLoader 不为空,调用 nativeLoad() 方法,去加载 so。源码在libcore/ojluni/src/main/native/Runtime.c,找到则返回。

    后面的分支是 ClassLoader 为空的情况

    1. 如果 ClassLoader 为空,调用 System.mapLibraryName() 去获取so 库的完整名称
    2. 如果 ClassLoader 为空,遍历 getLibPaths()去查找 so,并且获取路径。
    3. 调用 nativeLoad() 方法,使用完整 so 路径去加载 so 库

    综上,我们只要分析 ClassLoader.findLibrary() 和 nativeLoad() 方法即可。

    loader.findLibrary() 方法

    以包名为com.rockets.livedemo 为例子,对应的 ClassLoader 为 PathClassLoader 如下:

    dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.rockets.livedemo-ABxsabDzcYLzAaEA5wukSw==/base.apk"],nativeLibraryDirectories=[/data/app/com.rockets.livedemo-ABxsabDzcYLzAaEA5wukSw==/lib/arm, /data/app/com.rockets.livedemo-ABxsabDzcYLzAaEA5wukSw==/base.apk!/lib/armeabi-v7a, /system/lib]]]
    

    其中,fileName 为:

    /data/app/com.rockets.livedemo-ABxsabDzcYLzAaEA5wukSw==/lib/arm/libnative-lib.so
    

    这里使用的 ClassLoader 是 PatchClassLoader ,其并没有实现 findLibrary() 方法,而是由其子类 BaseDexClassLoader 实现的,源码如下:

    this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted); 
    
    @Override
     public String findLibrary(String name) {
       return pathList.findLibrary(name);
    }
    

    这里交由 DexPathList 的 findLibrary() 方法去实现,如下:

    public String findLibrary(String libraryName) {
        String fileName = System.mapLibraryName(libraryName);
    
        for (NativeLibraryElement element : nativeLibraryPathElements) {
            String path = element.findNativeLibrary(fileName);
    
            if (path != null) {
                return path;
            }
        }
    
        return null;
    }
    

    这里会先调用System.mapLibraryName() 去查找 so 库的完整名称,具体的可以看后面针对这个函数的分析,总之这里会从你传入的 native-lib 名称,变成 libnative-lib.so 这样完整的名称。

    拿到完整的 so 库的名称之后,会通过遍历 nativeLibraryPathElements 去查找 so 库,那么 nativeLibraryPathElemts 是怎么初始化的?如下:

    NativeLibraryElement[] nativeLibraryPathElements;
    /** List of application native library directories. */
    private final List<File> nativeLibraryDirectories;
    /** List of system native library directories. */
    private final List<File> systemNativeLibraryDirectories;
    
    this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
    this.systemNativeLibraryDirectories =
              splitPaths(System.getProperty("java.library.path"), true);
         List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
         allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
    
    this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
    

    nativeLibraryDirectories 的获取路径是在安装过程,赋值到 ApplicationInfo 中去的,如下:

    public class ApplicationInfo extends PackageItemInfo implements Parcelable {
    /**
     * Full path to the directory where native JNI libraries are stored.
     */
    public String nativeLibraryDir;
    }
    

    具体细节不分析,总的来说,如果 apk 是系统应用,则 nativeLibraryDir 为 /system/lib/xxx 或者 system/app/xxx/lib。但是对于我们自己的应用来说,这个值为 data/app/包名/lib。

    以华为手机,安装抖音极速版为例:

    抖音极速版的lib 目录为 /data/data/com.ss.android.ugc.aweme.lite/lib,其中 com.ss.android.ugc.aweme.lite 为包名。

    systemNativeLibraryDirectories 是通过静态方法 getProperty() 去获取,这里的代码比较复杂,直接跳过,最终我们获取到的路径是:

    //http://androidxref.com/9.0.0_r3/xref/bionic/linker/linker.cpp
    #if defined(__LP64__)
    static const char* const kSystemLibDir     = "/system/lib64";
    static const char* const kOdmLibDir        = "/odm/lib64";
    static const char* const kVendorLibDir     = "/vendor/lib64";
    static const char* const kAsanSystemLibDir = "/data/asan/system/lib64";
    static const char* const kAsanOdmLibDir    = "/data/asan/odm/lib64";
    static const char* const kAsanVendorLibDir = "/data/asan/vendor/lib64";
    #else
    static const char* const kSystemLibDir     = "/system/lib";
    static const char* const kOdmLibDir        = "/odm/lib";
    static const char* const kVendorLibDir     = "/vendor/lib";
    static const char* const kAsanSystemLibDir = "/data/asan/system/lib";
    static const char* const kAsanOdmLibDir    = "/data/asan/odm/lib";
    static const char* const kAsanVendorLibDir = "/data/asan/vendor/lib";
    #endif
    
    static const char* const kAsanLibDirPrefix = "/data/asan";
    
    static const char* const kDefaultLdPaths[] = {
      kSystemLibDir,
      kOdmLibDir,
      kVendorLibDir,
      nullptr
    };
    

    也就是说,如果是有宏定义 LP64 则定义为 /system/lib64,/odm/lib64,/vendor/lib64,如果没有这个宏定义则为 /system/lib,/odm/lib,/odm/lib。这里没去细究 LP64 的定义,从字面理解就是 64 位的系统,会有这个宏定义。

    所以,如果是 64 位系统,systemNativeLibraryDirectories 的值为 /system/lib64,/odm/lib64,/vendor/lib64,接着会调用 makePathElements(this.systemNativeLibraryDirectories) 去构造 nativeLibraryPathElements,最终构造了一系列的 NativeLibaryElemt,其实 NativeLibraryElemt 就是包括了两个字段 File zip, String zipDir,大概如下:

        public NativeLibraryElement(File dir) {
            this.path = dir;
            this.zipDir = null;
        }
    
    • NativeLibraryElemt(File(data/app/包名/lib));
    • NativeLibraryElemt(File(/system/lib64));
    • NativeLibraryElemt(File(/odm/lib64));
    • NativeLibraryElemt(File(/vendor/lib64));

    最终,调用 NativeLibraryElemt 的 findLibrary()方法,如下:

        public String findNativeLibrary(String name) {
            maybeInit();
    
            if (zipDir == null) {//这里为 null
                String entryPath = new File(path, name).getPath();
                if (IoUtils.canOpenReadOnly(entryPath)) {
                    return entryPath;
                }
            } else if (urlHandler != null) {
                // Having a urlHandler means the element has a zip file.
                // In this case Android supports loading the library iff
                // it is stored in the zip uncompressed.
                String entryName = zipDir + '/' + name;
                if (urlHandler.isEntryStored(entryName)) {
                  return path.getPath() + zipSeparator + entryName; 
                }
            }
    
            return null;
        }
    

    由于 zipDir 为空,所以这里最后返回的就是 File(path, name).getPath(),也就是默认的文件夹名+完整的so库名,比如 /data/data/包名/libnative-lib.so 。

    nativeLoad() 方法

    从上一步拿到的完整路径之后。

    nativeLoad() 方法位于 Runtime.c 文件中,如下:

    JNIEXPORT jstring JNICALL
    Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename,
                       jobject javaLoader)
    {
        return JVM_NativeLoad(env, javaFilename, javaLoader);
    }
    

    最终,会调用到 java_vm_ext.cc 中的,LoadNativeLibrary() 方法中

    bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
                                      const std::string& path,
                                      jobject class_loader,
                                      std::string* error_msg) {
      error_msg->clear();
    
      // See if we've already loaded this library.  If we have, and the class loader
      // matches, return successfully without doing anything.
      // TODO: for better results we should canonicalize the pathname (or even compare
      // inodes). This implementation is fine if everybody is using System.loadLibrary.
      SharedLibrary* library;
      Thread* self = Thread::Current();
      {
        // TODO: move the locking (and more of this logic) into Libraries.
        MutexLock mu(self, *Locks::jni_libraries_lock_);
        library = libraries_->Get(path);
      }
      void* class_loader_allocator = nullptr;
      {
        ScopedObjectAccess soa(env);
        // As the incoming class loader is reachable/alive during the call of this function,
        // it's okay to decode it without worrying about unexpectedly marking it alive.
        ObjPtr<mirror::ClassLoader> loader = soa.Decode<mirror::ClassLoader>(class_loader);
    
        ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
        if (class_linker->IsBootClassLoader(soa, loader.Ptr())) {
          loader = nullptr;
          class_loader = nullptr;
        }
    
        class_loader_allocator = class_linker->GetAllocatorForClassLoader(loader.Ptr());
        CHECK(class_loader_allocator != nullptr);
      }
      if (library != nullptr) {
        // Use the allocator pointers for class loader equality to avoid unnecessary weak root decode.
        if (library->GetClassLoaderAllocator() != class_loader_allocator) {
          // The library will be associated with class_loader. The JNI
          // spec says we can't load the same library into more than one
          // class loader.
          //
          // This isn't very common. So spend some time to get a readable message.
          auto call_to_string = [&](jobject obj) -> std::string {
            if (obj == nullptr) {
              return "null";
            }
            // Handle jweaks. Ignore double local-ref.
            ScopedLocalRef<jobject> local_ref(env, env->NewLocalRef(obj));
            if (local_ref != nullptr) {
              ScopedLocalRef<jclass> local_class(env, env->GetObjectClass(local_ref.get()));
              jmethodID to_string = env->GetMethodID(local_class.get(),
                                                     "toString",
                                                     "()Ljava/lang/String;");
              DCHECK(to_string != nullptr);
              ScopedLocalRef<jobject> local_string(env,
                                                   env->CallObjectMethod(local_ref.get(), to_string));
              if (local_string != nullptr) {
                ScopedUtfChars utf(env, reinterpret_cast<jstring>(local_string.get()));
                if (utf.c_str() != nullptr) {
                  return utf.c_str();
                }
              }
              env->ExceptionClear();
              return "(Error calling toString)";
            }
            return "null";
          };
          std::string old_class_loader = call_to_string(library->GetClassLoader());
          std::string new_class_loader = call_to_string(class_loader);
          StringAppendF(error_msg, "Shared library \"%s\" already opened by "
              "ClassLoader %p(%s); can't open in ClassLoader %p(%s)",
              path.c_str(),
              library->GetClassLoader(),
              old_class_loader.c_str(),
              class_loader,
              new_class_loader.c_str());
          LOG(WARNING) << *error_msg;
          return false;
        }
        VLOG(jni) << "[Shared library \"" << path << "\" already loaded in "
                  << " ClassLoader " << class_loader << "]";
        if (!library->CheckOnLoadResult()) {
          StringAppendF(error_msg, "JNI_OnLoad failed on a previous attempt "
              "to load \"%s\"", path.c_str());
          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.
    
      // Below we dlopen but there is no paired dlclose, this would be necessary if we supported
      // class unloading. Libraries will only be unloaded when the reference count (incremented by
      // dlopen) becomes zero from dlclose.
    
      // Retrieve the library path from the classloader, if necessary.
      ScopedLocalRef<jstring> library_path(env, GetLibrarySearchPath(env, class_loader));
    
      Locks::mutator_lock_->AssertNotHeld(self);
      const char* path_str = path.empty() ? nullptr : path.c_str();
      bool needs_native_bridge = false;
      void* handle = android::OpenNativeLibrary(env,
                                                runtime_->GetTargetSdkVersion(),
                                                path_str,
                                                class_loader,
                                                library_path.get(),
                                                &needs_native_bridge,
                                                error_msg);
    
      VLOG(jni) << "[Call to dlopen(\"" << path << "\", RTLD_NOW) returned " << handle << "]";
    
      if (handle == nullptr) {
        VLOG(jni) << "dlopen(\"" << path << "\", RTLD_NOW) failed: " << *error_msg;
        return false;
      }
    
      if (env->ExceptionCheck() == JNI_TRUE) {
        LOG(ERROR) << "Unexpected exception:";
        env->ExceptionDescribe();
        env->ExceptionClear();
      }
      // Create a new entry.
      // TODO: move the locking (and more of this logic) into Libraries.
      bool created_library = false;
      {
        // Create SharedLibrary ahead of taking the libraries lock to maintain lock ordering.
        std::unique_ptr<SharedLibrary> new_library(
            new SharedLibrary(env,
                              self,
                              path,
                              handle,
                              needs_native_bridge,
                              class_loader,
                              class_loader_allocator));
    
        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) {
        LOG(INFO) << "WOW: we lost a race to add shared library: "
            << "\"" << path << "\" ClassLoader=" << class_loader;
        return library->CheckOnLoadResult();
      }
      VLOG(jni) << "[Added shared library \"" << path << "\" for ClassLoader " << class_loader << "]";
    
      bool was_successful = false;
      void* sym = library->FindSymbol("JNI_OnLoad", nullptr);
      if (sym == nullptr) {
        VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
        was_successful = true;
      } 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.)
        ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
        self->SetClassLoaderOverride(class_loader);
    
        VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]";
        typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
        JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
        int version = (*jni_on_load)(this, nullptr);
    
        if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
          // Make sure that sigchain owns SIGSEGV.
          EnsureFrontOfChain(SIGSEGV);
        }
    
        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 (JavaVMExt::IsBadJniVersion(version)) {
          StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"%s\": %d",
                        path.c_str(), version);
          // 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.
        } else {
          was_successful = true;
        }
        VLOG(jni) << "[Returned " << (was_successful ? "successfully" : "failure")
                  << " from JNI_OnLoad in \"" << path << "\"]";
      }
    
      library->SetResult(was_successful);
      return was_successful;
    }
    

    上述流程比较复杂,可以简单概述为以下几个步骤:

    1. 先判断是否已经加载过 so 库,并且判断加载 so 的 ClassLoader 是不是同一个 ClassLoader。
    2. 调用 android::OpenNativeLibrary() 去打开 so

    上面的逻辑关注点在 OpenNativeLibrary() 这里,会将完整的路径,ClassLoader 的引用传递进去,打开 so。

      void* handle = android::OpenNativeLibrary(env,
                                                runtime_->GetTargetSdkVersion(),
                                                path_str,
                                                class_loader,
                                                library_path.get(),
                                                &needs_native_bridge,
                                                error_msg);
    

    最终,经过层层调用会进去到 linker.cpp 中,调用 dl_open() 方法,将so 加载到内存中,

    mapLibrary()

    这个函数主要作用是使用传入的名称,拼接完整的 so 库的名称(不包含路径),具体的实现源码在 System.c 文件中,如下:

    JNIEXPORT jstring JNICALL
    System_mapLibraryName(JNIEnv *env, jclass ign, jstring libname)
    {
        int len;
        int prefix_len = (int) strlen(JNI_LIB_PREFIX);//JNI_LIB_PREFIX 就是 lib
        int suffix_len = (int) strlen(JNI_LIB_SUFFIX);//JNI_LIB_SUFFIX 就是 .so
    
        jchar chars[256];
        if (libname == NULL) {
            JNU_ThrowNullPointerException(env, 0);
            return NULL;
        }
        len = (*env)->GetStringLength(env, libname);
        if (len > 240) {//so 名称不能大于 240
            JNU_ThrowIllegalArgumentException(env, "name too long");
            return NULL;
        }
        cpchars(chars, JNI_LIB_PREFIX, prefix_len);
        (*env)->GetStringRegion(env, libname, 0, len, chars + prefix_len);
        len += prefix_len;
        cpchars(chars + len, JNI_LIB_SUFFIX, suffix_len);
        len += suffix_len;
    
        return (*env)->NewString(env, chars, len);
    }
    

    其中,涉及到的宏定义如下,在源码 jvm_md.h 中:

    #define JNI_LIB_PREFIX "lib"
    #ifdef __APPLE__
    #define JNI_LIB_SUFFIX ".dylib"
    #define VERSIONED_JNI_LIB_NAME(NAME, VERSION) JNI_LIB_PREFIX NAME "." VERSION JNI_LIB_SUFFIX
    #else
    #define JNI_LIB_SUFFIX ".so"
    #define VERSIONED_JNI_LIB_NAME(NAME, VERSION) JNI_LIB_PREFIX NAME JNI_LIB_SUFFIX "." VERSION
    #endif
    #define JNI_LIB_NAME(NAME) JNI_LIB_PREFIX NAME JNI_LIB_SUFFIX
    

    这里判断 so 名称不能多于 240 个字符。

    上述方法将传入的 so 名称进行拼接,例如传入 audio_shared 名称,最后会生成 libaudio_shared.so 这个完整名称。

    相关文章

      网友评论

          本文标题:android so 加载过程源码分析

          本文链接:https://www.haomeiwen.com/subject/ewiytltx.html