[iOS] 组件二进制化 & 库的各种小知识

2021-01-17  本文已影响0人  木小易Ying

组件化之前已经讨论过啦,如果感兴趣可以康康:https://www.jianshu.com/p/72aa6e6f21e4

这次想康康的是组件二进制化,最近看到一个二进制库切换源码打断点的功能感觉很好玩,所以想聊聊这个topic~

我们日常的库的演变过程是神马样子的呢?


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只能是静态库哈。

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

总结一下是酱紫的:(这里是系统动态库哦)

链接做了啥?

当编译器对所有的源代码文件编译完成后,接下来的步骤就是链接了。链接的主要功能就是将所有目标文件中的各个相同段和节的信息依次连接起来拼装成一个单独的可执行文件。同时还会将所有目标文件中需要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做设置:

我们可以在主程序工程的项目中将 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 来控制是不是使用二进制库的~

所以我理解的流程大概是:

如何能够再不重新 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

上一篇下一篇

猜你喜欢

热点阅读