Category

208 阅读15分钟

Category 向已有的类动态添加方法(实例方法和类方法)

1. 基本使用

对 Person 类分别新建分类 Run 和 Eat

Person.h
@interface Person : NSObject

- (void)instanceTest;
+ (void)classTest;

@end

Person.m
- (void)instanceTest
{
    NSLog(@"instanceTest");
}
+ (void)classTest
{
    NSLog(@"classTest");
}
***********************************
分类 Person+Run.h
- (void)instanceRun;
+ (void)classRun;

分类 Person+Run.m
- (void)instanceRun
{
    NSLog(@"instanceRun");
}
+ (void)classRun
{
    NSLog(@"classRun");
}

***********************************
分类 Person+Eat.h
- (void)instanceEat;
+ (void)classEat;

分类 Person+Eat.m
- (void)instanceEat
{
    NSLog(@"instanceEat");
}
+ (void)classEat
{
    NSLog(@"classEat");
}

测试:

Person *p = [[Person alloc] init];
[p instanceTest];
[Person classTest];

[p instanceRun];
[Person classRun];

[p instanceEat];
[Person classEat];

log:
CategoryTest[75946:2164852] instanceTest
CategoryTest[75946:2164852] classTest
CategoryTest[75946:2164852] instanceRun
CategoryTest[75946:2164852] classRun
CategoryTest[75946:2164852] instanceEat
CategoryTest[75946:2164852] classEat

实例方法(或者类方法)调用流程是,是通过 isa 找到所属的类对象(或元类对象),从方法列表中查找并并调用。上述 Person 类的分类对象方法和类方法,可以正常调用,因此编写的分类的方法会合并到类对象(或者元类对象)中去。

合并时机?编译的时候还是运行的时候?

2. 底层分析

2.1 底层结构

编译 Person+Run.m 生成 c++文件 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Run.m 在.ccp文件中,可以看到有个 _category_t 的结构体:

struct _category_t {
	const char *name;  //类名,即属于哪个类(Person)
	struct _class_t *cls;
	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;  //属性列表
};

继续查看 .cpp 文件,其中有一个类型为 _category_t 结构体类型的变量_OBJC_$_CATEGORY_Person_$_Run,该结构体变量存放的值很明显和我们创建的 Person+Run 类信息一一对应:

static struct _category_t _OBJC_$_CATEGORY_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"Person",  //类名
	0, // &OBJC_CLASS_$_Person,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Run, //对象方法列表
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Run, //类方法列表
	0,  //协议列表
	0,  //属性列表
};

上述结构体中 _OBJC_$_CATEGORY_Person_$_Run 中的_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Run (对象方法列表),同样可以在 .cpp 中找到对应的具体定义信息:

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"instanceRun", "v16@0:8", (void *)_I_Person_Run_instanceRun}}
};

同样结构体 _OBJC_$_CATEGORY_Person_$_Run 中的变量_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Run (类方法列表),也可以可以在 .cpp 中找到对应的具体定义信息:

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"classRun", "v16@0:8", (void *)_C_Person_Run_classRun}}
};

编译完成的时候,分类的信息其实是存放在 _category_t 这样的结构体中。

小结:分类在编译完成后,分类的信息就会变成对应的 _category_t 的结构体,一开始的时候分类方法信息都会存在这个结构体中,在程序运行的时候通过 运行时机制 将这个结构体里面的方法合并到类信息(类对象或者元类对象)之中。

2.2 源码分析

下载 objc 源码,在 objc-runtime-new.h 中可以找到 struct category_t 的定义:

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

objc-runtime-new.mm 中找到函数 attachCategories (直白翻译就是:将所有分类的信息附加到类信息中) 下面字面描述均已类对象(元类对象也是一样的)为例: static void attachCategories(Class cls, category_list *cats, bool flush_caches) 函数中相关信息见具体注释信息,不在过多讲述

/**
 以类对象为例:
 cls = [Person class]
 cats = [category_t, category_t]; Person+Run, Person+Eat
 
 */
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();
    
    // 方法数组(二维数组): 每一个数组元素对应每一个分类的方法列表(一维数组),大致为[[method_t, method_t], [method_t, method_t]]
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    
    // 属性数组: 每一个数组元素对应每一个分类的属性列表(一维数组),大致为[[property_t, property_t], [property_t, property_t]]
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    
    // 协议数组:  每一个数组元素对应每一个分类的协议列表(一维数组),大致为[[protoco_t, protoco_t], [protoco_t, protoco_t]]
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count *  sizeof(*protolists));

    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {
        
        // 取出分类
        auto& entry = cats->list[i];

        // 取出分类中的对象方法列表(或者分类中的类方法)列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

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

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    // 类对象里面的数据
    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    
    // 将所有分类的方法列表(即上面的方法二维数组),附加到类对象的方法列表中
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_cach es  &&  mcount > 0) flushCaches(cls);

    // 将所有分类的属性列表(即上面的方法二维数组),附加到类对象的属性列表中
    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    // 将所有分类的协议列表(即上面的方法二维数组),附加到类对象的的协议列表中
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

大致过程:
 1)创建二维方法数组:mlists;
 2)遍历传入的分类列表变量:cats,取出每个分类,并将每个分类的对象方法列表放入 mlists 中;
 3)取出类对象里面的数据 rw = cls->data(),调用方法 rw->methods.attachLists(mlists, mcount),将遍历完成后得到的所有分类方法(mlists)附加到类对象的方法列表中。

方法 void attachLists(List* const * addedLists, uint32_t addedCount) 的具体实现:以传入的是 mlists(分类列表二维数组)为例,所以以下备注均是针对分类列表做的文字描述。

/// 将分类的所有方法列表附加到类信息中(类对象或元类对象)
/// @param addedLists 所有分类的方法列表(二维数组)
/// @param addedCount 分类的个数
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;
        
        // 将原来的方法列表内存地址移动到 array() 中 lists + addedCoun的位置,长度为原来方法列表个数所占的内存长度
        memmove(array()->lists + addedCount, array()->lists, 
                oldCount * sizeof(array()->lists[0]));
        
        // 将分类的的方法列表内存地址拷贝到 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]));
    }
}

 1)扩容,容量为原来方法列表个数 + 传入的分类个数
 2)内存移动:将原来的方法列表对应的地址移动到位置为"lists + addedCount"的地方,该内存地址即指向原来的那个方法列表
 3)内存拷贝,将分类的地址拷贝到数组 lists 位置(即数组首元素地址)。

  通过上述方法的调用将分类的方法列表追加到了类信息方法列表的数组最前面的位置,而原来的方法列表则被移动到了追加的分类方法列表之后。

  我们知道 OC 的方法调用,实际上是消息发送(objc_msgSend,通过 isa 找到类对象(或者元类对象),遍历方法列表做出响应。因此,如果分类和本类有同样的方法,则调用的是分类的方法(分类方法列表在本类之前,找到方法后随即响应,不会找到本类的那个方法)。

另外在方法static void attachCategories(Class cls, category_list *cats, bool flush_caches)中,while 循环遍历分类的的时候是倒序遍历的,即最后的分类添加到了二维数组初始位置mlists[mcount++] = mlist;,所以当本类有多个分类,且分类中方法相同时,会根据编译顺序调用分类方法,最后编译的那个分类的方法会加到方法列表最前面,所以最后编译的那个分类的同名方法将被调用。

分类属性、协议在做附加操作的时候,也是调用上述void attachLists(List* const * addedLists, uint32_t addedCount)方法,其添加的原理类似,也会被附加到类信息对应列表的最前面。

Category 的加载处理过程:
通过 Runtime 加载某个类的所有 Category 数据 -> 把所有 Category 的方法、属性、协议数据,合并到一个大数组中:后面参与编译的Category数据,会在数组的前面 -> 将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面。

总结:

1、分类在编译完成后,分类的信息就会变成对应的 _category_t 的结构体,一开始的时候分类方法信息都会存在这个结构体中,在程序运行的时候通过 运行时机制将这个结构体里面的方法合并到类信息(类对象或者元类对象)之中;

2、通过 Runtime 加载某个类的所有 Category 数据,调用 static void attachCategories(Class cls, category_list *cats, bool flush_caches) 把所有 Category 的方法、属性、协议数据,合并到一个大数组中,后参与编译的 Category 数据,会在最终附加后的大数组数组的前面;

3、最后依次调用 memmove(处理本类信息)和 memcpy(处理分类信息),将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面;

4、多个分类和本类含有同名方法时,会调用最后参与编译的分类的方法。

3. +load 方法

分别在Peron类、Person+Run 分类 和 Person+Eat 分类添加 + (void)load 方法代码

Person.m
+ (void)load
{
    NSLog(@"Person load");
}

Person+Run.m
+ (void)load
{
    NSLog(@"Person (Run) load");
}

Person+Eat.m
+ (void)load
{
    NSLog(@"Person (Eat) load");
}

直接运行项目(main.m 文件中不添加任何代码),日志输出:

CategoryTest[65668:306566] Person load
CategoryTest[65668:306566] Person (Run) load
CategoryTest[65668:306566] Person (Eat) load

按照之前对应分类的分析,当本类和分类中均实现同名方法的时候,如果调用该同名方法,按照方法调用流程,最终应该会调用分类的方法,但此处在没有调用任何类信息的情况下,本类和所有分类的 + (void)load 均被调用了。 + (void)load 方法应该是一个特殊的方法。

3.1 +load 底层源码分析

objc源码:
objc-os.mm 文件中找到 void _objc_init(void) 方法,并进一步看到 void load_images(const char *path __unused, const struct mach_header *mh) 方法

该函数类的注释很清楚的写着:会调用 +load 方法,因此进入 call_load_methods() 中继续查看,在该函数中可以看到 while 循环中会调用本类和分类的 +load 方法

调用本类的 call_class_loads 方法

for 循环遍历,得到 cls 类,并进一步拿到 load_method 函数指针(指向本类 +load 方法)

load_method_t load_method = (load_method_t)classes[i].method;

然后取函数地址,直接进行函数调用

(*load_method)(cls, SEL_load);

由于是直接进行函数调用,因此一定会调用到本类的 +load 方法,而不会像之前那样通过 OC 消息发送机制,最终调用分类的方法过程。

同样分类的 load 方法调用函数 more_categories = call_category_loads();

分类也是直接进行函数调用,因此分类的 +load 方法也一定会被调用。

小结:
+ (void)load方法的调用,是直接通过函数指针进行函数调用,而非 msgSend 消息发送流程,故 +load 方法会在 runtime 加载类、分类时调用,即本类和分类的 +load 方法均会被调用。

3.2 继承情况下 load

上面我们已经知道,先调用类的 +load 方法,再调用分类的 +load 方法。
如果有多个类的时候,类与类之间的 load 方法调用顺序?
当既有父类和子类,且两者都有分类的时候,load 方法的调用顺序是怎么样的呢?

回到源码: 1、load 方法调用函数:static void call_class_loads(void)

函数中先通过取到存放所有类数组,然后遍历该数组,取出某一个类 load方法,直接进行调用,所以是按照数组的顺序去调用 load 方法的,而当有多个类时,优先调用哪一个类的 load 方法,就需要了解这个数组存放类的原理是什么?

2、在 void load_images(const char *path __unused, const struct mach_header *mh) 可以找到函数 prepare_load_methods 该函数可理解为 "准备调用 load 方法":

3、在void prepare_load_methods函数中:
  1)先调用 _getObjc2NonlazyClassList 函数得到一个数组,该函数的字面意思为:取到 obj2非懒加载的类列表,该数组中类元素顺序和累的编译顺序有关:即先编译的类先放入数组中;
  2)然后该遍历数组,调用函数schedule_class_load,接收参数为取出的每一个数组元素。

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertWriting();

    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t **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
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

4.进入 schedule_class_load 函数(定制、规划任务),可以看到函数内部会调用 add_class_to_loadable_list(cls);,即将类作为函数参数传进去,将类添加到loadable_list中,

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

5、在函数void add_class_to_loadable_list(Class cls)中:

该函数中把传入的类 cls 存入数组 loadable_classes 中,(loadable_classes,即为上面在调用所有类的 + (void)load 方法时,进行类遍历的那个数组),并且数组下标 loadable_classes_used++; 每次添加执行++操作,可以看出来在每次进行 class 添加的时候是执行追加的操作。所以上面 “4、” 中的 add_class_to_loadable_list(cls); 其作用是将 类cls 添加到 loadable_classes 数组的最后面。

所以:多个类的 load 方法调用顺序和类添加到 loadable_classes 数组中的顺序有关,先添加的先调用,而类添加到 loadable_classes 中和类的编译顺序有关。

再次回到schedule_class_load函数中,

在执行添加类到之前,schedule_class_load(cls->superclass); 会递归调用本函数(参数为类的 父类 “cls->superclass>”)。假如我们传入一个子类,则会将子类的 父类作为参数递归调用,先将对应的父类先添加到 loadable_classes 数组中之后,才会将子类添加到该数组中。所以会先调用父类的 +load 方法,再调用子类的 +load 方法。

小结:
在类的 + (void)load 调用之前的准备工作,先取到非懒加载的类列表classlist(和编译顺序相关) -> 然后遍历 classlist,调用 schedule_class_load -> 在函数 schedule_class_load 中递归调用,会先将父类添加到 loadable_classes,再将子类添加到 loadable_classes 中。 所以,多个类的 +load 调用,遍历 loadable_classes 取出某一个类 +load 方法(有父类的话,会先得到父类),然后直接展开调用+ (void)load 方法。

分类的 + (void)load 调用之前的准备工作:

void add_category_to_loadable_list(Category cat) 函数中,直接将 分类cat 添加到数组 loadable_categories 中,而不会像上面的类一样先去判断父类进行递归调用之类操作

这样分类的 + (void)load 调用完全是按照编译顺序执行的。

测试代码验证: 类Male 继承 类Person,类Male 和 类Person 分别添加 分类Run;Car 继承 NSObject。
所有类和分类,分别重写 + (void)load 方法,添加对应 NSLog

Person.m
+ (void)load
{
    NSLog(@"Person load");
}

Person+Run.m
+ (void)load
{
    NSLog(@"Person (Run) load");
}

Male.m
+ (void)load
{
    NSLog(@"Male load");
}

Male+Run.m
+ (void)load
{
    NSLog(@"Male (Run) load");
}

Car.m
+ (void)load
{
    NSLog(@"Car load");
}

Car+Run.m
+ (void)load
{
    NSLog(@"Car (Run) load");
}

编译顺序 和 对应 log:


3.2 +load 调用顺序总结

1、先调用类的+ (void)load 方法
  1)根据编译顺序调用+ (void)load 方法 - 先编译先调用;
  2)调用子类的+ (void)load 方法之前,会先调用父类的`+;

2、再调用分类的+ (void)load 方法;
  根据编译顺序调用+ (void)load 方法 - 先编译先调用。

4. initialize 方法

线添加一个 Person 类 和一个 Person+Run 分类,并分别添加 + (void)initialize 打印日志代码

Person.m
+ (void)initialize
{
    NSLog(@"Person: initialize");
}

Person+Run.m
+ (void)initialize
{
    NSLog(@"Person+Run: initialize");
}

添加测试代码:

去除测试代码:

我们可以知道当我们第一次使用 类的时候,才会调用(自动调用)initialize 的,而且从输出日志可以看到只调用了一次,由此可知应该是通过消息发送机制调用的initialize的

Class class = [Person class], 消息发送:objc_msgSend([[Person class], @selector(class)]),测试代码中并没有主动调用 initialize,所以initialize的调用应该发生在objc_msgSend的内部。

查看 objc 源码: 在源码中 objc_msgSend() 是通过汇编实现,因此只能换个思维:由于自动会调用 initialize,那就直接通过查找 class_getClassMethod,然后来到IMP lookUpImpOrForward()
在函数内部:

所以如果 (需要初始化 && 未初始化) 则会进入初始化方法void _class_initialize()

进入初始化方法,探索其初始化逻辑:

然后会来到callInitialize(cls)的调用入口

函数callInitialize(Class cls)具体实现:

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

即:objc_msgSend 调用 initialize 方法 通过上面对源码的分析,可以大致了解 +initialize 的调用过程:

初始化类的时候,会先调用父类的 +initialize,再调用本类的的 +initialize。而且已经初始化过类的不会再进行初始化,即:每个类只会初始化1次。(先初始化父类,再初始化子类,每个类只会初始化1次)。

在上面测试代码的基础上添加 Male类,继承 Person:

Male.m
+ (void)initialize
{
    NSLog(@"Male: initialize");
}

1、只使用子类:先调用父类 +initialize,再调用子类 +initialize

2、多次使用类:每个类只会调用 1 次 +initialize