分类Category的本质,load和initialize的本质

问题

  1. Category的实现原理,以及Category为什么只能加方法不能加属性。
  2. CategoryExtension的区别是什么?
  3. Category中有load方法吗?load方法是什么时候调用的?load方法能继承吗?
  4. loadinitialize的区别,以及它们在Category重写的时候的调用的次序。
  5. Category能否添加成员变量?如果可以,如何给Category添加成员变量?

1. Category的使用

使用下面的这一段简单代码来分析:

// Preson类 
// Preson.h
#import <Foundation/Foundation.h>
@interface Preson : NSObject
{
    int _age;
}
- (void)run;
@end

// Preson.m
#import "Preson.h"
@implementation Preson
- (void)run
{
    NSLog(@"Person - run");
}
@end

// Preson扩展1
// Preson+Test.h
#import "Preson.h"
@interface Preson (Test) <NSCopying>
- (void)test;
+ (void)abc;
@property (assign, nonatomic) int age;
- (void)setAge:(int)age;
- (int)age;
@end

// Preson+Test.m
#import "Preson+Test.h"
@implementation Preson (Test)
- (void)test
{
}

+ (void)abc
{
}
- (void)setAge:(int)age
{
}
- (int)age
{
    return 10;
}
@end

// Preson分类2
// Preson+Test2.h
#import "Preson.h"
@interface Preson (Test2)
@end

// Preson+Test2.m
#import "Preson+Test2.h"
@implementation Preson (Test2)
- (void)run
{
    NSLog(@"Person (Test2) - run");
}
@end

我们之前讲到过实例对象的isa指针指向类对象,类对象的isa指针指向元类对象,当p调用run方法时,通过实例对象的isa指针找到类对象,然后在类对象中查找对象方法,如果没有找到,就通过类对象的super_class指针找到父类对象,接着去寻找run方法。

那么当调用分类的方法时,步骤是否和调用对象方法一样呢?

分类中的对象方法依然是存储在类对象中的,同本类对象方法在同一个地方,调用步骤也同调用对象方法一样。如果是类方法的话,也同样是存储在元类对象中。
那么分类方法是如何存储在类对象中的,我们来通过源码看一下分类的底层结构。

2. 分类的底层结构

扩展的方法不是在编译时期合并至原来的类,而是在运行时合并的。

我们将OC编译的文件生成底层的C++实现

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

2.1 分类结构体

在分类转化为C++文件中可以找到_category_t 结构体中,存放着类名,对象方法列表,类方法列表,协议列表,以及属性列表。

struct _category_t {
    const char *name; // 类名
    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; // 属性列表
};

2.2 分类结构体的成员列表

2.2.1 分类的实例方法结构体

存放实例方法_method_list_t类型的结构体,如下所示

static struct /*_method_list_t*/ {
    unsigned int entsize;  // 方法占用的内存
    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 *)"test", "v16@0:8", (void *)_I_Person_Test_test},
    {(struct objc_selector *)"setAge:", "v20@0:8i16", (void *)_I_Person_Test_setAge_},
    {(struct objc_selector *)"age", "i16@0:8", (void *)_I_Person_Test_age}}
};

上面中我们发现这个结构体 _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test 从名称可以看出是INSTANCE_METHODS对象方法,并且一一对应为上面结构体内赋值。我们可以看到结构体中存储了方法占用的内存,方法数量,以及方法列表。并且从上图中找到分类中我们添加的对象方法,test , setAge, age三个方法。

2.2.2 分类的类方法结构体

存放类方法_method_list_t类型的类方法结构体,如下所示

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_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"abc", "v16@0:8", (void *)_C_Person_Test_abc}}
};

同上面实例方法列表一样,这个我们可以看出是类方法列表结构体 _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,同对象方法结构体相同,同样可以看到我们实现的类方法abc

2.2.3 分类的协议方法结构体

存放协议列表结构体_protocol_list_t,假如我们实现了NSCopying协议

static const char *_OBJC_PROTOCOL_METHOD_TYPES_NSCopying [] __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "@24@0:8^{_NSZone=}16"
};

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", 0}}
};

struct _protocol_t _OBJC_PROTOCOL_NSCopying __attribute__ ((used)) = {
    0,
    "NSCopying",
    0,
    (const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying,
    0,
    0,
    0,
    0,
    sizeof(_protocol_t),
    0,
    (const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCopying
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCopying = &_OBJC_PROTOCOL_NSCopying;

static struct /*_protocol_list_t*/ {
    long protocol_count; // 协议数量
    struct _protocol_t *super_protocols[1]; // 存储协议方法
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    1,
    &_OBJC_PROTOCOL_NSCopying
};

通过上述源码可以看到先将协议方法通过_method_list_t结构体存储

之后通过_protocol_t结构体存储在_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test中同_protocol_list_t结构体一一对应,分别为protocol_count协议数量以及存储了协议方法的_protocol_t结构体。

2.2.4 分类的属性列表

最后我们可以看到属性列表结构体_prop_list_t

static struct /*_prop_list_t*/ {
    unsigned int entsize;  // 占用空间
    unsigned int count_of_properties; // 属性数量
    struct _prop_t prop_list[1]; // 属性列表
} _OBJC_$_PROP_LIST_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"age","Ti,N"}} // age属性
};

属性列表结构体_OBJC_$_PROP_LIST_Person_$_Test_prop_list_t结构体对应,存储属性的占用空间,属性属性数量,以及属性列表,从上图中可以看到我们自己添加的age属性。

2.2.5 分类_category_t结构体总结

最后我们可以看到定义了_OBJC_$_CATEGORY_Person_$_Test结构体,并且将我们上面着重分析的结构体一一赋值。

struct _category_t {
    const char *name;
    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;
};

//************************ 上下一一对应  ******************************************

extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_Person;

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,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,
    (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Test,
};
static void OBJC_CATEGORY_SETUP_$_Person_$_Test(void ) {
    _OBJC_$_CATEGORY_Person_$_Test.cls = &OBJC_CLASS_$_Person;
}

并且我们看到定义原类_class_t类型的OBJC_CLASS_$_Preson结构体

最后将分类_OBJC_$_CATEGORY_Person_$_Testcls指针指向原类OBJC_CLASS_$_Preson结构体地址。我们这里可以看出,cls指针指向的应该是原类的类对象的地址。

3. 源码分析

通过查看分类的源码我们可以找到底层分类category_t结构体。

objc源码路径:runtime/objc-runtime-new.h

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

从源码基本可以看出我们平时使用categroy的方式,对象方法,类方法,协议,和属性都可以找到对应的存储方式。并且我们发现分类结构体中是不存在成员变量的,因此分类中是不允许添加成员变量的。分类中添加的属性并不会帮助我们自动生成成员变量,只会生成get set方法的声明,需要我们自己去实现。

通过源发现,分类的方法,协议,属性等好像确实是存放在categroy结构体里面的,那么他又是如何合并存储到原类的类对象中的?

3.1 分类是如何存储方法,属性,协议的

通过我们runtime的初始化函数_objc_init来探寻答案。

objc源码路径:runtime/objc-os.mm

入口函数:_objc_init:

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

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

    // images指是镜像、模块
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

我们来到&map_images读取模块(images这里代表模块)

↓↓↓

来到map_images_nolock函数

↓↓↓

来到_read_images函数

最终在_read_images函数中找到分类相关代码:

objc源码路径:runtime/objc-runtime-new.mm

// Discover categories. 
    for (EACH_HEADER) {
        // 获取到分类列表
        category_t **catlist = _getObjc2CategoryList(hi, &count); 
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        // 遍历,获取其中的方法,协议,属性
        // 内部调用 remethodizeClass
        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  
                ||  (hasClassProperties && 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);
                }
            }
        }
    }

从上述代码中我们可以知道这段代码是用来查找有没有分类的。通过_getObjc2CategoryList函数获取到分类列表之后,进行遍历,获取其中的方法,协议,属性等。可以看到最终都调用了remethodizeClass(cls)函数。我们来到remethodizeClass(cls)函数内部查看:

↓↓↓

remethodizeClass(cls)函数:

/***********************************************************************
* remethodizeClass
* Attach outstanding categories to an existing class.
* Fixes up cls's method list, protocol list, and property list.
* Updates method caches for cls and its subclasses.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertLocked();

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        
        // 开始附加分类的相关信息至原类
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

通过上述代码我们发现attachCategories函数接收了类对象cls和分类数组cats,如我们一开始写的代码所示,一个类可以有多个分类。之前我们说到分类信息存储在category_t结构体中,那么多个分类则保存在category_list中。

↓↓↓

我们来到attachCategories()函数内部:

/** 接受的参数
Class cls: 原类的类对象
category_list *cats: 所有的分类列表
*/
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // 0. 根据每个分类中方法列表、属性列表、协议列表分类存储
    
    // 方法数组 
    /** 是一个二维数组
    [
        [// 第一个分类的方法
            method_list_t, 
            method_list_t
        ],
        [// 第二个分类的方法
            method_list_t, 
            method_list_t
        ]
    ]
    */
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    // 属性数组
    /** 是一个二维数组
    [
        [// 第一个分类的属性
            property_list_t, 
            property_list_t
        ],
        [// 第二个分类的属性
            property_list_t, 
            property_list_t
        ]
    ]
    */
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    // 协议数组
    /** 是一个二维数组
    [
        [// 第一个分类的协议
            protocol_list_t, 
            protocol_list_t
        ],
        [// 第二个分类的协议
            protocol_list_t, 
            protocol_list_t
        ]
    ]
    */
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // 1. 遍历每一个分类 
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) { 
        // 取出一个分类
        auto& entry = cats->list[i];
        
        // 1.1 将所有分类中的所有方法合并存入mlist数组中
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
    
        // 1.2 将所有分类中的所有属性合并存入proplist数组中
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        // 1.3 将所有分类中的所有协议合并存入protolist数组中
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    // 2 调用attachLists()方法,赋值合并至原来的类对象
    // 获取原来的类对象rw:即 class_rw_t结构体,是class结构体中用来存储类对象中的对象方法、属性、协议的结构体
    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    
    // 2.1 将所有分类的类对象方法mlists数组,合并附加到类对象的方法列表中
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
    
    // 2.2 将所有分类的属性proplists数组,合并附加到类对象的属性列表中
    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    // 2.3 将所有分类的协议protolists数组,合并附加到类对象的属性列表中
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

上述源码中可以看出,首先根据方法列表,属性列表,协议列表,malloc分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址。

之后将每一个分类的所有的方法、属性、协议分别放入对应mlistproplistsprotolosts数组中,这三个数组放着所有分类的所有方法,属性和协议。

之后通过类对象的data()方法,拿到类对象的class_rw_t结构体rw,在class结构中我们介绍过,class_rw_t中存放着类对象的方法,属性和协议等数据,rw结构体通过类对象的data方法获取,所以rw里面存放这类对象里面的数据。

之后分别通过rw调用方法列表、属性列表、协议列表的attachList函数,将所有的分类的方法、属性、协议列表数组传进去。

我们大致可以猜想到在attachList方法内部将分类和本类相应的对象方法,属性,和协议进行了合并。

↓↓↓

我们来到attachLists函数内部:

objc源码路径:runtime/objc-runtime-new.h

/** 接收参数
addedLists:二维数组
addedCount: 二位数组数量
*/
void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // 原来的列表数组
            uint32_t oldCount = array()->count;
            
            // 1. 重新申请内存
            // 新的的列表数量:原来的数量 + 新添加的分类的数量
            uint32_t newCount = oldCount + addedCount;
            // 将原来的数组进行扩容,来存放新添加
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            // 数组数量为新的值
            array()->count = newCount;
           
            // 2. 移动原来的方法列表
            // array()->lists 是原来的方法列表位置
            // oldCount * sizeof(array()->lists[0] 是需要移动的字节数
            // array()->lists + addedCount新的位置
            // 内存移动,将原来的方法列表移动到新的位置,相当于前面空出了位置
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            
             // 3. 内存复制
             // array()->lists 是原来的列表位置
             // addedLists是所有的分类的所有列表
             // addedCount * 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]));
        }
    }

由上面的源码中可以看出,新添加的分类的其实是相当于移动到了原来存储的列表之前的,所以原类和分类重名的时候会优先调用分类的方法。

另外,当多个分类有重名的方法的时候由源码的attachCategories()函数可以知道,其实是有优先调用最后参与编译的分类的方法:

...
// 这儿是i--
while (i--) { 
        // 取出一个分类
        auto& entry = cats->list[i];
        ...
}
...

3.2 Extension类扩展

类扩展其实就是我们平常写的@interface

// 类扩展
@interface AppDelegate ()
// 可以扩展一些私有的成员变量、属性、方法
@end

// 实现
@implementation AppDelegate

@end

和分类categroy不同的是:类扩展的信息是在编译的时候已经合并在了类对象中,而分类是在运行时合并至原来中的。

3.3 memmovememcpy

上面的源代码中有两个重要的数组:

  1. array()->lists: 原类对象原来的方法列表,属性列表,协议列表。
  2. addedLists:传入所有分类的方法列表,属性列表,协议列表。

attachLists函数中最重要的两个方法为memmove内存移动和memcpy内存拷贝。我们先来分别看一下这两个函数:

// memmove :内存移动。
/*  __dst : 移动内存的目的地
*   __src : 被移动的内存首地址
*   __len : 被移动的内存长度
*   将__src的内存移动__len块内存到__dst中
*/
void    *memmove(void *__dst, const void *__src, size_t __len);

// memcpy :内存拷贝。
/*  __dst : 拷贝内存的拷贝目的地
*   __src : 被拷贝的内存首地址
*   __n : 被移动的内存长度
*   将__src的内存移动__n块内存到__dst中
*/
void    *memcpy(void *__dst, const void *__src, size_t __n);

下面我们图示经过memmovememcpy方法过后的内存变化。

3.3.1 内存移动memmove
memmove_bofore

经过memmove方法之后,内存变化为:

// array()->lists 原来方法、属性、协议列表数组
// addedCount 分类数组长度
// oldCount * sizeof(array()->lists[0]) 原来数组占据的空间
memmove(array()->lists + addedCount, array()->lists, 
                  oldCount * sizeof(array()->lists[0]));
memmove_after

经过memmove方法之后,我们发现,虽然本类的方法,属性,协议列表会分别后移,但是本类的对应数组的指针依然指向原始位置。

3.3.2 内存复制memcpy
// array()->lists 原来方法、属性、协议列表数组
// addedLists 分类方法、属性、协议列表数组
// addedCount * sizeof(array()->lists[0]) 原来数组占据的空间
memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
memcopy

我们发现原来指针并没有改变,至始至终指向开头的位置。并且经过memmovememcpy方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。

那么为什么要将分类方法的列表追加到本来的对象方法前面呢,这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。
其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用。本类的方法依然在内存中的。我们可以通过打印所有类的所有方法名来查看

2020-01-14 11:47:02.458927+0800 分类Category的本质[84870:4069262] Person (Test2) - run
2020-01-14 11:47:02.459080+0800 分类Category的本质[84870:4069262] Person类: age, run, run, setAge:, test, 

调用的是Test2中的run方法,并且Person类中存储着两个run方法。

3.4 总结

Category的实现原理,以及Category为什么只能加方法不能加属性?

分类的实现原理是将Category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。

Category可以添加属性,但是并不会自动生成成员变量及set get方法。因为category_t结构体中并不存在成员变量。

通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。

CategoryExtension的区别是什么?

和分类Categroy不同的是:类扩展的信息是在编译的时候已经合并在了类对象中,而分类是在运行时合并至原类中的。

4. loadinitialize

4.1 load方法

4.4.1 基本使用

load方法是runtime在加载类和分类的时候会调用,是在程序入口调用函数main之前调用,而且只会调用一次。

通过代码验证一下调用本类的load方法调用。

我们添加Student继承Presen类,并添加Student+Test分类,分别重写+load方法。

2020-01-14 14:07:53.689561+0800 分类Category的本质[92689:4179307] Person - load
2020-01-14 14:07:53.690079+0800 分类Category的本质[92689:4179307] Student - load
2020-01-14 14:07:53.690142+0800 分类Category的本质[92689:4179307] Student (Test) - load

通过验证我们发现不仅调用了分类的load方法,而且调用了原类的load方法,这和上面我们验证的优先调用分类的方法的逻辑相冲突,到底为什么会调用原类的方法,我们通过底层的源码一探究竟。

4.4.2 调用原理

同样的我们从runtime的入口_objc_init函数的load_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();
    lock_init();
    exception_init();

    // images指是镜像、模块
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

↓↓↓

最终来到了call_load_methods函数

objc源码路径:runtime/objc-loadmethod.mm

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) {
            // 先调用本类的load方法
            call_class_loads(); 
        }

        // 2. Call category +loads ONCE
        // 再调用分类的load方法
        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方法:

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;
    
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        // 得到load方法的函数地址
        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方法
        (*load_method)(cls, @selector(load));
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

↓↓↓

再调用分类的load方法:

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        
        // 获取分类的load方法地址
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;
        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            
            // 调用分类的load方法
            (*load_method)(cls, @selector(load));
            cats[i].cat = nil;
        }
    }
    
    ...
    ...
    ...
}

runtime的消息发送机制不同的是,消息发送机制是需要通过isa指针去逐层寻找,而load方法是不需要通过消息发送的,而是直接通过函数的地址来调用的

struct loadable_class {
    Class cls;  // may be nil
    IMP method; // 函数实现地址,指向的是原类的load方法
};

struct loadable_category {
    Category cat;  // may be nil
    IMP method; // 函数的实现地址,指向的是分类的load方法
};
4.4.3 调用顺序

即使是再复杂继承关系,原类、分类、子类的load方法都会被调用,并且是按照一定的顺序调用的。

通过上面的源码可以看到在调用原类和分类的load方法的时候,都是分别通过一个数组loadable_classesloadable_categories进行for循环去遍历的,所以知道数组的顺序,就可以知道方法的调用顺序

我们回到之前的入口函数load_images,发现在调用call_load_methods()函数之前调用了prepare_load_methods()方法:

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // 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();
}

↓↓↓

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

    runtimeLock.assertLocked();

    // 1. 先遍历所有的原类的的load方法
    // 这个顺序是有编译顺序决定的,可以手动设置顺序
    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 1.1 先将父类的load方法进行添加
        // 1.2 再将子类的load方法进行添加
        schedule_class_load(remapClass(classlist[i]));
    }

    // 2. 再遍历分类的load方法
    // 这个顺序也是有编译顺序决定的,可以手动设置编译顺序
    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);
    }
}

↓↓↓

原类的load方法添加到loadable_classes数组的顺序:

// 1. 会先添加父类的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;

    // 先通过递归调用 
    // 将父类的load方法添加到loadable_classes数组
    schedule_class_load(cls->superclass);

    // 再将子类的cls添加到loadable_classes数组的
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

// 2. 添加到loadable_classes数组
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++;
}

分类的load方法添加到loadable_categories数组的顺序:

// 直接添加到loadable_categories数组
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方法的调用顺序:先将元类添加到数组,同时会先去将父类添加到数组,再讲子类添加到数组,最后将分类添加到数组。所以在调用load方法的时候也会是按照类的添加顺序来调用。

4.4.4 总结
  1. 先调用所有原类的laod方法

    • 按照编译顺序调用(可以手动设置编译顺序)
    • 调用子类的load之前会先调用父类的load方法
  2. 再调用分类的laod方法

    • 按照编译顺序调用(可以手动设置编译顺序)
Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?

Category中有load方法,load方法在程序加载了类和分类的时候就会调用,在main函数之前调用。load方法可以继承。调用子类的load方法之前,会先调用父类的load方法。一般我们不会手动去调用load方法,而是让系统去调用。

如果非要手动调用load方法,那么就会按照消息发送机制通过isa指针来寻找方法。

4.2 initialize方法

我们为PresonPerson+TestStudentStudent+Test 添加initialize方法。

4.2.1 基本使用

initialize类第一次接收到消息时,就会调用,相当于第一次使用类的时候就会调用initialize方法。

initialize方法的调用是通过消息发送机制调用的,不像load方法是直接通过函数指针去调用。

再来验证一下调用顺序:

2020-04-28 21:22:18.874260+0800 分类Category的本质[90053:17829138] Person (Test) initialize
2020-04-28 21:22:18.874726+0800 分类Category的本质[90053:17829138] Student (Test) initialize

调用子类的initialize之前,会先保证调用父类的initialize方法。如果之前已经调用过initialize,就不会再调用initialize方法了。

当分类重写initialize方法时会先调用分类的方法不再调用原类的方法。

initialize是通过消息发送机制调用的,消息发送机制通过isa指针找到对应的方法与实现,因此优先找到分类方法中的实现,会优先调用分类方法中的实现。

另外还有一点需要注意:如果子类没有实现initialize方法,那么会调用父类的initialize方法,这一点我们会通过源码去验证。

4.2.2 源码分析

在底层源码里面通过函数class_getInstanceMethod和函数class_getClassMethod来找到实例方法和类方法

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.

    Method meth;
    meth = _cache_getMethod(cls, sel, _objc_msgForward_impcache);
    if (meth == (Method)1) {
        // Cache contains forward:: . Stop searching.
        return nil;
    } else if (meth) {
        return meth;
    }
        
    // 搜索方法
    lookUpImpOrForward(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);

    meth = _cache_getMethod(cls, sel, _objc_msgForward_impcache);
    if (meth == (Method)1) {
        // Cache contains forward:: . Stop searching.
        return nil;
    } else if (meth) {
        return meth;
    }

    return _class_getMethod(cls, sel);
}

↓↓↓

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    Class curClass;
    IMP methodPC = nil;
    Method meth;
    bool triedResolver = NO;

    methodListLock.assertUnlocked();

    // Optimistic cache lookup
    if (behavior & LOOKUP_CACHE) {
        methodPC = _cache_getImp(cls, sel);
        if (methodPC) goto out_nolock;
    }

    // Check for freed class
    if (cls == _class_getFreedObjectClass())
        return (IMP) _freedHandler;

    // 检查是否已经调用了+initialize方法,如果没有调用过initialize方法
    if ((behavior & LOOKUP_INITIALIZE)  &&  !cls->isInitialized()) {
        // 调用initialize方法
        initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
        // If sel == initialize, initializeNonMetaClass 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
    }
    
    ...
    ...
    ...
}

↓↓↓

void initializeNonMetaClass(Class cls)
{
    ASSERT(!cls->isMetaClass());

    Class supercls;
    bool reallyInitialize = NO;

    // 在调用initialize方法之前,需要先判断是否调用了父类的initialize方法
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);
    }
    
    ...
    ...
    ...
    
#if __OBJC2__
        @try
#endif
        {
            // 开始调用initialize方法
            callInitialize(cls);

            if (PrintInitializing) {
                _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
                             objc_thread_self(), cls->nameForLogging());
            }
        }
#if __OBJC2__
        @catch (...) {
            if (PrintInitializing) {
                _objc_inform("INITIALIZE: thread %p: +[%s initialize] "
                             "threw an exception",
                             objc_thread_self(), cls->nameForLogging());
            }
            @throw;
        }
        @finally
#endif
        {
            // Done initializing.
            lockAndFinishInitializing(cls, supercls);
        }
        return;
    }
    
    else if (cls->isInitializing()) {
        ...
        ...
        ...
    }
    
    else if (cls->isInitialized()) {
        
        return;
    }
    
    else {
        // We shouldn't be here. 
        _objc_fatal("thread-safe class init in objc runtime is buggy!");
    }
}

↓↓↓

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

最终来到了消息发送objc_msgSend函数,给类对象发送initialize消息,消息发送机制通过isa指针找到对应的方法与实现。

上面我们说到的如果子类没有实现initialize方法,那么当我们有多个类继承了父类的时候,父类的initialize方法有可能会调用多次:

[[Student alloc] init];
[[Student1 alloc] init];

StudentStudent1都继承自Person,只实现了两个子类的initialize方法,没有实现父类的,那么当第一次调用两个子类的时候会输出如下:

2020-04-28 22:20:48.690528+0800 分类Category的本质[92558:17870282] Person initialize
2020-04-28 22:20:48.690715+0800 分类Category的本质[92558:17870282] Person initialize
2020-04-28 22:20:48.690859+0800 分类Category的本质[92558:17870282] Person initialize

我们发现父类的initialize方法调用了3次,我们通过上面的源码我们已经知道了只有当类在第一次接收消息的时候才会被调用,也就是说每个类只会initialize一次,但是父类的``initialize```为什么会被多次调用呢?

我们通过底层的逻辑将上面的代码转化为伪代码:

// 1. 调用子类Student之前先判断父类的initialize
if (Student没有调用initialize) {
    if (Student的父类Person没有调用initialize) {
        // 1.1 调用父类Person的initialize
        objc_msgSend)(Person类, SEL_initialize)
    }
}

// 1.2 调用子类Student的initialize
objc_msgSend)(Student类, SEL_initialize)

// 2. 调用子类Student1之前先判断父类的initialize
if (Student1没有调用initialize) {
    // **在此,父类已经initialize了,所以不再执行**
    if (Student1的父类Person没有调用initialize) {
        // 2.1 调用父类Person的initialize
        objc_msgSend)(Person类, SEL_initialize)
    }
}

// 2.2 调用子类Student1的initialize
objc_msgSend)(Student1类, SEL_initialize)

所以其实相当于最后发送了3次消息:

objc_msgSend)(Person类, SEL_initialize)
objc_msgSend)(Student类, SEL_initialize)
objc_msgSend)(Student1类, SEL_initialize)

消息发送机制是通过isa指针寻找到各自的元类对象中的类方法initialize的实现,但是两个子类都没有实现initialize方法,所以会通过super_class指针找到父类的实现,所有最后才会来到父类的initialize方法。

注意:虽然父类被多次调用initialize方法,但是父类也是只初始化了一次。

4.3 总结

loadinitialize的区别,以及它们在category重写的时候的调用的次序。

区别在于调用方式和调用时刻

调用方式:load是根据函数地址直接调用,initialize是通过objc_msgSend消息发送调用

调用时刻:loadruntime加载类、分类的时候调用(只会调用1次),initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)

调用顺序:先调用本类的load方法,先编译那个类,就先调用load。在调用load之前会先调用父类的load方法。分类中load方法不会覆盖本类的load方法,先编译的分类优先调用load方法。initialize先初始化父类,之后再初始化子类。如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次),如果分类实现了+initialize,就覆盖类本身的+initialize调用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345