Android So动态加载 优雅实现与原理分析

Stella981
• 阅读 609

背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.

Android So动态加载 优雅实现与原理分析

点击上方“蓝字”关注我们吧!

Android So动态加载 优雅实现与原理分析

开源地址: https://github.com/AnyMarvel/ManPinAPP

本文默认认为大家对JNI开发有一定的了解。在 Android 开发中调用动态链接库文件*.so都是通过 jni 的加载方式,一般的开发方式往往是在 apk 或 jar 包中调用so文件时,都要将对应 so 文件打包进 apk 或 jar 包。

基于以上的常见的方式,我们一般使用java提供的用于加载So文件的API

  • System.loadLibrary(String libName):参数为so库名称,位于 apk 压缩文件中的 libs 目录,最后复制到 apk 安装目录下;

  • System.load(String pathName) :参数为 so 库在磁盘中完整的路径,可以加载自定义外部 so 库文件;

  • 使用第三方库ReLinker,有so加载成功、失败的回调,安全加载不崩溃。(relinker也是对上面两种api的封装,但更推荐大家使用这种加载方式)

看到上述API其实大家可以看出来System.load(String pathName)这个api其实已经可以实现So的动态加载,那为什么我们还要搞一些所谓的黑科技呢?

如果项目native库很多,还支持各种平台,为了减少apk size,so库动态下发,按需加载是不错的选择。比如x86库服务器下发,动态加载,瘦身效果将非常可观。但是采取常规load方式,改动有点大,底层jar包,第三库不好改加载路径。

在应用启动的时,一次注入本地so路径path,待程序使用过程中so准备后安全加载。(原因后面分析,我们先看下实践)

  • 一. 下载So文件到sdk卡

  • 二. copy So文件到app缓存空间

  • 三. 反射修改so文件映射列表(关键:把自定义的native库path插入native数组最前面,即使安装包libs目录里面有同名的so,也优先加载指定路径的外部so。)

  • 四. 正常加载so使用

有兴趣可以直接查看开源代码:https://github.com/AnyMarvel/ManPinAPP

核心类,修改nativeLibraryDirectories列表,加载需要的so文件映射

/** * com.google.android.apps.photolab.storyboard.soloader.LoadLibraryUtil * Description:动态加载so文件的核心,注入so路径到nativeLibraryDirectories数组第一个位置,会优先从这个位置查找so * 更多姿势,请参考开源库动态更新so的黑科技,仅供学习交流 * */public class LoadLibraryUtil {    private static final String TAG = LoadLibraryUtil.class.getSimpleName() + "-duqian";    private static File lastSoDir = null;    public static synchronized boolean installNativeLibraryPath(ClassLoader classLoader, File folder)            throws Throwable {        if (classLoader == null || folder == null || !folder.exists()) {            Log.e(TAG, "classLoader or folder is illegal " + folder);            return false;        }        final int sdkInt = Build.VERSION.SDK_INT;        final boolean aboveM = (sdkInt == 25 && getPreviousSdkInt() != 0) || sdkInt > 25;        if (aboveM) {            try {                V25.install(classLoader, folder);            } catch (Throwable throwable) {                try {                    V23.install(classLoader, folder);                } catch (Throwable throwable1) {                    V14.install(classLoader, folder);                }            }        } else if (sdkInt >= 23) {            try {                V23.install(classLoader, folder);            } catch (Throwable throwable) {                V14.install(classLoader, folder);            }        } else if (sdkInt >= 14) {            V14.install(classLoader, folder);        }        lastSoDir = folder;        return true;    }    private static final class V23 {        private static void install(ClassLoader classLoader, File folder) throws Throwable {            Field pathListField = ReflectUtil.findField(classLoader, "pathList");            Object dexPathList = pathListField.get(classLoader);            Field nativeLibraryDirectories = ReflectUtil.findField(dexPathList, "nativeLibraryDirectories");            List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);            //去重            if (libDirs == null) {                libDirs = new ArrayList<>(2);            }            final Iterator<File> libDirIt = libDirs.iterator();            while (libDirIt.hasNext()) {                final File libDir = libDirIt.next();                if (folder.equals(libDir) || folder.equals(lastSoDir)) {                    libDirIt.remove();                    Log.d(TAG, "dq libDirIt.remove() " + folder.getAbsolutePath());                    break;                }            }            libDirs.add(0, folder);            Field systemNativeLibraryDirectories =                    ReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");            List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);            //判空            if (systemLibDirs == null) {                systemLibDirs = new ArrayList<>(2);            }            Log.d(TAG, "dq systemLibDirs,size=" + systemLibDirs.size());            Method makePathElements = ReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class, List.class);            ArrayList<IOException> suppressedExceptions = new ArrayList<>();            libDirs.addAll(systemLibDirs);            Object[] elements = (Object[]) makePathElements.invoke(dexPathList, libDirs, null, suppressedExceptions);            Field nativeLibraryPathElements = ReflectUtil.findField(dexPathList, "nativeLibraryPathElements");            nativeLibraryPathElements.setAccessible(true);            nativeLibraryPathElements.set(dexPathList, elements);        }    }    /**     * 把自定义的native库path插入nativeLibraryDirectories最前面,即使安装包libs目录里面有同名的so,也优先加载指定路径的外部so     */    private static final class V25 {        private static void install(ClassLoader classLoader, File folder) throws Throwable {            Field pathListField = ReflectUtil.findField(classLoader, "pathList");            Object dexPathList = pathListField.get(classLoader);            Field nativeLibraryDirectories = ReflectUtil.findField(dexPathList, "nativeLibraryDirectories");            List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);            //去重            if (libDirs == null) {                libDirs = new ArrayList<>(2);            }            final Iterator<File> libDirIt = libDirs.iterator();            while (libDirIt.hasNext()) {                final File libDir = libDirIt.next();                if (folder.equals(libDir) || folder.equals(lastSoDir)) {                    libDirIt.remove();                    Log.d(TAG, "dq libDirIt.remove()" + folder.getAbsolutePath());                    break;                }            }            libDirs.add(0, folder);            //system/lib            Field systemNativeLibraryDirectories = ReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");            List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);            //判空            if (systemLibDirs == null) {                systemLibDirs = new ArrayList<>(2);            }            Log.d(TAG, "dq systemLibDirs,size=" + systemLibDirs.size());            Method makePathElements = ReflectUtil.findMethod(dexPathList, "makePathElements", List.class);            libDirs.addAll(systemLibDirs);            Object[] elements = (Object[]) makePathElements.invoke(dexPathList, libDirs);            Field nativeLibraryPathElements = ReflectUtil.findField(dexPathList, "nativeLibraryPathElements");            nativeLibraryPathElements.setAccessible(true);            nativeLibraryPathElements.set(dexPathList, elements);        }    }    private static final class V14 {        private static void install(ClassLoader classLoader, File folder) throws Throwable {            Field pathListField = ReflectUtil.findField(classLoader, "pathList");            Object dexPathList = pathListField.get(classLoader);            ReflectUtil.expandFieldArray(dexPathList, "nativeLibraryDirectories", new File[]{folder});        }    }    /**     * fuck部分机型删了该成员属性,兼容     *     * @return 被厂家删了返回1,否则正常读取     */    @TargetApi(Build.VERSION_CODES.M)    private static int getPreviousSdkInt() {        try {            return Build.VERSION.PREVIEW_SDK_INT;        } catch (Throwable ignore) {        }        return 1;    }}

上面说了把自定义的native库path插入nativeLibraryDirectories最前面,那为什么这么做会生效呢

  1. 首先了解一下So的加载流程

  2. So热修复技术介绍及如何动态加载So

这里先做下准备工作

So文件加载流程 不同的同学请戳这里 Android 的 so 文件加载机制

System.loadlibrary() 方法分析so文件的加载流程,如下图所示:

Android So动态加载 优雅实现与原理分析

System.loadLibrary()

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

此处VMStack.getCallingClassLoader()拿到的是调用者的ClassLoader,一般情况下是PathClassLoader。我们进入Runtime类的loadLibrary0()方法看看。

//Runtime.javasynchronized 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;    // 1. 如果classloder存在,通过loader.findLibrary()查找到so路径    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;    }    // 2. 如果classloder不存在,通过loader.findLibrary()查找到so路径    String filename = System.mapLibraryName(libraryName);    List<String> candidates = new ArrayList<String>();    String lastError = null;    for (String directory : getLibPaths()) {    // 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;        }    }    // 3. 都没找到,抛出 UnsatisfiedLinkError 异常    if (lastError != null) {        throw new UnsatisfiedLinkError(lastError);    }    throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);}

这里根据ClassLoader是否存在分了两种情况:

  • 当ClasssLoader存在的时候通过loader的 findLibrary()查看目标库所在路径;

  • 当ClassLoader不存在的时候通过getLibPaths()查找加载路径。

最终他们都会调用doLoad()加载动态库。

这里我们只查看ClasssLoader存在时的情况:

前面知道了这个ClassLoader其实是PathClassLoader,但是findLibrary位于PathClassLoader的父类BaseDexClassLoader中:

//BaseDexClassLoader.javapublic String findLibrary(String name) {    return pathList.findLibrary(name);}

其中pathList的类型为DexPathList,我们看看它的findLibrary()方法.关键点在DexPathList.findLibrary(String libraryName) 方法中

http://androidxref.com/5.0.0\_r2/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

/**371     * Finds the named native code library on any of the library372     * directories pointed at by this instance. This will find the373     * one in the earliest listed directory, ignoring any that are not374     * readable regular files.375     *376     * @return the complete path to the library or {@code null} if no377     * library was found378     */379    public String findLibrary(String libraryName) {380        String fileName = System.mapLibraryName(libraryName);381        for (File directory : nativeLibraryDirectories) {382            String path = new File(directory, fileName).getPath();383            if (IoUtils.canOpenReadOnly(path)) {384                return path;385            }386        }387        return null;388    }

可以看到首先会调用System.mapLibraryName函数获得so的名字,比如我传入的参数是Test(这个Test就是在调用System.loadLibrary(“Test”)时传入的),则这个函数的作用就是将其转换为类似libTest.so这样的名字,然后遍历nativeLibraryDirectories数组,这是一个File文件夹数组,看其文件夹下是否存在对应的so,并且是否可读,如果满足条件,则直接返回。

那么我们就可以将我们的patch的so所在目录插入到这个数组最前面即可完成so的修复。具体代码就不贴了,实践后得出的结论是这种方式是完全可行的,只不过Android 6.0以后版本中这部分代码逻辑发生了改变。

在Android 4.0-5.1中,只需要将文件夹目录插入到nativeLibraryDirectories数组最前面即可,这个过程直接使用反射插入patch的so所在目录到数组最前面。(nativeLibraryDirectories存储了so文件加载的映射表,这里相当于修改了应用加载so的列表)

61    /** List of native library directories. */62    private final File[] nativeLibraryDirectories;

但是在Android 6.0以后,查找逻辑转为了Elements查找

http://androidxref.com/9.0.0\_r3/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

/**536     * Finds the named native code library on any of the library537     * directories pointed at by this instance. This will find the538     * one in the earliest listed directory, ignoring any that are not539     * readable regular files.540     *541     * @return the complete path to the library or {@code null} if no542     * library was found543     */544    public String findLibrary(String libraryName) {545        String fileName = System.mapLibraryName(libraryName);546547        for (NativeLibraryElement element : nativeLibraryPathElements) {548            String path = element.findNativeLibrary(fileName);549550            if (path != null) {551                return path;552            }553        }554555        return null;556    }

所以在6.0中需要将so的patch目录转换为Element对象,插入到nativeLibraryPathElements最前面,Element的对象可以直接用反射去实现下面的代码进行构造即可。

66    /** List of native library path elements. */67    // Some applications rely on this field being an array or we'd use a final list here68    /* package visible for testing */ NativeLibraryElement[] nativeLibraryPathElements;

最后的难点就是如何将对应cpu类型的so拿到,这个过程还是十分复杂的,比如说一个so同时存在x86,armeabi-v7a,armeabi的patch,而手机cpu是armeabi-v7a的,这时候就应该加载armeabi-v7a的so。总之这种情况组合起来会十分复杂了。但一般的厂商都是只兼容armeabi架构,其他的架构就可以向上兼容了(除x86架构外)

手机的cpu结构类型可以通过Build.CPU_ABI和Build.CPU_ABI2拿到,后面做的事就是根据这两个值去加载对应目录下的so,其实把这两个目录都插进去就没问题了。

总结:

到此处,那么so文件的动态加载(也可以叫做So文件的热修复)已经介绍完了,其实还是比较简单的,只是修改了so文件列表的数组映射,加载了需要使用的真实的so文件.

经过动态加载,漫品客户端也由原来50M的体积缩小到了15M的输出包.

安利 欢迎大家的start

开源地址: https://github.com/AnyMarvel/ManPinAPP

热修复so代码包位置: com.google.android.apps.photolab.storyboard.soloader.LoadLibraryUtil

如有问题请留言,或github issure

Android So动态加载 优雅实现与原理分析

夯实基础,关注前沿,娱乐生活

掌握更多前沿技术,获取更多笑点 

请关注--------喘口仙氣

免费小说APP 漫品客户端欢迎下载

Android So动态加载 优雅实现与原理分析 Android So动态加载 优雅实现与原理分析

本文分享自微信公众号 - 喘口仙氣(gh_db8538619cdd)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
秃头王路飞 秃头王路飞
2个月前
webpack5手撸vue2脚手架
webpack5手撸vue相信工作个12年的小伙伴们在面试的时候多多少少怕被问到关于webpack方面的知识,本菜鸟最近闲来无事,就尝试了手撸了下vue2的脚手架,第一次发帖实在是没有经验,望海涵。languageJavaScript"name":"vuecliversion2","version":"1.0.0","desc
浅梦一笑 浅梦一笑
2个月前
初学 Python 需要安装哪些软件?超级实用,小白必看!
编程这个东西是真的奇妙。对于懂得的人来说,会觉得这个工具是多么的好用、有趣,而对于小白来说,就如同大山一样。其实这个都可以理解,大家都是这样过来的。那么接下来就说一下python相关的东西吧,并说一下我对编程的理解。本人也是小白一名,如有不对的地方,还请各位大神指出01名词解释:如果在编程方面接触的比较少,那么对于软件这一块,有几个名词一定要了解,比如开发环
技术小男生 技术小男生
2个月前
linux环境jdk环境变量配置
1:编辑系统配置文件vi/etc/profile2:按字母键i进入编辑模式,在最底部添加内容:JAVAHOME/opt/jdk1.8.0152CLASSPATH.:$JAVAHOME/lib/dt.jar:$JAVAHOME/lib/tools.jarPATH$JAVAHOME/bin:$PATH3:生效配置
光头强的博客 光头强的博客
2个月前
Java面向对象试题
1、请创建一个Animal动物类,要求有方法eat()方法,方法输出一条语句“吃东西”。创建一个接口A,接口里有一个抽象方法fly()。创建一个Bird类继承Animal类并实现接口A里的方法输出一条有语句“鸟儿飞翔”,重写eat()方法输出一条语句“鸟儿吃虫”。在Test类中向上转型创建b对象,调用eat方法。然后向下转型调用eat()方
刚刚好 刚刚好
2个月前
css问题
1、在IOS中图片不显示(给图片加了圆角或者img没有父级)<div<imgsrc""/</divdiv{width:20px;height:20px;borderradius:20px;overflow:h
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
小森森 小森森
2个月前
校园表白墙微信小程序V1.0 SayLove -基于微信云开发-一键快速搭建,开箱即用
后续会继续更新,敬请期待2.0全新版本欢迎添加左边的微信一起探讨!项目地址:(https://www.aliyun.com/activity/daily/bestoffer?userCodesskuuw5n)\2.Bug修复更新日历2.情侣脸功能大家不要使用了,现在阿里云的接口已经要收费了(土豪请随意),\\和注意
晴空闲云 晴空闲云
2个月前
css中box-sizing解放盒子实际宽高计算
我们知道传统的盒子模型,如果增加内边距padding和边框border,那么会撑大整个盒子,造成盒子的宽度不好计算,在实务中特别不方便。boxsizing可以设置盒模型的方式,可以很好的设置固定宽高的盒模型。盒子宽高计算假如我们设置如下盒子:宽度和高度均为200px,那么这会这个盒子实际的宽高就都是200px。但是当我们设置这个盒子的边框和内间距的时候,那
艾木酱 艾木酱
1个月前
快速入门|使用MemFire Cloud构建React Native应用程序
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专
helloworld_28799839 helloworld_28799839
2个月前
常用知识整理
Javascript判断对象是否为空jsObject.keys(myObject).length0经常使用的三元运算我们经常遇到处理表格列状态字段如status的时候可以用到vue