关于Category
的使用,大家肯定都手到擒来了。
今天就来盘它Category
。
面试的时候可能被问到
类的load方法中,能调用分类的方法吗?category如何被加载的?+load 方法调用顺序?
基于objc4-779.1
源码,debug来看一看Category。
首先第一个问题类的load方法中,能调用分类的方法吗?
,答案是肯定的,可以
- 通过
runtime
的入口函数可以知道先加载完分类,后调用的load
方法
我们通过捋清楚Category的加载来一步步解答。
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();
runtime_init();
exception_init();
cache_init();
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
忽略掉一堆 init
,重点来看 _dyld_objc_notify_register(&map_images, load_images, unmap_image);
这个方法会注册3个事件并给出回调。
重点来看一下map_images
和load_images
;
从这俩个回调方法里看,你会发现Category
在map_images
会加载完毕,而load_images
会调用+load
方法。这就解释了第一个问题:类的load方法中,能调用分类的方法。
主要看点:map_images
从map_images
里来看看Category
的加载。map_images
最终会调用_read_images
,这里就是核心部分了,从中间我们只看处理分类及类的部分
// Discover categories.
for (EACH_HEADER) {
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
auto processCatlist = [&](category_t * const *catlist) {
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
//printf("%s\n",cat->name);
Class cls = remapClass(cat->cls);
locstamped_category_t lc{cat, hi};
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Ignore the category.
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
if (strcmp(cat->name, "HJJJJJ")==0) {
}
// Process this category.
if (cls->isStubClass()) {
// Stub classes are never realized. Stub classes
// don't know their metaclass until they're
// initialized, so we have to add categories with
// class methods or properties to the stub itself.
// methodizeClass() will find them and add them to
// the metaclass as appropriate.
if (cat->instanceMethods ||
cat->protocols ||
cat->instanceProperties ||
cat->classMethods ||
cat->protocols ||
(hasClassProperties && cat->_classProperties))
{
objc::unattachedCategories.addForClass(lc, cls);
}
} else {
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
if (cls->isRealized()) {
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
} else {
objc::unattachedCategories.addForClass(lc, cls);
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
if (cls->ISA()->isRealized()) {
attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
} else {
objc::unattachedCategories.addForClass(lc, cls->ISA());
}
}
}
}
};
processCatlist(_getObjc2CategoryList(hi, &count));
processCatlist(_getObjc2CategoryList2(hi, &count));
}
for (EACH_HEADER) {
classref_t const *classlist =
_getObjc2NonlazyClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = remapClass(classlist[i]);
if (!cls) continue;
//printf("11-%s\n",object_getClassName(cls));
addClassTableEntry(cls);
if (cls->isSwiftStable()) {
if (cls->swiftMetadataInitializer()) {
_objc_fatal("Swift class %s with a metadata initializer "
"is not allowed to be non-lazy",
cls->nameForLogging());
}
// fixme also disallow relocatable classes
// We can't disallow all Swift classes because of
// classes like Swift.__EmptyArrayStorage
}
const char *tempChar;
tempChar = object_getClassName(cls);
if (strcmp(tempChar, "Person")==0) {
}
realizeClassWithoutSwift(cls, nil);
}
}
通过断点会发现 分类部分的代码 objc::unattachedCategories.addForClass(lc, cls);
,只是将class和其对应的category做了一个映射,插入到map中。真正要看的还是realizeClassWithoutSwift
这个方法。
这个地方又会牵扯到几种情况:分类的懒加载和非懒加载,类的懒加载和非懒加载。
区分懒加载和非懒加载,按苹果的意思就是实现了+load方法的类/分类是非懒加载,否则是懒加载
这就会产生4种情况,我们一种一种的来看。
老规矩,先创建个Person
类,再创建个Person+HJJJJJ
分类
分类添加2方法
-(void)sayYes;
-(void)sayNo;
1. 非懒加载类,非懒加载分类。
通过realizeClassWithoutSwift
一直往后走 ,最终走到attachCategories
,附加分类的东西。
在方法内部增加 断点,通过分类名字来断。
const char *tempChar;
tempChar = cls->nameForLogging();
if (strcmp(tempChar, "Person")==0) {
}
/// 依次读取每一个category,将其methods,property,protocol添加到mlists,proplist,protolist中存储
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
//printf("22-%s\n",entry.cat->name);
if (mlist) {
if (strcmp(entry.cat->name, "HJJJJJ")==0) {
}
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
mcount = 0;
}
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
打印一下从分类里取出来的方法method_list_t *mlist = entry.cat->methodsForMeta(isMeta)
,元类取的是类方法,类取的是实例方法。
我添加了2个实例方法,那么来看看isMeta为false,会发现从分类里取出的sayYes和sayNo
方法, 而后会通过attachLists
添加到rw的methods里。在之后打印rw的methods 看看
所以流程是
map_images
->_read_images
->realizeClassWithoutSwift
->methodizeClass
->objc::unattachedCategories.attachToClass
->attachCategories
->rw->methods.attachLists
2. 非懒加载类,懒加载分类。
发现并没有走attachCategories
,往前打断点,会发现methodizeClass
方法里直接attachLists
到rw里了
// Install methods and properties that the class implements itself.
method_list_t *list = ro->baseMethods();
if (list) {
const char *tempChar;
tempChar = cls->nameForLogging();
if (strcmp(tempChar, "Person")==0) {
}
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
rw->methods.attachLists(&list, 1);
}
打印结果:
流程是
map_images
->_read_images
->realizeClassWithoutSwift
->methodizeClass
->rw->methods.attachLists
3. 懒加载类,非懒加载分类。
既然是懒加载的类,那么只有在用到的时候才会加载了,map_images
则不会断住了,通过attachCategories
里的断点,看看堆栈信息。
可以看到是在
load_images
里加载的,在调用load
方法之前会做一些处理,prepare_load_methods
里会对非懒加载类和非懒加载分类进行处理。流程是
load_images
->prepare_load_methods
->realizeClassWithoutSwift
->methodizeClass
->objc::unattachedCategories.attachToClass
->attachCategories
->rw->methods.attachLists
3. 懒加载类,懒加载分类
这种情况需要第一个发送消息时才会加载,在main
方法[Person new]
一下,看看断点的堆栈信息。
流程是
第一次发送消息,调用objc_msgSend
->lookUpImpOrForward
->realizeClassWithoutSwift
->methodizeClass
->rw->methods.attachLists
再来看看attachLists
这个方法是如何将list合二为一的。
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
这里主要看memmove 和 memcpy
这俩函数,memmove
将原来的list
往后移了addedCount
,而memcpy
则是将新的addedLists
复制到了原来list
的前面。这就说明了分类覆盖主类的同名方法并不是真正的覆盖,而是分类的方法被排到了前面,当方法查找时,则返回了分类的方法实现
调用被分类插队的同名方法
在分类和主类都添加sayYes
方法,再在分类sayNo
方法里输入以下代码
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList([self class], &methodCount);
IMP lastIMP = nil;
for (NSInteger i = 0; i < methodCount; ++i) {
Method method = methodList[i];
NSString *selName = NSStringFromSelector(method_getName(method));
if ([@"sayYes" isEqualToString:selName]) {
lastIMP = method_getImplementation(method);
((void(*)(void))lastIMP)();
}
}
会发现主类和分类的sayYes
都走了
顺便捋一捋+load
方法的顺序
在 call_load_methods
之前,会调用prepare_load_methods
去发现+load
方法,而这里面会对非懒加载类和非懒加载分类做处理。
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);
//printf("%s--%s\n",cls->nameForLogging(),cat->name);
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);
}
进去schedule_class_load
方法,里面有一段代码
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
在将类的+load
方法添加进类的加载列表的时候,会schedule_class_load
先将父类的+load
加进去,这样保证父类在子类之前调用+load
,然后添加分类的+load
方法到分类的加载列表,而分类的+load
加载顺序则看文件编译的顺序了。
真正调用+load
方法的是call_load_methods
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
从代码里可以看出来,首先循环调用类的load
方法,再调用分类的load
方法。
验证一下
这里可能想到 分类里的+load
为啥没有'覆盖' 类的+load
方法打印呢,这里并没有做什么特别的处理,分类的+load
确实'覆盖' 了类的+load
方法前面了。
但是call_load_methods
的时候,并没有走消息转发的机制(objc_msgSend
),而是直接通过函数指针直接调用的((*load_method)(cls, @selector(load));
),所以只要实现了就会调用。
总结以下load
的调用顺序:父类先于子类调用,主类先于分类调用,分类则看文件编译顺序
分析就到这里,有误请大佬指正。