前言:
Category
在Objc中非常重要,在平时的iOS的面试中针对Category
的问题更是层出不穷,如:1)Category
中的方法加载顺序?2)Category
中的方法“覆盖”的原理?3)Category
能添加属性吗?等等。更深入的可能会问Category
中的方法是怎么加到方法列表中的?今天我们再来看看Category
,还有没有新的发现。
一、从runtime看Category的加载
_objc_init
作为OC的入口,主要的工作是注册dyld
的回调,当dyld
中的image
(镜像)卸载,加载状态(符号绑定完,静态初始化完)发生变化时给与OC回调。
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
// Register for unmap first, in case some +load unmaps something
_dyld_register_func_for_remove_image(&unmap_image);
dyld_register_image_state_change_handler(dyld_image_state_bound,
1/*batch*/, &map_2_images);
dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}
Category
的加载发生在map_2_images
回调函数中,即动态链接器(dyld
)完成符号绑定时的dyld_image_state_bound
的回调。这里我们省略掉中间的调用,最后在void _read_images(header_info **hList, uint32_t hCount)
中读取:
void _read_images(header_info **hList, uint32_t hCount)
{
//...
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
/* || cat->classProperties */)
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
//...
}
对于Category
,_read_images
做了如下事情:
1.通过_getObjc2CategoryList
获取Category
list;
2.将Category中的实例方法,协议,属性通过addUnattachedCategoryForClass
映射到对应的类上,然后通过remethodizeClass
添加到对应的类;
3.同上,将Category
中的类方法,协议添加到类的元类上;
后续添加方法的实现在void attachLists(List* const * addedLists, uint32_t addedCount)
中,大家应该比较熟悉了。
我们发现,Category
是从_getObjc2CategoryList
拿到的,我们看下它的实现:
#define GETSECT(name, type, sectname) \
type *name(const headerType *mhdr, size_t *outCount) { \
return getDataSection<type>(mhdr, sectname, nil, outCount); \
} \
type *name(const header_info *hi, size_t *outCount) { \
return getDataSection<type>(hi->mhdr, sectname, nil, outCount); \
}
// function name content type section name
//...
GETSECT(_getObjc2CategoryList, category_t *, "__objc_catlist");
//...
很显然,_getObjc2CategoryList
的作用就是从Mach-O
文件的__objc_catlist
段获取Category
的数据。
好了,通过Runtime
的源码我们不难理解Category
的"加载"过程:当动态链接器(dyld
)完成符号的绑定后,通过dyld_register_image_state_change_handler
的回调到Objc
,Objc
通过_getObjc2CategoryList
从Mach-O
文件的__objc_catlist
段获取Category
的数据,然后先通过addUnattachedCategoryForClass
把Category
映射到对应的类上,最后通过remethodizeClass
添加到对应的类。
二、就这?
从本文的题目不能猜出,Category
应该还不止这些,实际上关于Category
的原理我在N年前就读过源码。甚至产生过疑虑:Category
中的方法在启动时添加明明会消耗性能,为什么不能在编译/链接期间就完成呢,苹果没这么傻吧...直到在研究Mach-O
文件格式时有了点新的发现,下面我们通过Demo来看下,为了减少其他因素的干扰,我们创建一个最简单的MacOS的控制台程序,工程中创建一个Person
类和它的分类。
//Person.h
@interface Person : NSObject
- (void)go;
@end
//Person+test.h
@interface Person (test)
- (void)goTest;
@end
运行下,使用MachOView
看下Mach-O
中的数据:
_objc_catlist
中居然是空的,我们新增的Category
方法goTest
并没有保存在这里。Category
的数据保存在哪里了呢?我们再看下保存类方法的__objc_const
段:
从上图我们发现:Category
中的方法已经和主类中的方法合并到方法列表中了并且内存地址连续,同时Category
中的方法还"规规矩矩"的放在了主类方法的前面。如果是个同名方法呢?
嗯...没错,和Category
的特性一致...方法"覆盖"这时已经发生。
那么,这个优化发生在编译期还是静态链接期间呢?很简单,我们只需要把工程的类型Mach-O Type
改成Static Libaray
,因为静态库是经过编译,但还没链接的中间产物,同样通过MachOView
看下数据:
上图可知:Category
和它的主类分别生成了一个.o目标文件,Category
的信息还没有合并。从这里我们可以看出优化过程发生在静态链接期间,了解静态链接的同学可能会更容易理解,因为那时候才是真正给符号分配虚拟内存地址的时候。
好了,既然Category
的方法保存到了__objc_const
段,那_objc_catlist
是摆设?或者说什么情况下会保存到_objc_catlist
呢?我们可以想一下:既然静态链接的时候会进行优化,那么有没有什么情况给类添加Category
的时候已经过了静态链接的时期呢?这种情况会不会保存到_objc_catlist
呢?
答案是:动态库。
动态库是已经经过链接(静态链接)后的产物,如果给动态库中的某个类添加Category
应该不能优化了吧...这个猜想非常好验证,我们只需要给系统库中的类添加Category
即可。
@interface NSString (Trim)
- (NSString *)trim;
@end
我们给NSString
类添加trim
方法,然后看下Mach-O
文件:
果然,_objc_catlist
已经有数据了,但是_objc_catlist
中没有直接保存Category
,真正的数据保存在_objc_const
的Objc2 Caterory
段中。
三、还有?
本以为这就结束了,多谢 @皮拉夫大王在此的提示:“分类未实现load和实现了load方法后保存是不一样的”。我们知道在load方法的调用时会先调用当前镜像中的所有类的load方法,然后再调用分类的load方法,调用是分开的。再者,load方法作为特殊的存在,如果像上文所述进行优化的话,比较困难:如果优化了怎么方便的调用呢?这种情况苹果索性新增了一块区域保存,我们看下runtime中获取分类中load方法的代码:
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
_getObjc2NonlazyCategoryList
的实现:
GETSECT(_getObjc2NonlazyCategoryList, category_t * const, "__objc_nlcatlist");
实现了load方法的Caterory
在__objc_nlcatlist
和__objc_catlist
中都保存一份:
总结
从上面的分析我们可以看出苹果对Caterory
的优化已经非常细致:对于非动态库中的类的Caterory
中的方法会在静态链接期间优化,知道这个特性后我们可以在自己的模块内使用Caterory
干更多的事情(比如功能解耦),不用担心会影响启动速度。最后在这里再抛出一个问题:苹果既然对Caterory
中的方法进行了优化,那其他的特性(如"属性")呢?