库裁剪记录:动态库裁剪

1. 什么是库?

相信很多小白都跟我一样,在有了一定的应用程序开发经验后,对库这个概念多多少少都有一些自己的理解,但如果某一时刻被人问起库到底是什么时,或许也像我这样,一时半会也说不清楚。这里摘自维基百科上对库的一段描述,我觉得很清楚地阐述了计算机科学中库的概念:“在计算机科学中,(英语:library)是用于开发软件子程序集合。库和可执行文件的区别是,库不是独立的计算机程序,他们是向其他程序提供服务的代码。”[1]

实际应用中,又会根据库链接的不同方式,进一步将库分为静态库(Statically-linked library)和动态库(Dynamic-linked library)。此处,库链接指把库包含到程序中。库链接方式分为静态链接动态链接。静态链接是指链接器在链接时将库的内容加入到可执行程序中,对应静态库;动态链接是指在可执行文件装载或运行程序时,由操作系统的装载程序加载库,对应动态库[1]

采用静态库的优势在于,在以二进制发布时不需要考虑用户的计算机上是否存在库文件以及版本问题,同时可以避免DLL地狱的问题;但同时,生成的可执行文件体积较大,需要更多的系统资源,在装入内存时也会消耗更多的时间[2]

不同于静态库,动态链接提供了模块化的共享库,保证多个程序在调用同一个子程序时,不需要复制多份相同的代码,从而节约了应用程序所需的磁盘和内存空间。但同时,动态库会出现DLL地狱的问题,所谓DLL地狱,指的是多个应用程序在使用同一个共享动态库时,发生版本冲突,造成部分应用程序无法执行[3]

2. 什么是库裁剪?

顾名思义,库裁剪即通过裁剪工程文件中的冗余代码和链接文件,以达到减小编译生成库大小的目的。文献[4]系统地总结了库裁剪的几种方式,包括:(1)删除冗余文件,主要有:辅助程序、配置文件、数据模板以及不必要的注释内容;(2)共享库裁剪,即删除共享库中永远用不到的冗余代码;(3)采用具有同样功能但占用空间更小的替代软件包;(4)修改源码,主要有:重新配置、编译软件包,去掉不需要的功能增加软件的模块性,以提高裁剪效率;重新配置内核,以去掉不需要的驱动和模块。

库裁剪的应用场景很多,比如,当我们在开发软件时,引用了某个标准库或系统库,但其中不是所有的功能我们都有用到,则可以通过修改编译选项(如文[5]中提到的Qt库裁剪)或修改源代码(如文[6]中提到的libiconv库裁剪)进行库裁剪。

库裁剪除了可以减小编译生成库大小之外,还能有效减少编译时间,增加开发效率,并且,裁剪后的库也能提高运行效率、减小软件体积和降低软件发布成本。

3. 工程实践记录

当前项目组正在开发的项目,主要是基于已有代码,根据用户需求,重构生成新的独立库文件,提供给用户使用。这种开发模式在工程上很常见,可以有效节约成本和提高开发效率。在这样的开发实践过程中,为了严格平衡用户需求和开发成本,需要在老代码的基础上开发新的功能,同时也要删除老代码中不需要的功能。这种开发实践在一定阶段就需要进行库裁剪,根据用户平台不同,进行库裁剪的方式也会有所差异,但本质上都是通过删除或隔离无关代码,取消链接和编译不需要的文件,以实现库裁剪。这也是我本周的主要工作内容,主要根据客户需求,分别进行了windows、android、ios和mac四个平台的库裁剪。

(1)windows平台

windows平台的库裁剪实现起来比较容易。由于所使用的代码所在分支另有稳定发布版本正在使用,为了不影响稳定发布版本,不能直接删除无关代码。可以在解决方案中添加一个独立的工程来编译生成我们所需要的库文件,并配置工程文件的属性,需要重点关注的属性包括:附加包含目录、预处理器定义、附加依赖项,这些属性的配置项位置如图1~3所示。

包含文件

                                                                                                     图 1

预编译头

                                                                                                     图 2

附加依赖项

                                                                                                     图 3

其中,附加包含目录应删除工程不需要依赖的头文件所在的目录,预处理器定义中添加宏定义,该宏定义作用域为当前工程,可以实现在该工程中隔离不需要代码的目的。具体实现可按照下面的方式进行:

// 隔离的宏定义为IS_NEED_ISOLATION

#ifndef IS_NEED_ISOLATION
    {
        code block;    // 稳定发布版本需要但新开发项目不需要的代码块
    }
#endif

附加依赖项中删除项目工程文件不再依赖的静态库文件。除了修改工程属性并在代码中添加隔离宏外,由于部分不需要的代码是直接以文件的形式提供的,还需要在工程目录中删除这些不需要的文件,具体操作如图4所示。

                                                                                                     图 4

在完成了以上工作后,还有最后一步:即修改编译脚本文件。windows平台下的编译脚本一般以cmake文件提供,可以直接拷贝原来的release版本编译脚本并修改文件名为old_name_win_isolation.cmake(此处old_name_win代指原来的文件名),并且,编辑脚本文件中的内容,删除不再需要编译的部分。这样可以有效减少编译发布版本的时间。

(2)android平台

android平台库裁剪也包括无关代码的隔离和无关代码文件的删除。隔离代码通过.bat文件和Application.mk组合实现,比如:首先,在.bat文件中定义了一个变量IS_ISOLATION,并将其值置为true,这个变量作用于.bat文件调用到的所有.mk文件。然后,在Application.mk文件中,根据变量IS_ISOLATION的值,判断是否需要在APP_CFLAGS项添加宏定义IS_NEED_ISOLATION,这个宏定义作用于工程目录下的所有.c文件。(如果项目源文件是用C++编写的,则使用APP_CPPFLAGS)。无关代码文件的删除主要通过在Android.mk文件中隔离不需要的源文件名称来实现。

android平台库裁剪花费了很多时间,耗时的点主要在于:1)安卓编译脚本运行依赖环境搭建更为复杂;2)安卓编译脚本包括old_name_and.bat、Application.mk和Android.mk三类文件,这三者之间的关系没有第一时间厘清;3)安卓裁剪后的库需要借助android studio来验证,而android studio的环境搭建最为复杂。

首先,安卓编译脚本运行依赖环境包括三方面:cygwin、ndk和android sdk,三者缺一不可。cygwin是在windows下模拟unix环境的工具集,其中包括的动态库文件cywin1.dll是编译安卓库文件不可或缺的,这个文件的主要作用是模拟linux下的API,在编译的过程中,如果C/C++ 调用了linux中的API,cygwin就会利用cygwin1.dll 来编译 C/C++源代码,从而可以在windows下生成linux下的lib...so文件[7] 。ndk是一个官方的安卓工具集,能够让开发者以本地平台支持的编程语言(比如C/C++)开发部分安卓应用程序功能代码[8],可以理解为是安卓官方提供的一种跨平台开发的支持。ndk编译命令在检测到所编写代码为本地编程语言开发时,会自动构建并生成二进制文件。android sdk是安卓应用程序开发时必须依赖的官方工具集,使用它可以将由Kotlin、Java或C++编写的应用程序编译成.apk文件[9]

其次,编译安卓库时需要运行.bat文件,其中通过调用ndk_build.cmd文件实现编译。需要说明的是,每次调用ndk_build.cmd后,Application.mk和Android.mk文件会依次运行。起初我对这种运行机制并不清楚,在发现编译脚本.bat文件中定义的变量没有作用到所有.mk文件后,在所有脚本中都加了日志打印,才推理出这种运行机制。由于这种运行机制,在每次调用ndk_build.cmd文件时,必须先对变量IS_ISOLATION进行赋值,然后再执行ndk命令,比如:

call path_of_ndk_cmd\ndk_build.cmd IS_ISOLATION=true clean

Application.mk文件中声明了一些工程范围内的配置,这些配置内容一般包括:应用程序二进制文件接口(Application Binary Interface, ABI,提供了一种系统级的编程约定)、工具链(编译C/C++程序的编译工具)、编译模式(debug或release)和STL。这些配置内容可以通过已定义的宏变量实现,比如:可以通过配置APP_CFLAGS的值,创建应用于当前工程所有.c文件的宏IS_NEED_ISOLATION:[10]

APP_CFLAGS += -DIS_NEED_ISOLATION

Android.mk文件主要包含了本地库文件、定义了作用于当前文件夹下所有文件的宏变量和通过"JNI"目录编译的所有头文件[11]

最后,验证平台android studio在安装完成之后,需要配置代理设置,否则由于公司内网不能连接到相关网站,导致部分jar包无法下载,就会出现安卓工程编译不过的问题。这块主要通过软件cntlm绕过公司的代理,连接到了相关网站,具体细节就不赘述了。

(3) ios和mac平台

这里把ios和mac平台放在一起,主要是因为二者的库都是由.xcode工程编译出来的,二者的库裁剪步骤也是完全相同的。以ios平台为例,库裁剪主要包括无关代码的隔离和无关代码文件的删除,以及编译脚本中代码的修改,这块和windows平台相似,即在项目文件目录下,把原来生成动态库的.xcodeprj文件拷贝一份并重命名后,再通过xcode打开该工程文件,在build_settting选项的preprocessor_macro栏添加宏定义IS_NEED_ISOLATION,这样,工程中的所有无关代码都可以通过宏IS_NEED_ISOLATION实现隔离。并且,删除掉库依赖栏中不再依赖的库文件。最后,在xcode左侧的工程目录下,删除无关代码文件的依赖和不再依赖的库工程文件。在终端执行下面的命令,即可编译生成裁剪后的库文件:

cd path_of_ios_build_script
./old_name_ios_isolation.sh

4. 总结

库裁剪是软件开发过程中不可避免的一个过程,涉及的面比较广,既要求开发者对代码十分熟悉,又要求开发者掌握一定的运维知识,体现了开发者的综合能力。自以为这方面掌握的知识还有所欠缺,具体体现在应用于各平台的库文件生成原理,包括编译、链接和装载的具体原理,这方面的知识仍有待加强。希望通过后续静态库裁剪对这些内容有更深刻的理解。

refs: 

[1] https://zh.wikipedia.org/wiki/%E5%87%BD%E5%BC%8F%E5%BA%AB

[2] https://zh.wikipedia.org/wiki/%E9%9D%99%E6%80%81%E5%BA%93

[3] https://zh.wikipedia.org/wiki/%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5%E5%BA%93

[4] http://www.jsjkx.com/CN/article/openArticlePDF.jsp?id=9653

[5] http://blog.chinaunix.net/uid-25765968-id-359557.html

[6] https://blog.csdn.net/mayue_web/article/details/100108034

[7] https://www.cnblogs.com/xiaoxuetu/p/3467613.html

[8] https://developer.android.com/ndk

[9] https://developer.android.com/guide/components/fundamentals

[10] https://developer.android.google.cn/ndk/guides/application_mk.html

[11] https://developer.android.google.cn/ndk/guides/concepts

猜你喜欢

转载自www.cnblogs.com/niceland/p/0x0008.html