为什么使用so

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

说起.so文件就必须提一下arm64、armeabi、armeabi-v7a、x86、x86_64等文件夹了,这些文件夹分别代表不同的CPU架构。他们存在的意义就是来做不同CPU架构手机的兼容的。

文件夹下,.so文件存在用来做各种手机CPU架构不同的兼容的。而apicloud 模块开发的云平台的云编译,只会保留armeabi这个.所以就有用到了abiFilters来指定NDK需要兼容的架构。

例:

ndk {abiFilters "armeabi"  // 指定要ndk需要兼容的架构(这样其他依赖包里mips,x86,armeabi-v7a,arm-v8之类的so会被过滤掉)
}

apk安装过程中对so的选择

在Android上安装应用程序时,Package Manager会扫描整个apk文件,寻找符合下面文件路径格式的动态连接库:

lib/<primary-abi>/lib<name>.so

在这里,primary-abi是上面表中的abi的值,name对应的是我们在Android.mk中定义的LOCAL_MODULE的值,

如果在apk内并没有找到适合当前机器primary-abi的so,Package Manager会尝试寻找适合secondary-abi的so文件:

lib/<secondary-abi>/lib<name>.so

即安装应用时,系统会根据当前CPU架构选择最优ABI适配,如果找到了合适的so文件,包管理器会将该ABI文件夹下所有so库全部拷贝至应用的data目录下:data/data/<package_name>/lib/

注意:apk安装过程对so选择是基于整个ABI文件夹的,而非以单个so文件为粒度,也就是说把lib/armeabi 、lib/armeabi-v7a、lib/x86等等文件夹的其中一个文件夹内所有.so复制到应用的data目录下。

如果我们在代码中调用了某个so的功能,而最终拷贝的ABI文件夹下并没有提供这个文件,apk的安装过程中并不会报错,但是运行时会遇到java.lang.UnsatisfiedLinkError

so的加载

对于so的加载,Android在System类中提供了两种方法:

/**

* See {@link Runtime#loadLibrary}.

*/

public static void loadLibrary(String libName) {

Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());

}

/**

* See {@link Runtime#load}.

*/

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

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

按照ABI分别单独打包APK

我们可以选择在Google Play上传指定ABI版本的APK,生成不同ABI版本的APK可以在build.gradle中进行如下配置:

android {

// Some other configuration here...

splits {

abi {

enable true

reset()

include 'x86', 'armeabi', 'armeabi-v7a', 'mips' //select ABIs to build APKs for

universalApk false // generate an additional APK that contains all the ABIs

}

}

}

只提供armabi的so

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

android {

// Some other configuration here...

defaultConfig {

ndk {

abiFilters 'armeabi'

}

}

}

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

android.useDeprecatedNdk=true

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

进阶版方案

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

需要注意的问题

不要把so放错地方

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

尽可能为所有ABI提供so

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

所有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

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

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"

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

android .so深入理解 abiFilters的使用等相关推荐

  1. Android Activity的理解

    Android Activity的理解 Activity 生命周期的四个状态 Activity的生命周期分为运行.暂停.停止.销毁四个状态. 运行状态:该Activity生命开始,Activity在前 ...

  2. android 背光灯分析,Android灯光系统--深入理解背光灯

    Android灯光系统--深入理解背光灯 一.怎么控制背光灯(简述) APP将亮度值写入数据库 线程检测数据库的值是否发生变化 这种机制成为"内容观察者"--contentObse ...

  3. android handler的理解

    android handler的理解 在看handler源码前,我一直以为google构造handler的目的是方便开发者在其他线程中 调用执行主线程的方法或者在主线程中调用执行其他线程的方法.看完源 ...

  4. Android系统分区理解及目录细解

    Android系统分区 分区种类 Android 通常有以下分区: System分区: 就是我们刷ROM的分区 Data分区:   分区就是我们装APK的分区 Catch分区:是缓存分区 SDCard ...

  5. Android回调函数理解

    Android回调函数理解,比如我用一个activity去做显示下载进度的一个进度条,但是下载是另外一个B类来做的,这个时候我Activity获取下载的进度就可以提供一个回调接口,然后让下载类来回调就 ...

  6. 谈谈你对Android NDK的理解

    2019独角兽企业重金招聘Python工程师标准>>> 1.前言   6月 26 日, Google Android 发布了 NDK ,引起了很多发人员的兴趣. NDK 全称: Na ...

  7. Android进阶笔记07:Android之MVC 理解

     1. 为什么需要MVC ? 软件中最核心的,最基本的东西是什么?  答:是的,是数据.我们写的所有代码,都是围绕数据的.      围绕着数据的产生.修改等变化,出现了业务逻辑.      围绕着数 ...

  8. Android adt 初步理解和分析(三)

    前面已经安装好了sdk,虽然只是下载了一个最早的1.5,但至少有一个sdk了,下面就要用模拟器来运行这个sdk里面的镜像文件了. 打开 android virtual device manager 工 ...

  9. android ipc简单理解,Android IPC 机制【1】--简介

    一.android 中进程间通信常用的有以下几种机制 ------------------------------------------------------------------------- ...

最新文章

  1. 2022-2028年中国消防报警行业市场前瞻与投资战略规划分析报告
  2. 关于numpy中eye和identity的区别详解
  3. 具体解释站点沙盒期的原因表现与解决的方法
  4. t检验的p值对照表_论文数据分析实战 | 如何对汇总数据进行t检验
  5. Django入门 -- 框架操作基本流程
  6. 如何降低死循环的 CPU 占用
  7. 《拥抱机器人时代——Servo杂志中文精华合集》——3.6 物联网有多么重要
  8. WinInet 错误代码 (12001 - 12156 )
  9. C++使用Socks5协议进行代理上网(二)
  10. win10创建新的计算机用户名和密码,Win10怎么新建账户 Win10创建新用户图文教程...
  11. Kotlin学习系列之:协程的取消和超时
  12. 自动关闭QQ迷你首页的小程序(加上自己的一些理解)
  13. 平安性格测试题及答案_平安人寿做性格测试怎么?
  14. 修改CheckBox选择框、设置选择框颜色
  15. 网络速率方面的知识(Kbps Mbps KB/s Mb/s Kb/s等)
  16. 量化投资学习——股指期货ETF套利
  17. C语言编程用递归法求
  18. Unicode字符集下CString与char *转换 (解决中文乱码等)(转)
  19. 个人怎么做微信小程序?个人开发者也可以这样开发属于自己的小程序
  20. 基于大数据的用户标签体系建设思路和实践

热门文章

  1. 潜意识的力量 (四)
  2. 程序猿该应该学点经济学
  3. Android Service服务的相关介绍
  4. 开源版本的 uTools。可支持 uTools 所有插件生态
  5. LOBSEGMENT
  6. #午安,努力#11.30
  7. Failed to start connector [Connector[HTTP/1.1-8082]]端口连接失败
  8. 80+开源数据集资源汇总(包含目标检测、医学影像、关键点检测、工业检测等方向)
  9. [原]碟评:张韶涵《欧若拉》
  10. C语言rand()函数解析