[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 吼。

podfile

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 共享

image.png

首先在 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加的一个分类吼~

静态库找不到category报错

所以需要在主工程的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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容