程序员社区

OC-Category实现的原理(一)

面试题:

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

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

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

OC的类信息存放在哪里?

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

isa指针

OC-Category实现的原理(一)插图
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相关的底层信息

OC-Category实现的原理(一)插图1
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,
};
OC-Category实现的原理(一)插图2
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
    OC-Category实现的原理(一)插图3
    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(现在开始已经进入加载模块)
    OC-Category实现的原理(一)插图4
    image-20210418131923416
  3. _read_images中找到// Discover categories.(搜索分类),我们重点研究这里:
OC-Category实现的原理(一)插图5
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的方法列表按照编译顺序倒过来存到了一个数组里,供后续方法使用。

解释:

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

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

OC-Category实现的原理(一)插图6
编译顺序.png

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

总结:

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

补充:

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

OC-Category实现的原理(一)插图7
分类添加示意图.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对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!

赞(0) 打赏
未经允许不得转载:IDEA激活码 » OC-Category实现的原理(一)

一个分享Java & Python知识的社区