Android使用SO库时要注意的一些问题

转自:https://segmentfault.com/a/1190000005646078java

正好动态加载系列文章谈到了加载SO库的地方,我以为这里能够顺便谈谈使用SO库时须要注意的一些问题。或许这些问题对于常常和SO库开发打交道的同窗来讲已是老生长谈,可是既然要讨论一整个动态加载系列,我想仍是有必要说说使用SO库时的一些问题。android

在项目里使用SO库很是简单,在 加载SD卡中的SO库 中也有谈到,只须要把须要用到的SO库拷贝进 jniLibs(或者Eclipse项目里面的libs) 中,而后在Java代码中调用 System.loadLibrary("xxx") 加载对应的SO库,就可使用JNI语句调用SO库里面的Native方法了。nginx

可是有同窗注意到了,SO库文件能够随便改文件名,却不能任意修改文件夹路径,而是“armeabi”、“armeabi-v7a”、“x86”等文件夹名有着严格的要求,这些文件夹名有什么意义么?web

SO库类型和CPU架构类型

缘由很简单,不一样CPU架构的设备须要用不一样类型SO库(从文件名也能够猜出来个大概嘛 ╮( ̄▽ ̄")╭)。sql

记得还在学校的时候,说起ARM处理器时,老师说之后移动设备的CPU基本就是ARM类型的了。老师未曾欺我,早期的Android系统几乎只支持ARM的CPU架构,不过如今至少支持如下七种不一样的CPU架构:ARMv5,ARMv7,x86,MIPS,ARMv8,MIPS64和x86_64。每一种CPU类型都对应一种ABI(Application Binary Interface),“armeabi-v7a”文件夹前面的“armeabi”指的就是ARM这种类型的ABI,后面的“v7a”指的是ARMv7。这7种CPU类型对应的SO库的文件夹名是:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64。编程

不一样类型的移动设备在运行APP时,须要加载本身支持的类型的SO库,否则就GG了。经过 Build.SUPPORTED_ABIS 咱们能够判断当前设备支持的ABI,不过通常状况下,不须要开发者本身去判断ABI,android系统在安装APK的时候,不会安装APK里面所有的SO库文件,而是会根据当前CPU类型支持的ABI,从APK里面拷贝最合适的SO库,并保存在APP的内部存储路径的 libs 下面。(这里说通常状况,是由于有例外的状况存在,好比咱们动态加载外部的SO库的时候,就须要本身判断ABI类型了。)segmentfault

一种CPU架构 = 一种对应的ABI参数 =  一种对应类型的SO库api

到这里,咱们发现使用SO库的逻辑仍是比较简单的,可是Android系统加载SO库的逻辑仍是给咱们留下了一些坑。安全

使用SO库时要注意的一些问题

1. 别把SO库放错地方

SO库其实都是APP运行时加载的,也就是说APP只有在运行的时候才知道SO库文件的存在,这就没法经过静态代码检查或者在编译APP时检查SO库文件是否正常。因此,Android开发对SO库的存放路径有严格的要求。服务器

使用SO库的时候,除了“armeabi-v7a”等文件夹名须要严格按照规定的来自外,SO库要放在项目的哪一个文件夹下也要按照套路来,如下是一些总结:

  • Android Studio 工程放在 jniLibs/xxxabi 目录中(固然也能够经过在build.gradle文件中的设置jniLibs.srcDir属性本身指定);

  • Eclipse 工程放在 libs/xxxabi 目录中(这也是使用ndk-build命令生成SO库的默认目录);

  • aar 依赖包中位于 jni/ABI 目录中(SO库会自动包含到引用AAR压缩包到APK中);

  • 最终构建出来的APK文件中,SO库存在 lib/xxxabi 目录中(也就是说不管你用什么方式构建,只要保证APK包里SO库的这个路径没错就没问题);

  • 经过 PackageManager 安装后,在小于 Android 5.0 的系统中,SO库位于 APP 的 nativeLibraryPath 目录中;在大于等于 Android 5.0 的系统中,SO库位于 APP 的 nativeLibraryRootDir/CPU_ARCH 目录中;

既然扯到了这里,顺便说一下,我在使用 Android Studio 1.5 构建APK的时候,发现 Gradle 插件只会默认打包application类型的module的jniLibs下面的SO库文件,而不会打包aar依赖包的SO库,因此会致使最终构建出来的APK里的SO库文件缺失。暂时的解决方案是把全部的SO库都放在application模块中(这显然不是很好的解决方案),不知道这是否是Studio的BUG,同事的解决方案是经过修改Gradle插件来增长对aar依赖包的SO库的打包支持(GitHub有开源的第三方Gradle插件项目,使用Java和Groovy语言开发)。

2. 尽量提供CPU支持的最优SO库

当一个应用安装在设备上,只有该设备支持的CPU架构对应的SO库会被安装。可是,有时候,设备支持的SO库类型不止一种,好比大多的X86设备除了支持X86类型的SO库,还兼容ARM类型的SO库(目前应用市场上大部分的APP只适配了ARM类型的SO库,X86类型的设备若是不能兼容ARM类型的SO库的话,大概要嗝屁了吧)。

因此若是你的APK只适配了ARM类型的SO库的话,仍是能以兼容的模式在X86类型的设备上运行(好比华硕的平板),可是这不意味着你就不用适配X86类型的SO库了,由于X86的CPU使用兼容模式运行ARM类型的SO库会异常卡顿(试着回想几年前你开始学习Android开发的时候,在PC上使用AVD模拟器的那种感受)。

3. 注意SO库的编译版本

除了要注意使用了正确CPU类型的SO库,也要注意SO库的编译版本的问题。虽然如今的Android Studio支持在项目中直接编译SO库,可是更多的时候咱们仍是选择使用事先编译好的SO库,这时就要注意了,编译APK的时候,咱们老是但愿使用最新版本的build-tools来编译,由于Android SDK最新版本会帮咱们作出最优的向下兼容工做。

可是这对于编译SO库来讲就不同了,由于NDK平台不是向下兼容的,而是向上兼容的。应该使用app的minSdkVersion对应的版本的NDK标原本编译SO库文件,若是使用了过高版本的NDK,可能会致使APP性能低下,或者引起一些SO库相关的运行时异常,好比“UnsatisfiedLinkError”,“dlopen: failed”以及其余类型的Crash。

通常状况下,咱们都是使用编译好的SO库文件,因此当你引入一个预编译好的SO库时,你须要检查它被编译所用的平台版本。

4. 尽量为每种CPU类型都提供对应的SO库

好比有时候,由于业务的需求,咱们的APP不须要支持AMR64的设备,但这不意味着咱们就不用编译ARM64对应的SO库。举个例子,咱们的APP只支持armeabi-v7a和x86架构,而后咱们的APP使用了一个第三方的Library,而这个Library提供了AMR64等更多类型CPU架构的支持,构建APK的时候,这些ARM64的SO库依然会被打包进APK里面,也就是说咱们本身的SO库没有对应的ARM64的SO库,而第三方的Library却有。这时候,某些ARM64的设备安装该APK的时候,发现咱们的APK里带有ARM64的SO库,会误觉得咱们的APP已经作好了AMR64的适配工做,因此只会选择安装APK里面ARM64类型的SO库,这样会致使咱们本身项目的SO库没有被正确安装(虽然armeabi-v7a和x86类型的SO库确实存在APK包里面)。

这时正确的作法是,给咱们本身的SO库也提供AMR64支持,或者不打包第三方Library项目的ARM64的SO库。使用第二种方案时,能够把APK里面不须要支持的ABI文件夹给删除,而后从新打包,而在Android Studio下,则能够经过如下的构建方式指定须要类型的SO库。

productFlavors {
    flavor1 {
        ndk {
            abiFilters "armeabi-v7a"
            abiFilters "x86"
            abiFilters "armeabi"
        }
    }
    flavor2 {
        ndk {
            abiFilters "armeabi-v7a"
            abiFilters "x86"
            abiFilters "armeabi"
            abiFilters "arm64-v8a"
            abiFilters "x86_64"
        }
    }
}

须要说明的是,若是咱们的项目是SDK项目,咱们最好提供全平台类型的SO库支持,由于APP能支持的设备CPU类型的数量,就是项目中全部SO库支持的最少CPU类型的数量(使用咱们SDK的APP能支持的CPU类型只能少于等于咱们SDK支持的类型)。

5. 不要经过“减小其余CPU类型支持的SO库”来减小APK的体积

确实,全部的x86/x86_64/armeabi-v7a/arm64-v8a设备都支持armeabi架构的SO库,所以彷佛移除其余ABIs的SO库是一个减小APK大小的好办法。但事实上并非,这不仅影响到函数库的性能和兼容性。

X86设备可以很好的运行ARM类型函数库,但并不保证100%不发生crash,特别是对旧设备,兼容只是一种保底方案。64位设备(arm64-v8a, x86_64, mips64)可以运行32位的函数库,可是以32位模式运行,在64位平台上运行32位版本的ART和Android组件,将丢失专为64位优化过的性能(ART,webview,media等等)。

过减小其余CPU类型支持的SO库来减小APK的体积不是很明智的作法,若是真的须要经过减小SO库来作APK瘦身,咱们也有其余办法。

减小SO库体积的正确姿式

1. 构建特定ABI支持的APK

咱们能够构建一个APK,它支持全部的CPU类型。可是反过来,咱们能够为每一个CPU类型都单独构建一个APK,而后不一样CPU类型的设备安装对应的APK便可,固然前提是应用市场得提供用户设备CPU类型设别的支持,就目前来讲,至少PLAY市场是支持的。

Gradle能够经过如下配置生成不一样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 } } }

2. 从网络下载当前设备支持的SO库

说到这里,总算回到动态加载的主题了。⊙﹏⊙

使用Android的动态加载技术,能够加载外部的SO库,因此咱们能够从网络下载SO库文件并加载了。咱们能够下载全部类型的SO库文件,而后加载对应类型的SO库,也能够下载对应类型的SO库而后加载,不过不管哪一种方式,咱们最好都在加载SO库前,对SO库文件的类型作一下判断。

我我的的方案是,存储在服务器的SO库依然按照APK包的压缩方式打包,也就是,SO库存放在APK包的 libs/xxxabi 路径下面,下载完带有SO库的APK包后,咱们能够遍历libs路径下的全部SO库,选择加载对应类型的SO库。

具体实现代码看上去像是:

/** * 将一个SO库复制到指定路径,会先检查改SO库是否与当前CPU兼容 * * @param sourceDir SO库所在目录 * @param so SO库名字 * @param destDir 目标根目录 * @param nativeLibName 目标SO库目录名 * @return */
public static boolean copySoLib(File sourceDir, String so, String destDir, String nativeLibName) throws IOException {

    boolean isSuccess = false;
    try {
        LogUtil.d(TAG, "[copySo] 开始处理so文件");

        if (Build.VERSION.SDK_INT >= 21) {
            String[] abis = Build.SUPPORTED_ABIS;
            if (abis != null) {
                for (String abi : abis) {
                    LogUtil.d(TAG, "[copySo] try supported abi:" + abi);
                    String name = "lib" + File.separator + abi + File.separator + so;
                    File sourceFile = new File(sourceDir, name);
                    if (sourceFile.exists()) {
                        LogUtil.i(TAG, "[copySo] copy so: " + sourceFile.getAbsolutePath());
                        isSuccess = FileUtil.copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + nativeLibName + File.separator + so);
                        //api21 64位系统的目录可能有些不一样
                        //copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + name);
                        break;
                    }
                }
            } else {
                LogUtil.e(TAG, "[copySo] get abis == null");
            }
        } else {
            LogUtil.d(TAG, "[copySo] supported api:" + Build.CPU_ABI + " " + Build.CPU_ABI2);

            String name = "lib" + File.separator + Build.CPU_ABI + File.separator + so;
            File sourceFile = new File(sourceDir, name);

            if (!sourceFile.exists() && Build.CPU_ABI2 != null) {
                name = "lib" + File.separator + Build.CPU_ABI2 + File.separator + so;
                sourceFile = new File(sourceDir, name);

                if (!sourceFile.exists()) {
                    name = "lib" + File.separator + "armeabi" + File.separator + so;
                    sourceFile = new File(sourceDir, name);
                }
            }
            if (sourceFile.exists()) {
                LogUtil.i(TAG, "[copySo] copy so: " + sourceFile.getAbsolutePath());
                isSuccess = FileUtil.copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + nativeLibName + File.separator + so);
            }
        }

        if (!isSuccess) {
            LogUtil.e(TAG, "[copySo] 安装 " + so + " 失败 : NO_MATCHING_ABIS");
            throw new IOException("install " + so + " fail : NO_MATCHING_ABIS");
        }

    } catch (IOException e) {
        e.printStackTrace();
        throw e;
    }

    return true;
}

总结

  1. 一种CPU架构 = 一种ABI = 一种对应的SO库;

  2. 加载SO库时,须要加载对应类型的SO库;

  3. 尽可能提供全平台CPU类型的SO库支持;

题外话,SO库的使用自己就是一种最纯粹的动态加载技术,SO库自己不参与APK的编译过程,使用JNI调用SO库里的Native方法的方式看上去也像是一种“硬编程”,Native方法看上去与通常的Java静态方法没什么区别,可是它的具体实现倒是能够随时动态更换的(更换SO库就好),这也能够用来实现热修复的方案,与Java方法一旦加载进内存就没法再次更换不一样,Native方法不须要重启APP就能够随意更换。

出于安全和生态控制的缘由,Google Play市场不容许APP有加载外部可执行文件的行为,一旦你的APK里被检查出有额外的可执行文件时就很差玩了,因此如今许多APP都偷偷把用于动态加载的可执行文件的后缀名换成“.so”,这样被发现的概率就下降了,由于加载SO库看上去就是官方合法版本的动态加载啊(否则SO库怎么工做),虽然这么作看起来有点掩耳盗铃。

相关文章
相关标签/搜索