静态库中的Objective-C类别


153

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

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

通常,在其他项目的应用程序项目代码中使用最佳实践是什么?


1
好了,找到了一些答案,似乎这个问题已经在这里回答(很抱歉错过了stackoverflow.com/questions/932856/...
弗拉基米尔

Answers:


228

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

详细信息: 我在各种论坛,博客和Apple文档中找到了一些答案。现在,我尝试对搜索和实验做一个简短的总结。

问题是由以下原因引起的(来自Apple技术问答QA1490 https://developer.apple.com/library/content/qa/qa1490/_index.html的引用):

Objective-C并未为每个函数(或方法,在Objective-C中)定义链接器符号-而是仅为每个类生成链接器符号。如果使用类别扩展现有的类,则链接器不知道将核心类实现的对象代码与类别实现相关联。这样可以防止在结果应用程序中创建的对象响应类别中定义的选择器。

及其解决方案:

若要解决此问题,静态库应将-ObjC选项传递给链接器。该标志使链接器加载库中定义Objective-C类或类别的每个对象文件。尽管此选项通常会导致更大的可执行文件(由于将附加的目标代码加载到应用程序中),但它将允许成功创建包含现有类类别的有效Objective-C静态库。

iPhone开发常见问题解答中也有建议:

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

和标志说明:

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

- ObjC负载静态归档库中的所有成员实现的Objective-C类或类别。

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

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

是的,它与添加到项目中的* .a文件一起使用。但是我在将lib项目添加为直接依赖项方面遇到了麻烦。但是后来我发现这是我的错-直接依赖项目可能未正确添加。当我删除它并再次添加步骤时:

  1. 将lib项目文件拖放到应用程序项目中(或通过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博客中的想法感兴趣。作者说他可以使用lib中的类别,而无需设置-all_load或-ObjC标志。他只是将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


8
Apple技术说明似乎已被修改为:“要解决此问题,针对静态库的目标链接必须将-ObjC选项传递给链接器。” 这与上面引用的相反。我们刚刚确认,链接应用程序时必须包括您,而不是库本身。
Ken Aspeslagh 2010年

根据doc 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标志。”
罗宾

2
@Ken Aspelagh:谢谢,我遇到了同样的问题。需要将-ObjC和-all_load标志添加到应用程序本身,而不是库中。
titaniumdecoy

3
很好的答案,尽管对此问题的新手应该注意,它已经过时了。请查看tonklon的答案stackoverflow.com/a/9224606/322748(不再需要all_load / force_load)
杰伊·派耶

我在这些事情上停留了将近半个小时,但经过反复试验,我才知道。不管怎样,谢谢。这个答案值得+1,你就知道了!!!
Deepukjayan 2012年

118

弗拉基米尔的答案实际上是相当不错的,但是,我想在这里给出更多背景知识。也许有一天有人会找到我的答复,可能会有所帮助。

编译器将源文件(.c,.cc,.cpp,.m)转换为目标文件(.o)。每个源文件只有一个目标文件。目标文件包含符号,代码和数据。目标文件不能由操作系统直接使用。

现在,当构建动态库(.dylib),框架,可加载包(.bundle)或可执行二进制文件时,这些目标文件通过链接器链接在一起,以产生操作系统认为“可用”的东西,例如可以直接加载到特定的内存地址。

但是,在构建静态库时,所有这些目标文件都被简单地添加到大的归档文件中,因此扩展了静态库(.a用于归档)。因此,.a文件无非就是对象(.o)文件的存档。考虑一下没有压缩的TAR存档或ZIP存档。与在整个.o文件中复制单个.a文件相比,复制一个.a文件要容易得多(类似于Java,在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. 最终的解决方案是弗拉基米尔(Vladimir)在回答的最后添加的技巧。在任何仅声明类别的源文件中放置一个“ 假符号 ”。如果要在运行时使用任何类别,请确保在编译时以某种方式引用伪造的符号,因为这将导致链接器以及所有其中的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到最终的链接调用中,则链接器将在构建日志中打印由于使用了哪个符号而从哪个库中加载了哪个目标文件。它将仅打印考虑使用的第一个符号,但不一定是使用该目标文件的唯一符号。


1
感谢您的提及-whyload,尝试调试链接器为什么要执行某些操作可能非常困难!
本S

有一个选项Dead Code StrippingBuild Settings>Linking。与-dead_strip添加的内容相同Other Linker Flags吗?

1
@Sean是的,是一样的。只需阅读每种构建设置都存在的“快速帮助”,答案就在那里:postimg.org/image/n7megftnr/full
Mecki 2015年

@Mecki谢谢。我试图摆脱它-ObjC,所以我尝试了您的破解,但是它抱怨"import_NSString_jsonObject()", referenced from: importCategories() in main.o ld: symbol(s) not found。我放入import_NSString_jsonObject名为的嵌入式框架Utility,并在末尾添加#import <Utility/Utility.h>with __attribute__语句AppDelegate.h

@Sean如果链接器找不到符号,则说明您未针对包含该符号的静态库进行链接。仅从框架导入ah文件不会使Xcode与该框架链接。必须在与框架构建阶段的链接中将框架显式链接到。您可能想为自己的链接问题提出一个自己的问题,在评论中回答很麻烦,并且您也无法提供构建日志输出之类的信息。
梅基2015年

24

LLVM中解决此问题。该修补程序作为LLVM 2.9的一部分提供。包含此修补程序的第一个Xcode版本是LLVM 3.0附带的Xcode 4.2。仍需要使用XCode 4.2时不再使用-all_load-force_load -ObjC


你确定吗?我正在使用Xcode 4.3.2进行iOS项目,并使用LLVM 3.1进行编译,这对我来说仍然是一个问题。
Ashley Mills

好的,这有点不准确。该-ObjC标志仍然是必需的,并且将一直存在。解决方法是使用-all_load-force_load。而且不再需要了。我在上面修正了我的答案。
tonklon 2012年

包括-all_load标志是否有任何不利之处(即使不必要)?它以任何方式影响编译/启动时间吗?
ZS 2012年

我正在使用Xcode版本4.5(4G182),并且-ObjC标志将我无法识别的选择器错误从我试图使用的第三方依赖性中移到了看起来像Objective C运行时深度的地方:“-[__ NSArrayM map :]:无法识别的选择器已发送至实例...”。有什么线索吗?
罗伯特·阿特金斯

16

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

转到Xcode构建设置,然后将“执行单对象预链接”设置为“是”或 GENERATE_MASTER_OBJECT_FILE = YES在构建配置文件中。

默认情况下,链接器为每个.m文件生成一个.o文件。因此类别获得不同的.o文件。链接器查看静态库.o文件时,不会创建每个类的所有符号的索引(运行时无关紧要)。

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

希望可以澄清这一点。


这为我修复了此问题,而不必将-ObjC添加到链接目标。
马修·克伦肖

更新到最新版本的BlocksKit库后,我不得不使用此设置来解决问题(我已经在使用-ObjC标志,但仍然看到问题)。
2013年

1
实际上,您的答案不太正确。我不会“要求链接程序将同一类的所有类别打包在一起到一个.o文件中”,而是要求链接程序在从中创建静态库之前将所有目标文件(.o)链接到一个大的目标文件中。他们/它。一旦从库中引用了任何符号,就将加载所有符号。但是,如果未引用任何符号,则此方法将无效(例如,如果库中仅包含类别,则它将无效)。
Mecki 2014年

我认为如果您将类别添加到现有的类(例如NSData),这将不起作用。
鲍勃·怀特曼

我也很难在现有类中添加类别。我的插件在运行时无法识别它们。
David Dunham

9

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

苹果公司也没有在他们最近发布的iOS中使用“静态库”中强调这一事实。

我花了一整天的时间来尝试-objC和-all_load等的各种变化。.但是什么都没出现。. 这个问题引起了我的注意。(不要误会我..您仍然必须做-objC东西..但不仅限于此)。

另一个始终对我有帮助的动作是,我总是先自行构建包含的静态库。然后再构建封闭的应用程序。


-1

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.