深入静态库 & 动态库--[iOS] 组件二进制化 & 库的各种小知识

本质是静态库中的代码都被编译成:class + objc_msgsend

class 参与链接。

链接错误:找不到符号、符号冲突

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

 
深入静态库 & 动态库--[iOS] 组件二进制化 & 库的各种小知识
静态库找不到category报错

所以需要在主工程的Other Linker Flags做设置:

  • -ObjC:把所有静态库中定义的OC类的方法都链接到可执行程序中去,而不管这个类是否有被引用,也不管方法是否是分类方法。
  • -all_load:则主程序工程会把所有静态库中的所有代码全部链接到可执行程序中去,而不管代码是以何种语言实现的代码,以及不管代码是否被引用和调用。
  • -force_load 静态库路径:只想对某个静态库中的所有代码进行全部链接处理,不管语言哈。

我们可以在主程序工程的项目中将 DEAD_CODE_STRIPPING(Dead Code Stripping) 开关开启,用来优化可执行程序中的代码。需要注意的是这个开关是在代码链接完成后的优化行为。当这个开关被打开时链接器会删除可执行程序中所有没有被调用的C函数以及C++中的普通成员函数。但是不会删除没有被调用到的OC类的成员方法,以及Swift类的成员方法,以及C++类中的虚函数。在XCODE中这个开关默认是开启的。



作者:木小易Ying
链接:https://www.jianshu.com/p/5985e4366564
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。