再谈Android动态链接库
前不久,我们准备将自己开发的视频播放sdk提供给公司其他部门,在打包的时候,同事问了我一个问题,为什么我们打sdk的时候需要分别提供armeabi和arm64-v8a(ps,还有其他7种CPU架构)。其实这是一个常识问题,针对不同的架构我们肯定要提供不同的动态链接库,所以,在实际开发过程中,我们并不是将这7种so库都集成到我们的项目中去,我们会根据实际情况做一个取舍。
那么旧事重提,我们再来看看Android动态链接库。
简介
早期的Android系统几乎只支持ARMv5的CPU架构,不过到目前为止支持7种不同的架构:ARMv5,ARMv7 (从2010年起),x86 (从2011年起),MIPS (从2012年起),ARMv8,MIPS64和x86_64 (从2014年起),每一种都关联着一个相应的ABI。
所谓ABI,是指定义的二进制文件(尤其是.so文件)如何使用指令集,内存对齐到可用的系统函数库,如何运行在相应的系统平台上。
如果项目用到了NDK,Android apk文件将会到位于lib/ABI文件下读取相关.so文件。Android包管理器在安装APK文件时,会自动选择对应系统环境下预编译好的.so文件。在x86设备上,libs/x86目录中如果存在.so文件的 话,会被安装,如果不存在,则会选择armeabi-v7a中的.so文件,如果也不存在,则选择armeabi目录中的.so文件(因为x86设备也支 持armeabi-v7a和armeabi)。
ABI和CPU的关系
在使用so库应该注意:很多设备都支持多于一种的ABI,当一个应用安装在设备上,只有该设备支持的CPU架构对应的.so文件会被安装。
但是为了打包体积和使用的精准性,最好是针对特定平台提供相应平台的ABI文件。我们可以通过Build.SUPPORTED_ABIS得到根据偏好排序的设备支持的ABI列表。但你不应该从你的应用程序中读取它,因为Android包管理器安装APK时,会自动选择APK包中为对应系统ABI预编译好的.so文件。
7种CPU架构对比:
ABI(横向)和cpu(纵向) |
armeabi |
armeabi-v7a |
arm64-v8a |
mips |
mips64 |
x86 |
x86_64 |
---|---|---|---|---|---|---|---|
ARMv5 |
支持 |
||||||
ARMv7 |
支持 |
支持 |
|||||
ARMv7 |
支持 |
支持 |
支持 |
||||
MIPS |
支持 |
||||||
MIPS64 |
支持 |
支持 |
|||||
x86 |
支持 |
支持 |
支持 |
||||
x86_64 |
支持 |
支持 |
支持 |
说明:不同的ABI,针对不同的cpu架构有不同的优先权。例如,x86设备上,libs/x86目录中如果存在.so文件的话,会被安装,如果不存在,则会选择armeabi-v7a中的.so文件,如果也不存在,则选择armeabi目录中的.so文件。
64位设备(arm64-v8a, x86_64, mips64)能够运行32位的函数库,但是以32位模式运行,在64位平台上运行32位版本的ART和Android组件,将丢失专为64位优化过的性能(ART,webview,media等等)。
使用NDK时,你可能会倾向于使用最新的编译平台,但事实上这是错误的,因为NDK平台不是后向兼容(兼容过去的版本)的,而是前向兼容(兼容将来的版本)的。推荐使用app的minSdkVersion对应的编译平台。
使用C++运行时编译的.so文件
需要说明的是,.so文件可以依赖于不同的C++运行时,静态编译或者动态加载。 混合使用不同版本的C++运行时可能导致很多奇怪的crash。但是我们在使用不同环境进行编译的时候应该做到以下几点:
- 当只有一个.so文件时,静态编译C++运行时是没问题的
- 当存在多个.so文件时,应该让所有的.so文件都动态链接相同的C++运行时。
- 这意味着当引入一个新的预编译.so文件,而且项目中还存在其他的.so文件时,我们需要首先确认新引入的.so文件使用的C++运行时是否和已经存在的.so文件一致。
.so文件加载
关于.so文件的加载,Android在System类中提供了下面两种方法。
public static void loadLibrary(String libName) {
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}
public static void load(String pathName) {
Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader());
}
第一种,System.loadLibrary:
System.loadLibrary只需要传入so在Android.mk中定义的LOCAL_MODULE的值即可,系统会调用System.mapLibraryName把这个libName转化成对应平台的so的全称并去尝试寻找这个so加载。比如我们的so文件全名为libmath.so,加载该动态库只需要传入math即可。例如:
System.loadLibrary("math");
第二种,System.load 可以使用这个方法来指定我们要加载的so文件的路径来动态的加载so文件。如我们在打包期间并不打包so文件,而是在应用运行时将当前设备适用的so文件从服务器上下载下来,放在/data/data//mydir下,然后在使用so时调用。例如:
System.load("/data/data/<package-name>/mydir/libmath.so");
其实loadLibrary和load最终都会调用nativeLoad(name, loader, ldLibraryPath)方法,只是因为loadLibrary的参数传入的仅仅是so的文件名,所以,loadLibrary需要首先找到这个文件的路径,然后加载这个so文件。 而load传入的参数是一个文件路径,所以它不需要去寻找这个文件路径,而是直接通过这个路径来加载so文件。
注意 如果我们把从服务器下载的so文件放到sd会出现什么问题呢(如,/mnt/sdcard/libmath.so)?当你使用load加载的时候会报下面的错误:
java.lang.UnsatisfiedLinkError: dlopen failed: couldn't map "/mnt/sdcard/libmath.so" segment 1: Permission denied
ps:因为SD卡等外部存储路径是一种可拆卸的(mounted)不可执行(noexec)的储存媒介,不能直接用来作为可执行文件的运行目录,使用前应该把可执行文件复制到APP内部存储下再运行。
IDE导入ABI文件
在IDE中,如何导入ABI文件呢?
- Android Studio工程放在jniLibs/ABI目录中(当然也可以通过在build.gradle文件中的配置jniLibs.srcDir脚本)
- Eclipse工程放在libs/ABI目录中
其他说明: apk加载完成后,在Android 5.0以下系统中,.so文件位于app的nativeLibraryPath目录中;在Android 5.0以上系统中,.so文件位于app的nativeLibraryRootDir/CPU_ARCH目录中。
一键生成不然的ABI版本的APK
有时候为了方便,我们希望一键生成不同ABI版本的apk,当然这个包的体积有点大。
android {
...
splits {
abi {
enable true
reset()
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
universalApk true //generate an additional APK that contains all the ABIs
}
}
// map for the version code
project.ext.versionCodes = ['armeabi': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'mips': 5, 'mips64': 6, 'x86': 8, 'x86_64': 9]
android.applicationVariants.all { variant ->
// assign different version code for each output
variant.outputs.each { output ->
output.versionCodeOverride =
project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * 1000000 + android.defaultConfig.versionCode
}
}
}
如何减少apk体积
现在的apk动辄几十M或者更大,apk包大小的精简成为了开发过程中的重要一环。如果将7种CPU的ABI文件都打包到应用中将是灾难性的,所以,移除不必要的so来减小包大小是一个不错的选择。 例如,根据特定的平台提供特定的ABI文件(x86,armeabi,armeabi-v7a)。
android {
splits {
abi {
enable true
reset()
include 'x86', 'armeabi', 'armeabi-v7a',
universalApk false
}
}
}
上面的方法需要应用市场提供用户设备CPU类型更识别的支持,在国内并不是一个十分适用的方案。常用的处理方式是利用gradle中的abiFilters配置。 配置修改主工程build.gradle下的abiFilters:
android {
defaultConfig {
ndk {
abiFilters 'armeabi'
}
}
}
abiFilters后面的ABI类型即为要打包进apk的ABI类型,除此以外都不打包进apk里。然后,在gradle.properties加入一段配置:
android.useDeprecatedNdk=true
总结
使用兼容模式去运行arm架构的so,会丢失专门为当前ABI优化过的性能;其次还有兼容性问题,虽然x86设备能兼容arm类型的函数库,但是并不意味着100%的兼容,某些情况下还是会发生crash,所以x86的arm兼容只是一个折中方案,为了最好的利用x86自身的性能和避免兼容性问题,我们最好的做法仍是专为x86提供对应的so。 或者利用System.load方法动态加载当前设备对应的so文件也是一个不错的选择。
- 一次通过漏洞挖掘成功渗透某网站的过程
- 使用fuzzDB进行web安全测试
- Android Material Design系列之FloatingActionButton和Snackbar
- Fluent Nhibernate之旅(五)--利用AutoMapping进行简单开发
- Android Material Design系列之Toolbar
- Struts2 S2-020在Tomcat 8下的命令执行分析
- Struts2再曝S2-020补丁绕过漏洞 – 万恶的正则表达式
- 学习BlogEngine.Net解读笔记系列(一)
- Android面试系列之应用内多语言切换
- Android面试系列之AsyncTask
- Kali-Linux扩充弹药:Kali Linux metapackages
- 使用HackRF解调TDD-LTE信号
- 一个优秀的Android应用从建项目开始
- Ruby OpenSSL 私钥伪造脚本
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- SQL注入靶场之SQLiLabs搭建指南
- [OHIF-Viewers]医疗数字阅片-医学影像-redux-token实操(1)
- [OHIF-Viewers]医疗数字阅片-医学影像-屏蔽StudyList病例列表
- Ant Design for Vue的Table组件一列显示多个参数
- 【React】React-router的使用记录
- Blazor带我重玩前端(四)
- Android绘制系统简介
- E: Sub-process /usr/bin/dpkg returned an error code (1) 解决方案
- Linux 如何使用包管理器安装 Node.js
- CSS画图
- R语言聚类算法的应用实例
- Python时间序列选择波动率预测指数收益算法分析案例
- Linux 常用系统工作命令-date
- R使用LASSO回归预测股票收益
- Linux 常用系统工作命令-reboot、poweroff、wget