公司的一个IOS项目发生了duplicate symbol的编译错误,去掉-all_load之后可以编译通过,但是运行的时候其中一段代码报错了,大家肯定能想到是什么问题,‘unrecognized selector’,具体问题我将稍后详细分析。
从技术层面来讲,其实就是如何正确使用-Objc -all_load -force_load的问题,不过我发现网上很多帖子说的都不够准确,因此在这里整理一下。
为什么会有-Objc
这个问题还得从静态库文件说起,我们接触到的静态库就是.a文件。比如你写了一个数学运算的模块,里面包含 Add.m,Minus.m,Multiply.m,Divide.m四个源文件,编译之后生成了相应的四个.o文件, 使用 libtool -static -o../calculate.a *.o 命令就生成calculate.a,就是静态库文件。
此时Lib项目结构如下:
|--calculate
|-- Add.m
|-- Minus.m
|-- Multiply.m
|-- Divide.M
我们的项目是如何使用静态库文件的呢
一个代码文件引用了另一个代码文件中定义的符号时,在编译阶段,这些未定义的符号undefined symbol 会被写入object文件(即目标文件)。下一步进行链接时,linker会解析这些undefined symbol并把相应的文件取出来放入最终的可执行文件。
结合我们的例子,如果把上一步生成的calculate.a文件加入了项目,并且在某个文件中使用了Add这个类,那么链接阶段linker会从calculate.a中取出Add.o加入到最终生成的文件中。注意到这里,linker是按需加载的,这样最终生成的可执行文件体积不会太大。
其实以上都是很理想的情况。
如果你在原有Lib项目中针对Add增加了一个Category,并命名为SuperAdd.m,那么会发生什么呢?
此时Lib项目结构如下:
|--calculate
|-- SuperAdd.m(category了Add)
|-- Add.m
|-- Minus.m
|-- Multiply.m
|-- Divide.m
问题来了
由于oc独有的基于消息的方法调用机制,导致编译阶段linker只认类而不管方法。SuperAdd其实只是一堆方法的集合而已,因此如果你在项目的源码里调用了SuperAdd中定义的方法,链接时这些方法也不会被打入最终的可执行文件,因此运行时会报‘unrecognized selector’。怎么解决呢?方法就是加入 -Objc,使得Category可以被linker拿到。
这里特别指出,如果不使用-Objc这个flag,是方法找不到,而不是类找不到,那样编译阶段就报错了,又如何会抛出‘unrecognized selector’的运行时错误。(很多文章在一点上说错了)
那么是否就一切万事大吉了,显然还有问题。
Category的基础类出现在另一个库中了
如果在你的lib项目中,你又添加了一个Category,GeoCalc.m扩展了另一个Geo库中的类Geometry, 此时Lib项目结构如下:
|--calculate
|-- SuperAdd.m
|-- Add.m
|-- Minus.m
|-- Multiply.m
|-- Divide.m
|-- GeoCalc.m (category了Geo.a中的Geometry)
calculate库中不存在GeoCalc的基础类Geometry,如果你的项目也同时加入了Geo库,点击运行,会发生什么呢? linker有一个bug,如果一个类别的基础类不在本库中,那么即使使用-Objc这个flag,类别的方法集也不会被linker取到,所以运行时就报‘unrecognized selector’了。
那么怎么解决呢? -load_all就有用武之地了。使用这个flag之后,所有库中的类与category都会被pull到可执行文件。这里必须严谨的指出,至于load_all具体是如何让linker可以拿到这种“跨库Category”的,这就是属于编译器本身的一个问题了。只能说-load_all确实修正了基础类不在本库情况下的Category读取问题,并且使用这个flag确实所有库中的类与Category都被pull到了最终的文件中。不过,使用load_all会使最终文件很臃肿,必要及非必要的类都被打入了最终的可执行文件,太臃肿了,那该如何解决呢?
最后一个角色-force_load出场了,它也可以修复编译器只使用Objc情况下出现的bug,但是跟load_all有不一样的地方。 从名字就可以看出,这个flag是按需指定加载的。
这里,我再说明一点,并不是说xcode4.2以后就不必使用-load_all,-force_load了,笔者使用的是xcode 8.2.1依然有效。
总结:
-Objc, 使linker可以从静态库中pull到Category,适用于存在Category的库
-load_all, 使linker可以pull到所有库中的类与Category,适用于Category的基础类不存在于本库中的情况
-force_load, 与-load_all相似,但可以按需加载指定库中的类与Category,避免执行包过大
最后说一下公司IOS项目中所遇到的问题。加入一个视频录制的库之后,由于之前设置了-load_all,所有库中的类与Category都会被pull,出现了symbol重复,所以duplicate symbol,于是去掉了load_all。编译倒是通过了,但是由于项目中有一个库Category了另一个库中的类,导致方法没能打入到最终的执行文件,报‘unrecognized selector’。解决方法:force_load新加入的库。