zoukankan      html  css  js  c++  java
  • 【开发必备】今天我们来谈谈Android NDK动态链接库(so文件)的一些见解

    一、写在前面

          直到现在,基本我写的每一个项目都会用到NDK动态链接库的知识,可见这个也的确十分常用。那么,今天,咱们就来谈谈它。

    二、什么是ABI和so

    1、发展

          早起的Android系统几乎只支持ARMv5的CPU架构,而现在却发展到了7种:ARMv5,ARMv7 (从2010年起),x86 (从2011年起),MIPS (从2012年起),ARMv8,MIPS64和x86_64 (从2014年起),每一种都关联着一个相应的ABI。每一种ABI的详细介绍可以参见官方的介绍ABI Management

    2、关系

          我们可以通过Build.SUPPORTED_ABIS得到根据偏好排序的设备支持的ABI列表。但你不应该从你的应用程序中读取它,因为Android包管理器安装APK时,会自动选择APK包中为对应系统ABI预编译好的.so文件,如果在对应的lib/ABI目录中存在.so文件的话。

    ABI(横向)和cpu(纵向)armeabiarmeabi-v7aarm64-v8amipsmips64x86x86_64
    ARMv5 支持            
    ARMv7 支持 支持          
    ARMv8 支持 支持 支持        
    MIPS       支持      
    MIPS64       支持 支持    
    x86 支持(3) 支持(2)       支持(1)  
    x86_64 支持         支持 支持

         解析: x86设备上,libs/x86目录中如果存在.so文件的话,会被安装,如果不存在,则会选择armeabi-v7a中的.so文件,如果也不存在,则选择armeabi目录中的.so文件。

    x86设备能够很好的运行ARM类型函数库,但并不保证100%不发生crash,特别是对旧设备。

    64位设备(arm64-v8a, x86_64, mips64)能够运行32位的函数库,但是以32位模式运行,在64位平台上运行32位版本的ART和Android组件,将丢失专为64位优化过的性能(ART,webview,media等等)。

    三、我什么我们要关注so

    • so机制让开发者最大化利用已有的C和C++代码,达到重用的效果,利用软件世界积累了几十年的优秀代码;
    • so是二进制,没有解释编译的开消,用so实现的功能比纯java实现的功能要快;
    • so内存分配不受Dalivik/ART的单个应用限制,减少OOM;
    • 相对于java代码,二进制代码的反编译难度更大,一些核心代码可以考虑放在so中。

    四、NDK的兼容性

         使用NDK时,你可能会倾向于使用最新的编译平台,但事实上这是错误的,因为NDK平台不是后向兼容(兼容过去的版本)的,而是前向兼容(兼容将来的版本)的。推荐使用app的minSdkVersion对应的编译平台。

         这也意味着当你引入一个预编译好的.so文件时,你需要检查它被编译所用的平台版本。

    五、一个法则

        处理.so文件时有一条简单却并不知名的重要法则。

        你应该尽可能的提供专为每个ABI优化过的.so文件,但要么全部支持,要么都不支持:你不应该混合着使用。你应该为每个ABI目录提供对应的.so文件。

    六、so文件的加载

        对于so文件的加载,Android在System类中提供了下面两种方法。

     1 /**
     2   * See {@link Runtime#loadLibrary}.
     3   */
     4  public static void loadLibrary(String libName) {
     5      Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
     6  }
     7 /**
     8   * See {@link Runtime#load}.
     9   */
    10  public static void load(String pathName) {
    11      Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader());
    12  }

        1、System.loadLibrary

        这是我们最常用的一个方法,System.loadLibrary只需要传入so在Android.mk中定义的LOCAL_MODULE的值即可,系统会调用System.mapLibraryName把这个libName转化成对应平台的so的全称并去尝试寻找这个so加载。比如我们的so文件全名为libmath.so,加载该动态库只需要传入math即可:

    System.loadLibrary("math");

         2、System.load

         对于System.load方法,官方是这样介绍的: 

    Loads a code file with the specified filename from the local file system as a dynamic library.
    The filename argument must be a complete path name.

         所以它为动态加载非apk打包期间内置的so文件提供了可能,也就是说可以使用这个方法来指定我们要加载的so文件的路径来动态的加载so文件。
         比如我们在打包期间并不打包so文件,而是在应用运行时将当前设备适用的so文件从服务器上下载下来,放在/data/data/<package-name>/mydir下,然后在使用so时调用:   

    System.load("/data/data/<package-name>/mydir/libmath.so");

         即可成功加载这个so,开始调用本地方法了。

         其实loadLibrary和load最终都会调用nativeLoad(name, loader, ldLibraryPath)方法,只是因为loadLibrary的参数传入的仅仅是so的文件名,所以,loadLibrary需要首先找到这个文件的路径,然后加载这个so文件。
         而load传入的参数是一个文件路径,所以它不需要去寻找这个文件路径,而是直接通过这个路径来加载so文件。

         但是当我们把需要加载的so文件放在SdCard中,会发生什么呢?把上面so的路径改成/mnt/sdcard/libmath.so,再尝试加载时,会得到如下错误: 

    java.lang.UnsatisfiedLinkError: dlopen failed: couldn't map "/mnt/sdcard/libmath.so" segment 1: Permission denied

         这是因为SD卡等外部存储路径是一种可拆卸的(mounted)不可执行(noexec)的储存媒介,不能直接用来作为可执行文件的运行目录,使用前应该把可执行文件复制到APP内部存储下再运行。所以使用System.load加载so时要注意把so拷贝至/data/data/<package-name>/下。

    七、通过精简so来减小apk大小

         1、为什么

         现在的apk动辄几十M或者更大,apk包大小的精简成为了开发过程中的重要一环。通过上面的介绍,我们知道x86、x86_64、armeabi-v7a、arm64-v8a设备都支持armeabi架构的so,因此,通过移除不必要的so来减小包大小是一个不错的选择。

         2、按照ABI分别单独打包APK

         我们可以选择在Google Play上传指定ABI版本的APK,生成不同ABI版本的APK可以在build.gradle中进行如下配置:(引自别处,未考证) 

     1 android {
     2     // Some other configuration here...
     3     splits {
     4         abi {
     5             enable true
     6             reset()
     7             include 'x86', 'armeabi', 'armeabi-v7a', 'mips' //select ABIs to build APKs for
     8             universalApk false // generate an additional APK that contains all the ABIs
     9         }
    10     }
    11 }

         3、只提供armabi的so

         上面的方法需要应用市场提供用户设备CPU类型更识别的支持,在国内并不是一个十分适用的方案。常用的处理方式是利用gradle中的abiFilters配置。
    首先配置修改主工程build.gradle下的abiFilters

    1 android {
    2     // Some other configuration here...
    3     defaultConfig {
    4         ndk {
    5             abiFilters 'armeabi'
    6         }
    7     }
    8 }

         abiFilters后面的ABI类型即为要打包进apk的ABI类型,除此以外都不打包进apk里。然后在项目的根目录下的gradle.properties(没有的话新建一个)中加入下面这行:

    android.useDeprecatedNdk=true

         通过上面方法减少的apk体积是十分可观的,也是目前比较主流的处理方案。

        4、进阶版方案

         如果进一步,会发现上面的方案并不完美。首先是性能问题:使用兼容模式去运行arm架构的so,会丢失专门为当前ABI优化过的性能;其次还有兼容性问题,虽然x86设备能兼容arm类型的函数库,但是并不意味着100%的兼容,某些情况下还是会发生crash,所以x86的arm兼容只是一个折中方案,为了最好的利用x86自身的性能和避免兼容性问题,我们最好的做法仍是专为x86提供对应的so。
         针对这些问题,我们可以采用一个相对更好的方案:让所有so都来自于网路,应用下载服务器上的so库后,利用System.load方法动态加载当前设备对应的so.

    八、需要注意的问题

         1、不要把so放错地方

         首先要注意的是不要把另一个ABI下的so文件放在另一个ABI文件夹下(每个ABI文件夹下的so文件名是相同的,有可能会搞错)。

         2、尽可能为所有ABI提供so

         理想状况下,应该尽可能为所有ABI都提供对应的so,这一点的好处我们已经在上面讨论过了:在可以发挥更好性能的同时,还能减少由于兼容带来的某些crash问题。当然,这一点要结合实际情况(如SDK提供的so不全、芯片市场占有率、apk包大小等)去考量,如果使用的so本身就很小,我们大可为尽可能多的ABI都提供so。
    若是局限于包大小等因素,可以结合通过精简so来减小包大小一节中提供的第三个方案来调整so的使用策略。

         3、所有ABI文件夹提供的so要保持一致

         这是一个十分容易出现的错误。
         如果我们的应用选择了支持多个ABI,要十分注意:对于每个ABI下的so,但要么全部支持,要么都不支持。不应该混合着使用,而应该为每个ABI目录提供对应的.so文件。

         先举个例子,Bugtags的so支持所有的ABI:

    libs
    |
    ├── arm64-v8a
    │   └── libBugtags.so
    ├── armeabi
    │   └── libBugtags.so
    ├── armeabi-v7a
    │   └── libBugtags.so
    ├── mips
    │   └── libBugtags.so
    ├── mips64
    │   └── libBugtags.so
    ├── x86
    │   └── libBugtags.so
    └── x86_64
        └── libBugtags.so

        但不是所有开发者提供的so都支持所有ABI: 

    lib
    |
    ├── armeabi
    │   └── libImages.so
    └── armeabi-v7a
        └── libImages.so

          如果不做任何设置,最终打出来的apk的lib目录会是这样的:

    lib
    |
    ├── arm64-v8a
    │   └── libBugtags.so
    ├── armeabi
    │   ├── libBugtags.so
    │   └── libImages.so
    ├── armeabi-v7a
    │   ├── libBugtags.so
    │   └── libImages.so
    ├── mips
    │   └── libBugtags.so
    ├── mips64
    │   └── libBugtags.so
    ├── x86
    │   └── libBugtags.so
    └── x86_64
        └── libBugtags.so

          假设当前设备是x86机器,包管理器会先去lib/x86下寻找,发现该文件夹是存在的,所以最终只有lib/x86下的so–即只有libBugtags.so会被安装。当尝试在运行期间加载libImages.so时,就会遇上下面常见的UnsatisfiedLinkError错误:

    1 E/xxx   (10674): java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/xxx-2/base.apk"],nativeLibraryDirectories=[/data/app/xxx-2/lib/x86, /vendor/lib, /system/lib]]] couldn't find "libImages.so"
    2 E/xxx   (10674):     at java.lang.Runtime.loadLibrary(Runtime.java:366)

        所以,我们需要遵循这样的准则

    • 对于so开发者:支持所有的平台,否则将会搞砸你的用户。
    • 对于so使用者:要么支持所有平台,要么都不支持。

        然而,因为种种原因(遗留so、芯片市场占有率、apk包大小等),并不是所有人都遵循这样的原则。

        一种可行的处理方案是:取你所有的so库所支持的ABI的交集,移除其他(可以通过上面介绍的abiFilters来实现)。
    如上面的例子,最终生成的apk可以是:

    lib
    |
    ├── armeabi
    │   ├── libBugtags.so
    │   └── libImages.so
    └── armeabi-v7a
        ├── libBugtags.so
        └── libImages.so
  • 相关阅读:
    HashMap源码解析
    如何同时运行两个Tomcat
    Thymeleaf遍历List<Map>和Map<>
    Apache POI详解
    如何管理系统的右键新建菜单
    datatable的dom配置
    如何获取Html的height和width属性(网页宽、高)
    什么是跨域?如何解决跨域问题?
    C# 各类文件扩展名
    C# 接口与抽象类的区别? 什么情景下使用接口,什么情景下使用抽象类?
  • 原文地址:https://www.cnblogs.com/liushilin/p/6292263.html
Copyright © 2011-2022 走看看