iOS

Building Objective-C static libraries with categories

something fun in yykit

Posted by Miaocf on September 24, 2019

Q: How do I fix “selector not recognized” runtime exceptions when trying to use category methods from a static library?

如何解决 在 runtime 环境中尝试调用静态库中分类的方法时,报 “selector not recognized” 异常?

  今天在浏览 YYKit 库时,一个宏定义引起了我的注意:YYSYNTH_DUMMY_CLASS ,从字面意思看是定义了一个不起任何作用的类。相关描述如下:

/**
 Add this macro before each category implementation, so we don't have to use
 -all_load or -force_load to load object files from static libraries that only
 contain categories and no classes.
 More info: http://developer.apple.com/library/mac/#qa/qa2006/qa1490.html .
 *******************************************************************************
 Example:
     YYSYNTH_DUMMY_CLASS(NSString_YYAdd)
 */
#ifndef YYSYNTH_DUMMY_CLASS
#define YYSYNTH_DUMMY_CLASS(_name_) \
@interface YYSYNTH_DUMMY_CLASS_ ## _name_ : NSObject @end \
@implementation YYSYNTH_DUMMY_CLASS_ ## _name_ @end
#endif

  根据 YYKit 作者的描述,在分类中加一个这样的宏定义可以避免一些复杂操作,具体原理作者并没有解释,只是给了一个苹果官方开发文档的连接。这个链接可能因为官方整理文档的原因,URL路径已经失效了。但是在官网搜索 URL 中的关键字找到了这篇资料。根据这篇资料在这里深入了解一下其中的原理和如何解决这类问题。

官方文档以一个问题开篇,就是上面提出的那个问题,哈哈。接下来官方文档就给出了问题的解决方法:

A: If you’re seeing a “selector not recognized” runtime exception when calling a category method that is implemented in a static library, you are hitting the link-time build issue described here, and need to add the -ObjC linker flag to your project, by following these steps:

  1. In Xcode, choose View > Navigators > Show Project Navigator, or press ⌘1.
  2. Select your project under the PROJECT heading in the Project Navigator, then select the Build Settings tab.
  3. Scroll down to the Other Linker Flags build setting under the Linking collection, or type “Other Linker Flags” into the search bar.
  4. Set the value of the Other Linker Flags build setting to $(OTHER_LDFLAGS) -ObjC.

译:当你在运行时调用一个在静态库中实现的分类的方法时,遇到 “selector not recognized” 运行时异常,那么你就撞到了本篇文档中描述的所谓的 “the link-time build issue” 问题,同时你需要在你的工程中加上 -ObjC linker flag 来解决这个问题。具体步骤:(在这里就不翻译了么么哒)。

Figure 1: Modifying the Other Linker Flags build setting.


Troubleshooting

If adding the -ObjC flag isn’t fixing the problem, double check that a conflicting Target build setting is not overriding it, by following the above steps, but selecting the current target under “TARGETS” in step 2, instead of the project.

解决问题

  如果添加-ObjC flag没能解决这个问题,检查一下确保之前的操作没有与 Target build setting 冲突而使其无效。按照上面的步骤,在第二步时选择 “TARGETS”下的target,而不是选择 project。

Other Causes of selector not recognized Exceptions

The most common causes of a “selector not recognized” exception are:

No Such Method

The method really does not exist. Check your spelling. Check documentation to verify that the method exists on the version of the operating system your app is using.

Memory Management

Your app is trying to use an object after it has been deallocated, use the Zombies instrument to debug this kind of problem. You are seeing “selector not recognized” because the memory has been re-allocated as a different kind of object.

导致selector not recognized Exceptions异常的其他原因

  导致selector not recognized Exceptions异常的原因通常是:

方法不存在

  方法实际上是不存在的。检查你的拼写是否有误。查阅文档,确保你调用的方法在当前版本的操作系统内存在。

内存管理

  在对象已释放后,你的应用又尝试去访问它,原先存放对象的内存已被重新分配给其他类的对象,所以当你再次访问原先的对象时,就会发生 “selector not recognized”异常。使用Zombies instrument工具来debug这类问题。(这一段文档不通顺,稍作整理)

What causes those exceptions?

An impedance mismatch between UNIX static libraries and the dynamic nature of Objective-C can cause category methods in static libraries to not be linked into an app, resulting in “selector not recognized” exceptions when the methods aren’t found at runtime.

导致这些异常的原因

由于 UNIX static librariesdynamic nature of Objective-C不是很搭的原因,导致静态库中分类的方法不会被链接到app中去,以至于在 runtime 动态调用时无法找到方法,结果发生 “selector not recognized”异常。

The Linker

When a C program is compiled, each “source file” is turned into an “object file” that contains executable functions and static data. The linker glues these object files together into a final executable. That executable is eventually bundled into an app by Xcode.

链接器

当一个 C 程序被编译完成,所有”原文件”都被转换为”object file”,这些”object file”包含可执行方法和静态数据。链接器将这些”object file”粘合在一起,最终成为可执行文件。最终 Xcode 将这些可执行文件打包进APP中。

When a source file uses something (like a function) defined in another file, then an undefined symbol is written into the object file, to “stand in” for the missing thing. The linker resolves these symbols by pulling in the object files that include definitions of undefined symbols when building the final executable.

当一个原文件调用某些在其他文件中定义的方法或者数据时,那么一个未经定义的符号就被写入了 “object file”,用以代替缺失的方法或数据。在组成最终可执行文件时,链接器从那些包含 “undefined symbols” 定义的 “object files” 中抽取定义来解析这些符号。

For example, if main.c uses the function foo(), where foo is defined in another file, B.c, then the object file main.o will have an unresolved symbol for foo(), and B.o will include an implementation of foo(). At link time, B.o will be brought into the final executable, so that the code in main.o now references the implementation of foo() defined in B.o.

举个例子,假如 main.c 调用方法 foo(),而 foo 函数是在另一个文件 B.c 中定义的,那么 object file main.o 将会有一个未解析符号表示 foo(),同时 B.o 将会包含 foo() 方法实现。在链接阶段,B.o 将会被带入 最终执行文件,因此现在 main.o 中的代码就可以引用在 B.o 中对 foo() 的实现。

A UNIX static library is just a collection of object files. Normally the linker only pulls in an object file from a static library if doing so would resolve some undefined symbol. Not pulling in all object files reduces the size of the final executable.

一个 UNIX static library 只是将 object files 集合在一起。链接器通常只会从static library中提取那些包含undefined symbol解析的 object file,而不是把所有 object files 都集成进来。这样可以减小最终可执行文件的体积。

Objective-C

The dynamic nature of Objective-C complicates things slightly. Because the code that implements a method is not determined until the method is actually called, Objective-C does not define linker symbols for methods. Linker symbols are only defined for classes.

Objective-C

O-C 的动态特性使这个过程略微复杂了一些。因为实现一个方法的代码直到这个方法被调用时才会被确定下来。Objective-C 并不为方法定义链接符号,链接符号只为 进行定义。

For example, if main.m includes the code [[FooClass alloc] initWithBar:nil]; then main.o will contain an undefined symbol for FooClass, but no linker symbols for the -initWithBar: method will be in main.o.

比如: 假如 main.m 包含了这样的代码:[[FooClass alloc] initWithBar:nil];,那么 main.o 将会包含一个未定义符号表示 FooClass,但是在 main.o 文件中,并不生成链接符号去表示 -initWithBar: 方法。

Since categories are a collection of methods, using a category’s method does not generate an undefined symbol. This means the linker does not know to load an object file defining the category, if the class itself is already defined. This causes the same “selector not recognized” runtime exception you would see for any unimplemented method.

因为分类是一组方法的集合,假如一个类已经定义了,在其中使用分类的方法并不生成 undefined symbol,这意味着链接器不知道需要去加载对应的 object file 去定义分类。调用任何未实现的方法也一样会导致 “selector not recognized” 运行时异常。

###The -ObjC Linker Flag Passing the -ObjC option to the linker causes it to load all members of static libraries that implement any Objective-C class or category. This will pickup any category method implementations. But it can make the resulting executable larger, and may pickup unnecessary objects. For this reason it is not on by default.

The -ObjC Linker Flag

指定 链接器的 -ObjC 选项 可以使 链接器加载所有静态库总的成员,包括 Objective-C 的类和分类。这将选入所有分类方法的实现。同时也使最最终的可执行文件体积更大,这也是为什么不作为默认选项。

总结

YYKit 中的这个宏定义在 分类之前加了一个空类,这样.m文件就包含了类,这样生成的 .o文件就会被链接器打包进最终的可执行文件,从而避免了 “selector not recognized”异常的出现。

关于转载

知识共享许可协议

本作品采用知识共享署名 4.0 国际许可协议 进行许可。 转载时请注明原文链接。图片在使用时请保留图片中的全部内容,可适当缩放并在引用处附上图片所在的文章链接。