本篇关于面试总结分类考点,主要针对面试,刚开始学的时候因为没有写博客,所以最近都忘了,虽然这篇没有之前读源码步骤那么详细,但是满满干货
分类的优点
优点也可以叫做是使用场景,主要就只有两个
- 解耦,降低耦合性
- 为已有类添加方法
分类介绍
- Category的主要作用是为已经存在的类添加方法
- 特性:在运行时阶段动态的为已有类添加新方法
- 装饰者模式(其他模式:观察者模式KVO, 单例模式,代理模式)
- 拓展:在编译阶段与类同时编译的,是类的一部分
常见问题
- 分类可以添加属性,但是不能添加成员变量
分类可以声明属性,但是不能实现属性(就是不能生成set和get方法)
追溯本质,让我们来看一下分类的结构体
struct _category_t {
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; //属性
};
将新建的分类的.m文件,转为c++,就可以看到 上面分类的底层结构,_category_t结构体
在分类的结构体里,我们可以看到根本没有成员变量这个系列,所以自然往分类里面添加成员变量添加不进去,它都没地方放,怎么添加呢
2. 当调用分类和类中都有的方法时,先调用谁的方法?
答案是先调用分类的方法
我们从头说起,在对象一节中有此图
在类对象结构体中,bits里面存有class_rw_t和class_ro_t两个方法
有图我们可以得出。rw方法中存有方法列表,属性列表,协议列表等信息。ro列表中才存有成员变量列表
那么我们来看分类中,在添加分类时,我们都知道分类是运行时一起添加到类里面的,在分类中有一个方法叫做attachCategories:
分类追溯源码其核心是调用了attachCategories函数把我们的分类信息附加到该类中
该方法的简单步骤就是:
- 先创建方法列表、属性列表、协议列表的新列表并且给它们分配内存,然后存储该cls所有的分类的方法、属性、协议,然后转交给了attachMethodLists方法
- attachLists方法保证其添加到rw列表的前面
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// 创建方法列表、属性列表、协议列表,用来存储分类的方法、属性、协议
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0; // 记录方法的数量
int propcount = 0; // 记录属性的数量
int protocount = 0; // 记录协议的数量
int i = cats->count; // 从分类数组最后开始遍历,保证先取的是最新的分类
bool fromBundle = NO; // 记录是否是从 bundle 中取的
while (i--) { // 从后往前依次遍历
auto& entry = cats->list[i]; // 取出当前分类
// 取出分类中的方法列表。如果是元类,取得的是类方法列表;否则取得的是对象方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist; // 将方法列表放入 mlists 方法列表数组中
fromBundle |= entry.hi->isBundle(); // 分类的头部信息中存储了是否是 bundle,将其记住
}
// 取出分类中的属性列表,如果是元类,取得的是 nil
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
// 取出分类中遵循的协议列表
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
上面部分的代码仅仅是将分类中的方法、属性、协议插入到各自对应的大数组中
注意是从后往前加入的(为啥???)
--------------
// 取出当前类 cls 的 class_rw_t 数据
auto rw = cls->data();
// 存储方法、属性、协议数组到 rw 中【注意是rw哦】
// 准备方法列表 mlists 中的方法【为什么需要准备方法列表这一步?】
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
// 将新方法列表添加到 rw 中的方法列表中
rw->methods.attachLists(mlists, mcount);
// 释放方法列表 mlists
free(mlists);
// 清除 cls 的缓存列表
if (flush_caches && mcount > 0) flushCaches(cls);
// 将新属性列表添加到 rw 中的属性列表中
rw->properties.attachLists(proplists, propcount);
// 释放属性列表
free(proplists);
// 将新协议列表添加到 rw 中的协议列表中
rw->protocols.attachLists(protolists, protocount);
// 释放协议列表
free(protolists);
}
所以也就是说,分类方法的列表在运行时就添加到了元类列表的前面,所以当调用方法的时候,自然会先调用分类方法
load和initialize
分类之关联对象
背景:Category中虽然可以添加属性,但是不会生成对应的成员变量,也不能生成getter,setter方法。因此,在调用Category中声明的属性时会报错
我们可以自己借助关联对象来实现getter和setter方法。关联对象能够帮助我们在运行时阶段将任意的属性关联到一个对象上。具体需要用到以下几个方法:
剩下的在Notion上,这里说重点(关联对象其实就是一个哈希套哈希的结构)
- 一个objc对象不光有一个属性需要关联时,假设要关联name和age这两个属性,我们就以objc对象作为disguised_ptr_t,然后value是objectAssociationMap这个字典类型,(即object作为key,一个被关联对象的所有关联对象都存储在同一个ObjectAssociationMap中)在objectAssociationMap这个字典类型中,分别使用@"name"和@"age"作为key,传递进行的值和策略生成 objectAssociation作为value
- 如果有多个对象进行关联时,我们只需要在AssociationsHashMap中创造更多的键值对就可以解决这个问题
- 关联对象并不是存储在被关联对象本身内存中,而是存储在全局的统一的一个AssociationsManager中
主类和分类的普通同名方法的调用顺序
- 普通的方法中,分类的同名方法会覆盖主类的方法
多个分类中的同名方法的调用顺序
多个分类中的同名方法只会执行一个,即后编译的分类里面的方法会覆盖所有前面的同名方法