转载

如何让静态库中的Category变得可用 [译]

编译器把源文件(.c,.cc,.cpp,.m)转化成对象文件(.o),源文件和对象文件是一一对应的。对象文件(object file)里包含了符号、代码和数据,这种文件并不是直接被操作系统使用的。

当你组建(build)动态库(.dylib)、framework、bundle(bundle)或者一个可执行的二进制文件的时候,这些文件被链接器链接到一起来生成一些操作系统认为“可用的”的东西,例如一些可以直接载入指定内存地址的东西。

当开发者组建(build)静态库(static library)的时候,所有的对象文件被简单的添加到一个大的归档文件里,这就是.a(archive)文件的由来。所以.a文件就是一些对象文件的归档(想象一下没有经过压缩的tar归档或者zip归档)。拷贝单个.a文件要比拷贝一堆.o文件简单的多(java也是一样,为了使用方便你可以把一些.class文件放到一个.jar归档里)。

在把二进制链接到静态库(=archive)的时候,编译器会获得一张含有归档里所有符号的表,然后编译器会检查哪些符号被这些二进制文件引用了。只有包含被引用的符号的对象文件会被链接器真正的载入,并被链接进程处理。例如,如果你的文档中有50个对象文件,但是只有20个包含被二进制使用的符号,那么只有这20个文件会被链接器载入,另外30个会被链接进程完全忽略。

在C和C++代码里,这种机制能很好地工作,因为这些语言会尽可能的在编译期(C++也有一些runtime特性)去做这些事。然而Objective-C是一种与众不同的语言,OC非常依赖runtime特性,而且很多OC的特性都是只支持runtime的。OC类里面实际上也有类似于C函数或全局C变量的符号表(至少现在的OC runtime是这样)。编译器可以识别出一个类有没有被引用,从而确定这个类是不是被使用了。如果你使用了静态库里的对象文件中的类,这个对象文件就会被链接器载入,因为链接器发现它的一个符号被使用了。

Category是runtime下特有的特性,它并不会像类或者函数一样被符号化,也就是说,编译器并不能检测到Category是不是被使用了。

如果链接器载入了一个包含OC代码的对象文件,这些OC代码的所有部分都是编译阶段的一部分。所以当一个包含Category的对象文件因为任何符号被认为“在使用”(可能是一个类,可能是一个函数,也可能是一个全局变量),它的Category也会被载入并在运行期变得可用。基于前面的描述,一个只包含Cagegory的对象文件里没有被编译器认为“在使用”的符号,所以是不会被载入的。

为了使在静态库中的类别能被使用,可以通过下面的5中方法解决:

  • 通过在Other Linker Flags添加-all_load,它会告诉编译器“对于所有文档中的所有对象文件,不管里面的符号有没有被用到,全部都载入”,这种方法确实可以,但是会产生比较大的二进制文件。

  • 另一种方法是添加-force_load和指定的路径,这种方法和-all_load很像,不同的是它只载入指定的归档。

  • 最受欢迎的方法是在Other Linker Flags中添加-ObjC,这个标识告诉编译器“如果你在文档里的对象文件中发现了OC代码,就把它载入“,Category里当然也有OC代码。使用这种方法不会载入任何没有OC代码的文件。

  • 另一种解决方法是新Xcode里build setting中的 Perform Single-Object PreLink,如果启用这个选项,所有的对象文件都会被合并成一个单文件(这不是真正的链接,所以叫做预链接),这个对象文件(有时被称做主对象文件(master object file))被添加到文档中。现在如果主对象文件中的任何符号被认为是“在使用”,整个主对象文件都会被认为在使用,这样它里面的OC部分就会被载入了。因为里面的类都被正常符号化了,所以能使从这样的静态库中使用所有的Category。

  • 最后一种解决方法是在只有Category的源文件里添加Fake symbol。如果你想在运行时使用Category,一定要确保你以某种方法在编译时引用了fake symbol,这会使得对象文件以及它里面的OC代码被载入。例如,它可以是一个有空函数体的函数,也可以是一个被访问的全局变量(例如一个全局的int变量,只要它被读或者写了一次就足够了)。和上面其他的解决方法不一样,这种解决方法可以控制哪些category可以在运行时被编译后的代码使用(可以通过使用这个符号,使它们被链接并变得可用;也可以不使用这个符号,这样链接器就会忽略它)。

如果你有一个包含上百个对象的静态库,但是你的二进制文件只使用了其中的几个,应该避免使用1~4的方法。否则会得到一个非常大的包含了所有类(可能这些类根本没被用到)的二进制文件。对于一个Class,通常不用做特殊的处理;而对于一个Category,考虑一下e中解决方案,它可以让你只包含想要的Category。

例如:如果想使用NSData的类别,添加两个方法compressionData和decompressionData,可以创建一个这样的头文件。

// 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 (),attribute将会让编译器认为它被调用了。

如果添加了-whyload标识,链接器会打印组件日志告诉你“哪个二进制文件里的哪个对象文件因为哪一个符号被使用所以被载入了”。但是它只会打印第一个被认为是“使用中”的符号。

-dead strip对OC不适用,不感兴趣的可以不往下看了

编译器还有一个属性叫做 –dead_strip,如果编译器决定再入一个对象文件,这个文件里的所有符号都会变成链接后的二进制文件中的一部分,不管它们有没有被使用。比如有一个对象文件包含了100个函数,但是它们中只有一个被二进制文件使用了,所有的100个函数仍然会被添加到二进制文件里,因为对象文件只能被完整的添加或者不被添加。链接器不支持部分添加对象文件。

但是,如果告诉链接器“dead strip”, 链接器首先会把所有的对象文件添加到二进制文件,解决所有的引用,然后扫描二进制文件中的符号是不是在被使用(或者是被一些没有被使用的符号使用)。所有的被找到的没有被使用的符号都会在优化阶段被移除。在上面的例子里,99个没有被使用的函数会被移除。如果你使用了-load_all,-force_load或者Perform Single-Object PreLink,这个标识会非常有用,因为这三个选项在一些情况下很容易增加二进制文件的大小,而dead strip将会移除那些没用的代码和数据。

Dead strip对于C代码能很好的工作(例如:像预期的那样去掉没用的函数、变量和常量),它在C++上也能工作的不错(例如:没用的类能够被移除)。虽然它并不完美,在一些情况下一些符号没有被移除,但是在大多数情况下它能在这些语言下很好地工作。

在OC

OC里面,并不支持dead strp,因为OO是一种具有runtime特性的语言,编译器并不能在编译时确定符号是不是真的被使用了。例如,如果没有代码直接使用一个OC的类,那他就是没有被使用吗?当然不是,开发者可以动态创建一个包含类名的字符串,获得这个类的一个指针,并动态的执行alloc。

比如:

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

也可以写成

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

第二种写法并没有直接引用这个类,对象的创建通过runtime完成。

原文链接

感谢作者 Mecki

正文到此结束
Loading...