一、分类(Category)
问题1:你用分类都做了哪些事情?
- 声明私有方法
- 分解体积庞大的类文件
- 把Framework的私有方法公开
1.1、特点
运行时决议
编写完成的分类文件,并没有把分类内容添加到宿主类中。
也就说宿主中还没有分类中的方法,而是在运行时,通过runtime将分类的方法添加到宿主当中。
这也是分类的最大特点,也是分类和扩展的最大区别。可以为系统类添加分类
扩展不能为系统添加扩展
1.2:分类中都可以添加哪些内容?
- 实例方法
- 类方法
- 协议
- 属性
在分类中定义一个属性,实际是只是生成了setter和getter方法,并没有在分类添加实例变量。想要添加实例变量,可以通过关联对象的方法。
1.3、分类数据结构
Category 是表示一个指向分类的结构体的指针,其定义如下:
typedef struct objc_category *Category;
struct objc_category {
//分类名
char *category_name;
//分类所属的类名
char *class_name;
//实例方法列表
struct objc_method_list *instance_methods;
//类方法列表
struct objc_method_list *class_methods;
//分类所实现的协议列表
struct objc_protocol_list *protocols;
//实例属性列表
struct property_list_t *instanceProperties;
}
1.4、加载调用栈(了解)
备注:
- _objc_init:runtime初始化方法。
- images:指的是镜像。
1.5、源码分析
这里以添加实例方法举例:
从remethodizeClass开始入手分析(只列出主要代码):
1.5.1
我们只分析分类当中实例方法添加的逻辑
因此在这里我们假设 isMeta = NO
bool isMeta = cls->isMetaClass();
1.5.2
生成一个新的二维数组,用来存放分类中的方法
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
1.5.3
遍历分类数组,采用倒叙遍历
//宿主分类的总数
int i = cats->count;
while (i--) {
//获取一个分类
auto& entry = cats->list[i];
//获取该分类的方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
//最后编译的分类,方法最先添加到分类数组中
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
}
1.5.4
获取宿主类当中的rw数据,其中包含宿主类的方法列表信息
auto rw = cls->data();
1.5.5
将分类方法拼接到rw的methods上
参数/变量含义:
rw代表类。
methods代表类的方法列表。
attachLists方法:将含有mcount个元素的mlists拼接到rw的methods上。
rw->methods.attachLists(mlists, mcount);
1.5.6
attachLists方法解析
假设列表中原有元素总数为2(oldCount = 2)。
假设将要添加的分类元素总数为3(addedCount = 3)。
[ [method_t , method_t], [method_t],
[method_t , method_t , method_t] ]
这里只分析有数组的情况
//列表中原有元素总数。假设oldCount = 2
uint32_t oldCount = array()->count;
//拼接之后的元素总数
uint32_t newCount = oldCount + addedCount;
//根据新总数重新分配内存
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
//重新设置元素总数
array()->count = newCount;
/*
内存移动
[ [ ], [ ], [ ], [原有的第一个元素], [原有的第二个元素]]
*/
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
/*
内存拷贝
[
A ---> [addedLists中的第一个元素],
B ---> [addedLists中的第二个元素],
C ---> [addedLists中的第三个元素],
[原有的第一个元素],
[原有的第二个元素]
]
这也是分类方法会"覆盖"宿主类的方法的原因
*/
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
问题2:一个类的多个分类包含同名方法,最后哪个会生效?
解释:
谁最后编译,谁就生效。
1.6、分类总结
- 分类添加的方法可以"覆盖"原类方法
效果上是"覆盖",实际上原类方法还是存在的。 - 同名分类方法谁能生效取决于编译顺序
- 名字相同的分类会引起编译报错
二、关联对象
问题3:能否给分类添加成员变量?
解释:
可以添加。
在分类的声明时,不能添加成员变量;但是可以通过关联对象的方法添加。
问题4:给分类添加的成员变量是否添加到宿主中?
解释:
没有添加到宿主中。被存放在一个全局容器中,并且为不同类添加的关联对象都放在同一个全局容器中。
2.1、获取属性值
objc_getAssociatedObject(id object, const void *key);
- object
目标对象。即获取哪个对象的关联属性 - key
根据对应的key查找对象的属性
2.2、添加并设置属性值
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
- object
目标对象。给自己添加属性,就用self。 - key
属性名。 - value
关联值。 - policy
策略。属性以什么形式保存。
主要形式有以下几种:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // 指定一个弱引用相关联的对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 指定相关的对象被复制,非原子性
OBJC_ASSOCIATION_RETAIN = 01401, // 指定相关对象的强引用,原子性
OBJC_ASSOCIATION_COPY = 01403 // 指定相关的对象被复制,原子性
};
2.3、移除所有的关联对象
objc_removeAssociatedObjects(id object);
2.4、关联对象的本质
- 关联对象由AssociationsManager管理;并在AssociationsHashMap存储。
- 所有对象的关联内容都在同一个全局容器中。
这里包含三部分数据结构:
- AssociationsManager
管理对象,里面有一个AssociationsHashMap对象。 - AssociationsHashMap
可以理解为全局容器。
由多个obj:ObjectAssociationMap组成。
一个实例对象就对应一个ObjectAssociationMap。 - ObjectAssociationMap
由多个属性名key:ObjcAssociation组成。 - ObjcAssociation
由value和policy组成。
由上面可以知道,关联对象没有存放到原来的对象里面,而是自己维护了一个全局map来存放的。
问题5:怎样清除一个关联对象?
解释:
通过objc_setAssociatedObject,将其中value传值为nil,就可以了。
三、扩展(Extension)
- 声明私有属性
- 声明私有方法(只是为了方便阅读,无太大作用)
- 声明私有变量
问题6:属性和变量的区别?
属性 = 变量 + set方法 + get方法
问题7:分类和扩展的区别?
- 扩展是编译时决议,分类是运行时决议
- 扩展是以声明的形式存在,不是独立的存在,多数寄存于宿主类.m文件中;而分类是单独存在的。
- 不能为系统类添加扩展;但是可以为系统的类添加分类。
四、代理
- 是一种软件设计模式。
- iOS中以@protocal形式体现。
- 传递方式是一对一。
问题8:代理的工作流程是什么?
问题9:为什么代理声明的时候,要使用weak?
因为代理方一般强引用委托方,为了防止循环引用,委托方必须对代理方进行弱引用。
五、通知
- 使用观察者模式来实现的,用于跨层传递消息的机制。
- 传递方式为一对多。
问题10:通知和代理的区别?
- 代理是由代理模式实现的;通知是由观察者模式实现的。
- 代理传递是一对一,通知是一对多。
问题11:如何实现通知机制?
在通知中心,内部维护一个Map表,它的key是通知名称,value是观察者表List。观察者表List内部包含多个observer对象;observer对象包含通知接收的观察者,观察者调用的方法等等。
六、KVO
6.1、概念
- KVO是Key-value observing的缩写。
- KVO是OC对观察者设计模式的又一实现。
- Apple使用了isa混写(isa-swizzling)来实现KVO。
6.2、KVO调用发生过程图
- A类对象调用系统方法addObserver:forKeyPath:options:context:
- 在运行时会创建一个NSKVONotifying_A类,这个NSKVONotifying_A类是A类的子类
- 此时,A类的isa指针会指向NSKVONotifying_A类
- 然后在NSKVONotifying_A中会重写setter方法
- 在重写的setter方法中,先调用willChangeValueForKey: ➡️调用父类方法,给父类赋值➡️给子类属性赋值➡️调用didChangeValueForKey:➡️最终会调用observeValueForKeyPath通知了所有的观察者。
问题12:isa混写技术在KVO中是怎么体现的?
见6.2.KVO调用发生过程图
6.3、重写setter方法的具体实现
6.3.1、两个重要方法
6.3.2、具体实现
备注:
- 一定会调用父类方法实现,否则数值不统一。
- didChangeValueForKey调用后会触发observeValueForKeyPath方法。
问题13:通过KVC设置value能否生效?
可以生效
问题14:为什么通过KVC设置value能生效?
因为KVC内部原理表明,这样设置value会调用obj对象的setter方法。
问题15:通过成员变量直接赋值value能够生效?
- 不能。
- 可以手动触发KVO,在给变量赋值的代码前后加入willChangeValueForKey:和didChangeValueForKey:
-
代码如下:
6.4、KVO总结
- 使用setter方法改变值KVO才生效。
- 使用setValue:forKey:改变值KVO才会生效。
- 成员变量直接修改需手动添加KVO才会生效。
七、KVC
问题16:什么是KVC
- 键值编码技术、Key-value coding缩写
- (id)valueForKey: (NSString *)key
- (void)setValue:(id)value forKey:(NSString *)key
问题17:KVC会不会破坏面向对象的编程思想?
会破坏。
因为上面的key是没有限制的,即使是私有变量,也可以通过KVC设置私有变量的value。
7.1、valueForKey系统实现流程
7.1.1、流程图
- 先查找对应的get方法是否存在。
如果存在,则调用对应方法,结束流程。
如果不存在,则查找对应的实例变量是否存在。 - 查找实例变量是否存在。
系统提供了一个开关函数accessInstanceVariablesDirectly,默认是YES,允许查询相同或相似的实例变量。
如果实例变量存在,则获取当前值返回。 - 如果实例变量不存在,则会调用valueForUndefinedKey方法,抛出一个NSUndefinedKeyException异常。
7.1.2、流程图 get方法是否存在的判断规则
如果有以上命名的访问器方法,则默认当前get方法是存在的。
7.1.3、流程图 实例方法是否存在的判断规则
- _key
- _isKey
- key
- isKey
如果找到上述同名或者类似的变量,则默认当前的成员变量是存在的。
7.2、setValue:forKey:系统实现流程
7.2.1、流程图
- 先查找对应的set方法是否存在。
如果存在,则调用对应方法,结束流程。
如果不存在,则查找对应的实例变量是否存在。 - 其余步骤同valueForKey相同。
八、属性关键字
8.1、属性关键字分类:
1、读写权限
- readonly
- readwrite(默认关键字)
2、 原子性
- atomic(系统默认)
可以赋值和获取是线程安全的。但是操作对象是无法保证线程安全的。
比如:一个数组Array。如果对Array进行赋值或获取是线程安全的,如果对Array进行操作(添加/移除对象),则无法保证线程安全 - nonatomic
3、 引用计数
- retain/strong
retain:MRC
strong:ARC - assign/unsafe_unretained
unsafe_unretained:ARC基本不使用 - weak
8.2、assign特点
- 修饰基本数据类型,如int、Bool等。
- 修饰对象类型时,不改变其引用计数。
- 会产生悬垂指针。
assign指针指向的对象被释放后,assgin指针仍然指向原对象内存地址。
8.2、weak特点
- 不改变被修饰对象的引用计数。
- 所指对象在被释放后会自动置为nil。
问题18:assgin与weak区别
- 修饰数据类型方面
assign既可以修饰基本数据类型,也可以修饰对象。
weak只可以修饰对象。 - 指针
assgin可以产生悬垂指针。
weak指针自动指向nil。
二者的共同点:都不影响引用计数。
8.3、copy
问题19:见下图
- 如果赋值过来的是NSMutableArray,copy之后是NSArray。
- 如果赋值过来的是NSArray,copy之后是NSArray。
因为最终array是NSArray,不可变对象。
8.3.1、深拷贝和浅拷贝
是否开辟新的内存空间
深拷贝会,浅拷贝不会-
是否影响引用计数
深拷贝不会,浅拷贝会
可变对象的copy和mutableCopy都是深拷贝。
不可变对象的copy是浅拷贝,mutableCopy都是深拷贝。
copy方法返回的都是不可变对象。
九、笔试题总结
9.1、MRC下如何重写retain修饰变量的setter方法?
9.2、请简述分类的实现原理
1、分类的实现原理是由运行时决议的。
2、不同分类的含有同名分类方法,谁最后编译,谁就生效。
3、如果分类中的方法恰好与目标类中的方法同名,分类会覆盖同名的目标类方法。
9.3、KVO的实现原理是怎样的?
1、KVO是系统关于观察者模式的一种体现。
2、KVO运用了isa混写技术;在动态运行时,为某一个类动态添加一个子类,重写了它的setter方法;将原有类的isa指针指向新创建的子类。
9.4、能否为分类添加成员变量?
不能。因为它的数据结构中,没有成员变量。
可以通过关联对象。