天天看點

Android so動态庫加載原理分析

作者:網際網路雜談A

Java動态庫加載

對于Android上層的Java代碼來說,隻需要一行代碼就即可完成so動态庫的加載過程:

System.load("/data/local/tmp/lib_test_jni.so");
System.loadLibrary("lib_test_jni");           

以上兩個方法都用于加載動态庫,兩者的差別如下:

  • 加載的路徑不同:System.load(String filename)是指定動态庫的完整路徑名;而System.loadLibrary(String libname)則隻會從指定lib目錄下查找,并加上lib字首和.so字尾;
  • 自動加載庫的依賴庫的不同:System.load(String filename)不會自動加載依賴庫;而System.loadLibrary(String libname)會自動加載依賴庫。

動态庫加載原理分析

(1)System.loadLibrary

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

(2)Runtime.loadLibrary

void loadLibrary(String libraryName, ClassLoader loader) {
    if (loader != null) {
        //根據動态庫名檢視相應動态庫的檔案路徑[見小節2.3]
        String filename = loader.findLibrary(libraryName);
        if (filename == null) {
            throw new UnsatisfiedLinkError(...);
        }
        //成功執行完doLoad,則傳回 [見小節2.4]
        String error = doLoad(filename, loader);
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
        return;
    }

    //當loader為空的情況下執行[見小節2.3.3]
    String filename = System.mapLibraryName(libraryName);
    List<String> candidates = new ArrayList<String>();
    String lastError = null;

    //此處的mLibPaths取值 [見小節3.1]
    for (String directory : mLibPaths) {
        String candidate = directory + filename;
        candidates.add(candidate);
        //判斷目标動态庫是否存在
        if (IoUtils.canOpenReadOnly(candidate)) {
            //成功執行完doLoad,則傳回 [見小節2.4]
            String error = doLoad(candidate, loader);
            if (error == null) {
                return; //則傳回
            }
        }
    }
    ...
    throw new UnsatisfiedLinkError(...);
}           

該方法主要是找到目标庫所在路徑後調用doLoad來真正用于加載動态庫,其中會根據loader是否為空中間過程略有不同,分兩種情況:

  • 當loader不為空時, 則通過loader.findLibrary()檢視目标庫所在路徑;
  • 當loader為空時, 則從預設目錄mLibPaths下來查找是否存在該動态庫;

(3)BaseDexClassLoader.findLibrary

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);
        //dexPath一般是指apk所在路徑【小節2.3.1】
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    public String findLibrary(String name) {
        return pathList.findLibrary(name); //[見小節2.3.2]
    }
}           

ClassLoader一般來說都是PathClassLoader,從該對象的findLibrary說起. 由于PathClassLoader繼承于 BaseDexClassLoader對象, 并且沒有覆寫該方法, 故調用其父類所對應的方法.

(4)DexPathList初始化

public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
    ...
    this.definingContext = definingContext;

    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    //記錄所有的dexFile檔案
    this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);

    //app目錄的native庫
    this.nativeLibraryDirectories = splitPaths(libraryPath, false);
    //系統目錄的native庫
    this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
    List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
    allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
    //記錄所有的Native動态庫
    this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null,
                                                      suppressedExceptions);
    ...
}           

DexPathList初始化過程,主要功能是收集以下兩個變量資訊:

  1. dexElements: 記錄所有的dexFile檔案
  2. nativeLibraryPathElements: 記錄所有的Native動态庫, 包括app目錄的native庫和系統目錄的native庫.

(5)DexPathList.findLibrary

public String findLibrary(String libraryName) {
    //[見小節2.3.3]
    String fileName = System.mapLibraryName(libraryName);
    for (Element element : nativeLibraryPathElements) {
        //[見小節2.3.4]
        String path = element.findNativeLibrary(fileName);
        if (path != null) {
            return path;
        }
    }
    return null;
}           

從所有的動态庫nativeLibraryPathElements(包含兩個系統路徑)查詢是否存在比對的。一般地,64位系統的nativeLibraryPathElements取值:

  • /data/app/[packagename]-1/lib/arm64
  • /vendor/lib64
  • /system/lib64

(6)System.mapLibraryName

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

該方法的功能是将xxx動态庫的名字轉換為libxxx.so

(7)Element.findNativeLibrary

final class DexPathList {
    static class Element {

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

周遊查詢,一旦找到則傳回所找到的目标動态庫. 接下來, 再回到[小節2.2]來看看動态庫的加載doLoad:

(8)Runtime.doLoad

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) {
        //[見小節2.4.1]
        return nativeLoad(name, loader, ldLibraryPath);
    }
}           

此處ldLibraryPath有兩種情況:

  • 當loader為空,則ldLibraryPath為系統目錄下的Native庫;
  • 當lodder不為空,則ldLibraryPath為app目錄下的native庫;

(9)nativeLoad

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();
    //[見小節2.5]
    bool success = vm->LoadNativeLibrary(env, filename.c_str(), javaLoader, &error_msg);
    if (success) {
      return nullptr;
    }
  }

  env->ExceptionClear();
  return env->NewStringUTF(error_msg.c_str());
}           

nativeLoad方法經過jni所對應的方法是Runtime_nativeLoad()。

(10)LoadNativeLibrary

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) {
      //不能加載同一個采用多個不同的ClassLoader
      return false;
    }
    ...
    return true;
  }

  const char* path_str = path.empty() ? nullptr : path.c_str();
  //通過dlopen打開動态共享庫.該庫不會立刻被解除安裝直到引用技術為空.
  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;
  }


  bool created_library = false;
  {
    std::unique_ptr<SharedLibrary> new_library(
        new SharedLibrary(env, self, path, handle, class_loader));
    MutexLock mu(self, *Locks::jni_libraries_lock_);
    library = libraries_->Get(path);
    if (library == nullptr) {
      library = new_library.release();
      //建立共享庫,并添加到清單
      libraries_->Put(path, library);
      created_library = true;
    }
  }
  ...

  bool was_successful = false;
  void* sym;
  //查詢JNI_OnLoad符号所對應的方法
  if (needs_native_bridge) {
    library->SetNeedsNativeBridge();
    sym = library->FindSymbolWithNativeBridge("JNI_OnLoad", nullptr);
  } else {
    sym = dlsym(handle, "JNI_OnLoad");
  }

  if (sym == nullptr) {
    was_successful = true;
  } else {
    //需要先覆寫目前ClassLoader.
    ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
    self->SetClassLoaderOverride(class_loader);

    typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
    JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
    // 真正調用JNI_OnLoad()方法的過程
    int version = (*jni_on_load)(this, nullptr);

    if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
      fault_manager.EnsureArtActionInFrontOfSignalChain();
    }
    //執行完成後, 需要恢複到原來的ClassLoader
    self->SetClassLoaderOverride(old_class_loader.get());
    ...
  }

  library->SetResult(was_successful);
  return was_successful;
}           

該方法的主要工作過程:

  1. 檢查該動态庫是否已加載;
  2. 通過dlopen打開動态共享庫;
  3. 建立SharedLibrary共享庫,并添加到libraries_清單;
  4. 通過dlsym擷取JNI_OnLoad符号所對應的方法, 并調用該方法.

小結

System.loadLibrary()和System.load()都用于加載動态庫,loadLibrary()可以友善自動加載依賴庫,load()可以友善地指定具體路徑的動态庫。對于loadLibrary()會将将xxx動态庫的名字轉換為libxxx.so,再從/data/app/[packagename]-1/lib/arm64,/vendor/lib64,/system/lib64等路徑中查詢對應的動态庫。無論哪種方式,最終都會調用到LoadNativeLibrary()方法,該方法主要操作:

  • 通過dlopen打開動态共享庫。
  • 通過dlsym擷取JNI_OnLoad符号所對應的方法。
  • 調用該加載庫中的JNI_OnLoad()方法。

繼續閱讀