Category 底层分析

Category 主要作用是在不改变原有类的基础上,动态的给已存在的类添加一些方法和属性。

分类(Category)在编译之后的底层结构是 struct category_t,里面存储着当前分类的对象方法、类方法、属性、协议信息,程序在运行的时候,运行时(Runtime)会将分类(Category)中所有的方法、属性、协议数据,合并到类信息中(类对象、元类对象中)

分类编译之后的底层结构

  • 1.1 对当前已存在的类,新建一个分类(此处创建的是一个名为 YXCPerson+Test 的分类),然后使用 clang 将当前创建的分类进行转换
    YXCPerson+Test.h 文件

    // 属性:age 、num 还有一个 不同寻常的 custId 
    // 实例方法(对象方法): test()、eat()
    // 类方法 : sing()
    // 协议 : 遵守了 NSCopying, NSCoding 协议
    @interface YXCPerson (Test)<NSCopying, NSCoding>
    
    @property (nonatomic, assign) int age;
    @property (nonatomic, assign) int num;
    @property (nonatomic, copy, class) NSString *custId;
    
    - (void)test;
    
    - (void)eat;
    
    + (void)sing;
    
    @end
    

    clang 指令

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 分类名称.m
    
  • 1.2 底层结构展示

    struct _category_t {
        const char *name; // 类名
        struct _class_t *cls; // 已存在的类(YXCPerson)
        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; // 属性列表
    };
    
  • 1.3 _class_t 底层结构

    struct _class_t {
        struct _class_t *isa; // isa 指针,实例对象指向类对象,类对象指向元类对象,元类对象指向基类的元类对象(一般是 NSObject)
        struct _class_t *superclass; // 父类
        void *cache; // 缓存
        void *vtable;
        struct _class_ro_t *ro; // 存储的原来类(非分类)的一些方法、协议、属性、成员变量等信息
    };
    
  • 1.4 _class_ro_t 底层结构

    struct _class_ro_t {
        unsigned int flags;
        unsigned int instanceStart;
        unsigned int instanceSize;
        const unsigned char *ivarLayout;
        const char *name;
        const struct _method_list_t *baseMethods;
        const struct _objc_protocol_list *baseProtocols;
        const struct _ivar_list_t *ivars;
        const unsigned char *weakIvarLayout;
        const struct _prop_list_t *properties;
    };
    
  • 1.5 YXCPerson+Test 分类在底层结构为

    static struct _category_t _OBJC_$_CATEGORY_YXCPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
    {
        "YXCPerson", // 原来的类名
        0, // &OBJC_CLASS_$_YXCPerson,
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_YXCPerson_$_Test, // 实例对象方法列表
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_YXCPerson_$_Test, // 类对象方法列表
        (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_YXCPerson_$_Test, // 协议信息列表
        (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_YXCPerson_$_Test, // 属性列表
    };
    

    可以发现,在编译之后 YXCPerson+Test 这个分类,在底层的结构是一个 _category_t 类型,并且名称为 _OBJC_$_CATEGORY_YXCPerson_$_Test 的结构体。由此可以推测,每个分类在编译之后,都是一个 _category_t 类型,并且命名按照 _OBJC_$_CATEGORY_已存在的类名_$_分类名称 这样的一个方式。

  • 1.6 实例(对象)方法

    _OBJC_$_CATEGORY_INSTANCE_METHODS_YXCPerson_$_Test 这样的一个结构体,顾名思义这是存储着我们当前这个分类的一些实例方法数据,并且为 _method_list_t 类型的结构体

    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_YXCPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method), // 获取到 _objc_method 结构体的所需要的内存空间大小,赋值到 entsize
        2, // method_count 为 2
        {
            {(struct objc_selector *)"test", "v16@0:8", (void *)_I_YXCPerson_Test_test},
            {(struct objc_selector *)"eat", "v16@0:8", (void *)_I_YXCPerson_Test_eat}
        } // 将 test()、eat() 方法放入一个数组,然后赋值给 method_list
    };
    

    struct objc_selector 实际上是一个 SEL

    typedef struct objc_selector *SEL;
    

    _objc_method 结构为

    struct _objc_method {
        struct objc_selector * _cmd; // SEL 地址
        const char *method_type; // 方法签名
        void  *_imp; // 方法实现
    };
    

    通过以上分析,可以总结出:

    1. OC 中实例(对象)方法在底层的实现是一个 _objc_method 类型的结构体,它包含了方法的声明、签名以及实现,编译器会将方法的声明、签名、实现信息放入到这个结构体当中存储起来。

    2. 将一个个的实例(对象)方法通过 _objc_method 结构体存储好后,放入一个 _method_list_t 结构体中的 method_list 数组中(这个数组的个数会根据当前分类的方法个数,分配空间),同时按照 _OBJC_$_CATEGORY_INSTANCE_METHODS_原类名称_$_分类名称 这样的一个格式给这个结构体取名。

    3. 最后将 _method_list_t 类型的赋值给 _category_t 中的 instance_methods,这样就将当前分类中的实例(对象)方法存储到了当前分类结构体中去了。

  • 1.7 类方法

    类方法存储到一个名为 _OBJC_$_CATEGORY_CLASS_METHODS_YXCPerson_$_Test 的结构体,这个结构体也是一个 _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_method 类型的结构体数组
    } _OBJC_$_CATEGORY_CLASS_METHODS_YXCPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        1,
        {{(struct objc_selector *)"sing", "v16@0:8", (void *)_C_YXCPerson_Test_sing}}
    };
    
    
  • 1.8 协议信息

    协议信息存储到了一个名为 _OBJC_CATEGORY_PROTOCOLS_$_YXCPerson_$_Test_protocol_list_t 类型的结构体中,_protocol_list_t 结构体。

    static struct /*_protocol_list_t*/ {
        long protocol_count;  // Note, this is 32/64 bit
        struct _protocol_t *super_protocols[2];
    } _OBJC_CATEGORY_PROTOCOLS_$_YXCPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        2,
        &_OBJC_PROTOCOL_NSCopying,
        &_OBJC_PROTOCOL_NSCoding
    };
    
  • 1.9 属性信息

    属性信息存储到了一个名为 _OBJC_$_PROP_LIST_YXCPerson_$_Test_prop_list_t 类型的结构体,其中这个结构体中有一个 prop_list 属性,里面存放的就是当前分类所有的属性,当然在底层的结构是一个 _prop_t 结构体。

    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_YXCPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_prop_t),
        2,
        {{"age","Ti,N"},
        {"num","Ti,N"}}
    };
    
    struct _prop_t {
        const char *name;
        const char *attributes;
    };
    

分类加载处理过程

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

下面通过源码来查看这个过程,下载最新的源码

  1. 找到 objc-os.mm 文件,并且找到 _objc_init 函数,在 _objc_init 函数中有一个 _dyld_objc_notify_register 函数,这个函数第一个参数传入了一个镜像(map_images

    /***********************************************************************
    * _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();
        runtime_init();
        exception_init();
        cache_init();
        _imp_implementationWithBlock_init();
    
        _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    
        #if __OBJC2__
        didCallDyldNotifyRegister = true;
        #endif
    }
    
  2. objc-runtime-new.mm 文件中,找到 map_images 函数,发现返回的结果是通过调用 map_images_nolock 函数得到的结果

    /***********************************************************************
    * map_images
    * Process the given images which are being mapped in by dyld.
    * Calls ABI-agnostic code after taking ABI-specific locks.
    *
    * Locking: write-locks runtimeLock
    **********************************************************************/
    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);
    }
    
  3. objc-os.mm 文件中找到 map_images_nolock 函数,查看该函数

    ...
    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
    
  4. 跳转到 _read_images 函数中查看,位于 objc-runtime-new.mm

    // Discover categories. Only do this after the initial category
    // attachment has been done. For categories present at startup,
    // discovery is deferred until the first load_images call after
    // the call to _dyld_objc_notify_register completes. rdar://problem/53119145
    if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            // 加载分类信息
            load_categories_nolock(hi);
        }
    }
    
  5. 跳转到 load_categories_nolock 函数中查看

    ...
    if (cat->instanceMethods ||  
        cat->protocols ||  
        cat->instanceProperties)
        {
            if (cls->isRealized()) {
                // 拼接分类信息
                attachCategories(cls, &lc, 1, ATTACH_EXISTING);
            } else {
                objc::unattachedCategories.addForClass(lc, cls);
            }
        }
    ...
    
  6. 跳转到 attachCategories 这个函数中查看

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