再谈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。但是我们在使用不同环境进行编译的时候应该做到以下几点:

  1. 当只有一个.so文件时,静态编译C++运行时是没问题的
  2. 当存在多个.so文件时,应该让所有的.so文件都动态链接相同的C++运行时。
  3. 这意味着当引入一个新的预编译.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文件也是一个不错的选择。

个人公众号:这里写图片描述

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/app/5973.html

(0)
上一篇 2021年7月17日 00:29
下一篇 2021年7月17日 00:29

相关推荐

发表回复

登录后才能评论