学习MJ的视频课程,整理总结知识点–Category的本质
[TOC]
Category基本使用
category 是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。
category 还可以对类进行模块拆解,甚至模拟多继承等操作。具体可参考官方文档Category
Category底层结构
我们知道category的基本用法,前面我们也已经知道了NSObject、class的底层结构,及调用机制。那么category的结构是什么样的,OC是通过怎样的机制来实现category?
想知道category的底层结构(基于经验,完全可以大胆的推测它是结构体),结合前面的经验,我们可以去两个地方查线索。
- 用
clang
重写category的编译文件来查看项目中的源码 - 阅读objc-runtime-new.h开源的源码,搜索
category
关键字进行查看
这里通过Demo,用clang
重写category的编译文件,分析重写后生成的cpp文件,来探索category的底层结构。
创建MJPerson.h
、MJPerson.m
,创建MJPerson+Test.h
、MJPerson+Test.m
,然后通过class重写MJPerson+Test.m
文件,生成为C++文件,如下:
1 | clang -rewrite-objc MJPerson+Test.m -o MJPerson+Test.cpp |
打开MJPerson+Test.cpp
文件,我们看到如下代码,简单阅读一下,这个看起来就是我们要找到的MJPerson+Test.m
重写后的另一种表达样式。
1 | static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = |
我们还可以找到_CATEGORY_INSTANCE_METHODS_MJPerson_
,_CATEGORY_CLASS_METHODS_MJPerson_
的具体实现结构。
1 | static struct /*_method_list_t*/ { |
同时,我们还能在MJPerson+Test.cpp
文件中找打如下代码:
1 | struct _category_t { |
看起来很像category结构体的标准格式。
name
注意,并不是category小括号里写的名字,而是类的名字cls
要扩展的类对象,编译期间这个值是不会有的,在app被runtime加载时才会根据name对应到类对象instance_methods
这个category所有的-方法class_methods
这个category所有的+方法protocols
这个category实现的protocol,比较不常用在category里面实现协议,但是确实支持的properties
这个category所有的property,这也是category里面可以定义属性的原因,不过这个property不会@synthesize实例变量,一般有需求添加实例变量属性时会采用objc_setAssociatedObject和objc_getAssociatedObject方法绑定方法绑定,不过这种方法生成的与一个普通的实例变量完全是两码事。
上面这些是我们利用编译器重写OC代码生成的C++源码,同时我们还可以在apple开源的objc中找到category的源码,在objc-runtime-new.h
文件中看到定义的源码如下:
1 | struct category_t { |
Category底层结构部分Demo的下载地址
Category源码分析
通过解读category的源码,我们知道category的底层结构长这个样子
1 | struct category_t { |
我们知道OC中所有的方法调用都会转成runtime的objc_msgSend
,这个函数需要两个参数id, SEL
,函数通过这个id
参数查找对象的isa指针,通过isa找到class的方法列表,在方法列表中匹配SEL
进行调用。
但是在之前的class源码解读中(struct objc_class : objc_object {...}
),我们已经知道,实例方法信息都是存储在class的方法列表中(struct objc_class -> class_rw_t* data() -> method_array_t methods
,注:在不同的objc源码版本中可能会有细微差异)
那么category里的方法信息存储在哪里?是否就在本来的category_t中? 程序运行时如何调用到category里面的信息?
实际上,category的信息最终也会合并到class中,且在runtime加载完成后,category的原始信息在类结构里将不会存在。
我们先通过打印class的方法列表
1 | // 打印class的方法列表,发现category中的方法也会被打印出来,看起来分类的方法似乎存在于Class中了 |
发现Class的方法列表中包含了category的方法,可以说明category的信息会添加到class中。
下面我们通过runtime的加载入口为切入点,来解读category的信息是如何添加到到class中。
这需要探究下runtime对category的加载过程,这里就简单说一下
- objc runtime的加载入口是一个叫
_objc_init
的方法,在library加载前由libSystem dyld调用,进行初始化操作 - 调用
map_images
方法将文件中的image
map到内存 - 调用
_read_images
方法初始化map后的image,这是一个很重要的函数。这里面干了很多的事情,像load所有的类、协议和category,著名的+ load
方法就是这一步调用的
仔细看category的初始化,调用了_getObjc2CategoryList
方法获取所有的categoriescategory_t **catlist = _getObjc2CategoryList(hi, &count);
,接下来runtime终于开始了category的处理,简化的代码如下
1 | // Process this category. |
首先分成两拨,一拨是实例对象相关的调用addUnattachedCategoryForClass
,一拨是类对象相关的调用addUnattachedCategoryForClass
,判断每一拨里面是否实现有方法、协议、属性,有的话就remethodizeClass
重新组织class信息结构。
在remethodizeClass
函数中有一个重要方法attachCategories
。attachCategories(cls, cats, true);
将分类列表cats
的附加到类cls
。
接下来讲解attachCategories
函数,不粘贴大片的代码,我用注释说明主要做的事情,需要的可以去看源码如下:
1 | // Attach method lists and properties and protocols from categories to a class. |
在上面的第3步(rw->methods.attachLists(mlists, mcount)
),往cls里面添加数组数据的时候,我们讲解一下个重要方法attachLists
.
1 | void attachLists(List* const * addedLists, uint32_t addedCount) { |
attachLists
流程有3个关键的地方,realloc
、memmove
、memcpy
,有一个非常重要的地方,就是memcpy
,在categories加载的过程中就是把categories的信息(方法、属性、协议)添加到新数组的其实位置。因为runtime的方法调用机制就是找到对应的方法后方法查找流程就结束,然后进行方法调用。正是因为每次categories的信息都是添加到cls的方法数组的最前面,所以遇到category和class方法同名时,优先调用的是category的方法。
学习了categories的加载机制,我们还可以解释另外一个问题,就是同一个class的多个category,每个category都有同样方法,则最后参与编译的分类方法优先调用。
因为在attachCategories
过程中,有这样一段代码
1 | // 创建用于存储同一个class的所有category方法的数组 |
因为是i--
的while循环,所以最后加载的category会添加到数组的最前端。这样runtime在方法查找的时候,会优先查找到最前面的方法,也就意味着最后参与编译的分类方法优先调用。
总结
- Category编译之后的底层结构是
struct category_t
,里面存储着分类的对象方法、类方法、属性、协议信息在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中) - category信息添加到class时有几个重要的函数
- _objc_init // runtime的加载入口
- map_images // 将文件中的imagemap到内存
- _read_images // 重要:初始化map后的image
- _getObjc2CategoryList // 获取所有的categories
- remethodizeClass // 为了添加category重修Class
- attachCategories // 添加categories到Class
- attachLists // Class的结构体重新分配大小,把category中的信息添加的最前面
参考和源码
Demo源码:
Category的底层结构-Demo.zip