文章目录
概述
OC是面向对象语言,其中“对象”就是基本 “基本构造单元”;在对象之间传递数据并执行任务的过程就叫做 “消息传递”;当应用程序运行起来后,为其提供相关支持的代码就称为 “Objective-C的运行期环境”。
理解“属性”这一概念
“属性”是Objective-C的一项特性,用来封装数据。OC对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”来访问。其中“获取方法用来读取变量值”,而“设置方法”用来卸乳变量值。
- 访问属性时,使用“点语法”和直接调用存取方法之间没有丝毫差别;因为 编译器会把“点语法”转化为对存取方法的调用。
-
编译器会为属性自动合成存取方法,若不想让编译器自动合成存取方法,可以使用
@dynamic
关键字。例如:
@interface EOCPerson : NSManageObject
@property NSString *firstName;
@end
@implementation
@dynamic firstName;
@end
编译器就不会再为上面的这个类自动合成存取方法或实例变量。
属性特质
- 原子性:在默认情况下,由编译器所合成的方法
会通过锁定机制确保其原子性
。但是当属性具备nonatomic
特质,则不使用同步锁。尽管没有名为“automic”的特质,但是只要属性不具有nonatomic特质,那么它就时“原子性”的。 - 读写权限:
(1)具备readwrite(
读写)特质的属性拥有“获取方法”和“设置方法(setter)”。若该属性由@synthesize
实现,则编译器会自动生成这两个方法。
(2)具备readonly
(只读)特质的属性仅拥有获取方法。 - 内存管理语义:
(1)assign
:“设置方法”只会执行针对“纯量类型”(例如CGFloat、NSInteger等)的 简单赋值操作。
(2)strong
:拥有此此特质的属性,当为这种属性设置新值时,设置方法会 先保留新值,并释放旧值,然后在将新值赋上去。
(3)weak
:拥有此此特质的属性,当为这种属性设置新值时,设置方法会 既不会保留新值,也不会释放旧值;然而在属性所指的对象遭到摧毁时,属性值也会清空。
(4)unsafe_unretained
:此特质的语义与assign相同,但是它适用于“对象类型” 该特质表达一种“非拥有关系”(unretain,不保留),当目标对象被摧毁时,属性值不会自动清空,这一点与weak有区别。
(5)copy
:此特质所表达的所属关系与strong相类似。然而设置方法并不保留新值,而是将其“拷贝”
。相当于拷贝一份新的对象,然后对其操作。
atomic和nonatomic的区别?
具备atomic特质的获取方法会通过锁定机制
来确保操作的原子性。也就是说,如果两个线程读写同一属性,那么不论何时,总能看到有效的属性值;若是不加锁的话,当其中一个线程读写属性时,另外一个线程突然闯入把未修改好的值读取出来。这种情况就会导致读取的值可能不对。
尽量将所有属性都声明为nonatomic
这样做的原因:在iOS中使用同步锁的开销较大,这会带来性能问题。一般情况下,并不要求属性必须时“原子的”,因为这并不能保证“线程安全”,若要保证线程安全,还得采用更为深层的锁定机制。
要点
- 可以用@property语法来定义对象中封装的数据。
- 通过“特质”来指定存储数据时所需的正确语义。
- 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
- 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。
在对象内部尽量直接访问实例变量
在写入实例变量时,通过其“设置方法”来做,而在读取实例变量时,则直接访问。之所以要通过“设置方法”来写入实例变量,其首要原因在于,这样做能够确保相关的“内存管理语义”得以贯彻。但是需要注意几个问题:
- 在
初始化方法中直接访问实例变量
,因为子类kennel会重写设置方法。 -
“惰性初始化”
(懒加载),在这种情况下,必须通过“获取方法”来访问属性,否则实例变量就永远不会初始化。 类似下面代码:
//这个属性不经常用,且创建该属性的成本较高,就可以“惰性初始化”。
- (EOCBrain*)brain {
if (!_brain) {
_brain = [Brain new];
}
return _brain;
}
若没有“获取方法”就直接访问实例变量,则会看到尚未设置好的brain,所以说,如果使用了“惰性初始化”,就必须通过存取方法来访问brain属性。
要点:
- 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
- 在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据。
- 有时会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据。
理解:“对象等同性”这一概念
有时候使用 “ == ”并不能得到我们想要的结果,因为“ ==”比较的是两个指针本身,而不是其所指的对象。应该使用“isEqual:”方法来判断两个对象的等同性。不了解i“sEqual:”和“ ==”的,可以看一下这篇博客:【iOS】——==与isEqual方法。
- NSObject协议中有两个用于判断等同性的关键方法:
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
NSObject类对这两个方法的默认实现是:当且仅当其“指针值”完全相等时
,这两个对象才相等。
2. 如果“isEqual:
”方法判定两个对象相等,那么其hash
方法也必须返回同一个值。但是如果两个对象的hash方法返回同一个值,那么“isEqual:”方法未必会认为两者相等。
3. 在实现两个方法时:isEqual
方法可以根据自己的需求来重写,hash
方法最好返回 对象的每个属性的hash值进行与运算的结果。
特定类所具有的等同判定方法
除了刚才提到的NSString之外,NSArray
与NSDictionary
类也具有特殊的等同性判断方法,前者为“isEqualToArray:
”,后者为“isEqualToDictionary:
”。如果和其比较的对象不是数组或字典,那么这两个方法会抛出异常。因为OC在编译期不做强类型检查,所以开发者要保证所传对象的类型时正常的
。
在自己编写判定方法时,要一并重写“isEqual:
”方法。后者实现法方式为:如果受测的参数与接受消息的对象都属于同一个类,那么就调用自己编写的判定方法,否则就交由超类判断。像下面代码写的一样:
等同性判定的执行深度
以NSArray为例,NSArray的检测方法为先看两个数组所含对象个数是否相同,若相同则在每个对应的位置的两个对象身上调用其isEqual:
方法。如果对应位置上的对象均相等,那么这两个数组就相等,这叫做“深度等同性判断”。有时候可以比较唯一不变的标识符
,若标识符一样,则可判定为两个对象相同。
容器中可变类的等同性
当把某个对象放入collection
后,就不应该再改变其哈希码了。因为collection
会把各个对象按照其哈希码分装到不同的“箱子数组”中。如果某对象在放入“箱子”之后,哈希码又发生变化,那么其所处的这个箱子对他来说就是“错误”的。
要点:
- 若想检测对象的等同性,请提供
isEqual:
方法和hash
方法。 - 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
- 不要盲目地逐个检测每条属性,而是应该按照具体需求来制定检测方案。
- 编写hash方法时,应该使用计算速度快而且哈希码碰撞纪律低的算法算法。
以“类族模式”隐藏实现细节
“类族”是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节。
创建类族
首先定义抽象基类,将一些状态等变量用枚举、switch语句
等来完成,然后新建一个类,声明一些方法,然后再创建一个新类(实体子类
),继承前面那个类,然后实现在基类中声明的方法。这其实是一种“工厂模式
”。
Cocoa里的类族
- 判断某个实例所属的类是否位于类族中,使用类型信息查询方法,不能直接检测两个“类对象”是否等同,而应该采用下列代码:
id maybeAnArray = /*...*/
if(maybeAnArray isKindOfClass:[NSArray class]) {
//will be hit
}
- 我们经常需要新增实体子类,若是没有“工厂方法”的源代码,就无法向其中新增类别了。对于Cocoa中的NSArray这种类族来说,还是有方法新增的,但是需要遵守下面几条规定:
- 子类应该继承自类族中的抽象基类。
- 子类应该定义自己的数据存储方式。
- 子类应当重写超类文档中指明需要重写的方法。
要点:
3. 类族模式可以把实现细节隐藏在一套简单的公共借口后面。
4. 系统框架中经常使用类族
5. 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。
在既有类中使用关联对象存放自定义数据
有时需要在对象中存放相关信息。这时我们通常会从对象所属的类中继承一个子类,然后改用这个子类对象。然而并非所有情况下都能这么做,有时候类的实例可能由某种机制所创建的,而开发者无法令这种机制创建出自己所写的子类实例。Objective-C有一项特性可以解决此问题,就是“关联对象
”。
可以给某个对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”,用以维护相应的“内存管理语义”。存储策略由名为objc_AssociationPolicy
的枚举所定义。下表列出了与之等效的@property
属性:假如关联对象成为了属性,那么它就会具备对应的语义。
以下方法可以管理关联对象:
要点:
- 可以通过“关联对象”机制来把两个对象连起来
- 定义关联对象可以制定内存管理语义,用来模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
- 只有在其他做法不可行时才选用关联对象,因为这种做法比使用关联对象要好。
理解objc_msgSend的作用
- 在Objective-C中,如果向某对象传递信息,那就会使用
动态绑定机制
来决定需要调用的方法。在底层所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全由运行期决定,甚至可以在程序运行时改变。这些特性使Objective- C成为一门真正的动态语言。 - 给对象发送信息可以这样写:
id returnValue = [someObject messageName:parameter];
在本例中,someObject
叫做“接收者”,messageName
叫做“选择子”。==选择子和参数合起来称为“消息”。==编译器看到此条消息后,将其转化为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_masgSend
,其原型如下:
void objc_msgSend(id self, SEL cmd, ...)
这是个“参数可变的函数”,能接受两个或两个以上的参数。第一个参数代表接收者,第二个参数代表选择子(SEL是选择子的类型)后续参数就是消息中的那些参数,其顺序不变。选择子值得就是方法的名字。“选择子”与“方法”这两个词经常交替使用。编译器会把上面那个例子中的消息转化为如下函数:
id returnValue = objc_msgSend(someObject,
@selector(messageName:)
parameter);
objc_msgSend
函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接受者所属的类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,则跳至其实现代码。若是找不到,,那就沿着继承体系继续向上找,等找到合适的方法之后再跳转。如果最终还是找不到,那就执行“消息转发”操作。
2. 消息调用情况中的一些“边界情况”,需要由下面的一些函数来处理:
-
objc_msgSend_stret
:如果待发送的消息要返回结构体,则可使用此函数处理。仅限于返回结构体不是太大的情况下。 -
objc_msgSend_fpret
:如果消息返回的是浮点数,则可使用此函数处理。 -
objc_msgSendSuper
:如果需要给超类发消息,则可使用此函数处理。
要点:
- 消息由接收者、选择子及参数构成。给某对象“发送信息”也就相当于在该对象上“调用方法”。
- 发给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。
理解消息转发机制
上面讲了对象的消息传递机制,并强调了其重要性,这条讲解一下对象在收到无法解读的消息之后会发生什么情况。
- 当对象接收到无法解读的消息时,就会启动“消息转发机制”,程序员就可以将此过程告诉对象应该如何处理未知消息。
- 消息转发分为两大阶段:第一阶段先征询接收者,所属的类,看是否能动态添加方法,已处理当前这个“未知的选择子”,这叫做“动态方法解析”。第二阶段涉及“完整的消息转发机制”。
动态方法解析
对象在收到无法解读的消息后,首先调用其所属类的下列类方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector
该方法的参数就是那个未知的选择子,其返回值为Boolean
类型,表示这个类是否能新增一个实例方法用以处理此选择子。假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另外一个方法,该方法与“resolveInstanceMethod:”类似,叫做“resolveClassMethod:”。
备援接收者
当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期会问它:能不能把这条消息转给其他接收者来处理。处理方法如下:
- (id)forwardingTargetForSelector:(SEL)selector
方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到,就返回nil
。通过此方案,我们可以用“组合”来模拟“多重继承”的某些特性。注意
:我们无法操作经由这一步所转发的消息。若是想在发送给备援接收者之前修改消息内容,那就得通过完整的消息转发机制来做。
完整的消息转发
当转发已经来到这一步的时候,那么唯一能做的就是启用完整的消息转发转发机制了。
步骤:
- 首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子、目标及参数。在触发NSInvocation对象时,“消息派发系统”把消息指派给目标对象。此步骤会调用下列方法:
- (id)forwardInvocation:(NSInvocation*)invocation
这个方法可以实现得很简单,只需要改变调用目标,使消息在新目标上得以调用即可。
2. 实现上面方法时,若发现某调用操作不应由本类处理,则需调用超类同名的方法。这样的话,继承体系的每个类都有机会调用此请求,直至NSObject
。如果最后调用了NSObject
类的方法,那么该方法还会继而调用“doesNotRecognizeSelector
:”以抛出异常,此异常表明选择子最终未能得到处理。
消息转发全流程
下图描述了消息转发机制处理消息的各个步骤。
接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价越大。最好能在第一步就能处理完,这样的话,运行期系统就可以将此方法缓冲起来。
要点:
- 若对象无法响应某个选择子,则进入消息转发流程。
- 通过运行期的动态方法解析功能,我们可以在需要用到用到某个方法时再将其加入类中。
- 对象可以把其无法解读的某些选择子转交给其他对象来处理。
- 经过上述两步之后,如果还没办法处理选择子,那就启动完整的消息转发机制。
用“方法调配技术”和“黑盒方法”
- 既不需要源代码,也不需要通过继承子类来重写方法就能改变这个类本身的功能,这样一来,新功能将在本类的所有实例中生效,而不是仅限于重写相关方法的那些子类实例。此方案称为“方法调配”。
- 类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做
IMP
,其原型如下:
id (*IMP)(id, SEL,...)
可以通过选择子,然后找到对应的IMP,就像下图:
- 如何实现互换两个方法?可用下列函数:
void method_exchangeImplementations(Method M1, Method M2)
此函数的两个参数表示待交换的两个方法实现,而实现方法则可通过下列函数获取。
Method class_getInstanceMethod(Class aClass, SEL aSelector)
此函数根据给定的选择从类中取出与之相关的方法。执行下列代码,即可交换前面的lowercaseString和uppercaseString方法实现:
Method originalMethod = class_getInstanceMethod([NSStringclass], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSStringclass], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
从现在开始,如果在NSString实例上调用lowercaseString
方法,那么执行的将是uppercaseString
的原有实现。
- 在现实中其实很少去呼唤两个方法实现,但是可以用这个方法实现增添新功能。通过此方案,开发者可以为那些“完全不知道具体实现的”黑盒方法增加日志记录功能。
要点:
- 在运行期,可以向类中新增或替换选择子所对应的方法实现。
- 使用另一种实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。
- 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。
理解“类对象”的用意
- 一般情况下,应该指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有的消息。
- “在运行期检视对象类型”这一操作也叫作“类型信息查询”,这个特性内置于
Foundation
框架的NSObject
协议里。 - 每个Objective-C对象实例都是指向某块内存的数据的指针。
- 通用对象类型id本身就是指针。
- 每个对象结构体的首个成员是
Class
类变量。该变量定义了对象所属的类,通常称为“isa
”指针。
在类继承体系中查询类型信息
- 可以用类型信息查询方法来检视类继承体系。“
isMemberOfClass:
”能够判断出对象是否为某个特定类的实例,而“isKindOfClass:
”能够判断出对象是否为某类或派生类的实例。 - 类型信息查询方法使用
isa
指针获取对象所属的类,然后通过super_class
指针在继承体系中游走。 - 应尽量使用类型查询方法,而不应该直接比较两个类对象是否等同,因为前者可以正确处理那些使用了消息传递机制的对象。
要点:
- 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
- 如果对象类型无法在编译器确定,那么就应该使用类型信息查询方法来探知。