OC-Category

341 阅读21分钟

什么是Category

Category又叫分类,类别,类目,作为Objective-C 2.0之后添加的语言特性,Category在如今的OC工程中使用很广,它最主要的作用就是为已有的类增加方法,属性,协议等等,但是不能增加成员变量,它可以在即使不知道源码的情况下为类添加方法,根据官方文档Category其主要作用有:

  1. 分离类的实现到独立的文件,这样做的好处有:
    • 减小单个文件的代码量。
    • 把不同功能组织到不同的Category。
    • 方便多人维护一个类
    • 按需加载想要的Category
  2. 定义私有方法
  3. 模拟多继承
  4. 把framework的私有方法公开
  • 需要注意的如果是增加属性的话,只会增加属性的声明,并不会生成成员变量和属性的setter和getter方法。

Category的底层实现

查看编译之后的Category结构

一个类的对象方法和协议是存放在类对象的方法列表和协议列表中的,而类方法则是存放在元类对象的方法列表中,而且这些方法和协议等等都是在编译时生成的,编译完成之后会存放到内存当中,等待开发者使用。而通过Category增加的方法和协议并不是在编译后就存在类对象或元类对象里的,而是在运行时动态的合并到类对象中,我们通过一个简单分类看一下编译之后的结构。

  • 首先,创建一个Person对象,然后为Person对象增加一个Category,扩展出方法和属性,具体代码如下:
@interface Person : NSObject

@end

@interface Person (Test)
@property (nonatomic,assign) int age;
- (void)run;
@end

@implementation Person (Test)
- (void)setAge:(int)age
{
    
}
- (int)age
{
    return 0;
}
- (void)run
{
    NSLog(@"%s",__func__);
}

然后通过以下命令,将Person+Test.m文件文件重写成Person+Test.cpp文件,这个Person+Test.cpp文件其实就相当于编译之后的产物

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test.m

_category_t

  • 然后查看Person+Test.cpp文件,可以得到Category在编译之后的结构如下,结构体中存放着方法列表、属性列表和协议列表等等
struct _category_t {
	const char *name; //类名  
	struct _class_t *cls; //存放isa和superclass的结构体
	const struct _method_list_t *instance_methods; //实例方法列表
	const struct _method_list_t *class_methods; //类方法列表
	const struct _protocol_list_t *protocols; //协议列表
	const struct _prop_list_t *properties; //属性列表
};
  • 在Person+Test.cpp中,我们还可以找到一个名为_category_t _OBJC_$_CATEGORY_Person_$_Test的静态结构体变量,它的结构如下:
static struct _category_t _OBJC_$_CATEGORY_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"Person",
	0, // &OBJC_CLASS_$_Person,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
	0,
	0,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Test,
};
// 这里传入的参数,对应上面结构体的
  • 由此可见在编译完成之后,每一个Category中的内容包括方法列表、属性列表等等,都会存放到一个_category_t类型的结构体变量中,而不是在编译时就直接合并到Person类中去。

_method_list_t

  • 然后我们再以_method_list_t为例,来窥探一下分类中实例方法的内部结构,如下
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count; 
	struct _objc_method method_list[3]; 
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	3,
	{{(struct objc_selector *)"setAge:", "v20@0:8i16", (void *)_I_Person_Test_setAge_},
	{(struct objc_selector *)"age", "i16@0:8", (void *)_I_Person_Test_age},
	{(struct objc_selector *)"run", "v16@0:8", (void *)_I_Person_Test_run}}
};
  • 我们在分类中添加的实例方法,存放在了_objc_method类型的结构体中,在看一下_objc_method的内部结构如下
struct _objc_method {
	struct objc_selector * _cmd; //方法selector
	const char *method_type;    //返回值类型
	void  *_imp;                //方法实现的地址
};
  • 由此我们可以得出结论,每个分类在编译完成之后都会生成一个_category_t类型的结构体变量,类似上文中的_category_t _OBJC_$_CATEGORY_Person_$_Test,内部存放着我们在分类中定义的方法列表、属性列表和协议列表。

Category的加载流程

  • Category在编译时生成category_t类型的静态变量,然后在运行时合并到类对象中。下面我们就通过runtime的执行流程来查看Category的合并过程。

具体加载流程

  • 首先,在objc-os.mm中找到runtime的入口_objc_init函数,它内部会通过dyld去注册images(模块)
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);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

_dyld_objc_notify_register函数中是直接通过map_images的内存地址调用map_images函数。

  • 查看map_images函数,内部调用map_images_nolock函数
void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}
  • map_images_nolock函数是用来执行所有类注册和修复操作,它内部会调用_read_iamges函数,核心代码如下
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    ......
	// Discover categories. Only do this after the initial category
    // attachment has been done. For categories present at startup,
    // discovery is deferred until the first load_images call after
    // the call to _dyld_objc_notify_register completes. rdar://problem/53119145
    if (didInitialAttachCategories) { // didInitialAttachCategories的值为false
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }
    ......
}
  • 通过注释discovery is deferred until the first load_images call after发现合并分类的操作放在了load_images方法中,和之前的执行流程不一样。
  • 查看load_images函数,内部调用loadAllCategories函数
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();
    }

    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}
  • loadAllCategories函数的实现,里面调用了load_categories_nolock函数
static void loadAllCategories() {
    mutex_locker_t lock(runtimeLock);

    for (auto *hi = FirstHeader; hi != NULL; hi = hi->getNext()) {
        load_categories_nolock(hi);
    }
}
  • load_categories_nolock函数
static void load_categories_nolock(header_info *hi) {
    bool hasClassProperties = hi->info()->hasCategoryClassProperties();

    size_t count;
    auto processCatlist = [&](category_t * const *catlist) {
        for (unsigned i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            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;
            }

            // 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));
}
  • 我们重点关注 attachCategories方法,看一下这个方法的实现:
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }

    /*
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    auto rwe = cls->data()->extAllocIfNeeded();

    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) flushCaches(cls);
    }

    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
  • 此处需要注意的是,每个类的Category附加的顺序和Category装载进内存的顺序有关,最先装载进内存的Category最后进行attach操作。
  • attachCategory做的工作相对比较简单,把所有Category的方法、属性、协议数据,合并到一个大数组中后面参与编译的Category数据,会在数组的前面,然后转交给了attachLists方法:
void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;
		//这里以方法列表为例
    	//array()->lists表示原来类中的方法列表
   	 	//addedLists表示所有Category中的方法列表
        if (hasArray()) {
            // many lists -> many lists
            //获取原来类中方法列表的长度
            uint32_t oldCount = array()->count;
            //得到方法合并之后的新的数组长度
            uint32_t newCount = oldCount + addedCount;
            //给array重新分配长度为newCount的内存空间
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            //将原来array()->lists中的数据移动到数组中oldCount的位置
      	    //也就是相当于将array()->lists的数据在内存中往后移动了addedCount个位置
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            //将Category中的方法列表copy到array()->lists中
	        //并且是从数组的起始地址开始存放
            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]));
        }
    }
  • 下面以合并方法列表为例,attachLists函数有两个参数addedLists和addedCount,addedLists表示所有Category中的方法列表。addedCount表示新增加的方法个数。addedLists中存放的内容如下:
addedLists:[[method_t, method_t], 
             [method_t, method_t], 
             [method_t, method_t], 
             ......]
  • 首先会通过原来类方法列表的长度和新添加方法列表的长度,得到合并之后的数组大小,然后重新为数组分配新的内存空间。然后调用memmove函数将原来array()->lists中的元素往后移动addedCount个位置,最后调用memcpy函数将addedLists中的元素复制到array()->lists中,从第0个位置开始存放。由此可以看出,所有分类中的方法,在合并到类原来的方法列表中时,是插入到原来的数据之前。
    大致流程图如下:

总结

通过阅读runtime的源码,我们可以得出如下结论:

  • 程序在编译完成之后,会将所有的Category转换成category_t类型的结构体
  • 然后通过runtime加载某个类的所有Category数据,包括方法列表、属性列表和协议列表
  • 把Category的方法列表、属性列表和协议列表存放到一个大的数组中去,这里需要注意的是,由于是逆序遍历,最先装载进内存的Category数据会存放到数组的最后面。
  • 将合并后的分类数据(方法列表、属性列表和协议列表)通过memmove函数和memcpy函数插入到类原来的数据之前。因此,如果类和它的分类用于相同名称的方法,那么只会调用分类中的方法,不会调用父类中的方法。

OC的方法调用核心其实就是消息发送机制,方法底层会转换成objc_msgSend函数进行消息发送,如果当前类方法列表没有找到方法,会通过isa指针到元类对象的方法列表中查找,如果还没有找到会通过superClass到父类的方法列表中查找。一旦找到会立即执行方法。因此,一旦类和分类中有相同方法名的方法,分类的方法会放在类方法列表的最前面,当查找方法时,会直接拿到分类的方法执行。

Category补充

memmove和memcpy

memmove和memcpy两个函数的作用都是进行内存拷贝,唯一的区别就是当要拷贝的内存区域和目标内存区域有重叠部分的时候,memmove能够保证拷贝之后的结果是正确的,但是memcpy就不能保证拷贝之后的结果是正确的。

memmove

memmove的函数声明如下

void	*memmove(void *__dst, const void *__src, size_t __len);

memmove() 函数从src内存中拷贝n个字节到dest内存区域,但是源和目的的内存可以重叠,具体拷贝流程如下: 可以看到,src和dest的区域存在重叠部分,并且dest的区域在src的后面,所有在执行memmove操作时,会从src的最后一个内存中的元素开始,依次往后挪动,所以挪动完成之后就完成了拷贝操作,而且结果是正确的。

memcpy

memcpy的函数声明如下

void	*memcpy(void *__dst, const void *__src, size_t __n);

memcpy()函数从src内存中拷贝n个字节到dest内存区域,但是源和目的的内存区域不能重叠,具体拷贝流程如下

可以看到src和dest的区域存在重叠部分,在执行memcpy操作时,会从src起始内存地址开始,依次向后copy,在图中即表示为将src中第一个元素拷贝到dest第一个地址中,将src第二个元素拷贝到dest的第二个地址中,所以就造成了覆盖操作。

+load()和+initialize()方法

+load()方法

在OC中,+load()方法会在runtime加载类和分类到内存中的时候调用,而且每个类或者分类的+load()方法只会调用一次。而且如果同时存在子类和分类的情况下,会先调用父类的+load()方法,再调用子类的+load()方法,最后调用分类的+load()方法。下面我们通过源码来验证这一结论。

  • 首先,我们还是同上文一样,找到_objc_init函数,此处是runtime的入口函数,省略了部分代码,只保留我们需要的代码。_objc_init函数中的load_images函数就是用来执行所有类的+load()方法。
void _objc_init(void)
{
    ...
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    ...
}
  • 继续查看load_images的源码如下,通过注释我们能发现prepare_load_methods函数用来发现所有的+load()方法,而call_load_methods函数则用来调用所有的+load()方法。
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();
    }

    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

Discover load methods

  • 查看prepare_load_methods函数,发现它内部是通过调用_getObjc2NonlazyClassList函数来获取到所有不是懒加载的类,由于_getObjc2NonlazyClassList不开源,所以我们猜测通过这个函数获取到的classlist它的顺序和类编译的顺序相同。
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);
    }
}
  • 当获取到所有类的集合之后,通过遍历来调用schedule_class_load函数来递归调度类的+load方法和所有它的父类的+load方法,而且通过schedule_class_load(cls->superclass);这句可以看出,在调度+load方法是,是先父类再子类的顺序,如下
static void schedule_class_load(Class cls)
{
    if (!cls) return;
    ASSERT(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}
  • 查看add_class_to_loadable_list函数源码,可以发现通过递归来调度的类以及它的+load方法最后被添加到了一个全局的loadable_classes列表中去了,这个列表中存放的是所有需要被调用+load方法的类,所有的类都被包装成了struct loadable_class类型的结构体。
struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method

    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }

    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }

    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

这里有个注意点,因为是通过递归来添加,所以父类总是会优先调用add_class_to_loadable_list函数,因此在loadable_classes中,父类总是存放在最前面。而且通过查看结构体loadable_class可以发现,它内部是直接存放了IMP类型的成员变量,也就是说直接保存的+load方法的内存地址,后续可以直接通过内存地址来调用+load方法

  • 加载完所有类的+load方法之后,接下来会加载所有分类的+load方法,分类同类一样,也是通过内部函数_getObjc2NonlazyCategoryList获取到项目中所有的分类列表,然后通过遍历调用add_category_to_loadable_list函数,将分类中的+load方法加到全局的list中去。如下
void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    if (!method) return;

    if (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                     _category_getClassName(cat), _category_getName(cat));
    }
    
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}
  • 此处将所有的分类以及它的+load方法封装成了一个struct loadable_category类型的结构体,然后存放到全局的struct loadable_categories列表中去,实现方法和上文中类的方式相同,唯一不同的是Category的加载不是通过递归来进行的。

  • 此时在全局列表loadable_classes和loadable_categories中就存放了所有需要调用+load方法的类和分类的信息,而且类和它的+load方法直接存放在了struct loadable_class中,而分类和它的+load方法则直接存放在了struct loadable_category中。

Call +load methods

  • 经过prepare_load_methods函数之后,所有类和分类的+load方法分别存放到了loadable_classes和loadable_categories中,之后通过调用call_load_methods函数对所有的+load方法进行调用

  • 查看call_load_methods的源码,发现是通过call_class_loads来调用所有类的+load方法,通过call_category_loads函数来调用所有分类的+load方法

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;
}

此处有个objc_autoreleasePoolPush()函数和objc_autoreleasePoolPop()函数,这两个函数的作用其实就是创建一个自动释放池autoreleasepool,有兴趣的可以查看自动释放池的底层实现。

  • 首先查看call_class_loads函数的源码,发现其实就是拿到上文所说的全局列表loadable_classes,然后通过遍历拿到里面的loadable_class,依次取出cls和+load方法的内存地址,直接调用+load方法
static void call_class_loads(void)
{
    int i;

    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }

    // Destroy the detached list.
    if (classes) free(classes);
}
  • call_category_loads函数的实现方式和call_class_loads函数基本相同。

总结

  • +load方法是在runtime加载类、分类是进行调用的
  • 每个类、分类的+load方法,在程序运行过程中只会调用一次
  • 如果存在多个子类,则会先调用父类的+load方法,再调用子类的+load方法,子类+load方法的调用顺序和子类的编译顺序相同,先编译的子类优先调用
  • 如果同时存在多个分类和多个子类,那么首先会调用父类的+load方法,再调用子类的+load方法,最后才会调用分类的+load方法,多个分类+load方法的调用顺序和编译顺序相同,先编译的分类先调用。
  • 不管是类还是分类,+load方法都是通过直接拿到方法的内存地址进行调用,而不是通过消息发送机制来调用

+initialize()方法

+initialize执行时机

  • +initialize方法会在类第一次接收到消息的时候调用,也就是说当我们调用+initialize方法时,最后都会转换成objc_msgSend(obj, @selector(initialize)),所以想要查看+initialize方法的调用流程,就可以查看runtime进行方法查找的源码。具体查看objc-runtime-new.mm下的class_getInstanceMethod函数,这个函数就是用来查找某一个类下的实例方法
Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    // This deliberately avoids +initialize because it historically did so.

    // This implementation is a bit weird because it's the only place that 
    // wants a Method instead of an IMP.

#warning fixme build and search caches
        
    // Search method lists, try method resolver, etc.
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}
  • 继续查看lookUpImpOrForward函数的源码,找到其中的核心代码,如果函数参数initialize为YES并且当前类没有进行过初始化,则会调用initializeAndLeaveLocked函数。
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
 	......
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) 	{
    cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    // runtimeLock may have been dropped but is now locked again

    // If sel == initialize, class_initialize will send +initialize and 
    // then the messenger will send +initialize again after this 
    // procedure finishes. Of course, if this is not being called 
    // from the messenger then it won't happen. 2778172
    }
    ......
}
  • 查看initializeAndLeaveLocked函数发现它的内部会调用initializeAndMaybeRelock函数来获取cls的类对象,并且通过isRealized判断当前类是否初始化。
static Class initializeAndMaybeRelock(Class cls, id inst,
                                      mutex_t& lock, bool leaveLocked)
{
    lock.assertLocked();
    assert(cls->isRealized());

    if (cls->isInitialized()) {
        if (!leaveLocked) lock.unlock();
        return cls;
    }
    Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);

    //isRealized()方法用来判断当前类中cls->data是class_rw_t还是class_ro_t
    //也就是判断类是否被初始化
    if (nonmeta->isRealized()) {
        lock.unlock();
    } else {
        nonmeta = realizeClassMaybeSwiftAndUnlock(nonmeta, lock);
        cls = object_getClass(nonmeta);
    }

    // runtimeLock is now unlocked, for +initialize dispatch
    assert(nonmeta->isRealized());
    //调用+initialize方法
    initializeNonMetaClass(nonmeta);

    if (leaveLocked) runtimeLock.lock();
    return cls;
}
  • 最后,initializeNonMetaClass函数就是最核心的函数,它的作用就是根据需要向任意的未初始化的类发送一个“+initialize”消息,并且会首先执行超类的初始化,具体实现查看以下源码,源码中省略了部分关于初始化状态设置的一些代码,保留了核心的函数调用,如果想看完整代码,可以自行查看最新的objc4的源码
void initializeNonMetaClass(Class cls)
{
    assert(!cls->isMetaClass());

    Class supercls;
    bool reallyInitialize = NO;
    //此处通过cls->superclass来找到cls的父类,然后通过递归来查看父类是否被初始化,从而确保在初始化cls之前,它的父类已经初始化完毕
    // Make sure super is done initializing BEFORE beginning to initialize cls.
    // See note about deadlock above.
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);
    }
    // Try to atomically set CLS_INITIALIZING.
    SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;
    {
        monitor_locker_t lock(classInitLock);
        //如果当前类并未初始化,则设置类的状态为“正在初始化”
        if (!cls->isInitialized() && !cls->isInitializing()) {
            cls->setInitializing();
            reallyInitialize = YES;
            // Grab a copy of the will-initialize funcs with the lock held.
            localWillInitializeFuncs.initFrom(willInitializeFuncs);
        }
    }
    ......
    //如果当前的类未进行初始化,则调用callInitialize进行初始化
    if (reallyInitialize) {
        callInitialize(cls);
        return;
    }
    ......
}
  • 可以看到,首先会通过递归找到cls的父类,判断父类是否进行过初始化,如果父类未进行初始化,则通过callInitialize函数调用父类的+initialize,然后再调用子类的+initialize。并且callInitialize内部其实也是通过objc_msgSend来发送+initialize消息。

总结

  • +initialize方法是在类第一次接收到消息时调用
  • 如果存在多个子类,并且实现了+initialize方法,会先初始化父类,调用父类的+initialize方法,然后再初始化子类,调用子类的+initialize方法
  • 如果子类没有实现+initialize方法,则会调用父类的+initialize方法,所以父类的+initialize可能会被调用多次。
  • 如果分类实现了+initialize方法,则会覆盖类本身的+initialize调用,因为+initialize方法的调用本质上是通过objc_msgSend来发送一个+initialize消息,所以一旦分类实现了+initialize方法,则会将分类的+initialize方法插入到类方法列表的最前面,会覆盖原来类的+initialize方法。

给分类添加成员变量?(关联对象)

  • 默认情况下,由于分类的底层结构的限制,不能在分类中添加成员变量,但是我们可以通过runtime提供的Api为类增加关联对象。

关联对象的实现

关联对象主要通过以下三个函数进行实现

  • 添加关联对象
void objc_setAssociatedObject(id object, const void * key,
                                id value, objc_AssociationPolicy policy)
  • 获取关联对象
id objc_getAssociatedObject(id object, const void * key)
  • 移除所有的关联对象
void objc_removeAssociatedObjects(id object)

举个🌰来看关联对象的实现

@interface Person (Test)

@property(nonatomic, copy) NSString *name;

@end

@implementation Person (Test)

- (NSString *)name{
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY);
}
@end

这里key需要传入一个内存地址,可以自己定义,只要保证在setter和getter中用的是相同的key就行。最常用的方式就是使用当前getter方法的内存地址,也就是上文中的@selector(name),_cmd是编译器特性,_cmd其实和@selector(name)等同,都是表示指向name()方法的指针。

  • 使用关联对象时会用到objc_AssociationPolicy,它其实和我们OC中的属性修饰符一一对应,使用什么修饰符,取决于你定义的属性的类型,对应关系如下
objc_AssociationPolicy修饰符
OBJC_ASSOCIATION_ASSIGNassign
OBJC_ASSOCIATION_RETAIN_NONATOMICstrong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMICcopy, nonatomic
OBJC_ASSOCIATION_RETAINstrong, atomic
OBJC_ASSOCIATION_COPYcopy, atomic

关联对象的实现原理

objc_setAssociatedObject

  • 了解了关联对象的使用,接着我们通过runtime源码来更深层次的了解关联对象的实现原理。

  • 首先在objc-runtime.mm文件中找到objc_setAssociatedObject函数调用

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    SetAssocHook.get()(object, key, value, policy);
}
---------------------------------------------------------------------------
static void
_base_objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
  _object_set_associative_reference(object, key, value, policy);
}
---------------------------------------------------------------------------
static ChainedHookFunction<objc_hook_setAssociatedObject> SetAssocHook{_base_objc_setAssociatedObject};
  • 再次查看_object_set_associative_reference函数,就能看到完整的设置关联对象的流程
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;

    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));

    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    association.acquireValue();

    {
    	//关联对象的管理类
        AssociationsManager manager;
        //关联对象的哈希表,里面存放着的key为object的地址,value为ObjectAssociationMap类型的映射表
        AssociationsHashMap &associations(manager.get());

        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                object->setHasAssociatedObjects();
            }

            /* establish or replace the association */
            auto &refs = refs_result.first->second;
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}
  • 上述源码中,有几个非常重要的类,AssociationsManager、AssociationsHashMap、ObjectAssociationMap、ObjcAssociation

  • 首先AssociationsManager是一个关联对象的管理类,它内部有一个AssociationsHashMap类型的静态变量

class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    static Storage _mapStorage;

public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }

    AssociationsHashMap &get() {
        return _mapStorage.get();
    }

    static void init() {
        _mapStorage.init();
    }
};
  • AssociationsHashMap是一个哈希表,通过当前的object调用DISGUISE(object)来得到索引值,然后通过索引值来获取或者存放ObjectAssociationMap类型的映射表。
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
  • ObjectAssociationMap是一个映射表,是以参数中传过来的key值作为映射表的key,以ObjcAssociation类型的对象作为映射表的value
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
  • ObjcAssociation对象中其实只有两个属性,_policy和_value,分别对应参数中的value和policy。
class ObjcAssociation {
    uintptr_t _policy;
    id _value;
}

objc_getAssociatedObject

  • 相对于objc_setAssociatedObject函数来说,objc_getAssociatedObject的实现要简单的多
id
objc_getAssociatedObject(id object, const void *key)
{
    return _object_get_associative_reference(object, key);
}
  • 查看核心函数_object_get_associative_reference的实现如下
id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();
}

objc_removeAssociatedObjects

  • 之前说到调用objc_setAssociatedObject函数函数时如果value传nil,就会从映射表中移除关联对象,但是这一次只能移除一个关联对象,而objc_removeAssociatedObjects函数则可以移除一个object的所有关联对象。
void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}
  • _object_remove_assocations中就是完整的移除关联对象的操作
void
_object_remove_assocations(id object)
{
    ObjectAssociationMap refs{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            refs.swap(i->second);
            associations.erase(i);
        }
    }

    // release everything (outside of the lock).
    for (auto &i: refs) {
        i.second.releaseHeldValue();
    }
}

关联对象总结

  • 使用runtime的Api可以在分类中为类设置关联对象。
  • 关联对象并不是存放在被关联对象本身的内存中,而是存储在一个全局统一的AssociationsManager中
  • 每一个类都有一个对应的映射表ObjectAssociationMap,存放在全局的哈希表AssociationsHashMap中,通过类对象的内存地址计算出哈希表的索引。
  • 类的每一个关联对象的值都封装成了一个ObjectAssociation对象,存放在映射表中。

关联对象的关系图如下