【问题标题】:Objective-C categories in static library静态库中的 Objective-C 类别
【发布时间】:2011-02-03 18:44:08
【问题描述】:

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

所以我的问题是如何将具有某些类别的静态库添加到其他项目中?

一般来说,在其他项目的应用项目代码中使用的最佳做法是什么?

【问题讨论】:

标签: iphone objective-c static-libraries categories


【解决方案1】:

解决方案:从 Xcode 4.2 开始,您只需转到链接库的应用程序(而不是库本身),然后单击项目导航器中的项目,单击应用程序的目标,然后构建设置,然后搜索“Other Linker Flags”,单击 + 按钮,并添加“-ObjC”。不再需要“-all_load”和“-force_load”。

详情: 我在各种论坛、博客和苹果文档上找到了一些答案。现在我试着对我的搜索和实验做一个简短的总结。

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

Objective-C 没有定义链接器 每个函数(或方法, 在 Objective-C 中) - 相反,链接器 符号只为每个生成 班级。如果你扩展一个预先存在的 带有类别的类,链接器会 不知道关联目标代码 核心类的实现和 类实现。这 防止在 响应产生的应用程序 到在 类别。

以及他们的解决方案:

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

iPhone Development FAQ 中也有推荐:

如何链接所有的 Objective-C 静态库中的类?设置 其他链接器标志构建设置为 -ObjC。

和标志说明:

-all_load 加载静态归档库的所有成员。

-ObjC 加载所有实现了 Objective-C 类或类别。

-force_load(path_to_archive) 加载指定静态的所有成员 档案库。注意:-all_load 强制所有档案的所有成员 被加载。此选项允许您 定位特定档案。

*我们可以使用 force_load 来减少应用程序二进制文件大小并避免 all_load 在某些情况下可能导致的冲突。

是的,它适用于添加到项目中的 *.a 文件。 然而,我在将 lib 项目添加为直接依赖项时遇到了麻烦。但后来我发现这是我的错 - 可能没有正确添加直接依赖项目。当我删除它并通过步骤再次添加时:

  1. 将 lib 项目文件拖放到 app 项目中(或通过 Project->Add to project... 添加)。
  2. 单击 lib 项目图标上的箭头 - 显示 mylib.a 文件名,将此 mylib.a 文件拖放到 Target -> Link Binary With Library 组中。
  3. 在第一页(常规)中打开目标信息并将我的库添加到依赖项列表中

之后一切正常。在我的情况下,“-ObjC”标志就足够了。

我也对来自http://iphonedevelopmentexperiences.blogspot.com/2010/03/categories-in-static-library.html 博客的想法感兴趣。作者说他可以在不设置 -all_load 或 -ObjC 标志的情况下使用 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 项目中的任何地方 那么在任何类中我们都可以使用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

【讨论】:

  • Apple 技术说明似乎已被修改为“要解决此问题,针对静态库的目标链接必须将 -ObjC 选项传递给链接器。”这与上面引用的相反。我们刚刚确认您必须在链接应用程序时包含,而不是库本身。
  • 根据文档developer.apple.com/library/mac/#qa/qa1490/_index.html,我们应该使用 -all_load 或 -force_load 标志。如前所述,链接器在 64 位 Mac App 和 iPhone App 中确实存在错误。 “重要提示:对于 64 位和 iPhone OS 应用程序,有一个链接器错误会阻止 -ObjC 从仅包含类别且不包含类的静态库中加载对象文件。解决方法是使用 -all_load 或 -force_load 标志。”
  • @Ken Aspelagh:谢谢,我遇到了同样的问题。 -ObjC 和 -all_load 标志需要添加到 应用程序本身,而不是库。
  • 很好的答案,虽然这个问题的新手应该注意它现在已经过时了。查看 tonklon 的回答 stackoverflow.com/a/9224606/322748(不再需要 all_load/force_load)
  • 我被这些东西卡住了将近半个小时,经过反复试验,我才弄明白了。不管怎样,谢谢。这个答案值得 +1,你明白了!!!
【解决方案2】:

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 部分始终是链接阶段的一部分。因此,如果加载了包含类别的目标文件,因为其中的任何符号都被认为是“正在使用”(无论是类、函数还是全局变量),类别也会被加载并且在运行时可用.然而,如果目标文件本身没有加载,则其中的类别在运行时将不可用。包含类别的目标文件从不加载,因为它包含无符号链接器会永远认为“正在使用” ”。这就是这里的全部问题。

已经提出了几种解决方案,现在您知道所有这些是如何一起发挥作用的,让我们再看看提出的解决方案:

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

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

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

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

  5. 最终的解决方案是弗拉基米尔在回答的最后添加的技巧。将“假符号”放入任何仅声明类别的源文件中。如果您想在运行时使用任何类别,请确保在编译时以某种方式引用 fake symbol,因为这会导致目标文件被链接器加载,因此所有 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 添加到最后的链接调用中,链接器将在构建日志中打印由于使用了哪个符号,它从哪个库加载了哪个目标文件。它只会打印考虑使用的第一个符号,但这不一定是该目标文件使用的唯一符号。

【讨论】:

  • 感谢您提及-whyload,尝试调试链接器执行某些操作的原因可能非常困难!
  • Build Settings>Linking 中有一个选项Dead Code Stripping。和Other Linker Flags中添加的-dead_strip一样吗?
  • @Sean 是的,它是一样的。只需阅读每个构建设置都存在的“快速帮助”,答案就在那里:postimg.org/image/n7megftnr/full
  • @Mecki 谢谢。我试图摆脱-ObjC,所以我尝试了你的hack,但它抱怨"import_NSString_jsonObject()", referenced from: importCategories() in main.o ld: symbol(s) not found。我将import_NSString_jsonObject 放入名为Utility 的嵌入式框架中,并在AppDelegate.h 末尾添加#import <Utility/Utility.h>__attribute__ 语句。
  • @Sean 如果链接器找不到该符号,则您没有链接到包含该符号的静态库。仅从框架导入 h 文件不会使 Xcode 链接到框架。必须在与框架构建阶段的链接中明确链接到框架。您可能想为您的链接问题打开一个自己的问题,在 cmets 中回答很麻烦,您也无法提供构建日志输出等信息。
【解决方案3】:

这个问题是fixed in LLVM。该修复程序作为 LLVM 2.9 的一部分提供 包含该修复程序的第一个 Xcode 版本是 Xcode 4.2 与 LLVM 3.0 一起提供。 使用 XCode 4.2 时不再需要使用 -all_load-force_load 仍然需要 -ObjC

【讨论】:

  • 你确定吗?我正在使用 Xcode 4.3.2 开发一个 iOS 项目,使用 LLVM 3.1 进行编译,这对我来说仍然是个问题。
  • 好吧,这有点不精确。 -ObjC 标志仍然是需要的,并且永远都是。解决方法是使用-all_load-force_load。这不再需要了。我在上面固定了我的答案。
  • 包含 -all_load 标志有什么缺点(即使它是不必要的)?它会以任何方式影响编译/启动时间吗?
  • 我正在使用 Xcode 版本 4.5 (4G182) 并且 -ObjC 标志将我无法识别的选择器错误从我试图使用的第 3 方依赖项移到看起来像目标的深度C 运行时:“-[__NSArrayM map:]:无法识别的选择器发送到实例...”。有什么线索吗?
【解决方案4】:

在编译静态库时,您需要执行以下操作才能完全解决此问题:

要么转到 Xcode Build Settings 并将 Perform Single-Object Prelink 设置为 YES 要么 GENERATE_MASTER_OBJECT_FILE = YES 在您的构建配置文件中。

默认情况下,链接器会为每个 .m 文件生成一个 .o 文件。所以类别得到不同的 .o 文件。当链接器查看静态库 .o 文件时,它不会为每个类创建所有符号的索引(运行时会,不管怎样)。

该指令将要求链接器将所有对象打包到一个大的 .o 文件中,从而强制处理静态库的链接器获取所有类类别的索引。

希望澄清一下。

【讨论】:

  • 这为我修复了它,而无需将 -ObjC 添加到链接目标。
  • 在更新到最新版本的BlocksKit 库后,我不得不使用这个设置来解决这个问题(我已经在使用 -ObjC 标志但仍然看到这个问题)。
  • 其实你的答案不太对。我没有“要求链接器将同一类的所有类别打包到一个 .o 文件中”,它要求链接器在创建静态库之前将所有目标文件 (.o) 链接到单个大目标文件中他们/它。从库中引用任何符号后,将加载所有符号。但是,如果没有引用符号,这将不起作用(例如,如果库中只有类别则不起作用)。
  • 我认为如果您将类别添加到现有类(例如 NSData)中,这将不起作用。
  • 我也无法将类别添加到现有课程。我的插件在运行时无法识别它们。
【解决方案5】:

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

Apple 在他们最近发布的Using Static Libraries in iOS 中也没有强调这一事实。

我花了一整天的时间尝试各种 -objC 和 -all_load 等变体。但没有任何结果。this 问题引起了我的注意。 (不要误会我的意思.. 你仍然需要做 -objC 的事情.. 但不仅如此)。

另一个一直对我有帮助的动作是我总是先自己构建包含的静态库..然后我构建封闭的应用程序..

【讨论】:

    【解决方案6】:

    您可能需要在静态库的“公共”标题中包含类别:#import "MyStaticLib.h"

    【讨论】:

      猜你喜欢
      • 2011-10-12
      • 2012-01-26
      • 2010-11-06
      • 1970-01-01
      • 1970-01-01
      • 2011-06-18
      • 2011-08-27
      • 2011-01-04
      • 1970-01-01
      相关资源
      最近更新 更多