在我们的开发过程中,经常被忽视,但经常使用的语法知识,虽然我们懂得如何运用,但是,对于他们的实现原理我们又掌握了多少呢?在职场面试的我们又该如何去应对这方面的问题呢?下面让我们一起来探讨一下,这方面的底层实现吧!
简介
本章所涉及到了分类、扩展、关联对象、代理、通知、KVO、KVC、关键字(copy,weak,assign,strong,atomic,nonatomic等语义)等相关的使用方法以及内部原理实现。
一、分类Category
(1) Category的用途是什么?
- 可以声明私有方法
- 分解体积庞大的类文件
- 把Framework的私有化方法公开
(2) Category的特点是什么?
- Category是在运行决议的:所谓运行的决议:就是说在编译该分类的时候,并没有将该分类的内容附加到宿主类上面,而是,在运行的某一时刻,运用runtime将该分类的内容添加到该宿主类的上面的。扩展是编译时是决议。
- 能为系统类添加分类,但是不能为宿主类添加扩展。
(3) 分类中可以添加哪些内容?
- 添加方法(实例方法、类方法)、属性(只生成get\set 方法,并没有产生相应的实例变量)、添加协议
- 也可以使用关联对象,添加实例变量。
(4) 分类的原理流程是什么?
我们结合runtime代码实现来讲述一下。
- 最初的调用方法
remethodizeClass-->判别是哪一种分类(类方法、实例方法)-->尝试获取所有未完成整合的分类(unattachedCategoriesForClass)---->就将未完成整合的分类,拼接到宿主类上面(attachCategories) - 如何进行拼接的呢??
attachCategories-->判断是否有分类-->判别是哪一种分类(类方法、实例方法)--->创建一个分类的方法列表(method_list_t)-->对分类cats进行倒叙遍历,并添加到分类的方法列表中-->通过attachLists方法将分类的方法附加到宿主类的上面。 - 具体实现:
attachLists-->判断是否有分类-->更改原有宿主的方法总数-->根据新总数重新分配内存-->并通过内存移动(memmove)内存拷贝(memcpy)来完成分类附加到宿主类的任务,从而使宿主类具有分类的方法。
(5) 简要总结
Category 具有运行时决议、同时可以为系统类的添加分类的特点,所谓运行时决议,就是在运行中,通过runtime将分类的内容附加到宿主类上面。当一个类有多个分类,并在分类中声明了同名方法时,这些同名方法中最终会有一个方法的实现生效,到底是哪一个分类的方法有效呢?取决于该宿主类的分类的编译顺序,也就是最后参与编译的那个分类的方法实现会生效。这就是分类会“覆盖”原有宿主类的现象,这里的覆盖,宿主类方法仍然存在只是没有生效。
二、关联对象
通常情况,我们使用关联对象为分类添加实例变量。下面我们一起来学习一下它的具体的使用方法,可参考Demo中的事例代码。
- 关联对象过程中主要涉及的方法
- objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);-->(获取关联)
- objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy);-->设置关联
- objc_removeAssociatedObjects(id _Nonnull object);--> 移除关联
相对应的参数注释:
- object 关联的源类对象
- key 关键的key 可以使用一个@selector()选择器或者在分类中的声明的方法,也可以使用_cmd(是类型SEL的指针),也可以使用静态的变量的地址,只要的是,恒定不变的地址值就可以。
- value 是要附加到源类的实例的变量的值,如果想移除该源类的关联,就可以设置 value=nil 就可以了。
- policy 与关联引用相关的策略(使用的策略取决于在分类中的声明时的语义)。
2. 如何将一个实例变量关联到源类对象??关联的对象存储到了什么地方??
我们再次结合runtime实现代码的来学习一下。
打开Xcode 代码目录:
我们以设置对象关联的方法为例:
调用_object_set_associative_reference方法,在该方法中,
- 首先根据策略值对value进行处理-->newValue
通过AssociationsManager获取全局容器AssociationsHashMap-->根据对象的指针地址按位取反作为该对象在AssociationsHashMap存储的对象指针。 - newValue != nil -->获取ObjectAssociationsMap 如果说本次进行关联的时候,之前有其他对象进行关联过,那么获得就不为空,否则,为空。
- i != associations.end()(非第一次)-->根据对象关联的对象指针获取所关联的ObjectAssociationMap(对象关联的map)-->在对象关联的map中,根据我们传递的key进行查找-->如果说查找到了将value更换成最新的,如果没有查到,就进行创建一个ObjcAssociation,将newValue,policy组成的数据封装成ObjcAssociation并与key映射到ObjectAssociationMap[(*refs)[key] = ObjcAssociation(policy, new_value);]
- i == associations.end()(第一次)-->创建一个ObjectAssociationMap-->将ObjectAssociationMap与disguised_object(对象指针)映射到AssociationsHashMap中(ObjectAssociationMap refs = new ObjectAssociationMap;
associations[disguised_object] = refs;)-->将newValue,policy组成的数据封装成ObjcAssociation并与key映射到ObjectAssociationMap[(refs)[key] = ObjcAssociation(policy, new_value);]-->附加到相应的源对象上面。
- newValue == nil-->根据对象获取对象关联Map(ObjectAssociationMap)-->查找到了,就根据key从ObjectAssociationMap获取ObjcAssociation,获取到了,就进行了一个擦出的操作,取消对象关联。
具体的数据结构
objcAssociation(policy,value)-->ObjectAssociationMap(key,objcAssociation)映射到-->(ObjectAssociationMap)----->实际上是放在AssociationsHashMap中的(通过当前被关联对象的指针值,来建立与ObjectAssociationMap的映射来实现将一个值关联到一个实例对象AssociationsHashMap上面来实现的。如下图所示:
总结
以上就是关联对象的运行流程。
三、扩展 EXtension
(1)一般使用扩展的用途??
来添加私有属性,添加私有方法,声明私有成员
(2) Category的特点是什么?
- 编译时决议
- 只以声明的方式存在,多数情况下,寄生于宿主类的.m中.
- 不能为系统类添加扩展。
具体的使用方法,可参考Demo中的事例代码。
四、代理 Delegate
(1) 什么是代理 Delegate ?
- 代理是一种软件设计模式
- iOS当中以@protocol形式表现出来
- 传递方式是一对一。(通知一对多)
(2)代理的工作流程(协议、代理方、委托方):
- 委托方,要求代理方需要实现的全部接口,定义在协议(属性、方法)当中;由代理方按照协议进行方法的实现;可能需要一个返回值,返回一个处理结果,给委托方(协议方法调用方);委托方需要调用代理方遵从的协议中的方法;
如下图所示:
- 代理以及委托方是以怎样的关系存在的??
- 一般我们会在委托方当中,声明为weak以规避循环引用。
- 代理方会强持有他的委托方,而此时,委托方需要有一个代理方的声明为弱引用(weak),这样就规避了循环引用。 就是我们声明delegate的 weak 语义。
五、通知 Notification
(1)什么是通知?
- 通知是使用观察者模式来实现的,用于跨层传递消息的机制。(网络层--数据层---业务逻辑层---UI层)
- 传递方式一对多。
(2)如何实现??
在通知中心可能会维持一个Map表或者是字典:NotificationMap(key是notificationName Value:是通知列表(Observers_list包含通知接受的观察者(Observer)以及观察者调用的方法))
如图所示:
六、观察者 KVO
(1)什么是KVO?
- KVO全称为Key-value observing 的缩写
- KVO的实现模式是观察者模式。
- Apple 运用了 isa混写(isa-swizzling)来实现了KVO
(2)isa-swizzling 是如何实现KVO的。
NSKVONotifying_A 是 A 的一个子类,之所以创建这样一个子类,就是为了重写父类的Setter方法,负责通知所有对象。
*==============================
NSKVONotifying_A的setter方法的具体的实现
-(void)setValue:(id)obj{
[self willChangeValueForKey:@"keyPath"];
[super setValue:obj];
[self didChangeValueFroKey:@"keyPath"];
}
=============================*
(2)KVO的实现机制是什么?
注册观察者(addObserverForPath:)--> 比如说:观察者观察对象A的成员变量或者属性 --> 系统会为我们动态生成一个NSKVONotifying_A的类 --> 又会将原来指向A的isa 指针 指向了 NSKVONotifying_A 类,现在创建 NSKVONotifying_A 类的时候,会重写A 的setter方法,在重写的setter方法中,会首先调用 [self willChangeValueForKey:@"keyPath"];接着调用父类的setter方法 [super setValue:obj];最后调用 [self didChangeValueFroKey:@"keyPath"];(如上代码)在调用最后的didChangeValueFroKey方法的时候,会出发观察者的observeValueForKeyPath方法,从而完成整个的观察者的工作流程。
(3)如何手动添加KVO?
如下添加即可:
[self willChangeValueForKey:@"value"];
_value += 1;
[self didChangeValueForKey:@"value"];
以上我们所熟知的观察的模式KVO。接下来,我们来思考这样两个问题。
1. 使用KVC给变量赋值,会触发KVO吗?
答案是肯定的,因为在使用KVC赋值的时候,触发了对象的setter方法,在Demo的有相应的印证代码。
2. 直接给成员变量赋值,会触发KVO吗?
答案:不能触发KVO的。从KVO的实现机制中,我们知道,系统为我们提供的KVO相当于在我们的setter 方法中插入了两行代码willChangeValueForKey:和 didChangeValueForKey:,那么我们是否也可以在成员变量赋值的时候,手动的添加这两行代码,来模拟系统的setter方法,来实现KVO呢,答案是肯定。这就是我们手动添加KVO的一个运用场景。具体实现,可参考Demo中的事例代码。
七、键值编码 KVC
(1)什么是KVC?
- KVC Key-value coding的缩写.
- (id)valueForKey:(NSString *)key;
- -(void)setValue:(id)value forKey:(NSString *)key;
(2)valueForKey 的系统实现流程:
首先:会判断通过Key访问的实例变量是否有相应的get方法,如果存在,就直接调用,然后结束。如果不存,就会判断实例变量是否存在,通过+(BOOL)accessInstanceVariablesDirectly判断实例变量是否存在,默认值为YES(key与成员变量相同或者相似都会返回YES)。如果不存,系统会调用当前实例的valueForUndefinedKey:方法,然后会抛出一个为定义Key 的异常,然后结束valueForKey的调用流程。
(3)访问器方法是否存在的判断规则:getKey key isKey 都说get方法存在。
实例变量说明:_key_isKey\key\isKey 都可以说明key成员变量存在。
如图所示:
(4)setValue:forKey:的流程同valueForKey:基本相同。
(5)我们使用键值编码,是否会破坏面向对象的编程方法?
会。如果我们知道了一个类的私有的成员变量,我们就可以使用键值编码进行更改与访问:类似这种:[obj setValue:@2 forKey:@"value"];
八、属性关键字
(1)常用的属性关键字
分成三类:读写权限、引用计数、原子性
- 读写权限(readonly, readwrite)
- 引用计数(retain/strong,assign/unsafe_unretain, weak, copy)
- 原子性:atomic 保证赋值和获取线程安全的,并不能保证其操作与访问的安全性。比如修饰的是数组:对数组进行赋值或者获取,可以保证线程安全的。对于数组的添加和删除,则不能保证线程安全。
(2) weak 和 assign 区别 ??
- assign 特点:
- 修饰数据类型
2.在修改对象时,引用计数不改变 - 会产生悬垂指针(在修饰的对象被释放掉后,其仍然指向该对象的内存地址。)(引起内存泄漏,野指针)
- weak 特点:
1.修饰对象类型
- 不改变被修饰对象的引用计数。
- 所指代的对象在被释放后,会自动置为nil.
(3)weak 指针在被废弃之后,为何会被置为nil呢?
该问题会在以后的学习中,解答。
(4)如何区别深拷贝&&浅拷贝?
- copy 的特点: 浅拷贝 拷贝的仅仅是指针,内存并没有发生改变,也就说,原指针和拷贝后的对象,都指向一个内存空间,也就是说,浅拷贝是对内存地址的复制,目标对象指针和源对象指针指向同一片内存空间。而深拷贝:拷贝的不仅仅是指针,还拷贝了内存空间,也就是,这是两个内容完全相同的内存空间。
总之,两者的区别是:
- 是否开辟了内存空间
- 是否会引起对象的引用计数的更改。
(5)对于使用copy关键字修饰的对象都有哪些特点呢?
如下图所示:
总之,
- 可变对象的copy与mutableCopy 都是深拷贝,
- 不可变对象的copy是浅拷贝,mutableCopy 是深拷贝。
- copy 方法返回的都是不可变对象。
(6)浅拷贝与深拷贝的具体实现
- 浅拷贝(指针复制,不会创建一个新的对象)
-(id)copyWithZone:(NSZone *)zone{
return self;
} - 深拷贝(内容复制,会创建一个新的对象)
-(id)copyWithZone:(NSZone *)zone{
//创建新的对象空间
Model1 *model = [[Model1 allocWithZone:zone]init];
//属性也进行深层拷贝
model.name = [self.name mutableCopy];
return model;
}
(7)MRC下如何重写retain修饰变量的setter方法??
@property(nonatomic,retain) id obj;
-(void)setObj:(id)obj{
if (_obj!=obj) {
[_obj release];
_obj = [obj retain];
}
}
为什么要进行判断_obj != obj;???
如果不进行判断?假设:_obj 与 obj 是同一个话,那么, [_obj release]; _obj 被释放,当我们在[obj retain];程序就会保存,Crash。
待续....