之前写了一篇关于 APK 瘦身的文章:
Android修炼系列(22),我的 apk 瘦身知识点
里面提到了关于 so 部分的优化,在实际的项目里,so 库文件一直都是包体积难以减小的杀手。在构建过程中,apkbuilder 会将 so 文件和 dex 文件等一起打包成 apk 文件,在这个过程里,系统会对 so 文件进行压缩,算法采用的是级数组别为 8 的 zip deflate 压缩算法。关于 s优先级是什么意思o 的优化,常用的主要有几种:
- 对于 so 文件,我们可以根据需要,减优先级队列少对于 cpu 平台的支持。需要注意的一点是,现在 google 要求app 适配 v8a 了,国内的 vivo、小米等商店也开始推进 v8a 的适配了:
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
-
对于 so 的占用包体积优化,我们还可以采用动态下发 so 的方式。
-
相对于 zip 压缩,我们可以采用压缩率更好的 7z lzma 算法压缩,基本单个文件的压缩率能提高约 10%,但有点需要注意,使用 7z 压缩后的 so,在加载之前,需要进行7z解压操作,这里会有一个耗时,建议此时项目加载 so 采用异步方式。
本节就来介绍下,我们将如何进行 so 文件的动态加载呢?
动态加载so数组去重方法
选择下发so名单
在介绍加载方案之前,我们先工龄差一年工资差多少来思考下,我们要如何选择要下发的 so 库呢?即使我们需要减小包体积,但了稳定性考虑,肯定也不能一股脑的都采用动appearance态下发的方式吧。
一般我们是通过 so 的加载频率来给 so 划优appearance先级的,大致数组排序分为 3 类:
-
对于使用频率高,或关键功能的 so,一般不采用动态下发,直接留在apk内,随安装包发布。
-
对于使用低频的,不常用的 so,我们可以采用预下载的方式,即 App 启动时就要检查和下载对应 so。
-
对于基本不用的so,就可以啥时侯用啥时候再去检查下载即可。
那怎么统计 so 的使用频率呢?我们可以采用埋点上报的方式,但这种方式统计不了三方库内的 so,这时如有必要,可以使用字节码插桩的方式。
选择so加载方案
so 的加载方式,java方式就两种:
-
采用 System.load(“libxxx.so”)
-
使用 System.loa数组词dLibrary(“xxx”)
System宫颈癌.load
方法简单,直接传入本地 so 文件路径即可使用appstore,但缺点优先级也明显,就是顾不到三方库内的 so 文件,一般三方库内的 so 加载都是使用的 System.loadLibrary
的,如果采用字节码插桩方式将三方库内的 Sy数组去重stem.loadLibrary
都改成 System.load
,且不说方案好不好,这本appointment身就是有工龄越长退休金越多吗实现成本的。
这样说,System.loadLibrary
确实是不错的选择,但我们不传入 soGo 文件地址,本地的 so 文件,又如何工资超过5000怎么扣税被识别呢?
// 如,libhello.so 的存储在了 /data/user/0/com.blog.a/app_libs 目录下了
System.loadLibrary("hello");
很自然的想到,System.loadLibrary
加载 so 文件的时候,会去 ClassLoader
的 libs 路径数组里查找并加载 so,那是不是也让它在我们的自定义路径下也查找一遍就行了。
基于此,我们可以直接通过反射的方式将我们的自定义路工资超过5000怎么扣税径,注入 ClassLoader
管理的路径字节码文件扩展名数组里即可。
那这个路径数组在哪里呢?就不贴源码了,感兴趣的可以去看下,传送门,简单写了下调用链:
System.loadLibrary("hello");
->Runtime#loadLibrary0(Reflection.getCallerClass(), libname);
->Runtime#loadLibrary0(classLoader, fromClass, libname);
->BaseDexClassLoader#findLibrary(libraryName)
->DexPathList#findLibrary(libraryName)
这里是关键代码,我们能看到 findLibrary
时,会遍历 natieLibraryPathElements
查找,那这个 natieLibraryPathElements
是什么呢?
[> src/main/java/dalvik/system/DexPathList.java]
通过下图可知,natieLibraryPathElements
是 natiappearveLibraryDirectories
和 systemNativeLibrar优先级yDirectories
两者的并集,其中 nativeLibraryDirectories
内存放了 App 内所有 Native 库的路径,systemNativeLibraryDirectories
内存放了系统 Native 库的路径字节码文件。
具体的反射方法,可以查看 tinker,公司让员工下班发手机电量截图 很全,而且适配了不同系统版本,这里以 version25 为例:
[tinker-android/tinker-android-lib/…/TinkerLoadLibrary.java]
private static void install(ClassLoader classLoader, File folder) throws Throwable {
// step1: 获取 pathList
final Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
final Object dexPathList = pathListField.get(classLoader);
// step2: 拿到我们的 nativeLibraryDirectories
final Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
List<File> origLibDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
// step3: 如果 origLibDirs 内有我们的路径了,移除掉
...
// step4: 将我们的路径放在集合首位,这样会优先加载,想想是不是也可以实现so的替换呢?
origLibDirs.add(0, folder);
// step5: 获取 pathList 系统路径集合
final Field systemNativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> origSystemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
// step6: addAll 两者所有路径
final List<File> newLibDirs = new ArrayList<>(origLibDirs.size() + origSystemLibDirs.size() + 1);
newLibDirs.addAll(origLibDirs);
newLibDirs.addAll(origSystemLibDirs);
// step7: 生成一个新的 natieLibraryPathElements 集合
final Method makeElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class);
final Object[] elements = (Object[]) makeElements.invoke(dexPathList, newLibDirs);
// step8: 覆盖掉原有的路径集合
final Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.set(dexPathList, elements);
}
代码很少,看完枸杞后我们想想,我们明明可以直接通过反射做到修改 nativeLibrary优先级是什么意思Directories
的值,那为何还优先级英文要创建一个新的集合来覆盖呢?这是因为 nappstoreativeLibraryDirectories
就工龄越长退休金越多吗是一个 ArrayList< File > ,是线程不安全的,所以要避免并发修改异常 ConcurrentModificatio优先级nException
,代价就是会出现并发读脏数据问数组指针题,但数组词影响不大。
link依赖问题
我们知道,在 Native 开发的时候,如果 libappetiteA.so 依赖了 libB.so,则要在 cAPPmake 中配置 target_link_libraries
,则能 link A to字节码文件 B,具体方法可看下我之前写的关于编译so库的博客:
# 通过 link 可将源文件构建的库和三方库都加载进来
target_link_libraries( # 源文件库的名字
A
# 引用的三方库
B
# 三方库log, included in the NDK.
${log-lib} )
现有场景:
我们动态下发 libA.so 和 li枸杞bB.so 到本地,并通过上面方法,将自定义路径注入到 nativeLibraryDirectories
内。
正常情况下,如果我们通过 System.loadLibrary("A")
来加载 libA.so,那么系统应该会自动通优先级队列过appearance A 的依赖信息,调用 dlopen
来帮我们加载 libB.so,我们根本不需要管理 libB.s优先级是什么意思o 的加载才对。
Android N 之前也确实是这样的,但不幸的是,N 以后 直接调用工商银行 System.loadLiapproachbrary("A")
就会执行报错:
java.lang.UnsatisfiedLinkError: dlopen failed: library "libB.so" not found
为啥会这样呢?
引用 cloud.tencent.com/deve优先级排序c语言loper/a…
Andr数组排序oid Native 用来链接 so 库的Li优先级队列nker.cpp dlopen优先级英文 函数的具体实现变化比较大(主要是引入了Namespace 机制)数组公式:以往的实现里,Linker 会在 ClassLode优先级最高的运算符r 实例的 nativeLibraryDirectories 里的所有路径查找相应的 so 文件;更龚俊新之后,Linker 里检索的路径在创建 ClassLoader 实例后就被系统通过 Namespace 机制绑定了,当我们注入新的公司让员工下班发手机电量截图路径之后appointment,虽然 ClassLoader 里的路径增加了,但公积金是 Linker 里 Namespace 已经绑定的路径集合并没有同步更新,所以出现了 libA.so 文件能找到,而 libB.so 找不到的情况。
至于 Namespace 机制的工作原理了,可以简单认为是一个以 C数组排序lassLoader 实例 HashCode 为apple Key 的 Map,Native 层通过 ClassLoader 实例获取 Map 里存放的 Value(也就是 so 文件路径集合)。
那我们要怎么解决呢?其实如果动态下发的 so 库的依赖关系,我们都清楚,我们只需要按照appear顺序手动加载即可,要注意顺序,优先级顺序不对,就会直接approachcrash:工龄越长退休金越多吗
// 在加载 A 之前,手动将依赖库 B 先加载起来就ok
System.loadLibrary("B")
System.loadLibrary("A")
实测是有效的。
但这个方案的缺点也很明显,那就是必须要知道 so 库的依赖关系,如果我们要动态下发的是个三方库,就会很麻烦,需要不断尝试加载顺序优先级排序c语言。
好的是,目前已有了多种解决方案:
- 自定义 S字节码ystem#load,加载 libxxx.so 前,先解析 libxxx.so 的依赖信息,再递归加载其依赖的 so 文件,就不用我们手动梳理了(推荐 SoLoader )
- 自定义 Linker,完全自己控制 so 文件的检索逻辑,从根本上解决 link 情况加载失效问题(推荐 ReLinker )
- 类似 Tinker,在合适的时机替换 ClassLoader 实例,新的 ClassLoader 实例绑定最新的路径集合。
好了,数组去重方法具体方案实现就不工商银行说了,感兴趣的自己看吧。
本节完。