Objective-C 类在静态库中的应用

你能指导我如何正确链接静态库到 iPhone 项目。我使用静态库项目添加到应用程序项目作为直接依赖(目标-> 一般-> 直接依赖)和所有工作确定,但类别。在静态库中定义的类别在应用程序中不起作用。

因此,我的问题是如何添加一些类别静态库到其他项目?

一般来说,在应用程序项目代码中使用其他项目的最佳实践是什么?

54273 次浏览

您可能需要在静态库的“ public”头中包含类别: # import“ MyStaticLib.h”

解决方案: 从 Xcode 4.2开始,你只需要找到与库链接的应用程序(而不是库本身) ,在 Project Navigator 中单击项目,单击应用程序的目标,然后构建设置,然后搜索“ Other Linker Flags”,单击 + 按钮,然后添加“-ObjecC”。不再需要“-all _ load”和“-force _ load”。

我在各种论坛、博客和苹果文档中找到了一些答案。现在我试着对我的研究和实验做一个简短的总结。

问题是由(引自苹果技术问答 QA1490 https://developer.apple.com/library/content/qa/qa1490/_index.html) :

Objective-C 没有定义链接器 每个函数(或方法)的符号, 在 Objective-C)中-改为链接程序 符号只为每个 如果您扩展了一个预先存在的 类,则链接器执行 不知道关联的目标代码 核心类的实现和 类别实现 中创建的对象 响应而产生的应用程序 中定义的选择器 类别。

他们的解决办法是:

为了解决这个问题,静态 库应该传递 -ObjecC 选项 此标志将导致 中加载每个目标文件的链接器 定义一个 Objective-C 类或类别 此选项通常会导致 更大的可执行文件(由于额外的 对象代码加载到 应用程序) ,它将允许 成功地创造了有效的 Objective-C 静态库 包含现有 课程。

此外,iPhone 开发常见问题解答(FAQ)中也提供了一些建议:

如何连接所有的 Objective-C 在静态库中设置 其他链接器标志生成设置为 目标。

及旗帜说明:

- 全部装载加载静态归档库的所有成员。

- 目的加载静态归档库的所有成员,这些库实现 Objective-C 类或类别。

- Force _ load (path _ to _ archive)加载指定静态的所有成员 注意:-all _ load 迫使所有档案馆成员 此选项允许您加载 瞄准一个特定的档案馆。

* 我们可以使用 force _ load 来减少应用程序的二进制大小,并避免在某些情况下 all _ load 可能导致的冲突。

是的,它可以处理添加到项目中的 * . a 文件。 然而,我在添加 lib 项目作为直接依赖项时遇到了麻烦。但后来我发现这是我的错误-直接依赖项目可能没有正确添加。当我删除它并重新添加步骤:

  1. 在 app Project 中拖放 lib 项目文件(或者使用 Project-> Add to Project... 添加它)。
  2. 单击 lib project icon-mylib.a 文件名上的箭头,拖动这个 mylib.a 文件并将其放入 Target-> Link Binary With Library 组。
  3. 在第一页打开目标信息(常规)并将我的库添加到依赖项列表

在此之后,所有的工作 OK。“-对象”标志在我的情况下是足够的。

我也对来自 http://iphonedevelopmentexperiences.blogspot.com/2010/03/categories-in-static-library.html博客的想法感兴趣。作者说他可以在不设置-all _ load 或-Object 标志的情况下使用 lib 中的分类。他只是添加到类别 h/m 文件空的虚拟类接口/实现,以强制链接器使用这个文件。是的,这个把戏起作用了。

但是作者也说他甚至没有实例化虚拟对象。嗯... 我发现我们应该明确地从类别文件中调用一些“真正的”代码。所以至少应该调用类函数。 我们甚至不需要虚类,单个 c 函数也是如此。

所以如果我们把 lib 文件写成:

// mylib.h
void useMyLib();


@interface NSObject (Logger)
-(void)logSelf;
@end




// mylib.m
void useMyLib(){
NSLog(@"do nothing, just for make mylib linked");
}




@implementation NSObject (Logger)
-(void)logSelf{
NSLog(@"self is:%@", [self description]);
}
@end

如果我们调用 useMyLib () ; 在 App project 中的任何地方 那么在任何类中我们都可以使用 logSelf 分类方法;

[self logSelf];

以及更多关于主题的博客:

Http://t-machine.org/index.php/2009/10/13/how-to-make-an-iphone-static-library-part-1/

Http://blog.costan.us/2009/12/fat-iphone-static-libraries-device-and.html

这个问题一直是 固定在 LLVM 中。这个补丁是 LLVM 2.9的一部分,第一个包含这个补丁的 Xcode 版本是 LLVM 3.0附带的 Xcode 4.2。在使用 XCode 4.2时,不再需要使用 ABC0或 -force_load仍然需要 -ObjC

在讨论静态库链接时,很少提到的一个因素是 还必须在构建阶段包括类别本身-> 复制文件和编译静态库本身的源代码

苹果在最近发布的 在 iOS 中使用静态库中也没有强调这个事实。

我花了一整天的时间尝试各种变化的-objecC 和-all _ load 等..。但是什么都没有。.这个的问题引起了我的注意。(不要误会我。.你仍然需要做 -obc 的东西。.但不仅仅是这样)。

另一个一直帮助我的动作是,我总是首先自己构建包含的静态库。.然后我构建附上的应用程序。.

下面是在编译静态库时完全解决这个问题需要做的事情:

进入 Xcode 生成设置,并将 PerformSingeObjectPrerelink 设置为 YES 或 构建配置文件中的 GENERATE_MASTER_OBJECT_FILE = YES

默认情况下,链接器生成一个。O 每个文件。M 档案。所以分类不同。档案。当链接器查看静态库时。O 文件,它不会为每个类创建所有符号的索引(运行时会创建,不管是什么)。

此指令将要求链接器将所有对象打包到一个大对象中。O 文件,并通过这个命令强制处理静态库的链接器获得所有类类别的索引。

希望这能说明问题。

弗拉基米尔的回答实际上相当不错,但是,我想在这里给出一些更多的背景知识。也许有一天,有人会发现我的回复,也许会觉得它有帮助。

编译器转换源文件(。C,.抄送。中心。M)转换为目标文件(。O).每个源文件有一个对象文件。目标文件包含符号、代码和数据。对象文件不能被操作系统直接使用。

现在,在构建动态库时(。Dylib)、框架、可加载包(。或者可执行的二进制文件,链接器将这些对象文件链接在一起,生成操作系统认为“可用”的东西,例如可以直接加载到特定内存地址的东西。

但是,在构建静态库时,所有这些对象文件都被简单地添加到一个大的归档文件中,因此扩展了静态库(。存档)。所以。文件只不过是对象的归档文件(。O)档案。可以考虑不进行压缩的 TAR 归档文件或 ZIP 归档文件。只是复制一个更容易。比一大堆。O 文件(类似于 Java,在其中打包。类文件转换为。为了便于分发而存档的 jar)。

当将二进制文件链接到静态库(= archive)时,链接器将获得存档中所有符号的表,并检查二进制文件引用了这些符号中的哪些。只有包含引用符号的对象文件才由链接器实际加载,并由链接进程考虑。例如,如果存档文件有50个目标文件,但只有20个包含二进制文件使用的符号,只有这20个由链接器加载,其他30个在链接过程中被完全忽略。

这对 C 和 C + + 代码非常有效,因为这些语言在编译时会尽可能多地进行编译(尽管 C + + 也有一些仅限于运行时的特性)。然而,Obj-C 是一种不同的语言。Obj-C 在很大程度上依赖于运行时特性,而且许多 Obj-C 特性实际上只是运行时特性。Obj-C 类实际上具有与 C 函数或全局 C 变量相当的符号(至少在当前的 Obj-C 运行时中是这样)。链接器可以查看一个类是否被引用,因此它可以确定一个类是否正在使用。如果使用静态库中对象文件中的类,则链接器将加载此对象文件,因为链接器看到正在使用的符号。类别只是运行时特性,类别不是类或函数之类的符号,这也意味着链接器无法确定类别是否正在使用。

如果链接器加载一个包含 Obj-C 代码的目标文件,那么它的所有 Obj-C 部分总是链接阶段的一部分。因此,如果一个包含类别的对象文件被加载,因为它中的任何符号都被认为是“正在使用”(可以是类,可以是函数,可以是全局变量) ,那么类别也会被加载,并且在运行时可用。但是,如果未加载对象文件本身,则其中的类别在运行时将不可用。一个包含 只有类别的目标文件是 永远不会加载,因为它包含 没有符号链接器将 永远不会认为“正在使用”。这就是问题所在。

已经提出了几个解决方案,现在您已经知道了所有这些是如何协同工作的,让我们再来看看提出的解决方案:

  1. 一种解决方案是将 -all_load添加到链接器调用中。那个连接器标志实际上会做什么?实际上它告诉链接器下面的“ 加载所有归档的所有对象文件,无论您是否看到任何正在使用的符号”。当然,这将工作,但它也可能产生相当大的二进制文件。

  2. 另一种解决方案是将 -force_load添加到链接器调用中,包括存档的路径。这个标志的工作方式与 -all_load完全相同,但只适用于指定的归档文件。当然,这也会起作用。

  3. 最流行的解决方案是将 -ObjC添加到链接器调用中。那个连接器标志实际上会做什么?这个标志告诉链接器“ 如果您看到所有存档中包含任何 Obj-C 代码,则加载所有目标文件”。“任何 Obj-C 代码”都包括类别。这也可以工作,它不会强制加载不包含 Obj-C 代码的目标文件(这些文件仍然只是按需加载)。

  4. 另一个解决方案是新的 Xcode 构建设置 Perform Single-Object Prelink。这个设置有什么用?如果启用,所有的对象文件(记住,每个源文件有一个)合并到一个单一的对象文件(这不是真正的链接,因此名称为 预链接) ,这个单一的对象文件(有时也称为“主对象文件”) ,然后添加到存档。如果现在考虑使用主对象文件的任何符号,则考虑使用整个主对象文件,因此总是加载其中的所有 Objective-C 部分。而且因为类是普通符号,所以使用这样一个静态库中的一个类来获取所有类别就足够了。

  5. 最终的解决方案是弗拉基米尔在他的答案的最后添加的技巧。在任何只声明类别的源文件中放置一个“ 假象征”。如果您希望在运行时使用任何类别,请确保在编译时以某种方式引用 假象征,因为这会导致链接器加载目标文件,因此也会加载其中的所有 Obj-C 代码。例如,它可以是一个带有空函数体的函数(当被调用时什么也不做) ,也可以是一个被访问的全局变量(例如,一次读取或写入全局 int,这就足够了)。与上述所有其他解决方案不同,这个解决方案将运行时哪些类别可用的控制权转移到编译代码(如果它希望它们被链接并可用,它就访问符号,否则它不访问符号,链接器就会忽略它)。

就这些了。

等等,还有一件事:
链接器有一个名为 -dead_strip的选项。这个选项有什么用?如果链接器决定加载一个对象文件,则该对象文件的所有符号都将成为链接二进制文件的一部分,不管它们是否使用。例如,一个对象文件包含100个函数,但是只有一个函数被二进制文件使用,所有100个函数仍然被添加到二进制文件中,因为对象文件要么作为一个整体被添加,要么根本不被添加。链接器通常不支持部分添加目标文件。

但是,如果你告诉链接器“死条”,链接器将首先添加所有的对象文件到二进制文件,解析所有的引用,最后扫描二进制文件的符号没有使用(或只有在使用的其他符号没有使用)。所有发现未使用的符号都将作为优化阶段的一部分删除。在上面的示例中,再次删除了99个未使用的函数。如果您使用像 -load_all-force_loadPerform 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添加到最后的链接调用中,链接器将在生成日志中打印由于使用了哪个符号而从哪个库加载了哪个对象文件。它将只打印在使用中考虑的第一个符号,但这不一定是该对象文件中使用的唯一符号。