动态加载so库文件

动态加载so库可以减小APK体积。在Android中,通常使用System.loadLibrary,其实质是动态加载。深入分析发现,加载过程涉及ClassLoader、DexPathList等,最终通过nativeLoad方法完成。此外,通过直接调用System.load,可以指定完整路径实现动态加载。实践案例中,将so库放在assets中,复制到内部存储后加载。这种方法适用于网络下载so库到内部存储的情况。

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

使用动态加载so库文件可以减小apk文件的大小,如:so库文件较大时,使用动态加载,在需要使用so库文件或者满足其他条件时,提示用户下载或自动下载,这样apk文件的大小就可以大大降低。

Android加载so库文件的机制:

加载so库文件基本都用的System类的loadLibrary方法,其实System类中还有一个load方法。

/**
     * See {@link Runtime#load}.
     */
    public static void load(String pathName) {
        Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader());
    }

    /**
     * See {@link Runtime#loadLibrary}.
     */
    public static void loadLibrary(String libName) {
        Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
    }

先看看loadLibrary,这里调用了Runtime的loadLibrary,进去一看,又是动态加载熟悉的ClassLoader了(这里也佐证了so库的使用就是一种动态加载的说法)。

    /*
     * Searches for and loads the given shared library using the given ClassLoader.
     */
    void loadLibrary(String libraryName, ClassLoader loader) {
        if (loader != null) {
            String filename = loader.findLibrary(libraryName);
            String error = doLoad(filename, loader);
            return;
        }
        ……
    }


看样子就像是通过库名称获取一个文件路径,再调用doLoad方法加载这个文件,先看看loader.findLibrary(libraryName)

    protected String findLibrary(String libName) {
        return null;
    }


ClassLoader只是一个抽象类,它的大部分工作都在BaseDexClassLoader类中实现,进去看看

public class BaseDexClassLoader extends ClassLoader {
    public String findLibrary(String name) {
        throw new RuntimeException("Stub!");
    }
}


不对啊,这里只是抛了个异常,什么都没做。。。

其实这里有个误区,Android SDK自带的源码其实只是给我们开发者参考的,基本只是一些常用的类,Google不会把整个Android系统的源码都放在这里来,因为整个项目非常大,ClassLoader类平时我们接触得少,所以它具体实现的源码并没有打包进SDK里。

到Android在线源码中可以看到:

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


再看DexPathList类

/**
     * Finds the named native code library on any of the library
     * directories pointed at by this instance. This will find the
     * one in the earliest listed directory, ignoring any that are not
     * readable regular files.
     *
     * @return the complete path to the library or {@code null} if no
     * library was found
     */
    public String findLibrary(String libraryName) {
        String fileName = System.mapLibraryName(libraryName);
        for (File directory : nativeLibraryDirectories) {
            File file = new File(directory, fileName);
            if (file.exists() && file.isFile() && file.canRead()) {
                return file.getPath();
            }
        }
        return null;
    }


到这里已经明朗了,根据传进来的libName,扫描apk内部的nativeLibrary目录,获取并返回内部so库文件的完整路径filename。再回到Runtime类,获取filename后调用了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) {
            return nativeLoad(name, loader, ldLibraryPath);
        }
    }


到这里就彻底清楚了,调用Native方法nativeLoad,通过完整的so库路径filename,把目标so库加载进来。

说了半天还没有进入正题呢,不过我们可以想到,如果使用loadLibrary方法,到最后还是要找到目标so库的完整路径,再把so库加载进来,那我们能不能一开始就给出so库的完整路径,然后直接加载进来呢?我们猜想load方法就是干这个的,看看

void load(String absolutePath, ClassLoader loader) {
        if (absolutePath == null) {
            throw new NullPointerException("absolutePath == null");
        }
        String error = doLoad(absolutePath, loader);
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
    }


我嘞个去,一上来就直接来到doLoad方法了,这证明我们的猜想可能是正确的,那么在实际项目中测试验证吧。

我们先把so文件放到asset中,再复制到内部存储,再使用load方法把其加载进来。

例子结构

这样做的原因是:native方法是包名+方法名的拼接,若不这样会找不到目标方法。

public class Hello {

    public static native String hello();
}


 

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        File dir = getDir("jniLibs", Context.MODE_PRIVATE);
        File distFile = new File(dir.getAbsolutePath() + File.separator + "libHello_jni.so");
        if (copyFileFromAssets(this, "libHello_jni.so", distFile.getAbsolutePath())) {
            System.load(distFile.getAbsolutePath());
            String hello = Hello.hello();
            System.out.println("hello:" + hello);
        }
    }

    private boolean copyFileFromAssets(Context context, String fileName, String path) {
        boolean copyFinish = false;
        try {
            InputStream is = context.getAssets().open(fileName);
            File file = new File(path);
            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();
            copyFinish = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return copyFinish;
    }
}


当然这里还可以使用反射来调用,方法名必须使用全方法名。

上面例子只是为了说明能动态加载so库,copy操作没有做必要的判断。另外so库可以从网络下载后再拷贝到内部存储中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值