Category探究

Category简介

category是Objective-C 2.0之后添加的语言特性,category的主要作用是为已经存在的类添加方法。除此之外,apple还推荐了category的另外两个使用场景。使用场景

  • 可以把类的实现分开在几个不同的文件里面。这样做有几个显而易见的好处:
  1. 可以减少单个文件的体积
  2. 可以把不同的功能组织到不同的类别里
  3. 可以由多个开发者共同完成一个类
  4. 可以按需加载想要的类别等等
  • 声明私有方法

Category和Extension

  • extension
    1> 编译期决议,他是类的一部分。
    2>一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加扩展,所以你无法为系统的类比如NSString添加扩展。
    3>可以添加实例变量

  • category
    1> 运行期决议。
    2>无法添加实例变量

Category底层

在objc-runtime-new.h中可以找到category_t,本文中用到的runtime源码是objc4-750.tar.gz

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

category_t结构体的定义可以看出,可以添加实例方法,类方法,协议,添加属性 ,但是不能添加实例变量。

为了弄清楚它的底层实现,我们先创建Person类和它的分类

@interface Person : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy)   NSString * name;
- (void)work;
@end
@implementation Person
- (void)work{
    NSLog(@"Person --> work");
}
@end

@interface Person (category1)
@property (nonatomic, copy) NSString * address;
- (void)work;
- (void)work1;
@end
@implementation Person (category1)
- (void)work{
    NSLog(@"Person (category1) --> work");
}

- (void)work1{
    NSLog(@"Person (category1) --> work1");
}
@end

进入相应的文件夹,在终端执行clang -rewrite-objc Person+category1.m,得到一个9w+多行的文件,在文件的最下面

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_Person_$_category1 
__attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {
{(struct objc_selector *)"work", "v16@0:8", (void *)_I_Person_category1_work},
{(struct objc_selector *)"work1", "v16@0:8", (void *)_I_Person_category1_work1}
    }
};
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_$_category1 
__attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {
{(struct objc_selector *)"category1", "v16@0:8", (void *)_C_Person_category1_category1}
    }
};
struct _objc_method {
    struct objc_selector * _cmd;
    const char *method_type;
    void  *_imp;
};

我们发现这两个结构体都是_method_list_t类型的,结构体中存放着内存的大小,方法的数量以及方法的列表,一个是对象方法,另一个是类方法,并分别进行赋值。

struct _protocol_t {
    void * isa;  // NULL
    const char *protocol_name;
    const struct _protocol_list_t * protocol_list; // super protocols
    const struct method_list_t *instance_methods;
    const struct method_list_t *class_methods;
    const struct method_list_t *optionalInstanceMethods;
    const struct method_list_t *optionalClassMethods;
    const struct _prop_list_t * properties;
    const unsigned int size;  // sizeof(struct _protocol_t)
    const unsigned int flags;  // = 0
    const char ** extendedMethodTypes;
};

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;  // Note, this is 32/64 bit
    struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_category1 
__attribute__ ((used, section ("__DATA,__objc_const"))) = {
    1,
    &_OBJC_PROTOCOL_NSCopying
};

首先我们看到_protocol_list_t中有协议的数量_protocol_t类型的结构体,在赋值的时候将1和_OBJC_PROTOCOL_NSCopying的地址赋给了_protocol_list_t结构体,而_OBJC_PROTOCOL_NSCopying_protocol_t类型的结构体,一一对应赋值,在为instance_methods赋值的时候调用了_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying,这是一个_method_list_t结构体类型,里面保存着要实现的协议方法copyWithZone

static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_category1 
__attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"address","T@\"NSString\",C,N"}}
};

struct _prop_t {
    const char *name;       // 属性的名称
    const char *attributes; // 属性的特质
};

然后就是_prop_list_t结构体了,分别是占用的内存大小,属性的数量和_prop_t类型的结构体,"T@"NSString" NSString类型,C:copy,N:Nonatomic。

Category的加载

在objc-os.mm中875行可以找到_objc_init

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

objc-runtime-new.mm文件中找到了map_images,load_images,unmap_imagemap_images内部会调用map_images_nolock函数,而map_images_nolock函数又会调用_read_images函数,在_read_images函数的最后找到分类相关代码:

// Discover categories. 
    for (EACH_HEADER) {
    // 循环遍历每一个类,取出当前类category_t的数组
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();
      // 遍历每一个类的category_t的数组,取出每一个category_t
        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);
                }
            }
        }
    }

    ts.log("IMAGE TIMES: discover categories");

通过_getObjc2CategoryList方法获得category的列表,然后遍历列表将category的实例方法,协议以及属性添加到类上,将category的类方法和协议添加到类的元类上。

static void addUnattachedCategoryForClass(category_t *cat, Class cls, 
                                          header_info *catHeader)
{
    runtimeLock.assertLocked();

    // 返回一个未添加的category的哈希表
    NXMapTable *cats = unattachedCategories();
    category_list *list;

    list = (category_list *)NXMapGet(cats, cls);
    if (!list) {
        list = (category_list *)
            calloc(sizeof(*list) + sizeof(list->list[0]), 1);
    } else {
        list = (category_list *)
            realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
    }
   //  更新list列表 
    list->list[list->count++] = (locstamped_category_t){cat, catHeader};
    NXMapInsert(cats, cls, list);
}
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertLocked();

    isMeta = cls->isMetaClass();

    // Returns the list of unattached categories for a class 
    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);
    }
}
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--) {
        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);
}

首先根据方法列表,属性列表,协议列表分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址,接着循环添加分类方法,属性以及协议放入对应mlist、proplists、protolosts数组中,调用attachLists方法进行添加合并操作。

 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;
            // 将原来数组地址向后移动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]));
        }
    }

在这个方法中将原来方法列表经过内存移动,再将category对应的方法列表拷贝到原来方法列表的前边,合并成一个新的方法列表,这也就是category重写了原来的方法会优先调用category方法的原因,只是在合并的时候将category的方法放在了前边,如果继续查找的话,就会找到原来类的方法。

深入理解的Objective-C:类别
探秘Runtime - 深入剖析Category

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

推荐阅读更多精彩内容