首页 文章

静态库中的Objective-C类别

提问于
浏览
149

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

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

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

6 回答

  • 9

    Solution: 从Xcode 4.2开始,您只需要转到链接库(而不是库本身)的应用程序并单击Project Navigator中的项目,单击您的应用程序's target, then build settings, then search for 1164532 , click the + button, and add ' -ObjC '. ' -all_load ' and ' -force_load'不是需要更久 .

    Details: 我在各种论坛,博客和苹果文档中找到了一些答案 . 现在我尝试简要总结一下我的搜索和实验 .

    问题是由(引自苹果技术问答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项目作为直接依赖添加了麻烦 . 但后来我发现这是我的错 - 直接依赖项目可能没有正确添加 . 当我删除它并再次添加步骤:

    • 在项目项目中拖放lib项目文件(或使用Project-> Add to project ...添加它) .

    • 单击lib项目图标上的箭头 - 显示mylib.a文件名,拖动此mylib.a文件并将其放入Target - > Link Binary With Library组 .

    • 在第一页(常规)中打开目标信息,并将我的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
    

    如果我们调用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

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

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

    现在构建一个动态库(.dylib),一个框架,一个可加载的bundle(.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部分始终是链接阶段的一部分 . 因此,如果加载包含类别的对象文件,因为它的任何符号都被认为是"in use"(无论是类,无论是函数,还是全局变量),类别也会被加载并在运行时可用 . 但是,如果未加载目标文件本身,则其中的类别将无法在运行时使用 . 包含 only 类别的目标文件是 never ,因为它包含 no symbols 链接器 ever 考虑"in use" . 这就是整个问题 .

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

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

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

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

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

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

    这就是所有人 .

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

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

    因此,如果您有一个包含数百个对象的静态库,但是大多数二进制文件只需要其中的一些,您可能不希望使用上面的解决方案(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 添加到最后一个链接调用,链接器将在构建日志中打印哪个目标文件从哪个库加载,因此使用中的符号 . 它只会打印使用中考虑的第一个符号,但这不一定是该目标文件使用的唯一符号 .

  • 110

    这个问题一直是fixed in LLVM . 该修补程序作为LLVM 2.9的一部分提供 . 包含该修复程序的第一个Xcode版本是带有LLVM 3.0的Xcode 4.2 . The usage of -all_load or -force_load is no longer needed when working with XCode 4.2 -ObjC 仍然需要 .

  • 224

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

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

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

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

    希望澄清一下 .

  • 16

    每当静态库链接讨论出现时很少提及的一个因素是 must also include the categories themselves in the build phases->copy files and compile sources of the static library itself .

    苹果也没有在最近发表的Using Static Libraries in iOS中强调这一事实 .

    我花了一整天时间尝试各种各样的-objC和-all_load等等 . 但是没有任何结果...... this问题引起了我的注意 . (不仅仅是这个) .

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

  • 24

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

相关问题