android so 加载过程源码分析

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 candidates = new ArrayList();
    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 nativeLibraryDirectories;
/** List of system native library directories. */
private final List systemNativeLibraryDirectories;

this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
          splitPaths(System.getProperty("java.library.path"), true);
     List 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 loader = soa.Decode(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 local_ref(env, env->NewLocalRef(obj));
        if (local_ref != nullptr) {
          ScopedLocalRef 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 local_string(env,
                                               env->CallObjectMethod(local_ref.get(), to_string));
          if (local_string != nullptr) {
            ScopedUtfChars utf(env, reinterpret_cast(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 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 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 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(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 加载过程源码分析)