常用到的OC语言知识剖析

2019-09-29  本文已影响0人  荷码人生

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

简介

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

一、分类Category

(1) Category的用途是什么?
(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. 关联对象过程中主要涉及的方法

相对应的参数注释:

2. 如何将一个实例变量关联到源类对象??关联的对象存储到了什么地方??
我们再次结合runtime实现代码的来学习一下。
打开Xcode 代码目录:

代码目录
我们以设置对象关联的方法为例:
调用_object_set_associative_reference方法,在该方法中,
  1. 首先根据策略值对value进行处理-->newValue
    通过AssociationsManager获取全局容器AssociationsHashMap-->根据对象的指针地址按位取反作为该对象在AssociationsHashMap存储的对象指针。
  2. newValue != nil -->获取ObjectAssociationsMap 如果说本次进行关联的时候,之前有其他对象进行关联过,那么获得就不为空,否则,为空。
  1. newValue == nil-->根据对象获取对象关联Map(ObjectAssociationMap)-->查找到了,就根据key从ObjectAssociationMap获取ObjcAssociation,获取到了,就进行了一个擦出的操作,取消对象关联。

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

数据结构 数据实现

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

三、扩展 EXtension

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

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

(2) Category的特点是什么?

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

四、代理 Delegate

(1) 什么是代理 Delegate ?
(2)代理的工作流程(协议、代理方、委托方):
  1. 委托方,要求代理方需要实现的全部接口,定义在协议(属性、方法)当中;由代理方按照协议进行方法的实现;可能需要一个返回值,返回一个处理结果,给委托方(协议方法调用方);委托方需要调用代理方遵从的协议中的方法;

如下图所示:


流程图
  1. 代理以及委托方是以怎样的关系存在的??

五、通知 Notification

(1)什么是通知?
(2)如何实现??

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

如图所示:


通知机制

六、观察者 KVO

(1)什么是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?
(2)valueForKey 的系统实现流程:

首先:会判断通过Key访问的实例变量是否有相应的get方法,如果存在,就直接调用,然后结束。如果不存,就会判断实例变量是否存在,通过+(BOOL)accessInstanceVariablesDirectly判断实例变量是否存在,默认值为YES(key与成员变量相同或者相似都会返回YES)。如果不存,系统会调用当前实例的valueForUndefinedKey:方法,然后会抛出一个为定义Key 的异常,然后结束valueForKey的调用流程。

(3)访问器方法是否存在的判断规则:getKey key isKey 都说get方法存在。

实例变量说明:_key_isKey\key\isKey 都可以说明key成员变量存在。
如图所示:

valueForKey实现流程
(4)setValue:forKey:的流程同valueForKey:基本相同。
setValue:forKey:实现流程
(5)我们使用键值编码,是否会破坏面向对象的编程方法?

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

八、属性关键字

(1)常用的属性关键字

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

(2) weak 和 assign 区别 ??
  1. 修饰数据类型
    2.在修改对象时,引用计数不改变
  2. 会产生悬垂指针(在修饰的对象被释放掉后,其仍然指向该对象的内存地址。)(引起内存泄漏,野指针)
  1. 不改变被修饰对象的引用计数。
  2. 所指代的对象在被释放后,会自动置为nil.
(3)weak 指针在被废弃之后,为何会被置为nil呢?

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

(4)如何区别深拷贝&&浅拷贝?
  1. 是否开辟了内存空间
  2. 是否会引起对象的引用计数的更改。
(5)对于使用copy关键字修饰的对象都有哪些特点呢?

如下图所示:

copy关键字的特点
总之,
(6)浅拷贝与深拷贝的具体实现
(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。

待续....

上一篇 下一篇

猜你喜欢

热点阅读