Cateogry底层学习

一、概述

Category,也就是分类,能够在不修改原类代码的基础上,为类增加一些方法和属性。

Category的好处:

  • category可以在原类的基础上扩展类的方法;可以实现私有类的方法扩展以及一些源码类的扩展。也就是说不用修改原类的代码结构,比继承更为轻量级;
  • category可以将类的实现分散到多个文件。减少类的冗余;
  • category可以创建私有方法的前向引用:在类别里提供一个类的私有方法的声明(不需要提供实现),当实例调用的时候,编译器不会报错,运行时会调用类的私有方法,继承不能继承私有方法的实现。
  • category可以向类添加非正式协议:因为Object-C里面所有类都是NSObject的子类,所以NSObject的类别里定义的函数,所有对象都能使用。

注意:

  • 1.category里面不能添加成员变量。但是可以通过关联对象间接的实现成员变量效果。这种方式在关联对象学习。会讲到。
  • 2.category定义的方法如果和类里面的方法同名,则会覆盖原来类定义的方法。
  • 3.如果一个类有多个category,每个category定义了同一个方法,则类调用的方法是按照类的编译顺序调用,即先调用最后编译的分类的方法。

二、category分类的原理

  • category编译之后的底层原理是struct category_t,里面存储着分类的对象方法、类方法、属性信息、协议信息。
  • 在程序运行的时候,runtime会动态的将分类的数据合并到类对象中。

在验证这个问题之前,我们先分析下分类中生成的底层结构。我们先看一段代码

@interface Person (Test)
- (void)test;
@end

@implementation Person (Test)
- (void)test{
    NSLog(@"%s",__func__);
}
@end

反编译成C++文件,其中有一段代码:

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

在这个结构体内,const char *name;是分类名;
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;是属性列表。

在C++文件中,在struct _category_t后面有这么一段代码

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

这段代码是按照顺序给struct _category_t 结构体内赋值。
其中_CATEGORY_INSTANCE_METHODS_Person_方法是test实例方法:如下

_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"test", "v16@0:8", (void *)_I_Person_Test_test}}
};

_CATEGORY_CLASS_METHODS_Person_方法是test2类方法,如下:

 _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"test2", "v16@0:8", (void *)_C_Person_Test_test2}}
};

接下来看下category方法合并的原理
runtime的源码可以直接在苹果官网上下载,这里看的是最新版objc4-723,按照

objc-os.mm
_objc_init
map_images
map_images_nolock
_read_images

轨迹,在objc-runtime-new.mm先找到_read_images函数:

这里只贴出加载category的源代码(不必细看)
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
 ...  
 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);
                }
            }
        }
 ... }

上面代码提到了比较重要的函数:
remethodizeClass(cls)

remethodizeClass方法的实现:

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertWriting();

    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方法的实现

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--) {
        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_caches  &&  mcount > 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

remethodizeClass函数调用attachCategories(cls, cats, true ) 方法,并给cls传递参数(类或者元类对象),给cats传递参数(分类列表)。cats分类列表中,包括类对象所有的分类。

   方法数组
    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));

while (i--)循环中

  • auto& entry = cats->list[i]方法取出分类列表中的某个分类;
  • entry.cat->methodsForMeta(isMeta)方法取出分类中的方法列表放到二维数组method_list_t中;
  • entry.cat->propertiesForMeta(isMeta, entry.hi)方法取出分类中的属性列表放到二维数组property_list_t中;
  • entry.cat->protocols方法取出分类中的协议信息列表,放到protocol_list_t二维数组中。
    经过循环,就把所有category里的方法,属性,协议列表都添加相应的二维数组中。
    objc_class结构
  • rw->methods.attachLists(mlists, mcount);是将所有分类的对象方法,附加到类对象的方法列表中;
  • rw->properties.attachLists(proplists, propcount);是将所有分类的属性,附加到类对象的属性列表中
  • rw->protocols.attachLists(protolists, protocount);是将所有分类的协议,附加到类对象的协议列表中。
    经过这些方法的调用,把所有分类中的信息合并到objc_class结构中。
合并过程
 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;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
......
}
  • memmove先会扩容,扩容的大小为newCount = oldCount + addedCount。然后把原来的内存单元中的内容,往后移动。移动的距离为addedCount数量。
  • memcpy把分类中的方法,属性或者协议,移动到上个步骤中的空置单元格。

因为移动后,分类中的方法或者属性或者协议在内存的最前面,所有在类对象调用方法时,先调用分类中的方法或者属性。

三、Category中的load方法

  • +load方法会在runtime加载类、分类时调用,根据函数地址直接调用。
  • 每个类、分类中的+load方法,在程序运行过程中只会调用一次。

调用顺序

  • 1、先调用类的+load
    按照编译顺序调用(先编译,先调用)
    调用子类的+load方法之前,会先调用父类的+load
  • 2、在调用分类的+load
    按照编译顺序调用(先编译,先调用)
void call_load_methods(void)
{
...
    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);
...
}

在苹果给的源码中可以看出,call_class_loads();先是调用类的+load;
call_category_loads()再调用分类的+load

static void call_class_loads(void)
{
...
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

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

在上面苹果给的源码中可以看出,load_method_t load_method = (load_method_t)classes[i].method;经过循环调用,找到classes存储的类的地址,直接调用+load方法。分类方法调用+load,也是如此。

四、Category中的initialize方法

  • +initialize方法会在类第一次接收到消息时调用;是通过objc
    调用顺序
  • 先调用父类的+initialize,再调用子类的+initialize
    如果子类没有实现+initialize,则再次调用父类的+initialize。即父类的+initialize可能调用多次。

在苹果给的源码中,根据以下过程

objc-runtime-new.mm
class_getInstanceMethod
lookUpImpOrNil
lookUpImpOrForward
_class_initialize
callInitialize
objc_msgSend(cls, SEL_initialize)

来分析

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
...
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
    }
...
}

上述方法中_class_initialize()传入相应的类,然后调用

void _class_initialize(Class cls)
{
...
  supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        _class_initialize(supercls);
    }
}
...
     {
            callInitialize(cls);
            if (PrintInitializing) {
                _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
                             pthread_self(), cls->nameForLogging());
            }
        }

在上述方法中,调用callInitialize(cls);并传入类

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

推荐阅读更多精彩内容