android中System.loadLibrary和System.load源码解析

一 概述

System.loadLibrary("jpeg"):加载动态库名称如libjpeg
System.load("D:\java\Test.so");加载so的绝对路径

二 System.load源码分析
//System类
  public static void load(String filename) {
        //Reflection.getCallerClass()返回的是当前调用类的字节码Class
        Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
    }

  //Runtime类
  synchronized void load0(Class<?> fromClass, String filename) {
        //1.判断是否是绝对路径,即路径中是否包含'/'
        if (!(new File(filename).isAbsolute())) {
            throw new UnsatisfiedLinkError(
                "Expecting an absolute path of the library: " + filename);
        }
        if (filename == null) {
            throw new NullPointerException("filename == null");
        }
        //2.调用 nativeLoad进行加载动态库,参数是文件路径和ClassLoader对象
        String error = nativeLoad(filename, fromClass.getClassLoader());
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
    }

三 System.loadLibrary源码解析

//System类
public static void loadLibrary(String libname) {
        Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
    }
//Runtime类
 void loadLibrary0(Class<?> fromClass, String libname) {
        //获取当前类的ClassLoader,在android中一般是PathClassLoader
        ClassLoader classLoader = ClassLoader.getClassLoader(fromClass);
        loadLibrary0(classLoader, fromClass, libname);
    }

private synchronized void loadLibrary0(ClassLoader loader, Class<?> callerClass, String libname) {
        if (libname.indexOf((int)File.separatorChar) != -1) {
            throw new UnsatisfiedLinkError(
    "Directory separator should not appear in library name: " + libname);
        }
        String libraryName = libname;
        //1.判断classLoader是否为null以及是否是BootClassLoader实例
        if (loader != null && !(loader instanceof BootClassLoader)) {
            //2.调用classLoader.findLibrary(libraryName)方法查找so库文件
            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".
               // 3.翻译:加载so的时候不要传lib以及后缀名so,否则会变成查找liblibMyLibrary.so.so
                throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                               System.mapLibraryName(libraryName) + "\"");
            }
            //4.最终也会通过jni调用nativeLoad加载动态库
            String error = nativeLoad(filename, loader);
            if (error != null) {
                throw new UnsatisfiedLinkError(error);
            }
            return;
        }

        // We know some apps use mLibPaths directly, potentially assuming it's not null.
        // Initialize it here to make sure apps see a non-null value.
        // 如果classLoader为null,执行的逻辑,从system.java.library中查找
        getLibPaths();
        String filename = System.mapLibraryName(libraryName);
        // 最终还是调用nativeLoad方法。
        String error = nativeLoad(filename, loader, callerClass);
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
    }
public static native java.lang.String mapLibraryName(java.lang.String libname);

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

三 BaseDexClassLoader.findLibrary源码分析
[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;
    }

[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找到目标文件之后会调用nativeLoad方法:
[Runtime.java]

if (libname.indexOf((int)File.separatorChar) != -1) {
            throw new UnsatisfiedLinkError(
    "Directory separator should not appear in library name: " + libname);
        }
        String libraryName = libname;
        // Android-note: BootClassLoader doesn't implement findLibrary(). http://b/111850480
        // Android's class.getClassLoader() can return BootClassLoader where the RI would
        // have returned null; therefore we treat BootClassLoader the same as null here.
        if (loader != null && !(loader instanceof BootClassLoader)) {
            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 = nativeLoad(filename, loader);
            if (error != null) {
                throw new UnsatisfiedLinkError(error);
            }
            return;
        }
四 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 \"{936b63963a8c9f2b24063da536a495a32039ff9ed9d82cacc18dd4741407c351}s\" already opened by "
          "ClassLoader {936b63963a8c9f2b24063da536a495a32039ff9ed9d82cacc18dd4741407c351}p; can't open in ClassLoader {936b63963a8c9f2b24063da536a495a32039ff9ed9d82cacc18dd4741407c351}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 \"{936b63963a8c9f2b24063da536a495a32039ff9ed9d82cacc18dd4741407c351}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 \"{936b63963a8c9f2b24063da536a495a32039ff9ed9d82cacc18dd4741407c351}s\"", path.c_str());
    } else if (IsBadJniVersion(version)) {
      StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"{936b63963a8c9f2b24063da536a495a32039ff9ed9d82cacc18dd4741407c351}s\": {936b63963a8c9f2b24063da536a495a32039ff9ed9d82cacc18dd4741407c351}d",
                    path.c_str(), version);
    } else {
      was_successful = true;
    }
  return was_successful;
}

这个函数有点长,重点都用注释标注了。
开始的时候会去缓存查看是否已经加载过动态库,如果已经加载过会判断上次加载的ClassLoader和这次加载的ClassLoader是否一致,如果不一致则加载失败,如果一致则返回上次加载的结果,换句话说就是不允许不同的ClassLoader加载同一个动态库。为什么这么做值得推敲。
之后会通过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.不能使用不同的ClassLoader加载同一个动态库
3.System.loadLibrary加载过程中会调用目标库的JNI_OnLoad方法,我们可以在动态库中加一个JNI_OnLoad方法用于动态注册(注册jni crash和anr异常捕获的信号处理函数是在这里注册的,在jni层捕获信号后抛给java层).
4.如果加了JNI_OnLoad方法,其的返回值为JNI_VERSION_1_2 ,JNI_VERSION_1_4, JNI_VERSION_1_6其一。我们一般使用JNI_VERSION_1_4即可
5.Android动态库的加载与Linux一致使用dlopen系列函数,通过动态库的句柄和函数名称来调用动态库的函数。


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

首先我们知道在Android(Java)中加载一个动态链接库非常简单,只需一行代码:
事实上这是Java提供的API,对于Java层实现基本一致,但是对于不同的JVM其底层(native)实现会有所差异。本文分析的代码基于Android 6.0系统。
应该知道上述代码执行过程中会调用native层的JNI_OnLoad方法,一般用于动态注册native方法。

System.loadLibrary("native-lib");

Android是基于Linux系统的,那么在Linux系统下是如何加载动态链接库的呢?
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); //关闭动态链接库  

可以通过下述命令可以查看上述函数的具体使用方法:

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

因为代码中使用了c++11标准新加的特性,所以编译的时候带上-std=c++11,另外使用了头文件dlfcn.h需要带上-ldl,编译生成的main文件即是二进制可执行文件,需要将动态库放在同级目录下执行。
上面就是Linux环境下创建动态库,加载并使用动态库的全部过程。

由于Android基于Linux系统,所以我们有理由猜测Android系统底层也是通过这种方式加载并使用动态库的。
loadLibrary动态库加载过程分析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351