OC-Category实现的原理(一)

面试题:

  1. Category的使用场合是什么?

  2. Category的实现原理是什么?

  3. Category和类扩展有什么区别呢?

OC的类信息存放在哪里?

  • 对象方法、属性、成员变量、协议信息,存放在class对象中
  • 类方法,存放在meta-class对象中
  • 成员变量的具体值,存放在instance对象

isa指针

isa指针

iOS中的Category我们经常使用,主要是给一些类添加新的方法,或者拆分类。进行方法调用的时候,如果调用的是写在类里面的方法,调用顺序是:首先,实例对象根据它的isa找到类对象,然后去类对象里面的方法列表里面寻找方法的实现,如果找到,就会调用这个方法,完毕。但是如果调用的方法是写在分类里面,那么调用流程是什么呢?

其实无论写多少分类,最后运行的时候,Runtime会把分类所有的对象方法合并到类对象的方法列表里面,把类方法合并到元类对象的方法列表里面

Category的使用

先来看一个最简单的category结构,一下代码定义了一个MJPerson类 和它的一个category MJPerson+Test

@interface MJPerson : NSObject


- (void)run;

@end


@implementation MJPerson
-(void)run
{
    NSLog(@"CLPerson Run");
}
@end
// ******************** CLPerson+Test
#import "MJPerson.h"
@interface MJPerson (Test)
-(void)test;
@end

#import "MJPerson+Test.h"
@implementation MJPerson (Test)
-(void)test{
    NSLog(@"Test");
}
@end
// ******************** MJPerson+Eat
#import "MJPerson.h"
@interface MJPerson (Eat)
-(void)eat;
@end

#import "MJPerson+Eat.h"
@implementation MJPerson (Eat)
-(void)eat{
    NSLog(@"Eat");
}
@end

请问❓❓❓:以下的两个方法调用,底层到底发生了什么,它们本质是否相同?

MJPerson *person = [[MJPerson alloc]init];
[person run]; //类的实例方法调用
[person test];//分类的实例方法调用
[person eat];//分类的实例方法调用

RUN>

======================打印输出=========================
2021-04-18 10:14:41.204107+0800 Interview03-Category[1372:39543] MJPerson (Test) - run
2021-04-18 10:14:41.204501+0800 Interview03-Category[1372:39543] test
2021-04-18 10:14:41.204554+0800 Interview03-Category[1372:39543] eat

objc_msgSend(person, @selector(run));

[实例对象 方法]这种写法,经过底层转换之后,实际上就是,objc_msgSend(类对象, @selector(实例方法)),也就我们oc的一个基本概念,消息发送机制。

我们可以推定,[person run]这句代码,在消息发送机制下,首先会根据 personisa指针找到MJPerson的类对象,然后在类对象的方法列表(method_list_t \* methods)里面找到该方法的实现,然后进行调用。

接下来

  • 那么[person test][person eat]呢?它的消息是发送给谁呢?
  • 是发送给person的类对象吗?
  • 还是说,对于MJPerson+Test.hMJPerson+Eat.h来说,也有其独立对应的分类对象呢?
    带着这些思考和问题,我们接下来一步一步地进行拆解。

底层结构——所有一切始于编译

首先看一下编译的时候分类发生了什么。
创建一个MJPerson类,给MJPerson类添加一个MJPerson+Eat分类,在分类中添加属性、协议、对象方法、类方法,代码如下:

#import "MJPerson.h"
@interface MJPerson (Eat) <NSCopying, NSCoding>

- (void)eat;

@property (assign, nonatomic) int weight;
@property (assign, nonatomic) double height;

@end
#import "MJPerson+Eat.h"

@implementation MJPerson (Eat)

- (void)run
{
    NSLog(@"MJPerson (Eat) - run");
}

- (void)eat
{
    NSLog(@"eat");
}

- (void)eat1
{
    NSLog(@"eat1");
}

+ (void)eat2
{
    
}

+ (void)eat3
{
    
}
@end

进入MJPerson+Eat.m文件所在路径执行以下命令-->

$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MJPerson+Eat.m

得到编译后的c++文件MJPerson+Eat.cpp,将其拖入xcode项目中进行查看,但是不要加入编译列表。直接查看文件底部,就可以找到category相关的底层信息

struct _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; //属性
};

这就是分类的底层结构,相当于,我们写的分类编译完成之后就会变成上面那种结构,有多少分类就有多少上面的结构体。当程序运行的时候就会把上面结构体的东西合并到类对象或者元类对象里面去。

我们的分类MJPerson+eat被转换为类型为static struct _category_t,变量名为:_OBJC_$_CATEGORY_MJPerson_$_eat:

static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "MJPerson",
    0, // &OBJC_CLASS_$_MJPerson,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Eat,
    (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_MJPerson_$_Eat,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MJPerson_$_Eat,
};
image-20210418103155337

所以分类的功能我们也知道了,分类可以添加属性、协议、对象方法、类方法。编译的时候,系统会给每一个category生成一个对应的结构体变量,而且他们都是struct _category_t类型的,然后把category里面的信息存到这个变量里面。

struct _category_t中定义了六个成员变量

  • const char *name;
    • 其值表示category所对应的类的名字。
  • const struct _method_list_t *instance_methods;
    • 其值就是实例方法列表,可以看到里面正好放了我们定义的实例方法 -test
  • const struct _method_list_t *class_methods;
    • 其值就是类方法列表,可以看到里面放了我们定义的类方法 -classTest
  • const struct _protocol_list_t *protocols;
    • 其值就是协议列表,可以看到里面存放了 NSCoping协议
  • const struct _prop_list_t *properties;
    • 其值就是属性,可以看到里面有我们定义的age属性
//对象方法列表
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_MJPerson_Eat_eat},
    {(struct objc_selector *)"eat1", "v16@0:8", (void *)_I_MJPerson_Eat_eat1}}
};

//类方法列表
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {{(struct objc_selector *)"eat2", "v16@0:8", (void *)_C_MJPerson_Eat_eat2},
    {(struct objc_selector *)"eat3", "v16@0:8", (void *)_C_MJPerson_Eat_eat3}}
};

//下面是关于NSCopy协议的一些描述
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;

//下面是关于NSCoding协议的一些描述
static const char *_OBJC_PROTOCOL_METHOD_TYPES_NSCoding [] __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "v24@0:8@\"NSCoder\"16",
    "@24@0:8@\"NSCoder\"16"
};

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[2];
} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {{(struct objc_selector *)"encodeWithCoder:", "v24@0:8@16", 0},
    {(struct objc_selector *)"initWithCoder:", "@24@0:8@16", 0}}
};

struct _protocol_t _OBJC_PROTOCOL_NSCoding __attribute__ ((used)) = {
    0,
    "NSCoding",
    0,
    (const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding,
    0,
    0,
    0,
    0,
    sizeof(_protocol_t),
    0,
    (const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCoding
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCoding = &_OBJC_PROTOCOL_NSCoding;

//协议列表
static struct /*_protocol_list_t*/ {
    long protocol_count;  // Note, this is 32/64 bit
    struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    2,
    &_OBJC_PROTOCOL_NSCopying,
    &_OBJC_PROTOCOL_NSCoding
};

//属性和列表
static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    2,
    {{"weight","Ti,N"},
    {"height","Td,N"}}
};

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

//把上面的列表传入这个结构体中
static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "MJPerson",
    0, // &OBJC_CLASS_$_MJPerson,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Eat,
    (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_MJPerson_$_Eat,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MJPerson_$_Eat,
};

观察代码的最后那个结构体,可以发现,上面的代码是给分类的_category_t结构体赋值,分别给名称赋值MJPerson、cls赋值0、对象方法列表赋值、类方法列表赋值、协议赋值、 属性赋值。

下面我们在objc4源码里面查看一下category_t结构体的定义,打开源码,搜索“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);
};

可以发现和C++里面的定义大同小异。

总结:

编译完成之后,分类里的所有东西都在category_t结构体中,暂时和类是分开的。

Runtime-category

下面我们就通过分析objc4源码证明,最后运行的时候,Runtime就会把这个类所有的对象方法合并到类对象的方法列表里面,把类方法合并到元类对象的方法列表里面去。

源码解读顺序:
objc-os.mm文件

_objc_init (运行时的入口,运行时的初始化)
map_images
map_images_nolock

objc-runtime-new.mm文件

_read_images (加载一些模块、镜像,参数传入所有的类信息)
remethodizeClass (核心方法,将类对象和元类对象重新组织下)
attachCategories (核心方法,参数传入类对象(或者元类对象)和分类)
attachLists
realloc、memmove、 memcpy

从Runtime源码查看Category

  1. 搜索并打开objc-os.mm源文件,找到void _objc_init(void)方法.此方法是Runtime初始化入口.
/***********************************************************************
* _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();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

_dyld_objc_notify_register(&map_images, load_images, unmap_image)。这个函数里面的三个参数分别是另外三个函数:

  • map_images -- Process the given images which are being mapped in by dyld.(处理那些正在被dyld映射的镜像文件)
  • load_images -- Process +load in the given images which are being mapped in by dyld.(处理那些正在被dyld映射的镜像文件中的+load方法)
  • unmap_image -- Process the given image which is about to be unmapped by dyld.(处理那些将要被dyld进行去映射操作的镜像文件)
  1. 点击进入map_images
    image-20210418131459652
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);
}
  1. 点击进入map_images_nolock:
  2. 点击进入_read_images(现在开始已经进入加载模块)
    image-20210418131923416
  3. _read_images中找到// Discover categories.(搜索分类),我们重点研究这里:
image-20210418132029035
// Discover categories. 
    for (EACH_HEADER) {
        category_t **catlist = //二位数组 category_t **catlist 是一个二位数组,里面存放的使我们给一个类创建的所有分类
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];//获取一个分类,比如MJPerson+Eat
            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. 这里开始处理category的内容
            // First, register the category with its target class. 1、将category注册到目标类
            // Then, rebuild the class's method lists (etc) if     2、重建 目标类里面的方法列表(如果这个类已经被实现)
            // the class is realized. 
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  //判断是否存在实例方法,协议,属性
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);//向目标类注册category
                if (cls->isRealized()) {
                    remethodizeClass(cls);//重新组织方法。可以理解为重建class的方法列表
                    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);//isa 元类对象,这一步是向元类对象注册category
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());//传入类的ISA指针,元类对象,重新组织方法,重建meta-class的方法列表
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }

category_t **catlist 是一个二位数组,里面存放的使我们给一个类创建的所有分类,比如上面我们给MJPerson类添加了的分类MJPerson+Test,MJPerson +Eat就存放在这个二维数组中:[[category_t_eat],[category_t_test]].

最核心的方法remethodizeClass();重新组织方法,传入类对象就是重新组织实例方法,传入cls->ISA()就是重新组织类方法.

  1. 点击进入remethodizeClass(),找到attachCategories(cls, cats, true /*flush caches*/);
/***********************************************************************
* 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*/);//cla 元类对象 ,cats 分类信息        
        free(cats);
    }
}
  1. 点击进入attachCategories(cls, cats, true /*flush caches*/);
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    //分配一块空间来放所有分类的方法数组,这里是一个二维数组,数组的每个成员,对应着某个分类的方法数组
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    //分配一开空间来放所有分类的属性数组,理解同上
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    //分配一块空间来放所有分类的协议数组,理解同上
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {//这里i--说明,是从
        //取出某个分类变量
          entry = cats->list[i];
        //提取分类中的对象方法/类方法
        /* mlists最终会是以下形式
         [
            [method_t, method_t],
            [method_t, method_t]
         ]
         
         */
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        //提取分类中的属性
        /* proplists最终会是以下形式
         [
         [property_t, property_t],
         [property_t, property_t]
         ]
         
         */
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
        
        //提取分类中的协议
        /* protolists最终会是以下形式
         [
         [protocol_t, protocol_t],
         [protocol_t, protocol_t]
         ]
         
         */
        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_caches  &&  mcount > 0) flushCaches(cls);
    //将所有分类的属性附加到类对象的属性列表中
    rw->properties.attachLists(proplists, propcount);
    free(proplists);
    //将所有分类的协议附加到类对象的协议列表中
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
    
    //搞定,结束
}

我们梳理一下attachCategories (cls,cats,true)方法.attach一词是附加的意思,从名字上我们可以看出这个方法大概意思是:附加分类.事实上它的确如此,下面我开始研究:

  1. 参数分析:
  • cls , 分类的本类,就是我们现在MJPerson
  • cats , 分类列表,[category_t (MJPerson+run),category_t (MJPerson+eat), category_t (MJPerson+Test)].
  1. 首先分配内存,创建三个二维数组mlists,proplists,protolists分别用来存放方法列表,属性列表,协议列表
  2. 通过while循环遍历分类列表cats,取出某一个分类.再取出分类中的方法列表,属性列表,协议列表,分别存放到mlists,proplists,protolists二维数组中.
  3. 取出classrw,rw中存放着类的方法,属性,协议,成员变量等信息.
  4. 分别调用rw
    rw->methods.attachLists(mlists, mcount),
    rw->properties.attachLists(proplists, propcount),
    rw->protocols.attachLists(protolists, protocount),
    把分类中的方法列表,属性列表,协议列表归并到本类中.

上面的代码主要做的是将所有的分类的数据拿出来放到数组中,并且是最后面的分类放到最前面,详细解释可一步步看注释,下面进入attachLists方法

  1. 点击进入void attachLists(List* const * addedLists, uint32_t addedCount);
void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return; //addedCount分类的个数
        
        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原来的方法列表
            //addedCount分类的个数
            //将原来的方法列表往后移addedCount位置 前面的就空出来了
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));

            //addedLists所有分类的方法列表 
            //将所有分类的方法列表放到前面
            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]));
        }
  }
  • addedLists--将要被添加的category中的方法列表组成的的数组,
  • addedCount--addedLists数组的元素数量

最终类的方法列表里面,如果class有自己对应的category,那么category中的方法列表会被合并放置在class的方法列表的前部,类本身的方法则会被往列表尾部挪,当我们通过[obj method]的方式调用方法的时候,系统会到在类的方法列表里面,从前往后遍历查找。

我们在上面分析attachCategories方法的时候得知,该方法实际上将category的方法列表按照编译顺序倒过来存到了一个数组里,供后续方法使用。

解释:

首先,扩容,然后将原来的方法列表挪到后面去,再将所有分类列表放到原来列表的位置。最后结果是分类在前面,原来的类在后面,所以,同样的方法会优先调用分类。如果不同分类中有相同的方法,由于后编译的分类放到了前面,所以后编译的分类会优先调用。

可在下图中查看和改变编译顺序:

编译顺序.png

在class的方法列表里面,最后参加编译的category的方法会出现在方法列表的最前面,先参加编译的category的方法会出现在方法列表的后面,列表的最后存着class自己的方法[对于meta-class也是一样的]

总结:

① 运行的时候,通过Runtime加载某个类的所有Category数据
② 把所有Category的属性、协议、方法数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面
③ 将合并后的分类数据(属性、协议、方法),插入到类原来数据的前面

补充:

memmove内存挪动和memcpy内存拷贝的区别:

分类添加示意图.png

如图,如果内存中的方法列表顺序是 3 4 1 ,如果想把3和4放到最后面两位(3 3 4),如果使用memcpy,会先变成 3 3 1,再变成 3 3 3。
如果使用memmove就直接一步到位将最后两位变成3 4,结果为 3 3 4。

所以,对于原来的类,肯定要一字不差的挪动过来,所以使用了memmove,而对于分类里的数据,就一个一个拷贝过来就好了,没必要用move。

面试题:

  1. Category的使用场合是什么?
    主要是给一些类添加新的方法,或者拆分类。
  2. Category的实现原理是什么?
    Category编译之后的底层结构是struct category_t,里面存储着分类的属性、协议、对象方法、类方法。
    在程序运行的时候,Runtime会将Category的数据,合并到类信息中(类对象、元类对象中)。
  3. Category和类扩展有什么区别呢?
    类扩展就是在类的.m里面添加一些属性,成员变量,方法声明,如下:
// class extension (类扩展)
@interface MJPerson()
{
    int _abc;
}
@property (nonatomic, assign) int age;

- (void)abc;
@end

其实类扩展就是相当于将原来写在.h文件里面的东西剪掉放到.m里面,把原来公开的属性,成员变量,方法声明私有化。所以,类扩展里面的东西在编译的时候就已经存在类对象里面了,这点和分类不同。

特别备注

本系列文章总结自MJ老师在腾讯课堂iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!

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

推荐阅读更多精彩内容