用OC等面向对象语言编程时,“对象”就是“基本构造单元”,开发者可以通过对象来存储并传递数据。对象之间传递数据并执行任务的过程就叫做“消息传递”。其实“对象”就可以理解为每个文件,“消息传递”就是文件之间的传递。
第6条:理解“属性”这一概念
“属性”是OC的一项特性,用于封装对象中的数据。OC对象通常会把其需要的数据保存为各种实例变量。实例变量通过“存取方法”来访问。其中,“获取方法”用于读取变量值,而“设置方法”用于写入变量值。
1.编译器是怎么存储实例变量的:
通常我们在写一个类时,其中的各种设置方法还需要我们自己去书写,例如:setter、getter方法,用于基本的赋值和获取。并且因为编译器存储实例对象时将每个实例变量替换为“偏移量”来存储的,这个偏移量是“硬编码”表示该变量距离存放对象的内存区域的起始地址有多远。这样便面看上去没有什么问题,但是若你在编译期完成之后再往实例变量的最前方加一个实例变量,那么它后边的所有实例变量的位置都会向后偏移,就会导致你到时候获取到的是一个错误的数据,所以我们每次在修改类定义之后必须重新编译,否则就会出错。
2.不兼容现象:
如果我们遇到不兼容现象,即改变了其原有的类定义程序所引发的现象,在OC中有两种解决方法,一种是:OC把实例变量当作一种存储偏移量所用的“特殊变量”,交由“类对象”保管。因为偏移量会在运行期查找,所以累的定义若变了,其存储的偏移量也就变了,它就可以找到正确的变量位置,这就是“应用程序二进制接口”(ABI)。另一种就是:尽量不要直接访问实例变量,而应该通过存取方法来做,就是我之前说过的setter、getter方法。
3.@property关键字
setter方法一般命名为set+实例变量的名称,用于设置实例变量的值,getter方法一般命名为实例变量的名称,用于获取实例变量的值。有了这两种方法用户就可以通过这两种方法来实现对实例变量的赋值和获取,但是如果定义的实例变量太多了,每个实例变量都得自己完成这些方法的话未免也太麻烦了,所以这里OC就出现了“@property”关键字,用它创建的变量就是一个属性,它会自行合成一个实例变量相应的setter、getter方法。这时候你如果想再访问其内容的话,你可以使用“点语法”这其实和C语言中的结构体很类似。这个过程由编译器在编译器执行,所以编译器里看不到这些“合成方法”的源代码。@property就像这样使用:
使用@property关键字它会自动生成一个带有下划线的名字,这就是你这个实例变量的名字,当然你也可以用“@synthesize”关键字来自己为你创建的实例变量起一个名字。就像这样:
4.@dynamic关键字:
这个关键字会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错,他相信这些方法能在运行期找到。使用方法如下:
5.属性特质:
原子性:
在默认情况下,由编译器所合成的方法通过锁定机制确保其原子性。在并发编程中,如果某操作具备整体性,也就是说,系统其他部分无法观察到其中间步骤所生成的临时结果,而只能看到操作前与操作后的结果,那么该操作就是“原子的”,或者说该操作具备“原子性”。如果属性具备nonatomic特质,则不使用同步锁,如果一个属性不具备nonatomic特质,那它就是“原子的”。
读/写权限:
- readwrite(读写)
顾名思义,它可以对一个属性进行读写操作,即可以改变属性的值,也可以获取属性的值。 - readonly(只读)
顾名思义,他只可以获取属性的值,而不可以更改。
内存管理语义
其实就是关键字,编译器在合成存取方法时,要根据此特质来决定生成的代码。如果自己编写存取方法,那么就必须同有关属性所具备的特质相符。
方法名
就是可以自己设定一个属性的setter、getter方法的名字。
6.atomic与nonatomic的区别:
如果两个线程读写同一属性,那么不论何时,总能看到有效的属性值。若是不加锁的话(或者说使用nonatomic语义),那么当其中一个线程正在改写某属性值时,另外一个线程也许会突然闯入,把尚未修改好的属性值读取出来。发生这种情况时,线程读到的属性值可能不对。
7.为什么几乎所有的属性都可以使用nonatomic:
在iOS中使用同步锁的开销较大,这会带来性能问题。一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全”,若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。
8.要点:
- 可以通过@property语法来定义对象中所封装的数据。
- 通过“特质”来指定存储数据所需的正确语义。
- 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
- 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。
第7条:在对象内部尽量直接访问实例变量
在对象之外访问实例变量时,总是应该通过属性来做,然而在对象内部又该如何呢?我觉得应该:**在读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来做。**因为在设置实例变量的时候考虑到还有关键字约束,要遵循约束就必须使用属性来做,而读取变量的时候不用考虑这些,并且直接访问实例变量更快,所以应该在读取实例变量的时候采用直接访问的形式。
1.直接访问和属性访问的区别:
2.惰性初始化:
也叫做“延迟初始化”。在惰性初始化的情况下,必须通过“获取方法”来访问属性,否则,实例变量就永远不会初始化。 一般用于:一个属性不常用,而且创建该属性的成本较高的情况。
3.要点:
- 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
- 在初始化方法及其dealloc方法中,总是应该直接通过实例变量来读写数据。
- 有时会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据。
第8条:理解“对象等同性”这一概念
众所周知,OC中的比较有“==”和“isEqual:方法”,因为“==”操作符比较的是两个指针本身,而不是其所指的对象,所以一般使用“isEqual:方法”来比较,但是对于不同的比较对象,比较的方法往往是不同的,所以OC中对于不同的比较都有不同的方法,就例如:“isEqualToString:”等等。但是这是远远不够的,对于一个自定义的类对象该怎么比较呢?
1.判断等同性的方法:
NSObject协议中有两个用于判断等同性的关键方法:
- -(BOOL)isEqual:(id)object;
- -(BSUIntrger)hash;
NSObject类对这两个方法的默认实现是:当且仅当其“指针值”(内存地址)完全相等时,这两个对象才相等。当hash函数和isEqual函数返回的值都完全相同的时候才能说这两个对象相等。仅hash函数返回相同但是isEqual函数返回的值也可能不同,因为hash函数的偶然性太大了。
2.如何自定义一个等同性判断:
想要自定义一个等同性判断就的实现hash函数和isEqual函数,其中hash函数的返回值对每个对象都有特定的值,这个可以自己确定,不可以是一个常量,因为若是常量的话,不论什么数据返回的值都是相同的,这就没有比较的意义了,所以这里一定是要靠一定计算方法得出的结果,而isEqual函数就可以自行定义,只要能保证它比较的结果是正确的就可以。 这里建议hash函数的返回值是一个位运算,因为计算机识别的是二进制,使用位运算计算机计算的会更快些,效率更高。
3.特定类所具有的等同性判定方法:
除了自定义类之外,每个类几乎都有其特定的等同性判定方法,NSArray类为“isEqualToArray:”,NSDictionary类为“isEqualToDictionary:”,若不是同一个类型的话,它就会直接抛出异常,是同一个类型才会开始一一处理。 在编写判定方法时,也应一并覆写“isEqual:”方法。后者的常见实现方式为:如果受测的参数与接收该消息的对象都属于同一个类,那么就调用自己编写的判定方法,否则就交由超类来判断。
4.等同性判定的执行深度:
就用NSArray来说,NSArray的检测方式为:先看两个数组所含的对象个数是否相同,若相同,则在每个对应位置的两个对象身上调用其“isEqual:”方法。如果对应位置上的对象均相等,那么这两个数组就相等,这叫做“深度等同性判定”。 是否需要在等同性判定方法中检测全部字段取决于受测对象。只有类的编写者才可以确定两个对象实例在何种情况下应判定为相等。
5.容器中可变类的等同性:
当你创建了一个对象,它的地址就相当于一个“箱子”,但是如果某对象在放入“箱子”之后哈希码又变了,那么其现在所处的这个箱子对他来说就是“错误”的。因为它可变了,说不定改变之后的哈希码或者地址就被改变了,所以它就是“错误”的。 要想解决这个问题,需要确保哈希码不是根据对象的“可变部分”计算出来的,或是保证放入collection之后就不再改变对象内容了,读者可以从此为出发点,对自己的等同性判断进行相应的完整。
6.要点:
- 若想检测对象的等同性,请提供“isEqual:”与hash方法。
- 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
- 不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。
- 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
第9条:以“类族模式”隐藏实现细节
“类族”是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节,就是说将其变成一个黑匣子,用户看不到里边的实现代码,只能知道输入和输出的值。
1.创建类族:
首先要定义抽象基类,也就是一个新的类,其中可以包括你的类型选取,使用枚举器和switch语句来完成,并且还的定义你的类的相关方法,再创建一个新的类,继承你之前的类,并且完成之前的定义方法,使用覆盖的原理,完成这些方法。这种“工厂模式”是创建类族的办法之一。 如果你想创建的类中没有init 初始化的方法,那么这就是在暗示你该类的实例也许不应该由用户直接创建。总而言之,以后创建对象一定不要被其的表象迷惑住了,你可能觉得自己创建了某个类的实例,然而实际上创建的却是其子类的实例。
2.Cocoa里的类族:
系统框架中有许多类族,就用我们经常使用的NSArray 和NSMutableArray 来说,这样来看,它是两个抽象基类,但是他们两个拥有相同的方法,这个方法可能就是他们共同类族中的方法,而可变数组的特殊方法就是只适用于可变数组的方法,其他的共同方法可能就是类族中的方法。
在使用NSArray 的alloc 方法来获取实例时,该方法首先会分配一个属于某个类的实例,此实例充当一个“占位数组”,也就是说,你把这个位置是先分配给其类族的,后来其类族才将这个位置分配给你创建的具体数据类型的。所以像这些类的背后其实是一个类族,在对一些if 条件进行判断的时候一定要注意,例如: 像这样的判断条件它就是不可能成立的,因为[maybeAnArray class] 所返回的类绝不可能是NSArray 本身,因为由NSArray 的初始化方法所返回的那个实例其实类型是隐藏在类族公共接口后边的某个内部类型。
判断某个对象是否位于类族:
所以,若想判断某个对象是否位于类族中,还的使用“isKindOfClass: ”方法:
向已有类新增子类:
你若是想向NSArray这种已有类新增子类,那就得遵循以下规则:
- 子类应该继承自类族中的抽象基类。
- 子类应该定义自己的数据存储方式。
- 子类应当覆写超类文档中指明需要覆写的方法。
3.要点:
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
- 系统框架中经常使用类族。
- 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。
第10条:在既有类中使用关联对象存放自定义数据
平时我们在写程序的时候,肯定会出现没办法创建其实例对象的情况,当然这种情况我们也就别想着创建一个它的子类了,那这种情况应该怎么办呢,这就要用到下面的“关联对象”了。 我们可以给某对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”用以维护相应的“内存管理语义”。策略其实就和我们当时定义属性时的修饰符作用相似,下面就是其相应的关联类型: 使用下列方法可以管理关联对象: 我们可以把某对象想象成NSDictionary ,把关联到该对象的值理解为字典中的条目,于是,存取关联对象的值就相当于在NSDictionary 对象上调用[object setObject:value forKey:key] 与[object objectForKey:key] 方法,但是和字典不同的是,它的key 是一个“不透明的指针”。如果在两个键上调用“isEqual: ”方法的返回值是YES ,那么NSDictionary 就认为二者相等;然而在设置关联对象值时,若想令一个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,在设置关联对象值时,通常使用静态全局变量做键。
1.关联对象举例:
但是,不要想着能这样写就遇到个什么都这样写,这个是应该在其他办法都不行的情况下才去考虑它的,若是滥用,则很快就会令代码失控,使其难于调试。
2.要点:
- 可以通过“关联对象”机制来把两个对象连起来。
- 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
- 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug。
第11条:理解objc_msgSend的作用
在对象上调用方法是OC中经常使用的功能。用OC的术语来说,这叫做“传递消息”。消息有“名称”或“选择子”,可以接受参数,而且可能还有返回值。我们之前用C语言写出来的函数就是“静态绑定”的函数,就是说,他在编译期就能决定运行时所调用的函数。但是若是我们使用一个函数指针来实现函数调用的话,这时他就成为一个“动态绑定”了,因为所调用的函数直到运行期才能确定。 在OC中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有的方法都是普通的C语言函数,然而对象收到消息之后究竟该调用那个方法则完全取决于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言。 例如: 这里的someObject叫做“接收者”,messageName叫做“选择子”。选择子与参数合起来称为“消息”。编译器看到此消息后,会将其转换为一条标准的C语言函数调用,所调用的函数是消息传递机制中的核心函数,叫做objc_msgSend ,其“原型”如下:
void objc_msgSend(id self, SEL cmd, ...);
第一个参数代表接收者,第二个参数代表选择子(SEL是选择子的类型),后边还可以再加参数。所以编译器会把之前的那个语句转化为:
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
objc_msgSend 函数会依据接收者与选择子的类型来调用适当的方法,为了完成此操作,该方法需要在接收者所属类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”操作。
1.相关函数:
其实,我们理解的函数与编译器理解可能会有所不同,对编译器来说,函数名就相当于一个“键”值,我们是通过这个键值来查找对应的函数的,而每个类都会有一张表格,里边存储了很多方法。
2.“尾调用优化”技术:
3.要点:
- 消息由接收者、选择子及参数构成。给某对象“发送消息”也就相当于在该对象上“调用方法”。
- 发给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。
第12条:理解消息转发机制
当对象接收到无法解读的消息后,就会启动“消息转发”机制,程序员可经由此过程告诉对象应该如何处理未知消息。像之前我们会经常遇到的: 这种问题,他就是因为编译器无法识别函数造成的错误,这段错误是由NSObject的“doesNotRecognizeSelector: ”方法抛出的。
1.消息转发的阶段:
消息转发分为两大阶段,第一阶段先征询接收者,所属的类,看其是否能动态添加方法,处理当前这个“未知的选择子”,这叫做“动态方法解析”。第二阶段涉及“完整的消息转发机制”。
2.动态方法解析:
该方法的参数就是那个未知的选择子,其返回值为Boolean 类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法,假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另一个方法,该方法与“resolveInstanceMethod: ”类似,叫做“resolveClassMethod ”。
3.备援接收者:
当接收者还有第二次机会能处理未知的选择子,在这一步中运行期系统会问它:能不能把这条消息转给其他接收者来处理。与该步骤对应的处理方法如下:
- (id)forwardingTargetForSelector:(SEL)selector;
方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到就返回nil。
4.完整的消息转发:
首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子、目标及参数。在触发NSInvocation对象时,“消息派发系统”将亲自出马,把消息指派给目标对象。,此步骤会调用下列方法来转发消息:
- (void)forwardInvocation:(NSInvocation *)invocation;
这个方法可以实现得很简单,只需要改变调用目标,使消息在新目标上得以调用即可。
实现此方法时,若发现某调用操作不应由本类来处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用NSObject类的方法,那么该方法还会继而调用“doesNotRecognizeSelector: ”以抛出异常,此异常表明选择子最终未能得到处理。
5.消息转发全流程:
6.要点:
- 若对象无法响应某个选择子,则进入消息转发流程。
- 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
- 对象可以吧其无法解读的某些选择子转交给其他对象来处理。
- 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。
第13条:用“方法调配技术”调试“黑盒方法”
1.方法调配:
因为与给定的选择子名称相对应的方法是可以在运行期改变的,我们可以利用这个特性将在本类的所有实例中生效,而不仅限于覆写了相关方法的那些子类实例,这种方法就叫做“方法调配”。
2.IMP:
类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP,其原型如下:
id (*IMP)(id, SEL, ...);
其实这里的选择子和IMP的关系就相当于字典类型中的key和value之间的关系,通过选择子可以找到相应的IMP来完成函数的调用实现之类的。所以,对于我们不知道的类方法我们还想向其中添加新的代码,我们就可以使用此原理。
3.交换映射关系完成修改:
OC中给了我们相应的交换映射的方法,如下:
举例说明: 这里是先获取到两个选择子的相应IMP,然后使用交换将其进行交换。我们也可以利用这种方式来完善系统给我们的“黑盒子”: 先将我们想增添的内容写到一个新的方法中: 然后再实现两个IMP的交换: 这里我们是想向之前的lowercaseString 中添加一行新的输出代码,但是由于我们不知道其具体实现,所以我么利用交换IMP的方法完成,我们先将自己想添加的代码写入一个新的方法中,并在在此之前我们还调用了这个新方法自己,为什么调用自己呢?那是因为之后我们要实现IMP的交换,那么这里的自己就会代表的是先前的编译器中的lowercaseString 方法,这样就完成了我们对系统给定方法的代码更改。
通过此方案,开发者可以为那些“完全不知道其具体实现的”黑盒方法增加日志记录功能,这非常有助于程序的调试,但是它只能在调试的程序的时候有用,若是过于滥用,反而会令代码变得不易读懂且难于维护。
4.要点:
- 在运行期,可以向类中新增或替换选择子所对应的方法实现。
- 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。
- 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。
第14条:理解“类对象”的用意
1.id类型:
一般情况下,应该指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有信息。
每个OC对象实例都是指向某块内存数据的指针,所以在声明变量时,类型后面要跟一个“”字符: 若是你想在栈上为其分配内存,编译器就会报错。而对于通用类型的id,由于其本身就已经是指针了,所以我们写的时候不用加“”。
id类型的本身定义:
typedef struct objc_object {
Class isa;
} *id;
由此可见,每个对象结构体首歌成员是Class类的变量。该变量定义了对象所属的类,通常称为“is a”指针。存储的是这个变量的类型。
2.Class对象:
Class对象也定义在运行期程序库的头文件中:
typedef struct objc_class *Class;
struct objc_class {
Class isa;
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
};
此结构体存放类的“元数据”。其中的super_class ,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另一个类,叫做“元类”(metaclass)。并且每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。
3.在类继承体系中查询类型信息:
“isMemberOfClass: ”能够判断出对象是否为某个特定类的实例(只有与其出创建的类型相同时才返回YES),而“isKindOfClass: ”则能够判断出对象是否为某类或其派生类的实例。
从collection中获取对象时,通常会查询类型信息,这些对象不是“强类型的”,把他们从collection中取出来时,其类型通常是id。如果想知道具体类型,那就可以使用类型信息查询方法。 也可以使用比较类对象是否等同的方法来做,但是我们这里只能使用“==”,不能用“isEqual:”方法,因为类对象是“单例”,在应用程序范围内,每个类的Class仅有一个实例,也就是说,另外一种可以精确判断出对象是否为某个类的实例的办法是: class方法返回的的类表示发起代理的对象,而非接收代理的对象。
4.要点:
- 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
- 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知。
- 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。
|