【Effective Objective-C】——对象、消息、运行期

文章目录

概述

OC是面向对象语言,其中“对象”就是基本 “基本构造单元”;在对象之间传递数据并执行任务的过程就叫做 “消息传递”;当应用程序运行起来后,为其提供相关支持的代码就称为 “Objective-C的运行期环境”

理解“属性”这一概念

“属性”是Objective-C的一项特性,用来封装数据。OC对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”来访问。其中“获取方法用来读取变量值”,而“设置方法”用来卸乳变量值。

  1. 访问属性时,使用“点语法”和直接调用存取方法之间没有丝毫差别;因为 编译器会把“点语法”转化为对存取方法的调用
  2. 编译器会为属性自动合成存取方法,若不想让编译器自动合成存取方法,可以使用@dynamic关键字。例如:
@interface EOCPerson : NSManageObject
@property NSString *firstName;
@end

@implementation
@dynamic firstName;
@end

编译器就不会再为上面的这个类自动合成存取方法或实例变量。

属性特质

  1. 原子性:在默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性。但是当属性具备nonatomic特质,则不使用同步锁。尽管没有名为“automic”的特质,但是只要属性不具有nonatomic特质,那么它就时“原子性”的
  2. 读写权限:
    (1)具备readwrite(读写)特质的属性拥有“获取方法”和“设置方法(setter)”。若该属性由@synthesize实现,则编译器会自动生成这两个方法。
    (2)具备readonly(只读)特质的属性仅拥有获取方法。
  3. 内存管理语义:
    (1)assign:“设置方法”只会执行针对“纯量类型”(例如CGFloat、NSInteger等)的 简单赋值操作
    (2)strong:拥有此此特质的属性,当为这种属性设置新值时,设置方法会 先保留新值,并释放旧值,然后在将新值赋上去
    (3)weak:拥有此此特质的属性,当为这种属性设置新值时,设置方法会 既不会保留新值,也不会释放旧值;然而在属性所指的对象遭到摧毁时,属性值也会清空
    (4)unsafe_unretained:此特质的语义与assign相同,但是它适用于“对象类型” 该特质表达一种“非拥有关系”(unretain,不保留),当目标对象被摧毁时,属性值不会自动清空,这一点与weak有区别。
    (5)copy:此特质所表达的所属关系与strong相类似。然而设置方法并不保留新值,而是将其“拷贝”。相当于拷贝一份新的对象,然后对其操作。

atomic和nonatomic的区别?

具备atomic特质的获取方法会通过锁定机制来确保操作的原子性。也就是说,如果两个线程读写同一属性,那么不论何时,总能看到有效的属性值;若是不加锁的话,当其中一个线程读写属性时,另外一个线程突然闯入把未修改好的值读取出来。这种情况就会导致读取的值可能不对。

尽量将所有属性都声明为nonatomic

这样做的原因:在iOS中使用同步锁的开销较大,这会带来性能问题。一般情况下,并不要求属性必须时“原子的”,因为这并不能保证“线程安全”,若要保证线程安全,还得采用更为深层的锁定机制。

要点

  1. 可以用@property语法来定义对象中封装的数据。
  2. 通过“特质”来指定存储数据时所需的正确语义。
  3. 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
  4. 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。

在对象内部尽量直接访问实例变量

在写入实例变量时,通过其“设置方法”来做,而在读取实例变量时,则直接访问。之所以要通过“设置方法”来写入实例变量,其首要原因在于,这样做能够确保相关的“内存管理语义”得以贯彻。但是需要注意几个问题:

  1. 初始化方法中直接访问实例变量,因为子类kennel会重写设置方法。
  2. “惰性初始化”(懒加载),在这种情况下,必须通过“获取方法”来访问属性,否则实例变量就永远不会初始化。 类似下面代码:
//这个属性不经常用,且创建该属性的成本较高,就可以“惰性初始化”。
- (EOCBrain*)brain {
	if (!_brain) {
		_brain = [Brain new];
	}
	return _brain;
}

若没有“获取方法”就直接访问实例变量,则会看到尚未设置好的brain,所以说,如果使用了“惰性初始化”,就必须通过存取方法来访问brain属性。

要点:

  1. 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
  2. 在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据。
  3. 有时会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据。

理解:“对象等同性”这一概念

有时候使用 “ == ”并不能得到我们想要的结果,因为“ ==”比较的是两个指针本身,而不是其所指的对象。应该使用“isEqual:”方法来判断两个对象的等同性。不了解i“sEqual:”和“ ==”的,可以看一下这篇博客:【iOS】——==与isEqual方法

  1. NSObject协议中有两个用于判断等同性的关键方法:
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;

NSObject类对这两个方法的默认实现是:当且仅当其“指针值”完全相等时,这两个对象才相等。
2. 如果“isEqual:”方法判定两个对象相等,那么其hash 方法也必须返回同一个值。但是如果两个对象的hash方法返回同一个值,那么“isEqual:”方法未必会认为两者相等。
3. 在实现两个方法时:isEqual方法可以根据自己的需求来重写,hash方法最好返回 对象的每个属性的hash值进行与运算的结果

特定类所具有的等同判定方法

除了刚才提到的NSString之外,NSArrayNSDictionary类也具有特殊的等同性判断方法,前者为“isEqualToArray:”,后者为“isEqualToDictionary:”。如果和其比较的对象不是数组或字典,那么这两个方法会抛出异常。因为OC在编译期不做强类型检查,所以开发者要保证所传对象的类型时正常的
在自己编写判定方法时,要一并重写“isEqual:”方法。后者实现法方式为:如果受测的参数与接受消息的对象都属于同一个类,那么就调用自己编写的判定方法,否则就交由超类判断。像下面代码写的一样:
【Effective Objective-C】——对象、消息、运行期

等同性判定的执行深度

以NSArray为例,NSArray的检测方法为先看两个数组所含对象个数是否相同,若相同则在每个对应的位置的两个对象身上调用其isEqual:方法。如果对应位置上的对象均相等,那么这两个数组就相等,这叫做“深度等同性判断”。有时候可以比较唯一不变的标识符,若标识符一样,则可判定为两个对象相同。

容器中可变类的等同性

当把某个对象放入collection后,就不应该再改变其哈希码了。因为collection会把各个对象按照其哈希码分装到不同的“箱子数组”中。如果某对象在放入“箱子”之后,哈希码又发生变化,那么其所处的这个箱子对他来说就是“错误”的。

要点:

  1. 若想检测对象的等同性,请提供isEqual:方法和hash方法。
  2. 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
  3. 不要盲目地逐个检测每条属性,而是应该按照具体需求来制定检测方案。
  4. 编写hash方法时,应该使用计算速度快而且哈希码碰撞纪律低的算法算法。

以“类族模式”隐藏实现细节

“类族”是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节。

创建类族

首先定义抽象基类,将一些状态等变量用枚举、switch语句等来完成,然后新建一个类,声明一些方法,然后再创建一个新类(实体子类),继承前面那个类,然后实现在基类中声明的方法。这其实是一种“工厂模式”。

Cocoa里的类族

  1. 判断某个实例所属的类是否位于类族中,使用类型信息查询方法,不能直接检测两个“类对象”是否等同,而应该采用下列代码:
id maybeAnArray = /*...*/
if(maybeAnArray isKindOfClass:[NSArray class]) {
	//will be hit
}
  1. 我们经常需要新增实体子类,若是没有“工厂方法”的源代码,就无法向其中新增类别了。对于Cocoa中的NSArray这种类族来说,还是有方法新增的,但是需要遵守下面几条规定:
  • 子类应该继承自类族中的抽象基类。
  • 子类应该定义自己的数据存储方式。
  • 子类应当重写超类文档中指明需要重写的方法。

要点:
3. 类族模式可以把实现细节隐藏在一套简单的公共借口后面。
4. 系统框架中经常使用类族
5. 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。

在既有类中使用关联对象存放自定义数据

有时需要在对象中存放相关信息。这时我们通常会从对象所属的类中继承一个子类,然后改用这个子类对象。然而并非所有情况下都能这么做,有时候类的实例可能由某种机制所创建的,而开发者无法令这种机制创建出自己所写的子类实例。Objective-C有一项特性可以解决此问题,就是“关联对象”。
可以给某个对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”,用以维护相应的“内存管理语义”。存储策略由名为objc_AssociationPolicy的枚举所定义。下表列出了与之等效的@property属性:假如关联对象成为了属性,那么它就会具备对应的语义

【Effective Objective-C】——对象、消息、运行期

以下方法可以管理关联对象:
【Effective Objective-C】——对象、消息、运行期

要点:

  • 可以通过“关联对象”机制来把两个对象连起来
  • 定义关联对象可以制定内存管理语义,用来模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
  • 只有在其他做法不可行时才选用关联对象,因为这种做法比使用关联对象要好。

理解objc_msgSend的作用

  1. 在Objective-C中,如果向某对象传递信息,那就会使用动态绑定机制来决定需要调用的方法。在底层所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全由运行期决定,甚至可以在程序运行时改变。这些特性使Objective- C成为一门真正的动态语言。
  2. 给对象发送信息可以这样写:
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如果需要给超类发消息,则可使用此函数处理。

要点:

  • 消息由接收者、选择子及参数构成。给某对象“发送信息”也就相当于在该对象上“调用方法”。
  • 发给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。

理解消息转发机制

上面讲了对象的消息传递机制,并强调了其重要性,这条讲解一下对象在收到无法解读的消息之后会发生什么情况

  1. 当对象接收到无法解读的消息时,就会启动“消息转发机制”,程序员就可以将此过程告诉对象应该如何处理未知消息。
  2. 消息转发分为两大阶段:第一阶段先征询接收者,所属的类,看是否能动态添加方法,已处理当前这个“未知的选择子”,这叫做“动态方法解析”。第二阶段涉及“完整的消息转发机制”。

动态方法解析

对象在收到无法解读的消息后,首先调用其所属类的下列类方法:

+ (BOOL)resolveInstanceMethod:(SEL)selector

该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另外一个方法,该方法与“resolveInstanceMethod:”类似,叫做“resolveClassMethod:”。

备援接收者

当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期会问它:能不能把这条消息转给其他接收者来处理。处理方法如下:

- (id)forwardingTargetForSelector:(SEL)selector

方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到,就返回nil。通过此方案,我们可以用“组合”来模拟“多重继承”的某些特性。
注意:我们无法操作经由这一步所转发的消息。若是想在发送给备援接收者之前修改消息内容,那就得通过完整的消息转发机制来做。

完整的消息转发

当转发已经来到这一步的时候,那么唯一能做的就是启用完整的消息转发转发机制了。
步骤:

  1. 首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子、目标及参数。在触发NSInvocation对象时,“消息派发系统”把消息指派给目标对象。此步骤会调用下列方法:
- (id)forwardInvocation:(NSInvocation*)invocation

这个方法可以实现得很简单,只需要改变调用目标,使消息在新目标上得以调用即可。
2. 实现上面方法时,若发现某调用操作不应由本类处理,则需调用超类同名的方法。这样的话,继承体系的每个类都有机会调用此请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,此异常表明选择子最终未能得到处理。

消息转发全流程

下图描述了消息转发机制处理消息的各个步骤。
【Effective Objective-C】——对象、消息、运行期

接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价越大。最好能在第一步就能处理完,这样的话,运行期系统就可以将此方法缓冲起来。

要点:

  • 若对象无法响应某个选择子,则进入消息转发流程。
  • 通过运行期的动态方法解析功能,我们可以在需要用到用到某个方法时再将其加入类中。
  • 对象可以把其无法解读的某些选择子转交给其他对象来处理。
  • 经过上述两步之后,如果还没办法处理选择子,那就启动完整的消息转发机制。

用“方法调配技术”和“黑盒方法”

  1. 既不需要源代码,也不需要通过继承子类来重写方法就能改变这个类本身的功能,这样一来,新功能将在本类的所有实例中生效,而不是仅限于重写相关方法的那些子类实例。此方案称为“方法调配”。
  2. 类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP,其原型如下:
id (*IMP)(id, SEL,...)

可以通过选择子,然后找到对应的IMP,就像下图:
【Effective Objective-C】——对象、消息、运行期

  1. 如何实现互换两个方法?可用下列函数:
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的原有实现。

  1. 在现实中其实很少去呼唤两个方法实现,但是可以用这个方法实现增添新功能。通过此方案,开发者可以为那些“完全不知道具体实现的”黑盒方法增加日志记录功能。

要点:

  • 在运行期,可以向类中新增或替换选择子所对应的方法实现。
  • 使用另一种实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。
  • 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

理解“类对象”的用意

  • 一般情况下,应该指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有的消息。
  • 在运行期检视对象类型”这一操作也叫作“类型信息查询”,这个特性内置于Foundation框架的NSObject协议里。
  • 每个Objective-C对象实例都是指向某块内存的数据的指针。
  • 通用对象类型id本身就是指针
  • 每个对象结构体的首个成员是Class类变量。该变量定义了对象所属的类,通常称为“isa”指针。

在类继承体系中查询类型信息

  • 可以用类型信息查询方法来检视类继承体系。“isMemberOfClass:”能够判断出对象是否为某个特定类的实例,而“isKindOfClass:”能够判断出对象是否为某类或派生类的实例。
  • 类型信息查询方法使用isa指针获取对象所属的类,然后通过super_class指针在继承体系中游走。
  • 应尽量使用类型查询方法,而不应该直接比较两个类对象是否等同,因为前者可以正确处理那些使用了消息传递机制的对象。

要点:

  • 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
  • 如果对象类型无法在编译器确定,那么就应该使用类型信息查询方法来探知。
上一篇:『迷你教程』掌握Xgboost损失函数构建你的完美模型


下一篇:Linux基础命令---bzmore