iOS底层原理之七:category的实现

题记

作为iOS开发者,对category肯定不会陌生,category一般又叫分类,当我们需要为一个类增加额外的方法属性等时,分类便是我们的首选。根据前文可知,我们对象方法是存放在类中,类方法是存放在元类对象中,那么如果我们在category增加了方法属性等,它们又是怎么存放的?苹果内部是怎么实现的呢?


准备工作

老规矩我们准备一个继承自NSObject的JJPerson类,然后为它增加一个分类JJPerson+run,增加一个age属性,一个run对象方法以及一个eat类方法。


为了方便我们去了解它的底层实现,我们可以先把它的.m文件转成c++,并拽入Xcode方便查看,为了避免不必要的报错,我们不让这个.cpp文件参与编译,前文有详细操作步骤

准备工作到此完毕,下面我们可以进入分析阶段

编译文件分析


  • 底层结构
    我们搜索“_category_t”可以看到,一个分类在编译后转成C++文件,就是以上图这种结构体的方式存在。我们每增加一个分类,编译后就会多一个这样的结构体,然后在运行时阶段,把所有分类的结构体全部动态合并到我们JJPerson类里面去。
  • 结构体分析
    我们看到结构体包含下面几种常见数据类型
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;

(1)instance_methods是我们为分类增加的实例方法列表
(2)class_methods是我们为分类增加的类方法列表
(3)protocols是分类遵守的协议列表
(4)properties则是我们增加的属性列表
(5)对于结构体前面的name和class,我们可以猜测这个是类名和分类的名称相关的内容

  • 我们可以看到最下面红色框内就是编译后的结构体数据,也就是我们所说“ _category_t”的结构,箭头两个参数对应着实例方法和类方法,而且这两个方法编译后也是以结构体的形式存在在上面。最后两个参数为0,是因为我们的分类没有遵守协议也没有添加属性。
  • 我们每增加一个分类,编译后就会增加一个这样的结构体,格式一样,但是命名和参数以各自为准。
  • 在运行时阶段,才会真正的把结构体里面的数据合并到我们的类里面去,也就是前面文章提到的,类方法存放在元类对象里,属性,协议,实例方法等存放在类对象里

源码分析


  • 我们尝试从运行时源码去分析,看看苹果是怎么对分类进行处理


(1)首先我们打开下载好的objc源码,专题文章第一篇有介绍,这里不再赘述
(2)选择 objc-os.mm 文件,这里是运行时方法的入口
(3)搜索 _objc_init 方法,点击 &map_images 参数


(4)再点击 map_images_nolock 方法进入

(5)注释提到,hCount 是用来查找所有oc元数据模块的,那我们可以专门留意一下它

(6)通过搜索 hCount ,我们可以看到一个叫 _read_images(加载模块) 的方法里面有个参数是传入 totalClasses(所有类),我们可以点击进入作进一步查看

(7)然后我们往下拉可以看到一个关键注释,Discover categories,很明显接下来部分就是对找到的分类进行处理。尤其红色框框内我们还能看到熟悉的 category_t 类型,甚至还能看到方法名是叫做 _getObjc2CategoryList(获取分类列表),那么我们接下来就重点看这里的处理

(8)首先我们可以看到这里有两个是对类重新方法化的处理,分别把class和isa传了进去,这就符合了我们前面提到的,把类方法添加到元类对象的方法列表,把实例方法等添加到类对象的方法列表。所以我们需要看看这个方法的内部实现

(9)我们又看到一个名为附加协议的方法 attachCategories,再次点进去,我们就能看到真正的实现

(10)为了方便阅读,我在这里加了一些中文注释。这里会有一个参数 isMeta 记录传进来的是否是元类对象。然后调用malloc函数开辟存储空间创建一个方法列表的二维数组(属性列表和协议列表同理),然后用 while 循环,根据是否为元类对象,取出对应的类方法或者实例方法,然后把方法数组添加到前面创建的二维数组中(属性和协议处理方法同理)。

(11)最后获取类数据,把分类中所有的方法,属性,协议全部添加到类中去。我们可以再进一步看看 attachLists 这个方法里面做了什么,因为这里将是内存分配最重要一环

(12)我们可以看到,在这个方法里重新计算了列表新的长度,并且调用 realloc 重新分配了新的内存空间。接下来调用 memmove 方法,把原来的方法列表向后移动,前面留出了新列表的长度,再调用 memcpy 方法,把新方法列表插入到整个列表最前面。

(13)这一步内存移动的体现是,如果我们在分类中添加和原类中同名的方法时,我们调用该方法时会优先调用分类的方法,究其原因就是我们分类的方法在方法列表中重新插入在最前面,所以会优先调用,这也是我们常说的分类方法会覆盖原方法的原因。


总结


  • 每创建一个分类,编译时就会生成一个 _category_t 的结构体,里面包含着分类的所有信息(方法,属性,协议)
  • 在运行时阶段,分类的所有信息(方法,属性,协议)会被分别读取,并且逐一添加到类里面去
  • 由于内存操作的原因,分类的方法会排在方法列表最前面,所以分类方法会优先于原类方法的调用(所谓的分类方法覆盖,本质是排序超前)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。