前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >android 加载so过程分析

android 加载so过程分析

原创
作者头像
飞翔的小小小企鹅
发布2023-11-22 18:32:41
5150
发布2023-11-22 18:32:41
举报
文章被收录于专栏:Android开发分析Android开发分析

在实现android插件化过程中,在插件代码中加载so时出现了一些问题,因此特地研究了一下android系统中加载so的过程,记录下来,整理成文。

在android系统中,加载so一般会调用System.loadLibrary(name)或者是System.load(path),这两个函数都可以用来加载so文件,区别在于System.loadLibrary函数的参数为库文件名,而System.load函数的 参数为库文件的绝对路径,可以是任意路径(路径需要可执行权限)。这两个函数本质上都是一样的,只是搜索so的搜索目录略有差别。下面以System.loadLibrary函数为例来分析加载so的实现原理。

首先看一下System.loadLibrary函数的源码(ibcore/luni/src/main/java/java/lang/System.java):

代码语言:javascript
复制
   public static void loadLibrary(String libName) {
        SecurityManager smngr = System.getSecurityManager();
        if (smngr != null) {
            smngr.checkLink(libName);
        }
        Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
    }

如上所示,通过VMStack拿到了当前的ClassLoader,然后将加载so的动作委托给Runtime.loadLibrary去执行(libcore/luni/src/main/java/java/lang/Runtime.java):

代码语言:javascript
复制
 void loadLibrary(String libname, ClassLoader loader) {
        String filename;
        int i;

        if (loader != null) {
            filename = loader.findLibrary(libname);
            if (filename != null && nativeLoad(filename, loader))
                return;
            // else fall through to exception
        } else {
            filename = System.mapLibraryName(libname);
            for (i = 0; i < mLibPaths.length; i++) {
                if (false)
                    System.out.println("Trying " + mLibPaths[i] + filename);
                if (nativeLoad(mLibPaths[i] + filename, loader))
                    return;
            }
        }

        throw new UnsatisfiedLinkError("Library " + libname + " not found");

如上图所示,正常情况下Classloader不会为空,因此进入第一个if分支,可以看看出在此函数中完成了两件事:

1:通过调用ClassLoader.findLibrary函数去拿到so的真正的文件路径;

2:调用nativeLoad函数去实现真正的so加载;

这里会牵扯到一个问题,如何通过so的名称去ClassLoader拿到so真正的文件路径?Android系统中可供使用的ClassLoader有两个,分别是DexClassLoader和PathClassLoader,其中PathClassLoader一般用于加载已经安装过的系统app的dex文件,而DexClassLoader可以加载任意路径的apk/jar文件(此文件路径需要可执行权限),两者间的具体差别请参考developer.android.com。DexClassLoader和PathClassLoader对于findLibrary函数的实现大致相同,下面来看看PathClassLoader中findLibrary函数的实现(libcore/luni/src/main/java/java/PathClassLoader.java):

代码语言:javascript
复制
protected String findLibrary(String libname) {
        ensureInit();

        String fileName = System.mapLibraryName(libname);
        for (int i = 0; i < mLibPaths.length; i++) {
            String pathName = mLibPaths[i] + fileName;
            File test = new File(pathName);

            if (test.exists())
                return pathName;
        }

        return null;
    }

如上所示,首先调用System.mapLibraryName拿到so的前缀和后缀名,如libname为hello,那么经过此函数转换后变成了libhello.so,然后在mLibPaths搜索目录下搜寻libhello.so文件,如果文件存在,则代表找到了此so的文件,直接返回即可。mLibPaths是如何初始化的呢?还有如上所示,在mLibPaths搜索目录下搜寻是有序的,只要搜索到了就立即返回,因此如果在mLibPaths[0]和mLibPaths[1]目录下均有这个so,会优先返回mLibPaths[0]目录下so的文件路径的,因此我们也需要关注mLibPaths中搜索目录的顺序。

在findLibrary函数开始调用ensureInit函数后会初始化mLibPaths搜索目录,下面看看这个函数的具体实现(主要关注mLibPaths中搜索目录的内容和顺序,以下代码省略了部分无关代码)。

代码语言:javascript
复制
 private synchronized void ensureInit() {
        if (initialized) {
            return;
        }
        
        initialized = true;
        
        /*
         * Prep for native library loading.
         */
        String pathList = System.getProperty("java.library.path", ".");
        String pathSep = System.getProperty("path.separator", ":");
        String fileSep = System.getProperty("file.separator", "/");
        
        if (libPath != null) {
            if (pathList.length() > 0) {
                pathList += pathSep + libPath;
            }
            else {
                pathList = libPath;
            }
        }

        mLibPaths = pathList.split(pathSep);
        length = mLibPaths.length;

        // Add a '/' to the end so we don't have to do the property lookup
        // and concatenation later.
        for (int i = 0; i < length; i++) {
            if (!mLibPaths[i].endsWith(fileSep))
                mLibPaths[i] += fileSep;
            if (false)
                System.out.println("Native lib path:  " + mLibPaths[i]);
        }
    }

这段代码看上去挺简单,主要是从系统获取到"java.library.path"属性,libPath为应用程序的搜索目录,libPath是在构造PathClassLoader时由系统传进来的(一般不会为空),如果libPath不为空,则添加到mLibPaths,由代码可以确定搜索目录的顺序是系统的搜索目录优先,应用程序的搜索目录在最后。

总结一下,ClassLoader的findLibrary实际上会去两部分目录下搜索so,一部分是通过System.getProperty("java.library.path", ".")拿到的系统搜索目录,还有部分是在构造PathClassLoader时传进来的librarypath。在三星手机上,mLibPaths分别如下:

1:/vendor/lib

2:/system/lib

3:/data/data/应用包名/lib

每个手机可能根据系统的不同而有不同的应用程序搜索目录,如在有些手机上应用程序的搜索目录为/data/app-lib/.apk名称 目录下(可以参考PackageManagerService中部分代码);

解决了ClassLoader.findLibrary函数的问题,现在去看看nativeLoad函数的实现。nativeLoad是native函数,真正的实现位于/android_dalvik-eclair/vm//native/java_lang_Runtime.c:

代码语言:javascript
复制
static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args,
    JValue* pResult)
{
    StringObject* fileNameObj = (StringObject*) args[0];
    Object* classLoader = (Object*) args[1];
    char* fileName;
    int result;

    if (fileNameObj == NULL)
        RETURN_INT(false);
    fileName = dvmCreateCstrFromString(fileNameObj);

    result = dvmLoadNativeCode(fileName, classLoader);

    free(fileName);
    RETURN_INT(result);
}

可以看到,nativeLoad()实际上只是完成了两件事情,第一,是调用dvmCreateCstrFromString()将Java 的library path String 转换到native的String,然后将这个path传给dvmLoadNativeCode()做load,dvmLoadNativeCode()这个函数的实现在/android_dalvik-eclair/vm/native.c中,如下:

代码语言:javascript
复制
bool dvmLoadNativeCode(const char* pathName, Object* classLoader)
{
    SharedLib* pEntry;
    void* handle;

    LOGD("Trying to load lib %s %p\n", pathName, classLoader);

    /*
     * See if we've already loaded it.  If we have, and the class loader
     * matches, return successfully without doing anything.
     */
    pEntry = findSharedLibEntry(pathName);
    if (pEntry != NULL) {
        if (pEntry->classLoader != classLoader) {
            LOGW("Shared lib '%s' already opened by CL %p; can't open in %p\n",
                pathName, pEntry->classLoader, classLoader);
            return false;
        }
        LOGD("Shared lib '%s' already loaded in same CL %p\n",
            pathName, classLoader);
        if (!checkOnLoadResult(pEntry))
            return false;
        return true;
    }

    Thread* self = dvmThreadSelf();
    int oldStatus = dvmChangeStatus(self, THREAD_VMWAIT);
    handle = dlopen(pathName, RTLD_LAZY);
    dvmChangeStatus(self, oldStatus);

    if (handle == NULL) {
        LOGI("Unable to dlopen(%s): %s\n", pathName, dlerror());
        return false;
    }

    /* create a new entry */
    SharedLib* pNewEntry;
    pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib));
    pNewEntry->pathName = strdup(pathName);
    pNewEntry->handle = handle;
    pNewEntry->classLoader = classLoader;
    dvmInitMutex(&pNewEntry->onLoadLock);
    pthread_cond_init(&pNewEntry->onLoadCond, NULL);
    pNewEntry->onLoadThreadId = self->threadId;

    /* try to add it to the list */
    SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);

    if (pNewEntry != pActualEntry) {
        LOGI("WOW: we lost a race to add a shared lib (%s CL=%p)\n",
            pathName, classLoader);
        freeSharedLibEntry(pNewEntry);
        return checkOnLoadResult(pActualEntry);
    } else {
        LOGD("Added shared lib %s %p\n", pathName, classLoader);

        bool result = true;
        void* vonLoad;
        int version;

        vonLoad = dlsym(handle, "JNI_OnLoad");
        if (vonLoad == NULL) {
            LOGD("No JNI_OnLoad found in %s %p\n", pathName, classLoader);
        } 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.)
             */
            OnLoadFunc func = vonLoad;
            Object* prevOverride = self->classLoaderOverride;

            self->classLoaderOverride = classLoader;
            oldStatus = dvmChangeStatus(self, THREAD_NATIVE);
            LOGV("+++ calling JNI_OnLoad(%s)\n", pathName);
            version = (*func)(gDvm.vmList, NULL);
            dvmChangeStatus(self, oldStatus);
            self->classLoaderOverride = prevOverride;

            if (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 &&
                version != JNI_VERSION_1_6)
            {
                LOGW("JNI_OnLoad returned bad version (%d) in %s %p\n",
                    version, pathName, classLoader);
                /*
                 * 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.
                 */
                result = false;
            } else {
                LOGV("+++ finished JNI_OnLoad %s\n", pathName);
            }
        }

        if (result)
            pNewEntry->onLoadResult = kOnLoadOkay;
        else
            pNewEntry->onLoadResult = kOnLoadFailed;

        pNewEntry->onLoadThreadId = 0;

        /*
         * Broadcast a wakeup to anybody sleeping on the condition variable.         */
        dvmLockMutex(&pNewEntry->onLoadLock);
        pthread_cond_broadcast(&pNewEntry->onLoadCond);
        dvmUnlockMutex(&pNewEntry->onLoadLock);
        return result;
    }
}

dvmLoadNativeCode()首先会检测是否已经加载过这个so(findSharedLibEntry),如果已经加载过了,那么直接返回即可;如果没有加载,那么重新加载一遍,加载的过程可以用下面的流程来描述:

  1. 调用dlopen() 打开一个so文件,取得该so的文件句柄;
  2. 调用dlsym()函数,查找到so文件中的JNI_OnLoad()这个函数的函数指针;
  3. 执行JNI_OnLoad()函数;

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档