[iOS] 组件二进制化 & 库的各种小知识
组件化之前已经讨论过啦,如果感兴趣可以康康:https://www.jianshu.com/p/72aa6e6f21e4
这次想康康的是组件二进制化,最近看到一个二进制库切换源码打断点的功能感觉很好玩,所以想聊聊这个topic~
我们日常的库的演变过程是神马样子的呢?
- 所有代码都在一个大的project里面,只有一个git,不同组对应不同的folder,大家可以很方便的随意改
- 随着人越来越多代码量越来越大,我们开始每个组对应一个 git,这样每个组就可以给自己组的仓库提代码,大部分情况不用考虑别的仓库。那么就需要一个管理的方式了,所以用 pod 管理各个组的代码,这个时候可能都是development pod,并且集成源码
- 随着代码更多,编译时长会越长,虽然我们改的只是自己组的代码,却要编译整个app的源码,于是我们就改成了以静态库的方式集成我们不需要改的库
1. 如何以静态库的方式集成库
如果不用framework的方式继承是酱紫的:
源码引入
参考:https://blog.csdn.net/hanhailong18/article/details/107350396 & https://zhuanlan.zhihu.com/p/36439065
我用前者木有成功,可以用后者试一下哈,其实就是通过 cocoapods-binary
来帮助我们 hook 了pod install,增加了pod插件,在install的时候生成各个库的静态库。如果安装的时候提示木有权限可以用sudo gem install -n /usr/local/bin cocoapods-binary
吼。
cocoapods-binary
的主要原理就是在 pre install
的 hook 中,独立执行另外一个 pod install
。这个独立的 install 根据 podfile 中的配置过滤出要二进制化的 pod,生成 pod project,再使用 xcodebuild 编译出 framework。接着在正常的 install 过程中,通过运行时更改 CocoaPod 的代码,使得在集成的时候,对于指定的库使用的刚才编译好的 framework,而非源代码。这些 framework 作为该 pod 库的 vendored_framework 来实现引用。
然后看pod里面就是酱紫的啦:
pod集成framework
所以为什么使用静态库呢?主要是为了共享代码,方便使用;实现代码的模块化,固定的业务模块话,减少开发的重复劳动;和别人分享代码,但又不想让别人知道代码的具体实现。
2. 静态库 / 动态库 (.a / .framework /.dylib)
类似Android里面的jar包,无论任何代码都会有库的存在,可以减少重复实现,也可以隐藏代码实现,只对外暴露.h文件,让外面使用但是看不到具体的implement。
Framework 实际上是一种打包方式,将库的二进制文件,头文件和有关的资源文件打包到一起,方便管理和分发。库在使用的时候需要Link,Link的方式有两种,静态和动态,于是便产生了静态库和动态库。 .framrework 文件即可以是被作为 (fake) 动态的也可以静态,但是.a只能是静态库哈。
-
静态库 (.framework or .a)
静态库在编译的时候会被直接拷贝一份,复制到目标程序里,这段代码在目标程序里就不会再改变了。
.a(二进制文件) + .h + bundle资源 = .framework -
动态库 (.tbd or .dylib)
动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来 -
Embedded framework (.framework)
自定义的动态framework和系统的还是存在区别。Embedded 的意思是嵌入,但是这个嵌入并不是嵌入 app 可执行文件,而是嵌入 app 的 bundle 文件。当一个 app 通过 Embedded 的方式嵌入一个 app 后,在打包之后解压 ipa 可以在包内看到一个 framework 的文件夹。系统的动态库不需要拷贝到目标程序中,自定义的还是要拷贝,因此称为Embedded Framework,它和静态库的区别就是可以和 extension 共享
首先在 linded feameworks and libraries 这个下面我们可以连接系统的动态库、自己开发的静态库、自己开发的动态库。对于这里的静态库而言,会在编译链接阶段连接到app可执行文件中,而对这里的动态库而言,虽然不会链接到app可执行文件中。
如果你不想在启动的时候加载动态库,可以在 linked frameworks and libraries 删除,并使用dlopen加载动态库。
这个part欢迎阅读:https://github.com/Damonvvong/DevNotes/blob/master/Notes/framework.md
3. 如何创建 framework
可参考强推:https://www.jianshu.com/p/d2d15c2cb7de
下面简要的说下步骤,真的特别简要,建议不要看... 首先需要创建一个 framework project,然后 build 出一个framework包~
framework
注意所有 public 对外的 header 文件引入的 header 也要 public 哦~
然后把生成的 framework 拖入自己的 project 就可以使用啦~
生成的framework 使用
4. 如何创建 .a 库
参考:https://www.jianshu.com/p/5b5238b2dbb9
然后也是简要的说一下~ 和 framework 相似,也是创建一个 static library 的target,然后编译就可以生成一个小建筑图标的 .a 库啦~
创建一个 .a 库然后把 .a 和 .h 文件拖进来就可以啦~
引用 .a 文件
.a 文件使用的时候不用 import <XXX/XXX.h>。而 framework 需要。
5. 深入静态库 & 动态库
这个是摘抄自:https://www.jianshu.com/p/ef3415255808 被小哥哥推荐的真的很棒~
文中的二进制查看器是这个:
MachOView:https://www.jianshu.com/p/175925ab3355
我在看欧阳大哥文章的时候对 MachOView 的地方感觉不是很能看懂,里面只有几张截图,所以又找了一个专门讲 rebase 和 bind 在二进制文件中的跳转的文:https://blog.csdn.net/henry_lei/article/details/109822340
总结一下是酱紫的:(这里是系统动态库哦)
- App启动会生成一个 dyld 的动态 bind 表,每个库会对外提供一个自己的表,列出来自己提供的函数的真实地址。
- 当我们调用动态库里面的某个函数的时候,需要先通过 stub 得到一个地址,但是这个地址并不是真正的 call 到的函数的地址,需要先 rebase 一下,也就是先改一下基址,基址每次启动都是随机的,防止黑客攻击。
- rebase以后获取到真实地址的时候,其实还是 dyld_stub_binder,也就是需要通过动态库提供的 binder 找到自己想要调用函数的真实地址。
- 找到以后这个地址会放到最开始第一步的地址中。也就是动态库的binder是在app初始化的时候提供的,然鹅函数调用的 rebase 和 bind 是在第一次 call 这个函数以后才会做并且填充到最开始的地址,这样之后再 call 这个函数的时候就不用再走一编流程了。
链接做了啥?
当编译器对所有的源代码文件编译完成后,接下来的步骤就是链接了。链接的主要功能就是将所有目标文件中的各个相同段和节的信息依次连接起来拼装成一个单独的可执行文件。同时还会将所有目标文件中需要Relocation的部分的指令进行调整,因为此时可以知道每个引用符号的位置了。在链接时系统会分析每个目标文件中的依赖信息,也就是说链接成一个可执行文件中各段各节的内容总是无依赖的目标文件放在前面,而有依赖的目标文件放置在后面。
应用程序链接的过程最开始是以主程序工程中的所有目标文件为单位进行的,无论这个工程中的目标文件中的代码是否有被引用或者被调用都会链接进可执行程序中。在链接的过程中,如果发现某个符号没有在主程序工程中被定义,那么就会去导入的动态库文件或者静态库文件中查找。
如果符号在动态库中被定义那么就会为 动态库 的中的符号(这里假设符号就是某个函数) 生成stub代码并且将引用信息放入导入符号表以便在后续程序运行时动态的加载真实的函数地址。
而如果发现符号在 静态库 中被定义那么就会按如下的规则进行处理:
- 默认情况下是以静态库中的目标文件为单位进行链接的,只要某个目标文件中定义的符号被主程序引用,则这个目标文件中的所有代码都会链接到可执行程序中去。
- 如果这个目标文件中又引用了其他目标文件中定义的符号则链接会进行递归处理。
- 如果静态库中某个目标文件中的代码没有被任何其他地方引用则这个目标文件将不会链接到可执行程序中去。
OC类的方法列表的构建是在编译阶段完成的,但是对其中的方法调用都是在运行时动态确定的,所以在代码中的任何对静态库中定义的OC类的方法调用都不会被认为是对符号的引用,都不会产生链接行为。除非在代码中引用了这个OC类本身才会产生链接行为,此时会把静态库中定义的所有OC类的方法都链接到可执行程序中(因为OC类的方法列表在编译阶段已经构建完成)。也就是说静态库中的OC类定义的方法要么就全部都链接进可执行程序中,要么就一个方法也不会被链接。
假设某个静态库中定义了一个名字为CA的OC类:
//类中定义了2个方法。
@interface CA:NSObject
-(void)fn1
-(void)fn2;
@end
//假如在同一个文件中还定义了CB类
@interface CB:NSObject
@end
假设主程序中有两处会使用到静态库中定义的CA类的地方:
//虽然这里CA作为一个参数,并且里面调用了对应的方法,但是在链接时仍然不会将CA类链接进来,因为这个是一个运行时的间接方法调用过程。
void foo1(CA *p)
{
[p fn1];
}
//假设没有foo2这个函数则CA类中的代码是不会链接进可执行程序中的。
void foo2()
{
//只有明确的使用CA类来创建对象时,才表明是对CA类的引用。这样才会将CA类中的所有方法都链接进可执行程序中,这里虽然没有调用fn2但是fn2的实现也会被链接进去。
CA *p = [CA new];
[p fn1];
}
void main()
{
foo1(nil);
foo2();
}
因为CB和CA类在同一个.m文件中实现,所以即使CB类没有被引用,但是根据上述的按文件为单位的链接规则,CB类仍然会被链接到可执行程序中,除非CB类和CA类不在同一个文件中实现。
酱紫的话其实如果是方法调用在运行时被决定,静态库中的category就不能被链接啦,运行时会报错哒,下面的sayHi
是我在静态库里面给NSObject加的一个分类吼~
所以需要在主工程的Other Linker Flags
做设置:
-
-ObjC
:把所有静态库中定义的OC类的方法都链接到可执行程序中去,而不管这个类是否有被引用,也不管方法是否是分类方法。 -
-all_load
:则主程序工程会把所有静态库中的所有代码全部链接到可执行程序中去,而不管代码是以何种语言实现的代码,以及不管代码是否被引用和调用。 -
-force_load 静态库路径
:只想对某个静态库中的所有代码进行全部链接处理,不管语言哈。
我们可以在主程序工程的项目中将 DEAD_CODE_STRIPPING(Dead Code Stripping)
开关开启,用来优化可执行程序中的代码。需要注意的是这个开关是在代码链接完成后的优化行为。当这个开关被打开时链接器会删除可执行程序中所有没有被调用的C函数以及C++中的普通成员函数。但是不会删除没有被调用到的OC类的成员方法,以及Swift类的成员方法,以及C++类中的虚函数。在XCODE中这个开关默认是开启的。
6. 库的二进制
Finally 我要 move on 到库的二进制啦~~ 之前第一部分也讲过库集成源码的话编译时长比较长,如果直接集成二进制可以大大的减少编译时长。二进制化指的是通过编译把模块的源码转换成静态库或动态库,以提高该组件在项目中的编译速度。
为了实现这个目标,就需要一个人或者一个 CI Job,把编译好的二进制产物上传到某个的地方,集中化地管理这些二进制形式的依赖。然后在每个人 pod install 的时候,检查该 pod 版本对应的二进制是否存在,如果有就使用。
pod组件需要同时提供源码与二进制库
所以要求一个pod库的工程文件中不仅仅要包含源代码文件,还要包含将源代码编译成静态库或者动态库的二进制文件,切换二进制库与源码的时机应该在 pod install 的时候,而表明是构建源码还是二进制库,则需要通过install的时候,修改podspec文件中的s.source_files、s.public_header_files、s.ios.vendored_bibraries属性,来切换该pod库包含的内容。因为podspec文件本身为ruby文件,我们可以利用ENV对象,来获取命令行中执行pod install时候传入的环境变量,例如可以在podspec文件中这样写:
if ENV['SOURCECODE']
s.source_files = 'HBAuthentication/Classes/**'
else
s.source_files = 'Example/HBAuthenticationBinary/Products/Binary-universal/include/**'
s.public_header_files = '**/*.h'
s.ios.vendored_libraries = '**/**.a'
end
当在命令行中传入环境变量参数的时候 SOURCECODE=1 pod install 的时候,则podspec文件中if 语句通过ENV对象来获取SOURCECODE参数来表明不同的文件包含属性,从而能够切换该pod库源码或者二进制库。但我看的大多好像都是通过 tag 名字是否包含 binary 来控制是不是使用二进制库的~
所以我理解的流程大概是:
- 当我们提交代码到某个仓库的时候,会自动build出一个二进制的静态库,并上传到管理的地方
- 别人 pod install 的时候会判断,如果 podfile 里面的 tag 指定是 xxx-binary 就拉二进制使用,如果不是就用源码
- 如果想切换源码,则将 podfile 里面的 tag 修改一下重新 pod install
如何能够再不重新 pod install 的情况下来实现断点呢,这个大概可以参考美团的 zsource,首先代码需要下载 or 同时集成源码和binary。注意源码如果不加断点不要参与编译哈防止 duplicate,如果想某个文件参与编译需要把 binary 里面对应的 .o 文件删掉。其实具体细节我也是木有 figure out,只想说太厉害了,怎么能这么厉害呢,匿了匿了night~
refer to:
知乎二进制:https://zhuanlan.zhihu.com/p/44280283
强推:https://www.cnblogs.com/iOSer1122/p/13269702.html?utm_source=tuicool
https://www.jianshu.com/p/a6d0f37cdc27
美团zsource作者(膜拜):https://blog.csdn.net/liuhuiteng/article/details/106780308