iOS底层原理 - Category

首先我们定义一个分类,后面我们对该分类进行一系列的分析。

@implementation Person (man)
-(void)pWork{
    NSLog(@"work");
}
-(void)pStudy{
    NSLog(@"pStudy");
}
+(void)pEat{
    NSLog(@"eat");
}
@end

1. 底层结构

利用lldb命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+man.m 转换为 c++文件。

  • Person (man) 实际转换成_category_t 结构体类型
// _category_t 结构
struct _category_t {
    // class 名称
    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;
};

// ------------  下面为具体详细信息 ---------------
// Person (man) 实例 
static struct _category_t _OBJC_$_CATEGORY_Person_$_man __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Person",
    0, // &OBJC_CLASS_$_Person,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_man,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_man,
    0,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_man,
};

// 实例方法列表
static struct /*_method_list_t*/ {
  // sizeof 
    unsigned int entsize;  // sizeof(struct _objc_method)
  // 方法的个数
    unsigned int method_count;
  // 方法信息列表
    struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_man __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {{(struct objc_selector *)"pWork", "v16@0:8", (void *)_I_Person_man_pWork},
    {(struct objc_selector *)"pStudy", "v16@0:8", (void *)_I_Person_man_pStudy}}
};

// 类方法列表
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_$_man __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"pEat", "v16@0:8", (void *)_C_Person_man_pEat}}
};

// 属性列表
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_$_man __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"name","T@\"NSString\",C,N"}}
};

  • 代码分析:

_OBJC_$_CATEGORY_Person_$_man :Person(man)分类的底层结构, 如果Person有多个分类,则会有多个 _category_t 结构体与之对应
_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_man : 实例方法列表结构体,存放实例方法相关信息
_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_man:类方法列表结构体,存放类方法相关信息

2. Category的加载过程

  1. 在编译之后,分类的底层结构为struct category_t 类型,里面存放着对象方法、类方法、属性、协议信息
  2. 程序运行时,当初始化类时,通过Runtime加载该类的所有Category数据,并把所有Category的方法、属性、协议数据,合并到数组中,
  3. 最后将数组中信息合并到类信息中(类对象、元类对象)
2.1 流程概括

合并具体流程可以通过源码(objc4-779.1)进行查看。

在类初始化时,会将分类中方法、属性、协议等合并到原类中。通过 _objc_init 入口,进行查看。

objc-os.mm 中: 文件名
_objc_init: 类初始化入扣
map_images : 模块、镜像加载
map_images_nolock

objc-runtime-new.h
_read_images:读取镜像,也就是加载可执行文件到内存中
remethodizeClass: 重构方法列表
attachCategories: 遍历所有分类,将分类的方法、属性、协议合并到对应的三个数组中
attachLists: 将三个数组中对应的方法、属性、协议追加到原类中
realloc、memmove、 memcpy

源码分析:
  void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        /* 传过来的二维数组
         [[method_t,method_t,...], [method_t], [method_t,method_t],...]
         ------------------------  ----------  -------------------
            分类A中的方法列表         分类B中方法列表  分类C中方法列表      ...
         
         addedCount = 3
         */
        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]));
            // 内存拷贝
        /*
         [[分类A方法列表],[分类B方法列表],[分类A方法列表],[原有第一个元素],[原有第二个元素]]
         示例:
         [  [method_t,method_t,...],
            [method_t],
            [method_t,method_t],
            [原有第一个元素],
            [原有第二个元素]
         ]
 */
            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]));
        }
    }


通过上面源码分析得知:

  1. 分类会利用内存移动、内存复制,将分类中方法追加到原类方法前面,所以当分类方法与原类方法一致时,会优先调用分类中方法。
  2. 多个分类有同样方法时,按编译前后顺序进行加载,越后编译,分类方法则越早被调用。

运行时决议:是指Category中定义的方法,在运行时才被加入到类的方法列表当中的。

Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量

3. 如何调用原类方法

通过上面的介绍我们知道,如果分类中的方法与原类一致,在进行方法调用时,在方法列表中查询时优先找到的是分类中方法,进行调用。而非覆盖掉原类方法。

那么我们如何调用原类中方法呢?
因为我们知道,分类中方法是会优先被找到的。所以我们可以利用runtime获取方法列表,然后对方法列表进行遍历,找到最后一个跟我们调用的方法一致的Method。

@interface ZSView : UIView
-(void)redView;
@end

#import "ZSView.h"
@implementation ZSView
-(void)redView{
    NSLog(@"1111");
}
@end


@interface ZSView (leo)
-(void)redView;
@end
#import "ZSView+leo.h"
@implementation ZSView (leo)
-(void)redView{
    NSLog(@"22222");
}
@end

- (void)callOriginalMethod {
    ZSView *v = [[ZSView alloc] init];

    unsigned int count;
    Method *methods = class_copyMethodList([v class], &count);
    SEL sel = nil;
    Method meth = nil;
    SEL oriSel = @selector(redView);
      for (int i = 0; i < count; i++) {
          Method method = methods[i];
          SEL selector = method_getName(method);
          if (selector == oriSel) {
              sel = selector;
              meth = method;
          }
          NSString *name = NSStringFromSelector(selector);
          NSLog(@"实例方法:%@",name);
      }
      free(methods);
    
      //打印输出:22222
    [v performSelector:sel]; //仍会按照消息转发进行方法查找。
    // 打印输出:11111
    IMP imp = method_getImplementation(meth);
   //强制转换后调用w
    ((void (*)(id, SEL))imp)(v,oriSel);//IMP 为方法实现,直接进行调用
}

5. 给分类添加成员变量

Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了对象分配的内存大小。而分类是在运行时才去加载的。
根本原因是程序在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,因此 Category 中不能添加属性!只能使用 AssociatedObject 增加关联的关系!

// 设置关联对象
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
// 获取关联对象的值
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)


6. Category的应用场景

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

推荐阅读更多精彩内容