关于iphone:静态库中的Objective-C类别

Objective-C categories in static library

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

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

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


解决方案:从Xcode4.2开始,您只需转到与库链接的应用程序(而不是库本身),然后在项目导航器中单击项目,单击应用程序的目标,然后构建设置,然后搜索"其他链接器标志",单击+按钮,然后添加"-objc"。-不再需要所有"加载"和"强制加载"。

细节:我在各种论坛、博客和苹果文档中找到了一些答案。现在我试着对我的搜索和实验做一个简短的总结。

问题是由以下原因引起的(引用自Apple Technical Q&A QA1490 https://developer.apple.com/library/content/q a/qa1490/_index.html):

Objective-C does not define linker
symbols for each function (or method,
in Objective-C) - instead, linker
symbols are only generated for each
class. If you extend a pre-existing
class with categories, the linker does
not know to associate the object code
of the core class implementation and
the category implementation. This
prevents objects created in the
resulting application from responding
to a selector that is defined in the
category.

他们的解决方案是:

To resolve this issue, the static
library should pass the -ObjC option
to the linker. This flag causes the
linker to load every object file in
the library that defines an
Objective-C class or category. While
this option will typically result in a
larger executable (due to additional
object code loaded into the
application), it will allow the
successful creation of effective
Objective-C static libraries that
contain categories on existing
classes.

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

How do I link all the Objective-C
classes in a static library? Set the
Other Linker Flags build setting to
-ObjC.

和标志说明:

-all_load Loads all members of static archive libraries.

-ObjC Loads all members of static archive libraries that implement an
Objective-C class or category.

-force_load (path_to_archive) Loads all members of the specified static
archive library. Note: -all_load
forces all members of all archives to
be loaded. This option allows you to
target a specific archive.

*我们可以使用强制加载来减少应用程序二进制文件的大小,并避免在某些情况下所有加载都可能导致的冲突。

是的,它可以处理添加到项目中的*.a文件。然而,我对作为直接依赖添加的lib项目有困难。但后来我发现这是我的错——直接依赖项目可能没有正确添加。当我删除它并用步骤再次添加时:

  • 在应用程序项目中拖放lib项目文件(或使用project->add to project…)添加它。
  • 单击lib项目图标-mylib上的箭头。显示一个文件名,拖动这个mylib.a文件,并将其放入target->link binary with library group。
  • 在第一页(常规)中打开目标信息并将我的lib添加到依赖项列表中
  • 之后一切正常。"-在我的情况下,objc"标志就足够了。

    我也对来自http://iphonedevelopmentexperiences.blogspot.com/2010/03/categories-in-static-library.html博客的想法感兴趣。作者说他可以使用lib中的category而不设置-all-load或-objc标志。他只是将空的虚拟类接口/实现添加到H/M类文件中,以强制链接器使用该文件。是的,这个把戏就可以了。

    但作者也说他甚至没有实例化虚拟对象。嗯……正如我发现的,我们应该从类别文件中显式地调用一些"真正的"代码。所以至少应该调用类函数。我们甚至不需要假班。单C函数的作用相同。

    因此,如果我们将lib文件写为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 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();应用程序项目中的任何地方那么在任何类中,我们都可以使用logself-category方法;

    1
    [self logSelf];

    更多关于主题的博客:

    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)或可执行二进制文件时,链接器将这些对象文件链接在一起,以生成操作系统认为"可用"的内容,例如,可以直接加载到特定内存地址的内容。好的。

    但是,在构建静态库时,所有这些对象文件都只是简单地添加到一个大的存档文件中,因此扩展了静态库(.a用于存档)。所以.a文件只不过是对象(.o)文件的存档。可以想象一个tar存档或者一个没有压缩的zip存档。只复制一个文件,比一堆.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部分始终是链接阶段的一部分。因此,如果加载了包含类别的对象文件,因为其中的任何符号都被视为"正在使用"(无论是类、函数还是全局变量),则类别也将被加载,并在运行时可用。但是,如果没有加载对象文件本身,那么其中的类别在运行时将不可用。永远不会加载只包含类别的对象文件,因为它不包含链接器认为"正在使用"的符号。这就是这里的全部问题。好的。

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

  • 一种解决方案是将-all_load添加到链接器调用中。那个链接器标志实际上会做什么?实际上,它告诉链接器"加载所有归档文件的所有对象文件,无论您是否看到任何正在使用的符号"。当然,这是可行的,但它也可能产生相当大的二进制文件。好的。

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

  • 最流行的解决方案是将-ObjC添加到链接器调用中。那个链接器标志实际上会做什么?此标志告诉链接器"如果您看到所有对象文件包含任何obj-c代码,则从所有存档中加载所有对象文件"。"任何OBJ-C代码"包括类别。这也会起作用,并且不会强制加载不包含obj-c代码的对象文件(这些文件仍然是按需加载的)。好的。

  • 另一种解决方案是新的Xcode构建设置Perform Single-Object Prelink。这个设置将做什么?如果启用,则所有对象文件(记住,每个源文件都有一个)合并到一个对象文件中(这不是真正的链接,因此名称为prelink),然后将该单个对象文件(有时也称为"主对象文件")添加到存档中。如果现在考虑使用主对象文件的任何符号,则整个主对象文件都将被考虑使用,因此始终加载它的所有Objective-C部分。因为类是普通的符号,所以只需使用这样一个静态库中的一个类就可以得到所有的类。好的。

  • 最后的解决办法是弗拉迪米尔在回答的最后加了一个窍门。在任何源文件中放置一个"假符号",只声明类别。如果您想在运行时使用任何类别,请确保在编译时以某种方式引用假符号,因为这会导致对象文件被链接器加载,因此也会在其中加载所有的Obj-C代码。例如,它可以是一个具有空函数体的函数(在被调用时不会做任何事情),也可以是一个可访问的全局变量(例如,一次读取或写入全局int,这就足够了)。与上述所有其他解决方案不同,此解决方案将控制运行时可用的类别转换为已编译代码(如果它希望它们链接并可用,则它访问符号,否则它不访问符号,链接器将忽略它)。好的。

  • 这是所有人。好的。

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

    但是,如果告诉链接器"死区",链接器将首先将所有对象文件添加到二进制文件中,解析所有引用,最后扫描二进制文件以查找未使用的符号(或仅由未使用的其他符号使用)。所有未使用的符号将作为优化阶段的一部分删除。在上面的示例中,99个未使用的函数将再次被删除。如果您使用诸如-load_all-force_loadPerform Single-Object Prelink等选项,这非常有用,因为这些选项在某些情况下很容易使二进制文件的大小急剧增大,死区剥离将再次删除未使用的代码和数据。好的。

    死剥对于C代码非常有效(如未使用的函数、变量和常量按预期移除),并且它对C++也非常好(例如,未使用的类被删除)。它并不完美,在某些情况下,一些符号不会被删除,即使删除它们是可以的,但在大多数情况下,它对这些语言都非常有效。好的。

    OBJ-C怎么样?算了吧!对于obj-c没有死板的剥离。由于obj-c是一种运行时功能语言,编译器在编译时不能说一个符号是否真的在使用。例如,如果没有直接引用某个obj-c类的代码,则该类不在使用中,对吗?错了!您可以动态构建包含类名的字符串,为该名称请求一个类指针,并动态分配类。例如,而不是好的。

    1
    MyCoolClass * mcc = [[MyCoolClass alloc] init];

    我也会写好的。

    1
    2
    3
    4
    NSString * cname = @"CoolClass";
    NSString * cnameFull = [NSString stringWithFormat:@"My%@", cname];
    Class mmcClass = NSClassFromString(cnameFull);
    id mmc = [[mmcClass alloc] init];

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

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

    例如,如果要为nsdata创建一个类别,例如向其添加压缩/解压方法,则需要创建一个头文件:好的。

    1
    2
    3
    4
    5
    6
    7
    // NSData+Compress.h
    @interface NSData (Compression)
        - (NSData *)compressedData;
        - (NSData *)decompressedData;
    @end

    void import_NSData_Compression ( );

    以及一个实现文件好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // NSData+Compress
    @implementation NSData (Compression)
        - (NSData *)compressedData
        {
            // ... magic ...
        }

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

    void import_NSData_Compression ( ) { }

    现在,只要确保代码中的任何地方调用import_NSData_Compression()。无论是在哪里打电话,还是多久打一次电话。实际上,它根本不需要被调用,如果链接器这么认为就足够了。例如,您可以在项目中的任何位置放置以下代码:好的。

    1
    2
    3
    4
    5
    __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 -ObjC时,不再需要使用-all_load-force_load


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

    要么转到XCuffBudio设置,然后将单个对象预链接设置为"是"或"是"。生成配置文件中的GENERATE_MASTER_OBJECT_FILE = YES

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

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

    希望能澄清这一点。


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

    苹果在最近发布的iOS静态库中也没有强调这一点。

    我花了一整天的时间尝试各种各样的变化,比如Objc和Aull Load等等。但没有任何东西出来。这个问题引起了我的注意。(别误会我的意思。你仍然必须做-Objc的东西…但不仅仅是这样。

    另外一个总是帮助我的动作是,我总是首先自己构建包含的静态库。然后,我建立了封闭的应用程序。


    您可能需要在静态库的"public"头中输入类别:import"mystaticlib.h"