静态库中的Objective-C类别

Vla*_*mir 148 iphone objective-c static-libraries categories

你能指导我如何正确地将静态库链接到iPhone项目.我使用添加到app项目的静态库项目作为直接依赖(目标 - >一般 - >直接依赖)并且所有工作正常,但是类别.静态库中定义的类别在app中不起作用.

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

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

Vla*_*mir 224

解决方案:从Xcode 4.2开始,您只需要转到链接库(而不是库本身)的应用程序,然后单击Project Navigator中的项目,单击应用程序的目标,然后构建设置,然后搜索"其他" 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开发常见问题中也有推荐:

如何链接静态库中的所有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项目文件(或使用Project-> Add to project ...添加它).
  2. 单击lib项目图标上的箭头 - 显示mylib.a文件名,拖动此mylib.a文件并将其放入Target - > Link Binary With Library组.
  3. 在第一页(常规)中打开目标信息,并将我的lib添加到依赖项列表

之后一切正常.在我的情况下,"-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
Run Code Online (Sandbox Code Playgroud)

如果我们调用useMyLib(); 在App项目的任何地方然后在任何类中我们都可以使用logSelf类别方法;

[self logSelf];
Run Code Online (Sandbox Code Playgroud)

关于主题的更多博客:

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选项传递给链接器".这与上面引用的相反.我们刚刚确认您在链接应用程序时必须包括,而不是库本身. (8认同)
  • 很好的答案,虽然这个问题的新人应该注意到它现在已经过时了.查看tonklon的答案http://stackoverflow.com/a/9224606/322748(不再需要all_load/force_load) (3认同)
  • @Ken Aspelagh:谢谢,我有同样的问题.需要将-ObjC和-all_load标志添加到*app本身*,而不是库中. (2认同)

Mec*_*cki 113

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

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

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

但是,在构建静态库时,所有这些目标文件都只是添加到一个大型归档文件中,因此扩展了静态库(.a用于归档).因此.a文件不是对象(.o)文件的存档.想象一下没有压缩的TAR档案或ZIP档案.复制单个.a文件比在一堆.o文件中更容易(类似于Java,将.class文件打包到.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.这个设置会做什么?如果启用,所有目标文件(请记住,每个源文件有一个)将合并为一个目标文件(即不是真正的链接,因此名称为PreLink)和此单个目标文件(有时也称为"主对象")然后将文件")添加到存档中.如果现在考虑使用主对象文件的任何符号,则认为整个主对象文件正在使用中,因此总是加载它的所有Objective-C部分.并且因为类是普通符号,所以使用来自这样的静态库的单个类来获取所有类别就足够了.

  5. 最后的解决方案是弗拉基米尔在答案的最后添加的技巧.将" 假符号 "放入任何仅声明类别的源文件中.如果要在运行时使用任何类别,请确保在编译时以某种方式引用伪符号,因为这会导致链接器加载目标文件,因此也会加载其中的所有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];
Run Code Online (Sandbox Code Playgroud)

我也会写

NSString * cname = @"CoolClass";
NSString * cnameFull = [NSString stringWithFormat:@"My%@", cname];
Class mmcClass = NSClassFromString(cnameFull);
id mmc = [[mmcClass alloc] init];
Run Code Online (Sandbox Code Playgroud)

在这两种情况下mmc都是对"MyCoolClass"类的对象的引用,但在第二个代码示例中没有直接引用此类(甚至类名作为静态字符串也没有).一切都只在运行时发生.这就是即使类真正实符号.对于类别来说更糟糕,因为它们甚至不是真正的符号.

因此,如果您有一个包含数百个对象的静态库,但是大多数二进制文件只需要其中的一些,您可能不希望使用上面的解决方案(1)到(4).否则你会得到包含所有这些类的非常大的二进制文件,即使它们中的大多数从未使用过.对于类,您通常根本不需要任何特殊的解决方案,因为类具有实际符号,并且只要您直接引用它们(而不是在第二个代码示例中),链接器就会很好地识别它们的用法.但是,对于类别,请考虑解决方案(5),因为它可以只包含您真正需要的类别.

例如,如果您想要NSData的类别,例如向其添加压缩/解压缩方法,您将创建一个头文件:

// NSData+Compress.h
@interface NSData (Compression)
    - (NSData *)compressedData;
    - (NSData *)decompressedData;
@end

void import_NSData_Compression ( );
Run Code Online (Sandbox Code Playgroud)

和一个实现文件

// NSData+Compress
@implementation NSData (Compression)
    - (NSData *)compressedData 
    {
        // ... magic ...
    }

    - (NSData *)decompressedData
    {
        // ... magic ...
    }
@end

void import_NSData_Compression ( ) { }
Run Code Online (Sandbox Code Playgroud)

现在只需确保import_NSData_Compression()调用代码中的任何位置即可.调用它的位置或调用频率并不重要.实际上它根本不需要被调用,如果链接器这么认为就足够了.例如,您可以将以下代码放在项目的任何位置:

__attribute__((used)) static void importCategories ()
{
    import_NSData_Compression();
    // add more import calls here
}
Run Code Online (Sandbox Code Playgroud)

您不必调用importCategories()代码,该属性将使编译器和链接器相信它被调用,即使它不是.

最后一个提示:
如果添加-whyload到最后一个链接调用,链接器将在构建日志中打印哪个目录文件,因为使用了哪个符号,它所加载的库.它只会打印使用中考虑的第一个符号,但这不一定是该目标文件使用的唯一符号.


ton*_*lon 24

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


amo*_*sel 16

以下是在编译静态库时需要完全解决此问题的方法:

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

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

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

希望澄清一下.

  • 其实你的回答不太对。我不会“要求链接器将同一类的所有类别打包到一个 .o 文件中”,而是要求链接器在创建静态库之前将所有目标文件 (.o) 链接到一个单一的大目标文件中他们/它。一旦从库中引用了任何符号,就会加载所有符号。但是,如果没有引用符号,这将不起作用(例如,如果库中只有类别,则它不起作用)。 (2认同)

abb*_*ood 9

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

Apple也没有在iOS最近发布的使用静态库中强调这一事实.

我花了一整天时间尝试-objC和-all_load之类的各种变体......但是没有任何结果出来...... 这个问题引起了我的注意.(不要误解我的意思..你仍然需要做-objC的东西..但它不仅仅是那个).

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