组件库的二进制化
之前有两篇文章:
这两篇实际上是整个组件化开发的一个基础技术。关于组件化,首先我们需要知道怎么去创建以及发布我们的组件;然后才是怎么使用我们的组件库,在上面的两篇文章里,就是分别介绍这两部分内容的。如果是纯源码管理组件库,并不能提升我们的编译速度。本篇会介绍一下,怎么将我们的组件库二进制化,来实现编译效率的提升。
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. 4MRC下的静态库
我们知道,如果直接在我们的工程中以源码的方式添加一个MRC方式管理内存的类,需要在工程配置中,给该类做一个MRC的标识,如果这样的类很多,怎么办?
这个时候,我们就可以考虑使用静态库的方式,静态库使用MRC,在集成到我们的工程中时,是不需要单独去加MRC的标识的,能直接使用。
2.5静态库的分离
前面讲了静态库的合并,这在开发阶段挺有帮助,使得我们的静态库可以在模拟器和真机上都能正常运行。但这样存在一个问题,就是合并的静态库文件,会使得我们的安装包变大,那么有没有办法,在上传到Store的安装包,对静态库做分离呢?因为在Store里的安装包,我们是没有必要在模拟器上运行的。
这个是可以的,我们可以使用移除的方式来做操作,完整的指令和结果如下所示:
➜ Products lipo -info libGofStaticLib.a Architectures in the fat file: libGofStaticLib.a are: x86_64 arm64 ➜ Products lipo -remove x86_64 libGofStaticLib.a -output libGofStaticLibThin.a ➜ Products lipo -info libGofStaticLibThin.a Architectures in the fat file: libGofStaticLibThin.a are: arm64 ➜ Products lipo -info libGofStaticLib.a Architectures in the fat file: libGofStaticLib.a are: x86_64 arm64
上面是使用remove指令完成的,其实也可以使用thin指令来完成,比如说只想保留arm64架构的,则可以使用如下指令:
➜ Products lipo -info libGofStaticLib.a Architectures in the fat file: libGofStaticLib.a are: x86_64 arm64 ➜ Products lipo -thin arm64 libGofStaticLib.a -output libGofStaticLibThin2.a ➜ Products lipo -info libGofStaticLibThin2.a Non-fat file: libGofStaticLibThin2.a is architecture: arm64 ➜ Products lipo -info libGofStaticLib.a Architectures in the fat file: libGofStaticLib.a are: x86_64 arm64
3.项目二进制化
在上一章节,详细介绍了静态库的制作,包括静态库的合并和分离,这一部分,讲一下静态库在我们组件化中的应用。
在我们封装一个组件库的时候,希望是能用源码开发,并能方便调试,这样我们能快速的进行开发和修改组件库的BUG。但将组件库应用在主工程中的时候,我们大部分时候是不需要去修改组件库源码的,这时我们希望的是能快速编译,并且稳定的满足我们的功能要求。
总结起来就是:开发组件库阶段,使用源码;使用组件库阶段,使用二进制化。当然,有时候,我们在使用组件库阶段,也希望能够使用源码的方式,特别是在定位问题的时候。
我们可以使用什么方式来同时满足这个诉求点呢?
答案是:使用2.3小节介绍的混合工程的方式。这里我新建了一个GofMediator的工程,项目结构如图所示:
当然,如同上一章节的介绍,要创建一个静态库,有四个地方需要注意:
- Build Settings配置:修改"Build Active Architecture Only"选项为NO,这样在编译模拟器和真机版本时,自动加上所有支持的架构;
- Build Settings配置:修改“Mach-O Type”类型为“Static Library”,它默认的是“Dynamic Library”;
- Build Phases配置:将要对外提供的头文件(.h),在“Headers”中,从“Project”中移动到“Public”中;
- Scheme配置:修改成Release模式。
编写完我们的代码之后,接下来就是将静态库进行发布,核心就是编辑podspec文件,为了同时支持源码和静态库的方式,可以采取添加环境变量的方式。podspec文件示例:
Pod::Spec.new do |s| s.name = "GofMediator" s.version = "0.0.1" s.summary = "中间件" s.description = <<-DESC 中间件. DESC s.homepage = "https://gitee.com/LeeGof/GofMediator.git" s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { "LeeGof" => "ligaofeng0927@163.com" } s.source = { :git => "git@gitee.com:LeeGof/GofMediator.git", :tag => s.version.to_s } #存在环境变量,则导入源码 if ENV['Mediator'] s.source_files = "GofMediator/*.{h,m}" s.public_header_files = 'GofMediator/*.h' else #不存在环境变量,导入静态库 s.vendored_frameworks = 'GofMediatorLib/GofMediator.framework' s.source_files = "GofMediator/*.h" end s.platform = :ios, '9.0' s.requires_arc = true s.frameworks = 'Foundation', 'UIKit' end
核心地方就是上面加粗的位置,然后通过相应的校验以及发布指令,将组件库发布出去(可参考 CocoaPods应用篇之搭建并发布自己的私有库)。
使用的时候,我们可以在主工程中的Podfile文件中,通过这一句代码引入组件库:
pod 'GofMediator', '0.0.1'
然后执行指令:
pod install
就可导入静态库到主工程中。如果想导入源码,只需要修改一下指令即可:
Mediator=1 pod install
这种切换很方便,但有时候因为缓存的原因,可能导致源码和Framework出现一些问题,这个时候,可以通过下面的指令来清除缓存:
pod cache clean --all
这样如果问题还是存在的话,可以重新导入组件库再试即可。
附录:pod packager制作静态库
是一个打包工具, 可以把代码打包成静态库(.a和.framework)和动态库(.framework),当然从上面的章节中,可以看到使用Xcode也可以打包, 只是比较麻烦,使用pod package可以简化我们生成静态库的操作。
安装pod package
使用如下指令安装:
sudo gem install cocoapods-packager
查看pod package的相关参数:
pod package --help
重要参数介绍:
//强制覆盖之前已经生成过的二进制库 --force //生成静态.framework --embedded //生成静态.a --library //生成动态.framework --dynamic //动态.framework是需要签名的,所以只有生成动态库的时候需要这个BundleId --bundle-identifier //不包含依赖的符号表,生成动态库的时候不能包含这个命令,动态库一定需要包含依赖的符号表。 --exclude-deps //表示生成的库是debug还是release,默认是release。--configuration=Debug --configuration //表示不使用name mangling技术,pod package默认是使用这个技术的。我们能在用pod package生成二进制库的时候会看到终端有输出Mangling symbols和Building mangled framework。表示使用了这个技术。 //如果你的pod库没有其他依赖的话,那么不使用这个命令也不会报错。但是如果有其他依赖,不使用--no-mangle这个命令的话,那么你在工程里使用生成的二进制库的时候就会报错:Undefined symbols for architecture x86_64。 --no-mangle //如果你的pod库有subspec,那么加上这个命名表示只给某个或几个subspec生成二进制库,--subspecs=subspec1,subspec2。生成的库的名字就是你podspec的名字,如果你想生成的库的名字跟subspec的名字一样,那么就需要修改podspec的名字。 这个脚本就是批量生成subspec的二进制库,每一个subspec的库名就是podspecName+subspecName。 --subspecs //一些依赖的source,如果你有依赖是来自于私有库的,那就需要加上那个私有库的source,默认是cocoapods的Specs仓库。--spec-sources=private,https://github.com/CocoaPods/Specs.git。 --spec-sources
附录:cocoapods-generate
创建工程指令示例:
pod lib create GofNetworking