再谈Android动态链接库

时间:2022-04-27
本文章向大家介绍再谈Android动态链接库,主要内容包括简介、.so文件加载、如何减少apk体积、总结、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

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