Android虽然基于Linux系统,但Android本身做了大量的修改。其中对于系统C库更是自己重新实现——Bionic库。在源码目录构中bionic可以找到相关内容。话题好像偏离有点远,但System.loadLibrary最终还是调用到bionic库中的函数dlopen。这个dlopen搜索依赖so的路径与Linux本身有着不一样的实现,并且在4.2.2版本及以下有着无法隐式加载app目录下依赖so的缺陷,直到4.3才被修复。

关于System.loadLibrary和System.load两个在java中加载so的方法,已经有人进行个大致的分析(Java中System.loadLibrary() 的执行过程 文章中的分析内容是4.2.2版本,仍然不支持隐式加载app目录下依赖so),可以先看看这篇文章里关于以上两个java函数的实现,了解大部分的流程。然后,这里就要开始从dlopen开始分析,对于依赖so加载的问题。


具体分析

首先来看这样一个例子,有两个so,liba.so、libb.so。其中,liba.so引用了libb.so导出的符号,并且是直接引用符号,也就是

需要加载器在运行时的动态隐式链接。在加载liba.so的时候,Java层里,我们会调用System.loadLibrary(),或者System.load()来

指定so的路径,然后最终会调用到native代码中,使用dlopen加载liba.so,就会根据liba.so的Dynamic section中列出类型为

DT_NEEDED的依赖共享库so,并且会递归地一个一个地加载依赖so。在LInux下,加载这些依赖so的路径会是1. 环境变量LD_LIBRARY指明的路径。2. /etc/ld.so.cache中的函数库列表;3/lib目录,然后/usr/lib。但在4.2.2版本的Android的Bionic实现中,加载的目录只有LD_LIBRARY_PATH变量所指示的路径以及被hardcode进代码的/system/lib,/vendor/lib这两个路径。而事实上LD_LIBRARY_PATH是不建议修改的,而且默认值在Android中也是/system/lib,/vendor/lib这两个路径,因此事实上,被依赖的so加载路径只有/system/lib,/vendor/lib这两个!

不过这种问题在4.3开始得到改正。在4.3版本的代码里,nativeLoad这个方法接受多一个参数ldLibraryPath,这是通过PathClassLoader获取App的native lib路径,然后传给nativeLoad的。nativeLoad在获取这个参数之后,就会调用bionic的方法来显式更新内部的ldPath路径,这样每次加载的时候,就可以先搜索本地App的native lib路径,就能够正确加载依赖的so。

首先我们来看看4.3版本的实现:

private String doLoad(String name, ClassLoader loader) {        // Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH,        // which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH.        // The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load        // libraries with no dependencies just fine, but an app that has multiple libraries that        // depend on each other needed to load them in most-dependent-first order.        // We added API to Android's dynamic linker so we can update the library path used for        // the currently-running process. We pull the desired path out of the ClassLoader here        // and pass it to nativeLoad so that it can call the private dynamic linker API.        // We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the        // beginning because multiple apks can run in the same process and third party code can        // use its own BaseDexClassLoader.        // We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any        // dlopen(3) calls made from a .so's JNI_OnLoad to work too.        // So, find out what the native library search path is for the ClassLoader in question...        String ldLibraryPath = null;        if (loader != null && loader instanceof BaseDexClassLoader) {            ldLibraryPath = ((BaseDexClassLoader) loader).getLdLibraryPath();        }        // nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless        // of how many ClassLoaders are in the system, but dalvik doesn't support synchronized        // internal natives.        synchronized (this) {            return nativeLoad(name, loader, ldLibraryPath);        }    }
doLoad方法在System.loadLibrary里被调用,里面的英文注释可以看到这次4.3的改进机制以及原因,主要是通过classloader把native lib的路径传入到bionic中,然后每次加载依赖so都会先从这些路径开始搜索。另外,PathClassLoader的构造是在加载apk的时候,通过传递nativeLibraryDir来初始化的。

static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args, JValue* pResult){    //......    StringObject* ldLibraryPathObj = (StringObject*) args[2];    if (ldLibraryPathObj != NULL) {        char* ldLibraryPath = dvmCreateCstrFromString(ldLibraryPathObj);        void* sym = dlsym(RTLD_DEFAULT, "android_update_LD_LIBRARY_PATH");        if (sym != NULL) {            typedef void (*Fn)(const char*);            Fn android_update_LD_LIBRARY_PATH = reinterpret_cast<Fn>(sym);            (*android_update_LD_LIBRARY_PATH)(ldLibraryPath);        } else {            ALOGE("android_update_LD_LIBRARY_PATH not found; .so dependencies will not work!");        }        free(ldLibraryPath);    }    //......}

可以看到nativeLoad方法会通过显式获取bionic库中的android_update_LD_LIBRARY_PATH方法,然后这个路径最终会更新bionic库的全局变量gLdPaths,这个变量在加载依赖so的时候会产生作用。

static int open_library_on_path(const char* name, const char* const paths[]) {  char buf[512];  for (size_t i = 0; paths[i] != NULL; ++i) {    int n = __libc_format_buffer(buf, sizeof(buf), "%s/%s", paths[i], name);    if (n < 0 || n >= static_cast<int>(sizeof(buf))) {      PRINT("Warning: ignoring very long library path: %s/%s", paths[i], name);      continue;    }    int fd = TEMP_FAILURE_RETRY(open(buf, O_RDONLY | O_CLOEXEC));    if (fd != -1) {      return fd;    }  }  return -1;}static int open_library(const char* name) {  TRACE("[ opening %s ]", name);  // If the name contains a slash, we should attempt to open it directly and not search the paths.  if (strchr(name, '/') != NULL) {    int fd = TEMP_FAILURE_RETRY(open(name, O_RDONLY | O_CLOEXEC));    if (fd != -1) {      return fd;    }    // ...but nvidia binary blobs (at least) rely on this behavior, so fall through for now.  }  // Otherwise we try LD_LIBRARY_PATH first, and fall back to the built-in well known paths.  int fd = open_library_on_path(name, gLdPaths);  if (fd == -1) {    fd = open_library_on_path(name, gSoPaths);  }  return fd;}
可以看到,open_library会调用gLdPaths里面的路径来加载依赖的so。另外gSoPaths这个全局变量被初始化为/system/lib,/vendor/lib这两个系统路径。

在4.3以下的版本里,gLdPaths的初始化是在linker的初始化里通过读取LD_LIBRARY_PATH来决定的,并不是像4.3这样可以每次动态更新。具体实现如下:

/* skip past the environment */    while(vecs[0] != 0) {        if(!strncmp((char*) vecs[0], "DEBUG=", 6)) {            debug_verbosity = atoi(((char*) vecs[0]) + 6);        } else if(!strncmp((char*) vecs[0], "LD_LIBRARY_PATH=", 16)) {            ldpath_env = (char*) vecs[0] + 16;        }        vecs++;    }  //......         /* Use LD_LIBRARY_PATH if we aren't setuid/setgid */    if (ldpath_env && getuid() == geteuid() && getgid() == getegid())        parse_library_path(ldpath_env, ":");

以上这段代码是在__linker_init函数里的代码片段,parse_library_path函数的作用就是设置加载依赖so的路径。另外,关于至于如何解析so里的dynamic section以及如何加载,并且初始化so等这里不属于本次讨论范畴,就不再赘述了。


解决办法:

由于加载依赖so的路径在4.3版本以前会存在问题(即调用System.loadLibrary("a");会失败),所以我们只能够在显式加载每个依赖最低的库,这样最后加载依赖最高的so就会成功。根据上面的例子,我们可以先显式加载libb.so,然后再加载liba.so。代码如下:

System.loadLibrary("b");System.loadLibrary("a"); 
这样就可以成功加载两个so。



更多相关文章

  1. Android中使用WebView建立应用程序
  2. 传智播客--ContentProvider共享数据和ContentResolver的使用,Uri
  3. Android(安卓)Git 常用命令和规范
  4. 使用主题预加载背景
  5. RN JSBundle 拆分解决方案(1): 应用启动、视图加载原理解析
  6. android 6.0的变化
  7. Android图片加载库:最全面的Picasso讲解
  8. Android(安卓)开发系列(1) - 入门
  9. 用Eclipse开发与调试纯粹的Android(安卓)C++程序,非ndk-build、nd

随机推荐

  1. Android 视图绑定,找不到类 'ResultProfil
  2. android延迟进入主界面和点击按钮进入主
  3. Android自学笔记(番外篇):全面搭建Linux环境
  4. Android菜单应用(Menu)
  5. Android 如何使一个service 开机启动
  6. Activity切换导致的onCreate重复执行
  7. Android 之 adapter.notifyDataSetChange
  8. Android的contentDescription属性是什么?
  9. android 调用前摄像头进行拍照的方法及完
  10. Android 初识AppBarLayout 和 Coordinato