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的分析过程就算完成了。