静态库中的Objective-C类别

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

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

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

解决方案:从Xcode 4.2开始,你只需要去链接到库(不是库本身)的应用程序,然后点击Project Navigator中的项目,点击你的应用程序的目标,然后建立设置,然后搜索“其他链接器标志“,单击+按钮,并添加”-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开发常见问题中也有建议:

如何链接静态库中的所有Objective-C类? 将Other Linker Flags构建设置设置为-ObjC。

和标志说明:

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

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

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

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

是的,它可以与添加到项目中的* .a文件一起使用。 然而,我把lib项目添加为直接依赖的麻烦。 但后来我发现这是我的错 – 直接依赖项目可能没有被正确添加。 当我删除它,并添加步骤:

  1. 在应用程序项目中拖放lib项目文件(或将其添加到项目 – >添加到项目…)。
  2. 点击lib项目图标上的箭头 – 显示mylib.a文件名,拖动这个mylib.a文件,并将其放入Target – > Link Binary With Library组。
  3. 在第一页打开目标信息(一般),并将我的库添加到依赖关系列表

之后,所有的作品OK。 在我的情况下,“-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

弗拉基米尔的答案其实很不错,但是我想在这里提供一些更多的背景知识。 也许有一天有人发现我的回复,可能会发现它有帮助。

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

现在,当构建一个动态库(.dylib),一个框架,一个可加载的bundle(.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. 最终的解决方案是弗拉基米尔在答案最后加上的诀窍。 放置一个“ 假符号 ”到任何声明只有类别的源文件。 如果您想在运行时使用任何类别,请确保您在编译时以某种方式引用假符号 ,因为这会导致链接器加载对象文件,因此也会导致所有Obj-C代码在内。 例如,它可能是一个带有空函数体的函数(被调用时什么都不会做),也可能是一个全局变量(例如一次读取或一次写入的全局int ,这就足够了)。 与上述所有其他解决方案不同,此解决方案将有关哪些类别在运行时可用的控件转换为编译后的代码(如果希望它们被链接并可用,则会访问该符号,否则不会访问符号,链接器将忽略它)。

这就是所有人。

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

但是,如果您告诉链接器“死带”,链接器将首先将所有的目标文件添加到二进制文件中,解析所有的引用,并最终扫描二进制文件中未使用的符号(或仅由其他符号使用使用)。 所有未被使用的符号将作为优化阶段的一部分被删除。 在上面的例子中,99个未使用的函数被删除了。 如果使用诸如-load_all-force_loadPerform Single-Object Prelink类的-load_all ,这是非常有用的,因为在某些情况下,这些选项可以很容易地使二进制大小变大,并且死剥离将再次删除未使用的代码和数据。

对于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 ,则链接器将在构建日志中打印哪个库中的哪个对象文件由于使用哪个符号而加载。 它将仅打印使用中考虑的第一个符号,但这不一定是使用该对象文件的唯一符号。

这个问题已经在LLVM中解决了 。 该修补程序作为LLVM 2.9的一部分发布。包含修复程序的第一个Xcode版本是Xcode 4.2,LLVM 3.0随附。 使用XCode 4.2 -force_load时,不再需要-all_load-force_load的用法

编译你的静态库时,你需要做的就是彻底解决这个问题:

要么转到Xcode的生成设置,并在您的GENERATE_MASTER_OBJECT_FILE = YES配置文件中设置执行单个对象的预先链接YES或GENERATE_MASTER_OBJECT_FILE = YES

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

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

希望澄清一下。

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

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

我花了一整天的时间去尝试各种各样的-objC和-all_load等等,但是没有任何东西出来。 这个问题引起了我的注意。 (不要误解我的意思..你仍然需要做-objC的东西,但不止这些)。

也一直帮助我的另一个行动是,我总是建立自己的包括静态库首先..然后我建立封闭的应用程序..

你可能需要在你的类别是静态库的“公共”头:#import“MyStaticLib.h”