常用到的OC语言知识剖析

在我们的开发过程中,经常被忽视,但经常使用的语法知识,虽然我们懂得如何运用,但是,对于他们的实现原理我们又掌握了多少呢?在职场面试的我们又该如何去应对这方面的问题呢?下面让我们一起来探讨一下,这方面的底层实现吧!

简介

本章所涉及到了分类、扩展、关联对象、代理、通知、KVO、KVC、关键字(copy,weak,assign,strong,atomic,nonatomic等语义)等相关的使用方法以及内部原理实现。

一、分类Category

(1) Category的用途是什么?
  • 可以声明私有方法
  • 分解体积庞大的类文件
  • 把Framework的私有化方法公开
(2) Category的特点是什么?
  1. Category是在运行决议的:所谓运行的决议:就是说在编译该分类的时候,并没有将该分类的内容附加到宿主类上面,而是,在运行的某一时刻,运用runtime将该分类的内容添加到该宿主类的上面的。扩展是编译时是决议。
  2. 能为系统类添加分类,但是不能为宿主类添加扩展。
(3) 分类中可以添加哪些内容?
  1. 添加方法(实例方法、类方法)、属性(只生成get\set 方法,并没有产生相应的实例变量)、添加协议
  2. 也可以使用关联对象,添加实例变量。
(4) 分类的原理流程是什么?

我们结合runtime代码实现来讲述一下。

  1. 最初的调用方法
    remethodizeClass-->判别是哪一种分类(类方法、实例方法)-->尝试获取所有未完成整合的分类(unattachedCategoriesForClass)---->就将未完成整合的分类,拼接到宿主类上面(attachCategories)
  2. 如何进行拼接的呢??
    attachCategories-->判断是否有分类-->判别是哪一种分类(类方法、实例方法)--->创建一个分类的方法列表(method_list_t)-->对分类cats进行倒叙遍历,并添加到分类的方法列表中-->通过attachLists方法将分类的方法附加到宿主类的上面。
  3. 具体实现:
    attachLists-->判断是否有分类-->更改原有宿主的方法总数-->根据新总数重新分配内存-->并通过内存移动(memmove)内存拷贝(memcpy)来完成分类附加到宿主类的任务,从而使宿主类具有分类的方法。
(5) 简要总结

Category 具有运行时决议、同时可以为系统类的添加分类的特点,所谓运行时决议,就是在运行中,通过runtime将分类的内容附加到宿主类上面。当一个类有多个分类,并在分类中声明了同名方法时,这些同名方法中最终会有一个方法的实现生效,到底是哪一个分类的方法有效呢?取决于该宿主类的分类的编译顺序,也就是最后参与编译的那个分类的方法实现会生效。这就是分类会“覆盖”原有宿主类的现象,这里的覆盖,宿主类方法仍然存在只是没有生效。

二、关联对象

通常情况,我们使用关联对象为分类添加实例变量。下面我们一起来学习一下它的具体的使用方法,可参考Demo中的事例代码。

  1. 关联对象过程中主要涉及的方法
  • 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方法,在该方法中,

  1. 首先根据策略值对value进行处理-->newValue
    通过AssociationsManager获取全局容器AssociationsHashMap-->根据对象的指针地址按位取反作为该对象在AssociationsHashMap存储的对象指针。
  2. 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);]-->附加到相应的源对象上面。
  1. newValue == nil-->根据对象获取对象关联Map(ObjectAssociationMap)-->查找到了,就根据key从ObjectAssociationMap获取ObjcAssociation,获取到了,就进行了一个擦出的操作,取消对象关联。

具体的数据结构
objcAssociation(policy,value)-->ObjectAssociationMap(key,objcAssociation)映射到-->(ObjectAssociationMap)----->实际上是放在AssociationsHashMap中的(通过当前被关联对象的指针值,来建立与ObjectAssociationMap的映射来实现将一个值关联到一个实例对象AssociationsHashMap上面来实现的。如下图所示:

常用到的OC语言知识剖析_第1张图片
数据结构
常用到的OC语言知识剖析_第2张图片
数据实现

总结
以上就是关联对象的运行流程。

三、扩展 EXtension

(1)一般使用扩展的用途??

来添加私有属性,添加私有方法,声明私有成员

(2) Category的特点是什么?
  • 编译时决议
  • 只以声明的方式存在,多数情况下,寄生于宿主类的.m中.
  • 不能为系统类添加扩展。

具体的使用方法,可参考Demo中的事例代码。

四、代理 Delegate

(1) 什么是代理 Delegate ?
  • 代理是一种软件设计模式
  • iOS当中以@protocol形式表现出来
  • 传递方式是一对一。(通知一对多)
(2)代理的工作流程(协议、代理方、委托方):
  1. 委托方,要求代理方需要实现的全部接口,定义在协议(属性、方法)当中;由代理方按照协议进行方法的实现;可能需要一个返回值,返回一个处理结果,给委托方(协议方法调用方);委托方需要调用代理方遵从的协议中的方法;

如下图所示:


常用到的OC语言知识剖析_第3张图片
流程图
  1. 代理以及委托方是以怎样的关系存在的??
  • 一般我们会在委托方当中,声明为weak以规避循环引用。
  • 代理方会强持有他的委托方,而此时,委托方需要有一个代理方的声明为弱引用(weak),这样就规避了循环引用。 就是我们声明delegate的 weak 语义。

五、通知 Notification

(1)什么是通知?
  • 通知是使用观察者模式来实现的,用于跨层传递消息的机制。(网络层--数据层---业务逻辑层---UI层)
  • 传递方式一对多。
(2)如何实现??

在通知中心可能会维持一个Map表或者是字典:NotificationMap(key是notificationName Value:是通知列表(Observers_list包含通知接受的观察者(Observer)以及观察者调用的方法))

如图所示:


常用到的OC语言知识剖析_第4张图片
通知机制

六、观察者 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成员变量存在。
如图所示:

常用到的OC语言知识剖析_第5张图片
valueForKey实现流程
(4)setValue:forKey:的流程同valueForKey:基本相同。
常用到的OC语言知识剖析_第6张图片
setValue:forKey:实现流程
(5)我们使用键值编码,是否会破坏面向对象的编程方法?

会。如果我们知道了一个类的私有的成员变量,我们就可以使用键值编码进行更改与访问:类似这种:[obj setValue:@2 forKey:@"value"];

八、属性关键字

(1)常用的属性关键字

分成三类:读写权限、引用计数、原子性

  • 读写权限(readonly, readwrite)
  • 引用计数(retain/strong,assign/unsafe_unretain, weak, copy)
  • 原子性:atomic 保证赋值和获取线程安全的,并不能保证其操作与访问的安全性。比如修饰的是数组:对数组进行赋值或者获取,可以保证线程安全的。对于数组的添加和删除,则不能保证线程安全。
(2) weak 和 assign 区别 ??
  • assign 特点:
  1. 修饰数据类型
    2.在修改对象时,引用计数不改变
  2. 会产生悬垂指针(在修饰的对象被释放掉后,其仍然指向该对象的内存地址。)(引起内存泄漏,野指针)
  • weak 特点:
    1.修饰对象类型
  1. 不改变被修饰对象的引用计数。
  2. 所指代的对象在被释放后,会自动置为nil.
(3)weak 指针在被废弃之后,为何会被置为nil呢?

该问题会在以后的学习中,解答。

(4)如何区别深拷贝&&浅拷贝?
  • copy 的特点: 浅拷贝 拷贝的仅仅是指针,内存并没有发生改变,也就说,原指针和拷贝后的对象,都指向一个内存空间,也就是说,浅拷贝是对内存地址的复制,目标对象指针和源对象指针指向同一片内存空间。而深拷贝:拷贝的不仅仅是指针,还拷贝了内存空间,也就是,这是两个内容完全相同的内存空间。
    总之,两者的区别是:
  1. 是否开辟了内存空间
  2. 是否会引起对象的引用计数的更改。
(5)对于使用copy关键字修饰的对象都有哪些特点呢?

如下图所示:

常用到的OC语言知识剖析_第7张图片
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。

待续....

你可能感兴趣的:(常用到的OC语言知识剖析)