Vladimir 的回答其实还不错,不过,我想在这里提供更多的背景知识。也许有一天有人会发现我的回复,并且可能会觉得它很有帮助。
编译器将源文件(.c、.cc、.cpp、.m)转换为目标文件(.o)。每个源文件有一个目标文件。目标文件包含符号、代码和数据。操作系统不能直接使用目标文件。
现在,当构建动态库 (.dylib)、框架、可加载包 (.bundle) 或可执行二进制文件时,这些目标文件由链接器链接在一起,以生成操作系统认为“可用”的内容,例如它可以直接加载到特定的内存地址。
但是,在构建静态库时,所有这些目标文件都只是简单地添加到一个大存档文件中,因此静态库的扩展名(.a 用于存档)。因此,.a 文件只不过是对象 (.o) 文件的存档。想想没有压缩的 TAR 存档或 ZIP 存档。复制单个 .a 文件比复制一大堆 .o 文件更容易(类似于 Java,您将 .class 文件打包到 .jar 存档中以便于分发)。
将二进制文件链接到静态库(= 存档)时,链接器将获取存档中所有符号的表,并检查二进制文件引用了哪些符号。只有包含引用符号的目标文件才会由链接器实际加载并由链接过程考虑。例如。如果您的存档有 50 个目标文件,但只有 20 个包含二进制文件使用的符号,则链接器仅加载这 20 个,其他 30 个在链接过程中被完全忽略。
这对于 C 和 C++ 代码非常有效,因为这些语言会在编译时尽可能多地执行(尽管 C++ 也有一些仅运行时的特性)。然而,Obj-C 是一种不同的语言。 Obj-C 严重依赖运行时特性,许多 Obj-C 特性实际上是运行时特性。 Obj-C 类实际上具有与 C 函数或全局 C 变量相当的符号(至少在当前的 Obj-C 运行时)。链接器可以查看一个类是否被引用,因此它可以确定一个类是否正在使用。如果您使用静态库中目标文件中的类,则链接器将加载此目标文件,因为链接器看到正在使用的符号。类别是仅运行时的功能,类别不是类或函数之类的符号,这也意味着链接器无法确定类别是否正在使用。
如果链接器加载包含 Obj-C 代码的目标文件,则它的所有 Obj-C 部分始终是链接阶段的一部分。因此,如果加载了包含类别的目标文件,因为其中的任何符号都被认为是“正在使用”(无论是类、函数还是全局变量),类别也会被加载并且在运行时可用.然而,如果目标文件本身没有加载,则其中的类别在运行时将不可用。包含仅类别的目标文件从不加载,因为它包含无符号链接器会永远认为“正在使用” ”。这就是这里的全部问题。
已经提出了几种解决方案,现在您知道所有这些是如何一起发挥作用的,让我们再看看提出的解决方案:
一种解决方案是将-all_load 添加到链接器调用中。该链接器标志实际上会做什么?实际上它告诉链接器“加载所有档案的所有目标文件,无论你是否看到任何正在使用的符号'。当然,这会起作用;但它也可能产生相当大的二进制文件。
另一种解决方案是将-force_load 添加到链接器调用中,包括存档路径。此标志的工作方式与-all_load 完全相同,但仅适用于指定的存档。当然这也可以。
最流行的解决方案是将-ObjC 添加到链接器调用中。该链接器标志实际上会做什么?该标志告诉链接器“如果您发现它们包含任何 Obj-C 代码,则从所有档案中加载所有目标文件”。并且“任何 Obj-C 代码”包括类别。这也可以,并且不会强制加载不包含 Obj-C 代码的目标文件(这些仍然只是按需加载)。
另一个解决方案是相当新的 Xcode 构建设置 Perform Single-Object Prelink。这个设置会做什么?如果启用,所有目标文件(记住,每个源文件都有一个)将合并到一个目标文件中(这不是真正的链接,因此名称为 PreLink)和这个单个目标文件 (有时也称为“主目标文件”)然后添加到存档中。如果现在考虑使用主目标文件的任何符号,则认为整个主目标文件正在使用,因此它的所有 Objective-C 部分总是被加载。而且由于类是普通符号,因此使用这样一个静态库中的单个类来获取所有类别就足够了。
最终的解决方案是弗拉基米尔在回答的最后添加的技巧。将“假符号”放入任何仅声明类别的源文件中。如果您想在运行时使用任何类别,请确保在编译时以某种方式引用 fake symbol,因为这会导致目标文件被链接器加载,因此所有 Obj-C里面的代码。例如。它可以是具有空函数体的函数(在被调用时不会执行任何操作),也可以是访问的全局变量(例如,全局int,一旦读取或写入,就足够了)。与上述所有其他解决方案不同,此解决方案将有关哪些类别在运行时可用的控制转移到已编译的代码(如果它希望它们被链接并且可用,它访问符号,否则它不访问符号并且链接器将忽略它)。
这就是所有人。
哦,等等,还有一件事:
链接器有一个名为-dead_strip 的选项。这个选项有什么作用?如果链接器决定加载目标文件,则目标文件的所有符号都将成为链接二进制文件的一部分,无论它们是否被使用。例如。一个目标文件包含 100 个函数,但二进制文件只使用其中一个函数,所有 100 个函数仍被添加到二进制文件中,因为目标文件要么作为一个整体添加,要么根本不添加。链接器通常不支持部分添加目标文件。
但是,如果您告诉链接器“死区”,链接器将首先将所有目标文件添加到二进制文件中,解析所有引用,最后扫描二进制文件中未使用的符号(或仅由其他未使用的符号)。然后,作为优化阶段的一部分,所有发现未使用的符号都将被删除。在上面的示例中,再次删除了 99 个未使用的函数。如果您使用 -load_all、-force_load 或 Perform Single-Object Prelink 之类的选项,这将非常有用,因为这些选项在某些情况下很容易大幅增加二进制文件的大小,并且死剥离将再次删除未使用的代码和数据。
死区剥离对于 C 代码非常有效(例如,未使用的函数、变量和常量被按预期删除),它对 C++ 也非常有效(例如,未使用的类被删除)。它并不完美,在某些情况下,即使可以删除某些符号也不会删除它们,但在大多数情况下,它对于这些语言来说效果很好。
Obj-C 呢?忘掉它! Obj-C 没有死剥离。由于 Obj-C 是一种运行时特性语言,因此编译器无法在编译时判断符号是否真的在使用中。例如。如果没有直接引用它的代码,则不会使用 Obj-C 类,对吗?错误的!您可以动态构建包含类名的字符串,请求该名称的类指针并动态分配类。例如。而不是
MyCoolClass * mcc = [[MyCoolClass alloc] init];
我也可以写
NSString * cname = @"CoolClass";
NSString * cnameFull = [NSString stringWithFormat:@"My%@", cname];
Class mmcClass = NSClassFromString(cnameFull);
id mmc = [[mmcClass alloc] init];
在这两种情况下,mmc 都是对“MyCoolClass”类对象的引用,但在第二个代码示例中没有直接引用这个类(甚至没有像一个静态字符串)。一切都只发生在运行时。即使类是实际上是真正的符号。类别更糟糕,因为它们甚至不是真正的符号。
因此,如果您有一个包含数百个对象的静态库,但大多数二进制文件只需要其中的几个,您可能不希望使用上述解决方案 (1) 到 (4)。否则,您最终会得到包含所有这些类的非常大的二进制文件,即使它们中的大多数从未使用过。对于类,您通常根本不需要任何特殊解决方案,因为类具有真正的符号,并且只要您直接引用它们(不像第二个代码示例中那样),链接器将自行识别它们的用法。但是,对于类别,请考虑解决方案 (5),因为它可以只包含您真正需要的类别。
例如如果您想要 NSData 的类别,例如为其添加压缩/解压缩方法,您将创建一个头文件:
// NSData+Compress.h
@interface NSData (Compression)
- (NSData *)compressedData;
- (NSData *)decompressedData;
@end
void import_NSData_Compression ( );
和一个实现文件
// NSData+Compress
@implementation NSData (Compression)
- (NSData *)compressedData
{
// ... magic ...
}
- (NSData *)decompressedData
{
// ... magic ...
}
@end
void import_NSData_Compression ( ) { }
现在只需确保调用代码中的任何位置import_NSData_Compression()。调用它的位置或调用频率无关紧要。实际上根本不需要调用它,如果链接器这么认为就足够了。例如。您可以将以下代码放在项目中的任何位置:
__attribute__((used)) static void importCategories ()
{
import_NSData_Compression();
// add more import calls here
}
您不必在代码中调用importCategories(),该属性将使编译器和链接器相信它被调用,即使它不是。
最后一个提示:
如果将-whyload 添加到最后的链接调用中,链接器将在构建日志中打印由于使用了哪个符号,它从哪个库加载了哪个目标文件。它只会打印考虑使用的第一个符号,但这不一定是该目标文件使用的唯一符号。