so 库的动态加载

本文介绍了在Android客户端开发中,为了提高效率而使用Java调用C代码的情况。通常,这会导致多个平台的so文件,增大APK体积。因此,文章探讨了如何动态加载so库,详细解析了加载过程,包括`System.loadLibrary`的内部实现,以及如何通过绝对路径加载外部so文件。并提供了加载外部so库的示例代码,证明了动态替换so库的可行性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

so 库的动态加载

    在客户端开发过程中,有些时候在考虑到效率的时候不可避免的会用到java调用c以此来解决一些效率的问题,但是不可避免的,需要编译很多个平台的so文件,可能就会造成app文件过大,所以有时候需要采用其他方式来加载so文件。


通常我们在开发中遇到要加载本地方法时会这么写

  static {
        System.loadLibrary("native-lib");
    }

这是通过库名去找本地已加载的列表中去找对应的文件地址,在通过地址去加载so文件。

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

其实他是调用runtime的loadLibrary0 这个方法去加载。

    synchronized void loadLibrary0(ClassLoader loader, String libname) {
        if (libname.indexOf((int)File.separatorChar) != -1) {
            throw new UnsatisfiedLinkError(
    "Directory separator should not appear in library name: " + libname);
        }
        String libraryName = libname;
        if (loader != null) {
            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 = doLoad(filename, loader);
            if (error != null) {
                throw new UnsatisfiedLinkError(error);
            }
            return;
        }

        String filename = System.mapLibraryName(libraryName);
        List<String> candidates = new ArrayList<String>();
        String lastError = null;
        for (String directory : getLibPaths()) {
            String candidate = directory + filename;
            candidates.add(candidate);

            if (IoUtils.canOpenReadOnly(candidate)) {
                String error = doLoad(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的具体实现,无论这个逻辑怎么样,都是通过调用doLoad方法。

具体分析一下这个方法,如果classLoader 不为null的话 会去调用classLoader的findLibrary方法,这个方法的是在BaseDexClassLoader实现的,

    @Override
    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }

pathList其实是DexPathList实现的看一下他的实现

    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;
    }
        public String findNativeLibrary(String name) {
            maybeInit();

            if (isDirectory) {
                String path = new File(dir, name).getPath();
                if (IoUtils.canOpenReadOnly(path)) {
                    return path;
                }
            } 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 = new File(dir, name).getPath();
                if (urlHandler.isEntryStored(entryName)) {
                  return zip.getPath() + zipSeparator + entryName;
                }
            }

            return null;
        }

这个方法System.mapLibraryName(libraryName) 其实是把方法库映射一个特定的cpu架构平台库的名称,然后 findNativeLibrary这个方法是去找本地的这个库的具体路径。回头去看RunTime类的加载方法如果类加载器不存在时,也是先将库名映射到相应架构的库名称在去本地库去查这个文件

    private String[] getLibPaths() {
        if (mLibPaths == null) {
            synchronized(this) {
                if (mLibPaths == null) {
                    mLibPaths = initLibPaths();
                }
            }
        }
        return mLibPaths;
    }
    private static String[] initLibPaths() {
        String javaLibraryPath = System.getProperty("java.library.path");
        if (javaLibraryPath == null) {
            return EmptyArray.STRING;
        }
        String[] paths = javaLibraryPath.split(":");
        // Add a '/' to the end of each directory so we don't have to do it every time.
        for (int i = 0; i < paths.length; ++i) {
            if (!paths[i].endsWith("/")) {
                paths[i] += "/";
            }
        }
        return paths;
    }

上面就是去本地的库去查这个文件,最终都是调用doLoad方法。

    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 librarySearchPath = null;
        if (loader != null && loader instanceof BaseDexClassLoader) {
            BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
            librarySearchPath = dexClassLoader.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, librarySearchPath);
        }
    }

java的这个方法没什么好说的,具体就是将可搜索的文件历经传入c层,由c层完成加载。

System其实还有一个直接加载绝对路径的方法

    public static void load(String filename) {
        Runtime.getRuntime().load0(VMStack.getStackClass1(), filename);
    }

RunTime实现

    synchronized void load0(Class fromClass, String filename) {
        if (!(new File(filename).isAbsolute())) {
            throw new UnsatisfiedLinkError(
                "Expecting an absolute path of the library: " + filename);
        }
        if (filename == null) {
            throw new NullPointerException("filename == null");
        }
        String error = doLoad(filename, fromClass.getClassLoader());
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
    }

看到这个方法也是通过doLoad方法进行加载,而且还有判断是否是绝对路径,否则就抛出异常。

 

也就是说在android中其实也是可以去加载外部的so文件,以此来完成so的动态修复。

正常的so文件加载就不多说了,下面是加载外部so的演示代码

将so文件放在/storage/emulated/0/jniLibs路径下面,是编译之后的一个完整的路径文件


是一个完整的结构。

 

以下代码路径基本是固定的

    private void copySoAndLoadLib() {
        try {
            File dir = this.getDir("jniLibs", Activity.MODE_PRIVATE);
            File distFile = new File(dir.getAbsolutePath() + File.separator + "libnative-lib.so");
            File file = new File("/storage/emulated/0/jniLibs");
            if (copySoLib(file, "libnative-lib.so")) {
                //使用load方法加载内部储存的SO库
                Log.i(TAG,"path:" + distFile.getAbsolutePath());
                System.load(distFile.getAbsolutePath());
                loadCTest test = new loadCTest();
                String testStr = test.getTestStr();
                if (TextUtils.isEmpty(testStr)) {
                    testStr = "Not found string";
                }
                tv.setText(testStr);
            } else {
                tv.setText("失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
 

判断,然后加载so库,并设置显示

    /**
     * 将一个SO库复制到指定路径,会先检查改SO库是否与当前CPU兼容
     *
     * @param sourceDir     SO库所在目录
     * @param so            SO库名字
     * @return
     */
    public boolean copySoLib(File sourceDir, String so) throws IOException {

        boolean isSuccess = false;
        try {
            Log.d(TAG, "[copySo] 开始处理so文件");

            if (Build.VERSION.SDK_INT >= 21) {
                String[] abis = Build.SUPPORTED_ABIS;
                if (abis != null) {
                    for (String abi : abis) {
                        Log.d(TAG, "[copySo] try supported abi:" + abi);
                        String name = File.separator + abi + File.separator + so;
                        File sourceFile = new File(sourceDir, name);
                        if (sourceFile.exists()) {
                            Log.i(TAG, "[copySo] copy so: " + sourceFile.getAbsolutePath());
                            isSuccess = copyFile(sourceFile.getAbsolutePath());
                            //api21 64位系统的目录可能有些不同
                            //copyFile(sourceFile.getAbsolutePath(), destDir + File.separator +  name);
                            break;
                        }
                    }
                } else {
                    Log.e(TAG, "[copySo] get abis == null");
                }
            } else {
                Log.d(TAG, "[copySo] supported api:" + Build.CPU_ABI + " " + Build.CPU_ABI2);

                String name = "lib" + File.separator + Build.CPU_ABI + File.separator + so;
                File sourceFile = new File(sourceDir, name);

                if (!sourceFile.exists() && Build.CPU_ABI2 != null) {
                    name = "lib" + File.separator + Build.CPU_ABI2 + File.separator + so;
                    sourceFile = new File(sourceDir, name);

                    if (!sourceFile.exists()) {
                        name = "lib" + File.separator + "armeabi" + File.separator + so;
                        sourceFile = new File(sourceDir, name);
                    }
                }
                if (sourceFile.exists()) {
                    Log.i(TAG, "[copySo] copy so: " + sourceFile.getAbsolutePath());
                    isSuccess = copyFile(sourceFile.getAbsolutePath());
                }
            }

            if (!isSuccess) {
                Log.e(TAG, "[copySo] 安装 " + so + " 失败 : NO_MATCHING_ABIS");
                throw new IOException("install " + so + " fail : NO_MATCHING_ABIS");
            }

        } catch (IOException e) {
            e.printStackTrace();
            throw e;
        }

        return true;
    }

去找相应的cpu架构的so文件然后复制

 

    public boolean copyFile(String filePath) {
        boolean copyIsFinish = false;
        File dir = this.getDir("jniLibs", Activity.MODE_PRIVATE);

        if(!dir.exists()){
            dir.mkdirs();
        }

        File distFile = new File(dir.getAbsolutePath() + File.separator + "libnative-lib.so");

        try {
            File jniFile = new File(filePath);
            FileInputStream is = new FileInputStream(jniFile);
            File file = new File(distFile.getAbsolutePath());
            file.createNewFile();

            FileOutputStream fos = new FileOutputStream(file);
            byte[] temp = new byte[1024];
            int i = 0;
            while ((i = is.read(temp)) > 0) {
                fos.write(temp, 0, i);
            }
            fos.close();
            is.close();
            copyIsFinish = true;
        } catch (IOException e) {
            e.printStackTrace();
            Log.e("MainActivity", "[copyFileFromAssets] IOException " + e.toString());
        }
        Log.i(TAG,"filePath:" + distFile.getAbsolutePath());
        return copyIsFinish;
    }

具体copy文件的代码

#include <jni.h>
#include <string>

extern "C"
JNIEXPORT jstring JNICALL
Java_com_nat_ncreate_loadCTest_getNativeStr(JNIEnv *env,  jobject /* this */) {

    std::string hello = "asdfkjkjshafkjs";
    return env->NewStringUTF(hello.c_str());
}

这个是so文件,随便写了个返回值。

验证结果



验证是生效的,动态替换so方法,采用这种方式是可行的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值