10971259a3363bb7360843df78f82f03
核心知识篇:组件库的二进制化


之前有两篇文章:

这两篇实际上是整个组件化开发的一个基础技术。关于组件化,首先我们需要知道怎么去创建以及发布我们的组件;然后才是怎么使用我们的组件库,在上面的两篇文章里,就是分别介绍这两部分内容的。如果是纯源码管理组件库,并不能提升我们的编译速度。本篇会介绍一下,怎么将我们的组件库二进制化,来实现编译效率的提升。

1.静态库和动态库

首先我们先要明白一个概念,什么是库?
库是程序代码的集合,将 N 个文件组织起来,是共享程序代码的一种方式。
库有两种类型:

  • 开源库:源代码是公开的,可以看到每个实现文件(.m文件)的实现,例如GitHub上的常用的开源库:AFNetworking、SDWebImage 等;
  • 闭源库:不公开源代码,是经过编译后的二进制文件,看不到具体的实现。闭源库又分为:静态库 和 动态库。

1.1 静态库

静态库有两种:.a 的静态库和 .framework 的静态库。
那么在什么情况下,需要使用静态库呢?

  • 保护自己的核心代码,如讯飞语言摸索了好多年探索出的结果当然要保护起来了,都公开了公司还怎么生存。
  • 将 MRC 的项目打包成静态库,可以在 ARC 下直接使用,不用转换。如别人使用 MRC 写的开源库,放到自己 ARC 项目中,需要对每个文件加一个编译参数【-fno-objc-arc】,这样相对来说麻烦,将整个工程打包成静态库直接放到项目中即可,也不用对每个文件添加编译选项。

使用静态库有什么好处呢?

  • 模块化,分工合作,提高了代码的复用及核心技术的保密程度;
  • 避免少量改动经常导致大量的重复编译连接;
  • 可以重用。

1.2 动态库

动态库有三种:.framework、dylib和.tbd
优点:

  • 使用动态库,可以将最终可执行文件体积缩小,将整个应用程序分模块,团队合作,进行分工,影响比较小;
  • 使用动态库,多个应用程序共享内存中得同一份库文件,节省资源;
  • 使用动态库,可以不重新编译连接可执行程序的前提下,更新动态库文件达到更新应用程序的目的;
  • 应用插件化;
  • 软件版本实时模块升级;
  • 在其它大部分平台上,动态库都可以用于不同应用间共享, 共享可执行文件,这就大大节省了内存。iOS平台 在 iOS8 之前,苹果不允许第三方框架使用动态方式加载,从 iOS8 开始允许开发者有条件地创建和使用动态框架,这种框架叫做 Cocoa Touch Framework。虽然同样是动态框架,但是和系统 framework 不同,app 中使用 Cocoa Touch Framework 制作的动态库 在打包和提交 app 时会被放到 app main bundle 的根目录 中,运行在沙盒里,而不是系统中。也就是说,不同的 app 就算使用了同样的 framework,但还是会有多份的框架被分别签名,打包和加载。不过 iOS8 上开放了 App Extension 功能,可以为一个应用创建插件,这样主 app 和插件之间共享动态库还是可行的。苹果系统专属的 framework 是共享的(如UIKit), 但是我们自己使用 Cocoa Touch Framework 制作的动态库是放到 app bundle 中,运行在沙盒中的。

1.3 静态库和动态库的区别

  • 静态库:链接的时候,会被完整的复制到可执行文件中,如果多个 App 都使用了同一个静态库,那么每个 App 都会拷贝一份,缺点是浪费内存。比如说你手机里有两个 App,支付宝和微信,有一个静态库 A,这个 A 会在支付宝和微信里面分别拷贝一份,这样手机里会有两份。
  • 动态库:只有一份,运行时会动态加载到内存,系统只会加载一次,多个程序共用一份,节约了内存。同上面,如果是动态库的话,在手机里只会有一份。
  • .a 文件肯定是静态库,.dylib 和 .tbd 肯定是动态库,.framework 可能是静态库也可能是动态库;
  • 项目中如果使用了自己定义的动态库,苹果是不允许上架的,在 iOS8.0 以后苹果开放了动态加载 .dylib 的接口,用于挂载 .dylib 动态库。
  • 静态库和动态库是相对编译期和运行期的:静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要改静态库;而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。

2.静态库制作

2.1 .a 静态库制作

新建项目,选择模板【Cocoa Touch Static Library】,如下图所示:

建立好项目之后,先写一个简单的方法,然后分别使用真机和模拟器编译,可以看到在【Products】目录下,生成了两个不同的结果,分别是真机版本和模拟器版本:

为什么是分开的呢?因为模拟器和真机的架构方式是不一样的。
从上面的截图可以看到,编译结果中是没有【.h】文件的,而我们的静态库是需要对外提供【.h】文件的。我们怎么添加呢?可以通过下面的方式来添加:

然后重新编译,可以看到目录中多了【.h】文件:

这时,我们将这个编译好的【.a】文件和【.h】文件,添加到一个测试工程里面,然后用模拟器运行,结果会怎么样呢?

从运行结果可以看到,直接报错了,报错信息是:

这是因为静态库是使用真机编译的,而测试工程中是使用模拟器运行,两者架构不同。关于架构和指令的介绍,可以参考:iOS 指令集
那么怎么查看一个静态库使用的架构呢?可以通过这个指令:

lipo -info 你的静态库.a


这时候在静态库工程中,选择模拟器进行编译,将结果再拷贝到测试工程中,运行就不会报错了。

iPhone 5及以下的模拟器设备的架构是i386,和5s及以上的模拟器设备是不一样的。如果需要同时支持,可在工程中如下位置配置:

上面配置的意思就是,只要选择的是模拟器,就把所有模拟器的架构都包含,而不仅仅是当前编译的模拟器架构。

现在有一个新的问题:实际开发中,我们通常是需要在模拟器和真机中调试的,而上面可以看到,在模拟器和真机中编译的结果是两份,怎么办呢?能否合成一个?答案是肯定的:

lipo -create 真机中的静态库文件 模拟器中的静态库文件 -output libStaticCompose.a

因此上面示例的就可以用这个指令来合成:

lipo -create /Users/GofLee/Library/Developer/Xcode/DerivedData/GofStaticLib-ceiyclgkffneybhfaiukiergwvos/Build/Products/Debug-iphoneos/libGofStaticLib.a /Users/GofLee/Library/Developer/Xcode/DerivedData/GofStaticLib-ceiyclgkffneybhfaiukiergwvos/Build/Products/Debug-iphonesimulator/libGofStaticLib.a -output libStaticCompose.a

合成之后,我们可以通过指令来查看合成的【.a】文件的架构:

从结果可以看到,【x86_64】是支持的模拟器架构;【arm64】是支持的真机架构,两者都支持。
不过合成之后的包,有个很明显的问题:比单独的仅支持真机架构或模拟器架构的.a文件要大,大致是两者之和。

另外,还有一个问题,上面的静态库都是在【Debug】模式下生成的,而我们上传【Store】的都是【Release】包,那么该怎么配置成【Release】模式呢?很简单,通过在【Scheme】中做相应的修改即可:

2.2 Framework 静态库制作

上一小节讲了【.a】静态库的制作,而实际在组件化开发中,很少使用【.a】静态库,一般都是用【.framework】的静态库,因为使用【.a】静态库,还需要单独去拷贝【.h】文件;而【.framework】静态库本身是一个文件夹,里面包含了【.h】和【.a】文件。本节就来分析【.framework】的静态库制作。

首先,我们创建一个工程,选择模板,如下图所示:

创建好工程之后,先简单的添加一个【GofPerson】的类,然后编译,结果如下:

从结果可看到,在【Headers】中,并没有包含自己创建的【GofPerson】头文件,那么怎么添加呢?看下图:

重新编译,可以看到上面的【Headers】目录中,有了【GofPerson.h】文件。工程中默认生成的【GofFrameworkLib.h】文件没什么太大用,可以直接删掉。

其他的操作同上一小节操作,合成和【.a】静态库一样,可以使用指令:

lipo -create /Users/GofLee/Library/Developer/Xcode/DerivedData/GofFrameworkLib-enfuhseneqqtszcnpxtddsauuxbs/Build/Products/Debug-iphoneos/GofFrameworkLib.framework/GofFrameworkLib /Users/GofLee/Library/Developer/Xcode/DerivedData/GofFrameworkLib-enfuhseneqqtszcnpxtddsauuxbs/Build/Products/Debug-iphonesimulator/GofFrameworkLib.framework/GofFrameworkLib -output GofFrameworkLib

【Framework】静态库编译完成之后,我们也可以放到【test】工程中来使用,要注意一点:【Framework】的静态库是一个文件夹,因此【import】的时候,不能和【.a】静态库一样引入,如下图所示:

直接运行,会有一个崩溃,如下图所示:

这是为什么呢?我们先来看一下在【Framework】静态库工程中生成包的类型,使用如下指令即可:

file GofFrameworkLib

如下所示:

从结果可以看出,该【Framework】是一个动态库,怎么办呢?
在【Framework】静态库工程的如下配置,修改成【Static Library】,如图所示:

然后重新编译即可。重新执行【file】指令,结果如下:

将重新编译的【Framework】静态库,拷贝到【test】工程中,可以看到能正常运行了。

静态库中需要留意一下资源文件的添加,最好是使用【bundle】的方式。

2.3 静态库测试

从上一两小节的操作中,其实是有一个问题的:我们的静态库工程,并不是一个完整运行的项目,只是包含静态库的一些源码。但这些源码的测试路径,并没有方式去实际操作。如果是添加到我们的测试工程中,因为静态库是看不到源码的,我们也没有办法去做一个有效的调试,怎么办呢?可以通过【混合工程】的方式来实现。

首先创建一个普通工程【GofLibTest】,然后在这个工程里面,选择【项目】-【Targets】-【新建】,如下图所示:

这里,我创建了一个名为【GofStaticFramework】的静态库,如下图所示:

同样的,我们可以给这个【GofStaticFramework】静态库,添加一些类,这里新建了一个【GofPerson】类,里面添加了一个类方法【run】。

然后在【GofLibTest】工程中,调用【GofPerson】类,可以看到是完全正常的,如下图所示:

从上面可以看出,这样写出来的静态库,是可以测试,并进行调试的。

2.4 MRC 下的静态库

我们知道,如果直接在我们的工程中以源码的方式添加一个 MRC 方式管理内存的类,需要在工程配置中,给该类做一个【MRC】的标识,如果这样的类很多,怎么办?

这个时候,我们就可以考虑使用静态库的方式,静态库使用【MRC】,在集成到我们的工程中时,是不需要单独去加【MRC】的标识的,能直接使用。

top Created with Sketch.