前不久,我们装备将自己开发的视频播放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文件也是一个不错的选择。