静态库中的Objective-C类别

时间:2010-04-02 15:30:56

标签: iphone objective-c static-libraries categories

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

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

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

6 个答案:

答案 0 :(得分:224)

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

<强>详细信息: 我在各种论坛,博客和苹果文档上找到了一些答案。现在我尝试简要总结一下我的搜索和实验。

问题是由(Apple Apple Q&amp; A QA1490引用https://developer.apple.com/library/content/qa/qa1490/_index.html引起的):

  

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

他们的解决方案:

  

要解决此问题,请静态   库应该传递-ObjC选项   到链接器。这个标志导致   链接器加载每个目标文件   定义一个的库   Objective-C类或类别。而   此选项通常会导致a   更大的可执行文件(由于额外的   对象代码加载到   应用程序),它将允许   成功创造有效   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-&gt;添加到项目中添加...)。
  2. 单击lib项目图标上的箭头 - 显示mylib.a文件名,拖动此mylib.a文件并将其放入Target - &gt; Link Binary With Library group。
  3. 在第一页(常规)中打开目标信息并将我的lib添加到依赖项列表
  4. 之后一切正常。在我的情况下,“-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

答案 1 :(得分:112)

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

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

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

但是,在构建静态库时,所有这些目标文件都只是添加到一个大型归档文件中,因此扩展了静态库(.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部分始终是链接阶段的一部分。因此,如果加载了包含类别的对象文件,因为它中的任何符号都被认为是&#34;在使用中&#34; (无论是类,无论是函数,还是全局变量),类别都会被加载,并且在运行时可用。但是,如果未加载目标文件本身,则其中的类别将无法在运行时使用。包含从不类别的对象文件从不已加载,因为它包含无符号,链接器将 考虑&#34 ;在使用中&#34;。这就是整个问题。

已经提出了几种解决方案,现在您已经了解了所有这些解决方案的相互作用,让我们再看一下建议的解决方案:

  1. 一种解决方案是将-all_load添加到链接器调用中。该链接器标志实际上会做什么?实际上,它告诉链接器以下&#34; 加载所有存档的所有目标文件,无论您是否看到正在使用的符号&#39;。当然,那会奏效;但它也可能产生相当大的二进制文件。

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

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

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

  5. 最终的解决方案是弗拉基米尔在答案的最后添加的技巧。放置&#34; 假符号&#34;进入任何仅声明类别的源文件。如果要在运行时使用任何类别,请确保在编译时以某种方式引用伪符号,因为这会导致目标文件由链接器加载,因此也是所有Obj-C代码在里面。例如。它可以是一个具有空函数体的函数(在被调用时不会执行任何操作),或者它可以是访问的全局变量(例如,一旦读取或写入一次全局int,这就足够了)。与上述所有其他解决方案不同,此解决方案将对运行时可用类别的控制转移到已编译的代码(如果它希望链接和可用,则访问符号,否则它不会访问符号和链接器会忽略它。)

  6. 那是所有人。

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

    但是,如果您告诉链接器&#34; dead strip&#34;,链接器将首先将所有目标文件添加到二进制文件中,解析所有引用,最后扫描二进制文件以查找未使用的符号(或仅在未使用的其他符号中使用)。然后,作为优化阶段的一部分,移除所有未使用的符号。在上面的示例中,将再次删除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都是对类#34; MyCoolClass&#34;的对象的引用,但在第二个代码示例中对此类有没有直接引用 (甚至不将类名称作为静态字符串)。一切都只在运行时发生。即使类 实际上是真正的符号,也是如此。对于类别来说情况更糟,因为它们甚至不是真正的符号。

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

    E.g。如果你想要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添加到最后的链接调用中,链接器将在构建日志中打印哪个目标文件,因为使用了哪个符号,它所加载的库。它只会打印使用中考虑的第一个符号,但这不一定是使用该目标文件的唯一符号。

答案 2 :(得分:24)

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

答案 3 :(得分:16)

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

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

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

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

希望澄清它。

答案 4 :(得分:9)

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

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

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

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

答案 5 :(得分:-1)

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