作者:刘天宇(谦风)

系列文章回顾《向工程腐化开炮 | proguard治理》《向工程腐化开炮 | manifest治理》《向工程腐化开炮:Java代码治理》《向工程腐化开炮|资源治理》。本文为系列文章第五篇,聚焦于动态链接库so,这一细分畛域。对工程腐化,间接开炮!

在Android技术畛域,动态链接库so个别应用c/c++开发,近年随着rust的“闪耀“,无论在aosp零碎性能层面,还是app利用性能层面,都能看到其身影。但无论应用的开发语言是什么,最终在apk和运行时的存在模式,都是合乎ELF格局的so文件。本文聚焦于动态链接库so自身,对abi不兼容、反复、抵触、无用导出符号,这几种腐化状况,进行工具研发以及治理实际。

基础知识

本章并不会解说应用c/c++等语言,编写动态链接库so的相干常识,而是站在app整体层面,尝试以“内部”(非c/c++开发者)视角,来解说近些年在Android架构工作中,理解到的一些乏味知识点。

1.1 c++规范模版库(STL)

当应用c++开发动态链接库so时,如果应用到C++规范模版库,就须要指定具体应用哪一个。有以下几种可供选择:

  • libc++。LLVM的libc++是STL标准的一种实现,Android 5.0及当前版本os便开始应用此STL,更近一步,在ndkr18开始成为惟一可用STL。因而,libc++也是Android官网指定STL;
  • gnustl&gnustl_port。这两个都是GNU我的项目提供的STL标准实现,在旧版本ndk中提供了相干反对,正如上述所讲,ndkr18后已废除。在以后开发时尽量避免应用此STL;
  • system。Android零碎内置STL标准实现,仅提供new和delete,个别不应用。同样,也在ndkr18后废除。

在选定具体STL后,还有两种链接形式可供选择:

  • 动态链接。动态链接会将应用到的stl中代码,链接(拷贝)到so中;
  • 动静链接。在链接时,并不会将stl代码拷贝到so中,而是将应用到的STL符号,保留在so的动静链接符号表中,在运行时绑定并调用这些STL中的符号(位于STL的so中)。

当app只有一个so时,倡议应用动态链接形式,以减小包尺寸;当app蕴含多个so时,全副应用动态链接,stl代码实现会拷贝多份到不同so中,这会极大减少包大小,因而应该抉择动静链接。然而须要留神的是,无论是多个so动态链接同一个STL,还是多个so动静链接多个不同STL,都会导致运行时性能异样,甚至引发crash的危险,因而,最佳计划是:仅应用一种链接形式,同时,仅应用同一个STL。

1.2 so动静链接(依赖)

对于一个c/c++源码开发的模块,如果须要援用其余模块提供的性能,与对STL的应用相似,也有动静链接和动态链接两种形式可供选择。这里须要留神的是,如果依赖的这些模块,曾经以动态链接库so模式,存在于apk中,那么在这里应该抉择动静链接模式;否则,应该应用动态链接模式。如果应用动静链接形式,援用了其它so中的符号,在最终so中会蕴含这种动静依赖关系的信息。具体来讲,这个信息存在于so文件的“.dynamic”段中,咱们能够通过readelf工具(比拟罕用的一种)来读取,举一个例子:

Dynamic section at offset 0x2d18 contains 27 entries:  Tag        Type                         Name/Value 0x0000000000000001 (NEEDED)             Shared library: [liblog.so] 0x0000000000000001 (NEEDED)             Shared library: [libm.so] 0x0000000000000001 (NEEDED)             Shared library: [libc++_shared.so] 0x0000000000000001 (NEEDED)             Shared library: [libdl.so] 0x0000000000000001 (NEEDED)             Shared library: [libc.so] 0x0000000000000001 (NEEDED)             Shared library: [libslimlady_core.so] 0x000000000000000e (SONAME)             Library soname: [libslimlady.so]

从上述输入能够看到,Type为SONAME的条目,记录了so名称。留神,这个so名称仅用于其它so依赖这个so时,在搜寻门路中进行查找,与so文件名称不肯定完全一致,然而在Android环境下,个别咱们会将这二者保持一致。so之间的动静依赖关系,记录在Type为NEEDED的条目中,对上述例子,libslimlady.so动静链接(依赖)了六个so,咱们别离来看下都是什么:

  • libdl.so。动静链接器,提供动静加载其它so能力,Android平台中的so,都会蕴含此项依;
  • libc.so、libm.so。这个能够认为是c语言的根底运行时库,能够认为所有Android中应用的so都蕴含;
  • liblog.so。Android平台logcat日志库,在c/c++代码中如果须要将信息打印到logcat中,就须要动静链接这个库,并在代码中调用相干函数;
  • libc++_shared.so。这就是上一大节讲到的LLVM版本规范模版库libc++,动静链接模式的so名称;
  • libslimlady_core.so。这是apk中另一个已存在的so,libslimlady.so通过动静链接形式,依赖这个so,从而在代码中能够调用其定义的办法。

事实上,反对上述动静链接的零碎,还反对另一种更加灵便的so加载形式,即显式运行时链接。这种链接形式,不会在so文件中记录其依赖的so,而是在运行时依据需要,动静将其它so加载进来(dlopen),获取指标符号的地址(dlsym),而后进行调用,在这里不具体开展。

1.3 so加载过程剖析

接下来,咱们看看一个so的根本加载过程,是什么样的。

so加载过程剖析

当咱们在代码中调用System.loadLibrary办法,加载一个so时,首先是在Java API Framework层查找so文件的绝对路径,这个搜寻门路存储地位如下:

  • os小于6.0时,位于BaseDexClassLoade对象,DexPathList实例中的nativeLibraryDirectories成员变量;
  • os大于等于6.0时,位于DexPathList实例中的nativeLibraryPathElements成员变量。

在找到指标so文件的绝对路径后,java虚构机会判断此so是否曾经加载,如果已加载那么间接返回。如果未加载,会持续调用到nativeloader&linker层,真正的加载也是在这层中实现。首先,会解析so文件头,收集此so动静链接的其它so汇合,如果为空或者均已加载实现,则持续判断指标so是否已加载(这里有并发问题,因而在native层会再进行判断),如果未加载便间接进行加载。留神,这里的流程是简化过的,这个动静链接so汇合是否均已加载的判断并不存在,实际上是通过遍历so,并以广度优先准则,逐个实现各级依赖so的加载工作。在这个遍历过程中,同样须要依据so名称查找so文件绝对路径,这个搜寻门路起源如下:

  • os小于7.0时,就是在java层的搜寻门路中查找;
  • os大于等于7.0时,底层so的加载引入了Namespace概念,每当BaseDexClassLoader创立实例时,都会在nativeloader层创立一个Namespace与之对应,并将java层搜寻门路拷贝一份。

不同os版本的加载流程,并不完全一致,上述so加载过程,是一个形象简化后的示意流程,真实情况要简单很多。此外,so加载是线程平安的,因而不会呈现一个so被加载多份到内存中的问题,也正因为如此,并发加载so有可能会导致阻塞期待状况呈现,这一点须要特地留神。另外,如果想要加载非app内置so,有一种计划是在java层将外置门路增加进去,如果波及到几个so之间的动静链接(依赖)状况,java层搜寻门路和native层搜寻门路不统一问题,绝不可漠视:如何指标so不在apk中,那么可能导致so找不到,如果指标so在apk中,可能导致外置so和内置so都被加载到内存状况产生。

好了,基础知识局部,就讲到这里,仅理解这些还远远不够,作为一名Android开发者,即便在理论工作中不须要开发c/c++代码,多理解一些动态链接库so的相干常识,对全面理解app运行机制也大有裨益。举荐一本集体十分喜爱的书:程序员的自我涵养(链接、装载与库)。

治理实际

随着工程模块&性能减少,动静链路库so腐化逐渐积攒:对c++规范模版库的应用形形色色,大量动态链接STL导致不必要的包大小减少;新增或者更新so时,短少必须abi,导致在对应设施上,因为找不到so而解体;偶有产生的反复so问题,也对包大小和稳定性等带来负面影响;无用导出符号逐渐积攒,同样导致包大小减少。上述这些问题,都是过往优酷与动态链接库so“腐化”奋斗中,遇到的理论问题。通过相干工具建设无效的检测能力,并基于此造成日常研发卡口机制,在确保问题零新增前提下,逐渐消化已有存量问题。

在问题定位、排查过程中,疾速获取so来自哪个模块,是一个很天然的根本诉求。二、三方模块大量引入,以及app工程模块化水平进步,都使上述信息获取的老本变得越来越高。为此,首先开发了模块蕴含so列表性能,能够疾速查看指标so,位于哪个模块(app工程、subproject工程、flat aar、内部依赖模块),示例后果:

com.youku.android:YNativer:1.2.20210119.2|-- libPdora.so|   |-- armeabi|   |-- arm64-v8a|-- libaua.so|   |-- armeabi|   |-- arm64-v8acom.alient.media:Alier:2.20210202.16|-- libalier.so|   |-- armeabi-v7a|   |-- arm64-v8a

此外,前文基础知识局部,也讲到了so之间能够具备动静依赖关系。一个so依赖哪些其它so,能够通过相干工具间接查看,并不麻烦,然而站在整个apk视角,疾速获取一个so,被哪些其它so所依赖,却并不容易。因而,作为辅助工具,还开发了so被依赖关系检测性能,在apk全局范畴内,剖析所有so之间的这种动静链接(依赖)关系。剖析后果中,仅列出一级依赖,即如果A->(依赖)B->(依赖)C,那么列表中,只会蕴含C<-B,B<-A这两组依赖,示例剖析后果:

* libc++_shared.so    # so名称|-- armeabi-v7a ()    # abi,括号中是指标abi/so,来自哪些模块,如果括号中是空,阐明在apk中不存在。|   |-- libhnd.so (com.youku.arch:Hnd:2.8.15)    # armeabi-v7a/libhnd.so,位于Hnd模块。依赖了armeabi-v7a/libc++_shared.so。|   |-- libslimlady.so (project.extaar.app:slimlady:1.0)|-- arm64-v8a ()|   |-- Hnd.so (project:app:1.0,com.youku.arch:Hnd:2.8.15)|   |-- libslimlady.so (project.extaar.app:slimlady:1.0)* libusb100.so|-- armeabi-v7a (project:library-aar-2:1.0)|   |-- libUVCCamera.so (project:library-aar-2:1.0)|   |-- libuvc.so (project:library-aar-2:1.0)|   |-- libUSBAudioDevice.so (project:library-aar-2:1.0)

接下来,对各个so“腐化”项的治理实际,逐个解说。

2.1 abi不兼容

abi是application binary interface的缩写,代表利用二进制接口。不同Android设施应用不同的CPU,而不同CPU反对不同的指令集。CPU与指令集的每种组合都有专属的利用二进制接口 (ABI),对于Andriod平台来说,次要差别局部有以下两个:

  • 可应用的CPU指令集,以及扩大指令集;
  • 利用和零碎之间传递数据的标准(包含对齐限度),以及零碎调用函数时,如何应用堆栈和寄存器。

在以后Android生态中,次要是Arm指令集CPU,进一步开展则是32位和64位arm指令集。以后新手机设施,根本都是64位cpu,然而因为历史起因,很多app都仅反对32位arm,降级app到对64位arm反对,有多方面劣势:

  • 性能。64位armCPU对应的指令集,具备更高效的指令执行速度,充分利用这些指令集,能够无效进步app应用体验;
  • 内存。32位app过程,VirtualMemory最大值为2^32,即4GB,因为os等占用,理论可用小于4GB,随着屏幕分辨率、CPU计算能力等硬件程度的进步,app承载越来越简单的性能,因而对虚拟内存的需要也随之进步,进一步,虚拟内存有余导致的OOM问题愈发重大。反对64位后,在64位机型上,VirtualMemory的限度值将超过4GB,实践下限可达2^48,可能极大缓解虚拟内存导致的OOM问题。

当然,64位并非没有一点负面影响,包体积就是其中不可漠视的一项。对于同样的c/c++/rust等代码,编译后对应的64位so,因为指令集、数据等占用Byte数减少,导致so文件也会有显著增大。不出意外的,这并不能阻塞Android生态对64位app反对的步调:googleplay于2019年8月1日起,要求所有蕴含so的新增&降级app,必须反对64位arm,否则无奈通过审核;国内利用商店也相继跟上,例如三星和华为别离在2020年启动了相干限度或者推广,时至今日,已有更多利用商店退出到64位app反对的推动中。

在对64位app的反对模式上,googleplay提供了app bundle这一组件化技术,将不同abi的so汇合,作为一个feature module,商店依据设施进行apk组装,而在国内,不同利用商店对此的反对状况并不(也很难)对立。除了app bundle,另一种对64位app的反对模式是“分包”:一个32位apk,一个64位apk,利用商店依据终端手机cpu信息,主动出现对应apk,这同样也依赖利用商店反对,也面临反对状况不对立问题。当然,还有第三种反对模式是“合包”:一个app内既蕴含32位apk,又蕴含64位apk,这种模式不须要额定反对,然而apk大小却极速收缩。

64位反对计划比照

app bundle&分包两种模式,对于非应用商店渠道,为了保障apk可用性,只有两条路可选:应用32位apk,或者apk合包。非应用商店渠道,个别都是真金白银换来的流量,过大的包体积会极大升高下载&装置转化率,所以apk合包模式很难满足需要,个别状况下只能就义64位apk带来的用户体验,而不得不应用32位apk。当然,这两年在行业内,对于非应用商店渠道,头部app更偏向于应用独立的“极小包”来进行投放,抛开商业和经营等层面不谈,其中的技术实现也是一个比拟有意思的话题,但与本文关联较小,在此也不予开展。

优酷在2020年就采纳分包模式,实现了对64位app的反对,在革新过程中,有不少存量动态链接库,仅蕴含32位so,导致app整体无奈兼容64位设施。另一方面,如何在app性能迭代过程中,始终保持对32和64位的兼容性,也是一个不小的挑战:无论是so的新增,还是现有so的降级迭代,都有可能呈现32位和64位so的缺失问题,而不可能每一次工程或者代码的改变,都会全量在32位和64位apk中进行双重验证。

为此,研发了32/64位abi兼容性检测工具,对于同名so,当未同时具备32位arm(armeabi或者armeabi-v7a)和64位arm(arm64-v8a)时,即断定为abi不兼容。更近一步,提供选项,当检测后果不通过时,终止构建过程,造成卡口机制。示例检测后果如下,同时也给出了so来自于哪些模块:

libUVCCamera.so|-- armeabi-v7a ([project:library-aar-2:1.0])libjpeg-turbo1500.so|-- armeabi-v7a ([project:library-aar-2:1.0])

优酷在2021年1月上线so abi不兼容卡口至今,累计拦挡11次,无效保障对32/64位设施的兼容性。事实上,无论应用哪种模式进行64位apk的反对,这项检测能力和卡口机制,都可能完全一致的施展预期作用。

abi不兼容治理状况

2.2 反复so

反复so,是指雷同abi的不同名so,其文件md5值统一,一般来讲这都是同一个so改文件名之后的后果。在apk构建过程中,反复so均会进入到apk中,导致包大小减少。此外,一旦被全副加载到内存中,会导致多种运行时危险,起因和第一章讲述的STL被多个so动态链接相似。示例检测后果如下:

[armeabi-v7a] md5: c0598ed0b87843147152e14bba2b036f|-- libmitaec.so (com.youku.android:DQI4Android:1.2.0.10)|-- libNlsAEC.so (com.youku.android:DQI4Android:1.2.0.10)[armeabi] md5: 66a9cf3fcd1739ad01d637418e97ebc5|-- libwxjst.so (com.tb.android:ws:0.26.4.45-youku)|-- libwxjsb.so (com.tb.android:ws:0.26.4.45-youku)

反复so,也同样提供选项,当检测后果不通过时,终止构建过程,造成卡口机制。在理论迭代过程中,这种状况应该呈现频率较低,毕竟失常开发过程,不会刻意批改一个so的文件名。优酷近1年多的实际过程中,仅发现存量的两个反复so,在2021年2月卡口上线至今,未呈现此类问题导致的拦挡记录。之所以还要研发这样的检测能力,并部署上线对应卡口,是因为像这样不常见且没有任何“蛛丝马迹”的问题,一旦呈现后很难及时发现,可能会存在很久。而这,也正是“工程腐化”中暗藏较深的一种典型问题,不可不防。

2.3 抵触so

抵触so,是指雷同abi的同名so,其文件md5值不统一。在apk构建过程中,雷同abi下的同名so依据构建配置(packaginggptions),会导致构建失败(default,不容易定位同名so来自哪一个模块),或抉择第一个遇到的(pickFirsts,具备“随机性”,会导致不确定性危险)。研发的本项冲突检测性能,次要是为了不便定位抵触so,来自于哪些模块,因为Android Gradle Plugin在构建失败后,并不会给出这个so,来源于哪些模块。示例检测内容如下:

[armeabi-v7a] libaceManager.so|-- com.youku.arch:Hnd:2.8.16-SNAPSHOT (43392841f299f7b2e35df4bd85703272)|-- com.youku.android:ALib:1.0.2 (b7f8d6fc7ba25073e8743c061ed9e92a)

因为Android Gradle Plugin默认曾经对此类问题,实现了间接的拦挡(打包失败),因而本项检测能力,并没有部署上线对应卡口,而是在日常工作中,作为一项辅助性能应用。

2.4 无用导出符号

导出符号(exported symbol),是指在so内定义的对象、办法、全局变量,被设置为可被内部代码援用(导入)。而无用导出符号,正是在apk全局范畴内的所有so中,查找是否存在对此符号的导入(援用),如果没有就属于无用导出符号,能够在so构建过程的链接阶段,通过链接选项来进行清理。无用导出符号肯定是在apk全局范畴内,才可能失去无效的剖析,因为在各so编译阶段,除非是调用链最上层的so,否则很难确定到底哪些符号没有被内部应用。剖析后果,依照模块、so名称、abi逐级展现,示例内容如下:

* project:library-aar-2:1.0|-- libuvc.so|   |-- armeabi-v7a|   |   |-- _uvc_status_callback|   |   |-- uvc_print_format_desc_one|   |   |-- uvc_find_frame_desc_stream|   |   |-- uvc_any2iyuv420SP|   |   |-- uvc_print_configuration_desc|   |   |-- uvc_get_bus_number|   |   |-- uvc_parse_vc_extension_unit|   |   |-- uvc_get_stream_ctrl_format_size|   |   |-- uvc_yuyv2yuv420P|   |-- arm-v8a|   |   |-- _uvc_status_callback|   |   |-- uvc_print_format_desc_one|   |   |-- uvc_find_frame_desc_stream

对于检测后果,须要留神以下两点:

  • JNI办法已疏忽。os在绑定JNI办法时,会应用到JNI_OnLoad/JNI_OnUnload,以及所有“Java_”结尾的符号,然而在上述检测算法中,会被误检测为无用,因而在检测后果中,专门进行了剔除,避免出现误检状况;
  • 通过dlsym形式加载并调用的符号,会被误检为无用,须要结合实际代码性能,进行最终判断。

无用导出符号,思考到存在实践上的误检问题,以及大量无用导出符号,在短期内存在的合理性,并没有进一步造成卡口,而是作为包大小剖析后果中,一个可瘦身项来出现。2021年12月检测能力开发实现后,优酷这边存量无用导出符号约3.5万个,在进行了一轮集中式问题散发后,目前曾经降至约2.8万个。

2.5 治理全景

至此,对于动态链接库so,进行了较全面无效的防腐化能力建设和治理。最初,给出一份全景图:

动态链接库so治理全景

还能做些什么

事实上,动态链接库so作为二进制模式程序代码,蕴含了很多信息,例如在优酷的包大小剖析工具中,将动态链接STL、链接非标准STL,做为可瘦身检测项之一,为包瘦身提供无效领导。同时,绝对于java的jvm字节码,so的剖析难度要高很多,后续依然有宽泛的摸索空间,例如缺失导出符号、JNI办法不匹配等。

在Android开发畛域,java/kotlin这一下层技术栈,与c/c++/rust等底层技术栈,无论从源码编译过程、调试,还是运行时谬误定位剖析,都有着极大的差别。一方面,so对应源码很多时候,是同一套代码编译为多端(Android/ios)应用的库,无论是源码还是编译选项,可能都短少对Android的深刻优化;另一方面,java/kotlin代码与so相互调用局部,也因为技术栈上的gap,容易呈现“腐坏”的代码。

这须要从事Android畛域的开发者,可能扩大本身的语言&技术栈,从而以更全面的视角,写出优良的代码实现。与工程腐化的奋斗,须要不同技术栈开发者,往前迈一小步,突破这种技术边界导致的腐化问题,与诸君共勉。

【参考文档】

  • 【Book】程序员的自我涵养(链接、装载与库)
  • 【google】C++ Library Support:https://developer.android.com...

关注【阿里巴巴挪动技术】微信公众号,每周 3 篇挪动技术实际&干货给你思考!