category原理探究-1

category探究准备

先来创建我们测试需要的类:


<!-- Animal类 -->
@interface Animal : NSObject
- (void)animal;
@end

<!-- Animal+Eat分类 -->
@interface Animal (Eat) <NSCopying, NSCoding>
@property (nonatomic, assign) int age;

- (void)eat;
@end

<!-- Animal+Play分类 -->
@interface Animal (Play)
- (void)play;
@end

以Eat分类为例,请出 clang 命令:clang -rewrite-objc Animal+Eat.m ,生成.cpp文件。

category的真面目

在.cpp文件最下面可以找到category被编译后的结构:


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

  • name 这里的name表示的是 类名 而不是category的名字。
  • cls 要扩展的类对象,编译期间值为空,在被runtime加载时根据name对应到类对象。
  • instance_methods category所有的实例方法。
  • class_methods category所有的类方法。
  • protocols category实现的所有协议。
  • properties category的所有属性。

再来看看我们的Animal+Eat被编译成了什么:


static struct _category_t _OBJC_$_CATEGORY_Animal_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
  "Animal",
  0, // &OBJC_CLASS_$_Animal,
  (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Eat,
  0,
  (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Animal_$_Eat,
  (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Animal_$_Eat,
};

看一下结构体的名称:_OBJC_$_CATEGORY_Animal_$_Eat,最后面的Eat就是我们分类的名称,前面有表示CATEGORY和类名Animal,这也就是为什么同一个类的category不能重名的原因了。

再对应一下其他的结构,例如instance_methods:


static struct /*_method_list_t*/ {
  unsigned int entsize; // sizeof(struct _objc_method)
  unsigned int method_count;
  struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Eat __attribute__ ((used, section   ("__DATA,__objc_const"))) = {
  sizeof(_objc_method),
  1,
  {{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_Animal_Eat_eat}}
};

我们里面只有一个 eat 方法,被编译后为 _I_Animal_Eat_eat

最后可以看到所有的category被放到了一个数组中,存在了 __DATA 段下的 __objc_catlist section 里了:


static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
  &_OBJC_$_CATEGORY_Animal_$_Eat,
};

这里编译期间的工作就做完了,接下来进入runtime。

runtime加载category

先下载一下苹果官方runtime的源码 这里,当然官方的编译是失败,要想调试runtime的请看 这里

大致加载的流程如下:

  • 找到runtime的入口:objc-os.mm_objc_init 方法,在library加载前由libSystem dyld调用,进行初始化操作。
  • 调用map_images方法将文件中的image map到内存。
  • 调用_read_images方法初始化map后的image。
  • 找到 Discover categories 可以看到 category_t 是通过 _getObjc2CategoryList 方法初始化的,这个方法拿出来看看:

#define GETSECT(name, type, sectname)  \
  type *name(const headerType *mhdr, size_t *outCount) { \
  return getDataSection<type>(mhdr, sectname, nil, outCount);  \
 }  \

 type *name(const header_info *hi, size_t *outCount) {  \
   return getDataSection<type>(hi->mhdr(), sectname, nil, outCount); \
 }

GETSECT(_getObjc2CategoryList, category_t *, "__objc_catlist");

看到这里有没有很熟悉,在这里加载的 __objc_catlist 就是在编译期间存放的数据。

来看一下加载的源码:


for (EACH_HEADER) {
    /**
    * 取出 category 数据  此处为数组代表一个类所有的分类
    */
    category_t **catlist = _getObjc2CategoryList(hi, &count);
    for (i = 0; i < count; i++) {
        /**
        * 按顺序取出 category_t 
        */
        category_t *cat = catlist[i];
        /**
        * remapClass:加载category_t的class指针
        */
        Class cls = remapClass(cat->cls);

        if (cat->instanceMethods || cat->protocols || cat->instanceProperties) 
        {
            addUnattachedCategoryForClass(cat, cls, hi);
            if (cls->isRealized()) {
                remethodizeClass(cls);
            }
        }

        if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) 
        {
            addUnattachedCategoryForClass(cat, cls->ISA(), hi);
            if (cls->ISA()->isRealized()) {
                remethodizeClass(cls->ISA());
            }
        }
    }
}

可以看到每次循环中 category_t 的加载 addUnattachedCategoryForClass 方法有两个调用,对比一下参数可以发现第二个参数不同 cls 和 cls->ISA(),再结合判断条件的 cat->instanceMethods 和 cat->classMethods,这两次的加载是将category中的信息分别加载到类和元类中,然后再调用 remethodizeClass 重新组织结构。接下来调用附加信息的方法 attachCategories ,将分类的信息附加到类中:


attachCategories(Class cls, category_list *cats, bool flush_caches)
{
     /**
     * 是否为元类
     */ 
     bool isMeta = cls->isMetaClass();
     /**
     * 方法数组
     */
     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];
         /**
         * 取出某个分类的方法列表 (根据 isMeta 来判断取实例方法还是类方法)
         */
         method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
         if (mlist) {
             /**
             * 将分类方法列表正序添加到 mlists
             */        
             mlists[mcount++] = mlist;
         }
         /**
         * 取出某个分类的属性列表
         */
         property_list_t *proplist = entry.cat->propertiesForMeta(isMeta, entry.hi);
         if (proplist) {
             /**
             * 将分类属性列表正序添加到 proplists
             */  
             proplists[propcount++] = proplist;
         }
         /**
         * 取出某个分类的协议列表
         */
         protocol_list_t *protolist = entry.cat->protocols;
         if (protolist) {
             /**
             * 将分类协议列表正序添加到 protolists
             */  
             protolists[protocount++] = protolist;
         }
     } 
     /**
     * 取出类的信息数据 class_rw_t
     */
     auto rw = cls->data();
     /**
     * 初始化方法的一些信息,比如有没有实现retain、release、allocWithZone等方法。
     */
     prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
     /**
     * 将所有分类的方法、属性、协议列表附加到类的方法、属性、协议列表中。
     */
     rw->methods.attachLists(mlists, mcount);
     rw->properties.attachLists(proplists, propcount);
     rw->protocols.attachLists(protolists, protocount);
}

由上面的while循环可以看到加载方法、协议、属性的时候是 倒序 加载的,是不是想到了什么?如果Animal类和两个分类都有一个 -(void)run 方法,那么最终会调用哪个里面的run方法呢?答案当然是最后加载的那个run方法,不过没有被调用的run方法并没有被 覆盖 ,方法还在那里只是按顺序没有被调用。

最后看一下 methods.attachLists 方法:


/**
* 将分类的  方法、协议、属性等信息附加到类中
*/
void attachLists(List* const * addedLists, uint32_t addedCount) {
    uint32_t oldCount = array()->count;
    uint32_t newCount = oldCount + addedCount;
    /**
    * 重新分配内存(大小为: oldCount addedCount 原有count和要添加的count总和)
    */ 
    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]));
}

重新布局的时候有两个方法:

  • memmovevoid *memmove(void *__dst, const void *__src, size_t __len); 可以看到是将src变量的数据移动到dst,所以最终是将 array()->lists 的数据移动到了 array()->lists + addedCount 的位置。

  • memcpyvoid *memcpy(void *__dst, const void *__src, size_t __n); 可以看到是将src变量的数据copy到dst,所以最终是将分类中的信息 addedLists copy 到 array()->lists 的位置。

正如我们上面说的run方法,Animal类中的run方法是被最后加载的,因为Animal类中的方法列表被移动到了分类的后面,加载的时候会先调用分类中的方法,而且可以看到Animal中的run方法确实没有被覆盖,只是调用的时候发现分类中有不会再调用Animal的run方法而已。

class extention 与 category

上面知道了category,我们再来看看class extention,class extention算是一种特殊的分类(匿名分类),那么我们可以思考平时在 .m 文件的匿名分类中写的私有属性、方法等在加载的时候会不会和分类一样呢?我们来验证一下,在Animal的 .m 文件里添加属性 height 和方法 test:


@interface Animal ()
@property (nonatomic, assign) int height;
- (void)test;
@end

@implementation Animal
- (void)animal {

}

- (void)test {

}
@end

用clang命令来编译 Animal:clang -rewrite-objc Animal.m


/**
* 元类结构
*/
static struct _class_ro_t _OBJC_METACLASS_RO_$_Animal __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    1, sizeof(struct _class_t), sizeof(struct _class_t), 
    (unsigned int)0, 
    0, 
    "Animal",
    0, 
    0, 
    0, 
    0, 
    0, 
};

/**
* 类结构
*/
static struct _class_ro_t _OBJC_CLASS_RO_$_Animal __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    0, __OFFSETOFIVAR__(struct Animal, _height), sizeof(struct Animal_IMPL), 
    (unsigned int)0, 
    0, 
    "Animal",
    (const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Animal,
    0, 
    (const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_Animal,
    0, 
    0, 
};

/***************************************/
/**
* 可以看到类中方法列表 ‘_INSTANCE_METHODS_Animal’对应下面的结构
* animal、test、height、setHeight 方法都在类结构中
*/
static struct /*_method_list_t*/ {
    unsigned int entsize; // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[4];
} _OBJC_$_INSTANCE_METHODS_Animal __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    4,
    {{(struct objc_selector *)"animal", "v16@0:8", (void *)_I_Animal_animal},
    {(struct objc_selector *)"test", "v16@0:8", (void *)_I_Animal_test},
    {(struct objc_selector *)"height", "i16@0:8", (void *)_I_Animal_height},
    {(struct objc_selector *)"setHeight:", "v20@0:8i16", (void *)_I_Animal_setHeight_}}
};

编译的结果如上,可以看到匿名类别的编译结果并不是 category_t 的类型在 runtime 时加载的,而是直接在编译期间将相关的属性方法等加载到了类中,匿名分类声明的属性方法相当于在类的 .h 文件的声明。

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,081评论 1 32
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,282评论 8 265
  • 引导 对于从事 iOS 开发人员来说,所有的人都会答出「 Runtime 是运行时 」,什么情况下用 Runtim...
    Winny_园球阅读 4,185评论 3 75
  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,697评论 7 64
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9