category是什么?
category是Objective-C语言的一个特性,一般称之为“分类”,或者“类别”。其作用是在不修改类的源码的基础上,给类扩展一些接口。说的通俗一点,就是给已有类添加方法。
当需要为一个类添加新的方法时,在Objective-C中有四种方法可以做到。
1、直接在这个类里添加;但是这样,对于一些封装的类,会破坏其封装性,且对于一些系统库或者第三方动态库中的类,无法直接添加方法。
2、通过继承在子类中添加方法;继承的开销太大,且增加了代码的复杂度,同时也违背了继承的初衷,不建议使用。
3、通过协议,实现扩展一个类的方法;协议是个好东西,确实很实用,而且还能降低代码耦合度,但是实现起来过于复杂,代码量大,且对于只需要添加一两个方法时,就显得有点小题大做了。
4、使用category为类添加方法;一般用于比较简单的需求,例如仅仅只需要为类添加一两个特定功能的方法。
这里,我们重点讨论一下category的优缺点;
优点:
  1)在不改变一个类的情况下,对一个已存在的类添加新的方法。
  2)可以再没有源代码的情况下,对框架中的类进行扩展。
  3)当一个类中的代码量太大时,可以按功能将代码中的方法放入到不同的category中,减小单个文件的体积。
  缺点:
  1)类别中的方法的优先级高于类中的方法,所以类别中的方法有可能会覆盖类中的方法。(因此在使用category时,需要注意命名,不要重复了)
  2)不能直接添加成员变量。
  3)可以添加属性,但是不会自动生成getter和setter方法。
category的实现原理
category是Objective-C的语言特性,探讨其实现原理,需要从runtime源码下手,下面就借助源码简单分析一下其实现的原理吧。
先找到category的定义:
1  | #if __OBJC2__  | 
1  | struct category_t {  | 
很明显,Category和Class一样,其本质都是一个结构体;这个结构体中定义了name、cls、实例方法、类方法、协议、属性等等,所以按理说,分类其实是可以为类添加方法、属性、协议的,但是不能添加实例变量,因为这个结构体中没有定义用来存放实力变量的指针变量。
category加载过程
category的加载过程,相对来说就比较复杂。需要从objc的初始化开始说。
来到故事最开始的地方_objc_init:
1  | void _objc_init(void)  | 
前面几个函数的调用,其实都是准备工作,最主要的还是: _dyld_objc_notify_register(&map_images, load_images, unmap_image);
当然,我们并不需要去研究dyld的加载过程,所以我们并不关注这个函数的具体实现,我们关心的,是这个函数的第一个参数map_images。他是一个回调函数指针,在* objc-runtime-new.mm中可以找到它:
1  | void  | 
这个函数比较简单,就两个函数的调用,第一个lock,猜测应该是和线程有关的,不做深入研究,主要看第二个函数map_images_nolock。
这个函数有点长,但是其实我们并不需要对其深入研究,因为这个函数并不是加载category的细节,我们找到这个函数调用:
1  | if (hCount > 0) {  | 
_read_images函数也很长,里面有对类、协议等的加载过程,当然category的加载也在,我们只需要找到关于category的那部分即可:
1  | // Process this category.  | 
这一段的注释,其实也说的很明白了:首先,根据category的目标类注册category,然后,如果目标类已经实现的话,重新构建目标类的方法列表。
这段代码看起来也并不复杂,其逻辑就是category的实例方法、协议或者属性,只要有一个不为空,即调用addUnattachedCategoryForClass函数来注册category,然后判断category的目标类是否实现,如果实现,就调用remethodizeClass重新构建目标类的方法列表rebuild the class's method lists。
很显然,这里有两个关键函数:
注册函数:addUnattachedCategoryForClass;
重新构建方法的函数:remethodizeClass;
先看看addUnattachedCategoryForClass函数的实现,探究注册过程:
1  | static void addUnattachedCategoryForClass(category_t *cat, Class cls,  | 
这个函数可能看起来有点晕乎,但是仔细研究,其实也很简单:
首先,通过unattachedCategories ()取出runtime维护的MapTable —–category_map,这个MapTable是以TargetClass为key,category_list结构体对象为value的表,而category_list 结构体内,又定义了locstamped_category_t结构体数组,locstamped_category_t结构体中有定义了category_t结构体指针:
1  | struct locstamped_category_t {  | 
这就说明了category_list是通过locstamped_category_t 结构体数组来存储每一个category_t对象的;这也解释了为什么一个类可以定义多个category。
而这段代码:
1  | if (!list) {  | 
则说明了category的注册过程:
判断以TargetClass为key从MapTable中取出的类型为category_list的list指针是否为空,如果为空,则为list指针申请空间;如果不为空,则在list所占用的空间上,追加申请一个大小为sizeof(list->list[0])的空间,最后,将cat和catHeader存储到list->list数组中。
以上就是category的注册过程,接下来看看方法的重新绑定过程:
1  | /***********************************************************************  | 
这个函数寥寥数语,其实也很好搞懂,通过目标类名,获取unattached的category_list,并调用attachCategories 函数,将category_list绑定到目标类上。
这个绑定过程,涉及到属性、协议以及方法的绑定,这里,我们重点分析category的方法重新绑定过程:
1  | method_list_t **mlists = (method_list_t **)  | 
这段代码其实就做了一件事:将cats->list数组中的数据取出,并存放到mlists数组中,
然后将mlists作为参数,传递以给attachLists函数,当然,同样作为参数的还有数组的count。
所以咱们还得继续跟进到attachLists函数中:
1  | void attachLists(List* const * addedLists, uint32_t addedCount) {  | 
这里粗略的看看三个if  else语句内的代码,其实不难看出,这三个地方,都是在做同一个操作—–将addedLists中的数据,拷贝到array()->lists。我们第一个if拿出来分析一下:
1  | if (hasArray()) {  | 
首先,计算出新的array()->count:
1  | uint32_t oldCount = array()->count;  | 
然后,用新的array()->count,为array()->lists分配内存:
1  | setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));  | 
然后,将array()->lists中原来的数据,往后移addedCount*sizeof(array()->lists[0])个字节:
1  | memmove(array()->lists + addedCount, array()->lists,  | 
最后,将addedLists中的数据复制到array()->lists的前addedCount*sizeof(array()->lists[0])个字节。
至此,整个category的分析过程就算完成了。