[OC学习笔记]对象消息运行期

Posted Billy Miracle

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[OC学习笔记]对象消息运行期相关的知识,希望对你有一定的参考价值。

一、理解“属性”这一概念

属性(property)是OC的一项特性,用于封装对象中的数据。

属性特质:

1.原子性:默认情况下,由编译器合成的方法会通过锁定机制确保其原子性(atomicity)。如具备nonatomic特质,则不使用同步锁。
读写权限

  • readwirte(读写)
  • readonly(只读)

内存管理语义

  • assign:“设置方法”只会执行针对“纯量类型”(scalar type)的简单赋值操作。
  • strong:此特质表明该属性定义了一种“拥有关系”(owing relationship)。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。
  • weak:此特质表明该属性定义了一种“非拥有关系”(nonowing relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。同assign类似,然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。
  • unsafe_unretained:此特质语义和assign相同,但是它适用于“对象类型”(object type)该特质表达一种“非拥有关系”(“不保留”,unretained),当目标对象遭到摧毁时,属性值不会自动清空(“不安全”,unsafe),这一点与weak有区别。
  • copy:此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其拷贝(copy)。

2.方法名
指定存取方法的方法名:

  • getter=< name >指定“获取方法”的方法名:
@property (nonatomic, getter=isOn) BOOL on;
  • setter=< name >指定“设置方法”的方法名。

注意:开发ios程序时应该使用nonatomic属性,因为atomic会严重影响性能。

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

在对象之外访问实例变量,总是应该通过属性来做,然而在对象内部访问实例变量时又该如何呢?在这里,除一些特殊情况外,建议在读取实例变量的时候采用直接访问的形式,而在设置实例变量时通过属性来做。
例:

@interface ECOPerson : NSObject

@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;

- (NSString*)fullName;
- (void)setFullName:(NSString*)fullName;

@end

fullNamesetFullName这两个“便捷方法”可以这样来实现:

- (NSString*)fullName 
	return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];


- (void)setFullName:(NSString*)fullName 
	NSArray *components = [fullName componentsSeparatedByString:@" "];
	self.firstName = [components objectAtIndex:0];
	self.lastName = [components objectAtIndex:1];

在fullName的获取方法与设置方法中,我们使用“点语法”,通过存取方法来访问相关实例变量。现在假设重写这两个方法,不经由存取方法,而是直接访问实例变量:

 - (NSString*)fullName 
	return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];


 - (void)setFullName:(NSString*)fullName 
	NSArray *components = [fullName componentsSeparatedByString:@" "];
	_firstName = [components objectAtIndex:0];
	_lastName = [components objectAtIndex:1];

这两种写法有几个区别:

  • 不经过OC的“方法派发”(method dispatch)步骤,所以直接访问实例变量的速度更快。这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
  • 直接访问实例变量时,不会调用其“设置方法”,这就饶过了为相关属性定义的“内存管理语义”。比方说,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。
  • 如果直接访问实例变量,那么不会触发“键值观测”(KVO)通知。
  • 通过属性来访问有助于排查与之相关的错误。

有一个合理的折中方案:在写入实例变量时,通过其“设置方法”来做,而在读取实例变量时,则直接访问。此办法既能提高读取操作的速度,又能控制对属性的写入操作。之所以要通过“设置方法”写入实例变量,首要原因在于能够把“内存管理语义”得以贯彻。但仍需要注意一些问题。
第一个要注意的地方,在初始化方法中应该如何设置属性值。这种情况下总是应该直接访问实例变量,因为子类可能会“覆写”(override)设置方法。假设ECOPerson有一个字类叫做ECOSmithPerson,这个子类专门表示那些姓“Smith”的人。该子类可能会覆写lastName属性所对应的设置方法:

- (void)setLastName:(NSString*)lastName 
	if (![lastName isEqualToString@"Smith"]) 
		[NSException raise:NSInvalidArgumentException format:@"Last name must be Smith"];
	
	self.lastName = lastname;

在基类ECOPerson的默认初始化方法中,可能会将姓氏设为空字符串。此时若是通过“设置方法”来做,那么调用的将会是子类的设置方法,从而抛出异常。但是,某些情况下却又必须在初始化方法中调用设置方法:如果待初始化的实例变量声明在超类中,而我们又无法在子类中直接访问此实例变量的话,那么就需要调用“设置方法”了。
此外还有一个要注意的点是“惰性初始化”(懒加载,lazy initialization)这种情况下,必须通过“设置方法”来访问属性。否则,实例变量就永远不会被初始化。懒加载示例:

- (ECOBrain*)brian 
	if (!_brain) 
		_brain = [[Brain alloc] init];
	
	return _brain;

初始化方法以及dealloc方法中,总是应该直接通过实例变量来读写数据。

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

根据“等同性”(equality)来比较对象是一个非常有用的功能。不过,按照“==”操作符比较出来的结果未必是我们想要的,因为它比较的是指针本身,而非所指的对象。应该使用“isEqual:”方法判断等同性。一般来说,两个类型不同的对象总是不相等的。某些对象提供了特殊的“等同性判定方法”,如果知道了两个对象都属于一个类,那么就可以使用这种方法。例:

NSString *foo = @"Badger 123";
NSString *bar = [NSString stringWithFormat:@"Badger %i", 123];
        
BOOL equalA = (foo == bar);//NO
BOOL equalB = [foo isEqual:bar];//YES
BOOL equalC = [foo isEqualToString:bar];//YES

可以看出“==”和等同性判断方法之间是有区别的。NSString类实现了一个自己独有的等同性判断方法,名叫“idEqualToString:”。调用该方法比调用“isEqual:”方法快,因为它不知道受测对象的类型。
NSObject协议中有两个用于判断等同性的方法:

- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;

默认实现是:当且仅当其“指针值”相等时,这两个对象才相等。若想在自定义的对象中正确覆写这些方法,就必须先理解其约定(contract)。如果“isEqual:”判定两个对象相等,那么其hash方法也比须返回同一个值。但是,如果两个对象的hash方法返回同一个值,那么“isEqual:”方法未必会认为两者相等。
比如:

@interface ECOPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (noantomic, assign) NSUInteger age;
@end

若认为两个ECOPerson的所有字段均相等,那么这两个对象就相等,于是“isEqual:”方法可以写成:

- (BOOL)isEqual:(id)object 
	if (self == object) 
		return YES;
	
	if ([self class] != [object class]) 
		return NO;
	
	ECOPerson *otherPerson = (ECOPerson*)object;
	if (![_firstName isEqualToString:otherPerson.firstName]) 
		return NO;
	
	if (![_lastName isEqualToString:otherPerson.lastName]) 
		return NO;
	
	if (_age != otherPerson.age) 
		return NO;
	
	return YES;

首先判断指针,再比较类,不过,有时候可能认为一个类的实例可能与其一个子类的实例相等,需要注意。最后再检测各属性。
接下来该实现hash方法了,根据等同性约定,若两对象相等,则其哈希码(hash)也相等,但是,两个哈希码相同的对象未必相等。这是能否正确覆写“isEqual:”方法的关键所在。下面的方法完全可行:

- (NSUInteger)hash 
	return 1337;

这么写在collection中使用会产生性能问题,因为collection检索哈希表时会使用哈希码做索引。假如某个collection是用set实现的,在向set中添加对象时要根据哈希码找到数组,依次检查,耗费性能。
hash方法也可以这样实现:

- (NSUInteger)hash 
	NSString *stringToHash = [NSString stringWithFormat:@"%@:%@:%i", _firstName, _secondName, _age];
	return [stringToHash hash];

这样做还要负担创建字符串的开销,所以添加到collection中时,也会产生性能问题,因为想要添加,必须先计算其哈希码。
最后一种方法:

- (NSUInteger)hash 
	NSUInteger fistNameHash = [_firstName hash];
	NSUInteger lastNameHash = [_lastName hash];
	NNSUInteger ageHash = _age;
	return firstNameHash ^ lastNameHash ^ ageHash;

这样能够保持较高效率,又能使生成的哈希码至少位于一定的范围内,而不会过于频繁的重复。当然,此算法生成的哈希码还是会碰撞(collision)。编写hash方法时,应当用当前的对象做做实验,以便在减少碰撞频度与降低运算复杂度间取舍。
有关等同性判断,下面这篇文章也有简短的解读:OC处理对象

(一)特定类所具有的等同性判定方法

在这些特定方法中,必须保证传入的对象的类型是正确的。
自己创建等同性判定方法,可以稍加改动,使用这种判定方法编出来的代码更易读懂,而且不用检查两个受测对象的类型了。
在编写判定方法时,也应一并覆写“isEqual:”方法。后者的常见实现方式为:如果受测的参数与接受该消息的对象都属于同一个类,那么调用自己编写的方法,否则交由超类来判断,如:

- (BOOL)isEqualToPerson:(ECOPerson*)otherPerson 
	if (![_firstName isEqualToString:otherPerson.firstName]) 
		return NO;
	
	if (![_lastName isEqualToString:otherPerson.lastName]) 
		return NO;
	
	if (_age != otherPerson.age) 
		return NO;
	
	return YES;

- (BOOL)isEqual:(id)object 
	if ([self class] == [object class]) 
		return [self isEqualToPerson:(ECOPerson*)object];
	 else 
		return [super isEqual:object];
	

(二)等同性判定的执行深度

创建等同性判定方法时,需要决定是根据整个对象来判断等同性,还是仅根据其中几个字段来判断。NSArray的检测方式为先看两个数组所含的对象个数是否相同,若相同,则在每个对应位置的两个对象身上调用其“isEqual:”方法。若对应位置的对象均相等,那么两个数组相等,这叫做“深度等同判定”。不过有时候无须将所有的数据逐个比较,只需根据部分数据即可判明二者是否相同。
比如,假设EOCPerson类的实例是依据数据库里的数据创建而来,那么其中就可能会含有另外一个属性,此属性为“唯一标识符”(unique identifier),在数据库中作“主键”(primary key):

@property NSUInteger identifier;

在这种情况下,我们也许只会根据标识符来判断等同性,尤其是该属性为readonly时。就无须比较每一条数据了。
只有类的编写者才可以确定两个对象实例在何种情况下判定为相等。

(三) 容器中可变类的等同性

还有一种情况要特别注意,就是在容器放入可变对象的时候。把某个对象放入collection后,就不应该再改变其哈希码了。collection会把各个对象按照其哈希码分装到不同的“箱子数组”中。如果“入箱”后哈希码又变了,那么它现在所处的箱子对它来说就是“错误”的。所以,要解决这个问题,需要确保哈希码不是根据对象的“可变部分”(mutable portion)计算出来的,或者是保证放入collection后就不再改变对象内容了。例:
先把一个数组加入set中:

NSMutableSet *set = [[NSMutableSet alloc] init];

NSMutableArray *arrayA = [@[@1, @2] mutableCopy];
[set addObject:arrayA];
NSLog(@"set = %@", set);

输出:

set = ((1,2))

现在set里含有一个数组对象,数组中包含两个对象。再向set中加入一个数组,此数组与前一个数组所含对象相同,顺序也相同,于是,待加入的数组与set中已有的数组是相等的:

NSMutableArray *arrayB = [@[@1, @2] mutableCopy];
[set addObject:arrayB];
NSLog(@"set = %@", set);

输出:

set = ((1,2))

加入一个和已有数组对象相等的数组对象相等的数组对象,set不会改变。下面添加一个和set中已有对象不同的数组:

NSMutableArray *arrayC = [@[@1] mutableCopy];
[set addObject:arrayC];
NSLog(@"set = %@", set);

输出:

set = ((1),(1,2))

确实,加进去了。最后,改变下arrayC的内容,令其和最早加入的数组相等:

[arrayC addObject:@2];
NSLog(@"set = %@", set);

输出:

set = ((1,2),(1,2))

这下可好,set中竟然包含了两个彼此相等的数组!!!根据set的语义是不允许这样的。若是去拷贝set,那就更糟糕了:

NSSet *setB = [set copy];
NSLog(@"setB = %@", setB);

输出:

setB = ((1,2))

复制过的set中又只剩一个对象了。这个例子就是提示大家要注意把可变对象放入collection后响应带来的后果。

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

“类族”(class cluster)是一种很有用的模式,可以隐藏“抽象基类”(abstract base class)背后的实现细节。OC系统框架普遍使用此模式。使用“类族模式”,该模式可以灵活应对多个类,将它们的实现细节隐藏在抽象基类后面,以保持接口简洁。用户无须创建子类实例,只需要调用基类方法来创建即可。

(一)创建类族

假设有一个处理雇员的类,每个雇员都有“名字”和“薪水”两个属性,管理者可以命令其执行日常工作。但是,各种雇员的工作内容不同,经理无须关心每个人如何完成其工作,仅需指示开工即可。
首先定义抽象基类:

//EOCEmployee.h
typedef NS_ENUM(NSUInteger, EOCEmployeeType) 
    EOCEmployeeTypeDeveloper,
    EOCEmployeeTypeDesigner,
    EOCEmployeeTypeFinance,
;

@interface EOCEmployee : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger salary;

+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type;

- (void)doADaysWork;

@end
//EOCEmployee.m
#import "EOCEmployee.h"
#import "EOCEmployeeDeveloper.h"
#import "EOCEmployeeDesigner.h"
#import "EOCEmployeeFinance.h"

@implementation EOCEmployee

+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type 
    switch (type) 
        case EOCEmployeeTypeDeveloper:
            return [[EOCEmployeeDeveloper alloc] init];
            break;
            
        case EOCEmployeeTypeDesigner:
            return [[EOCEmployeeDesigner alloc] init];
            break;
            
        case EOCEmployeeTypeFinance:
            return [[EOCEmployeeFinance alloc] init];
            break;
    


- (void)doADaysWork 
    


@end

每个“实体子类”从基类继承而来。如:

//EOCEmployeeDeveloper.m
#import "EOCEmployeeDeveloper.h"

@implementation EOCEmployeeDeveloper

- (void)doADaysWork 
    NSLog(@"Develop");


@end

看看效果:

EOCEmployee *employee1 = [EOCEmployee employeeWithType:EOCEmployeeTypeDesigner];
[employee1 doADaysWork];
        
EOCEmployee *employee2 = [EOCEmployee employeeWithType:EOCEmployeeTypeDeveloper];
[employee2 doADaysWork];
        
EOCEmployee *employee3 = [EOCEmployee employeeWithType:EOCEmployeeTypeFinance];
[employee3 doADaysWork];

输出:

Design
Develop
Finance

本例中,基类实现一个“类方法”,该方法根据待创建的雇员类别分配好对应的雇员类实例。这种“工厂模式”(Factory pattern)是创建类族的办法之一。
可惜,OC这门语言没有办法指明某个基类是“抽象的”(abstract)。于是,开发者通常会在文档中写明用法。这种情况下,基类接口一般没有名为init的成员方法,暗示该类的实例也许不应该由用户直接创建。
注意,如果对象所属的类位于某个类族中,那么在查询其类型信息时就要当心了。你可能觉得自己创建了某个类的实例,然而实际上创建的是其子类的实例。上面的例子[employee1 isMemberOfClass:[EOCEmployee class]]返回NO,因为employee并非Employee类的实例,而是其子类的实例。

(二)Cocoa里的类族

系统框架中有许多类族。大部分collection类都是类族,例如NSArray与其可变版本NSMutableArray。这样看来,实际上有两个抽象基类,一个用于不可变数组,另一个用于可变数组。尽管具备公共接口的类有两个,但仍然可以和起来算做一个类族。不可变的类定义了对所有数组都通用的方法,而可变的类则定义了那些只适用于可变类型的方法。两个类属于同一个类族,这意味着二者在实现各自类型的数组可以共用实现代码。此外,还能够把可变数组复制成不可变数组,反之亦然。
像NSArray这样的类的背后是个类族(对于大部分collection类都是),下面给出错误示例:

id maybeAnArray = /*...*/;
if ([maybeAnArray class] == [NSArray class]) 
	//永远无法执行

因为NSArray是个类族。if语句永不为真。因为初始化方法所返回的那个歌实例隐藏在类族公共接口(public facade)后面的某个内部类型(internal type)。
不过,有办法判断出某个实例所属的类是否位于类族之中。我们可以改用信息类型查询方法(introspection method)。例:

id maybeAnArray = /*...*/;
if ([maybeAnArray isKindOfClass:[NSArray class]]) 
	//会被执行

我们常要向类族中新增实体子类,不过这么做的时候要留心。在Employee这个例子中,若是没有“工厂方法”(factory method)的源代码,那么就无法向其中新增雇员类别了。然而对于Cocoa中的NSArray这样的类族来说,还是有办法新增子类的,但需要遵守几条规则

  • 子类应该继承自类族中的抽象基类。
  • 子类应该定义自己的数据存储方式。
  • 子类应当覆写超类文档指明要覆写的方法。

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

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

关联类型等效的@property属性
OBJC_ASSOCIATION_ASSIGNassign
OBJC_ASSOCIATION_RETAIN_NONATOMICnonatomic,retain
OBJC_ASSOCIATION_COPY_NONATOMICnonatomic,copy
OBJC_ASSOCIATION_RETAINretain
OBJC_ASSOCIATION_COPYcopy

下列方法可以管理关联对象:

  • void objc_setAssociatedObject (id object, void *key, id value, objc_AssociationPolicy policy)
    此方法以给定的键和策略为某对象设置关联对象值。
  • id objc_getAssociatedObject (id object, void *key)
    此方法根据给定的键从某对象中获取相应的关联对象值。
  • void objc_removeAssociatedObjects(id object)
    此方法移除指定对象的全部关联对象。

我们可以把某对象想象成NSDictionary,把关联到该对象的键值理解为字典中的条目。于是,存取关联对象的值就相当于在NSDictionary对象上调用[object setObject:value forKey: key][object objectForKey:key]方法。然而两者之间有个重要差别:设置关联对象时用的键(key)是个“不透明的指针”(opaque pointer)。如果在两个键上调用“isEqual:”方法的返回值为YES,那么NSDictionary就认为二者相等;然而在设置关联对象值时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,在设置关联对象值时,通常使用静态全局变量做键。

关联对象用法举例

UIAlertView类的一个实例
注意:
这种做法也许很好用,但是只应该在其他办法行不通时才去考虑用它。若是滥用,则很快会使代码失控,使其难以调试。“保留环”(retain cycle)产生的原因很难查明,因为关联对象之间的关系并没有正式的定义,其内存管理语义是在关联的时候才定义的,而不是在接口中预先定好的。使用这种写法时要小心,不能仅仅因为某处可以用该写法就一定要用它

六、理解objc_msgSend的作用

对象上调用方法是OC中常用的功能,用OC的术语来说,叫做“传递消息”(pass a message)。消息有“名称”(name)或“选择子”(selector),可以接受参数,而且可能还有返回值。
由于OC是C的超集,所以最好先理解C的函数调用方式。C使用“静态绑定”(static bounding),也就是说,在编译期就能决定运行时所应调用的函数

#import <stdio.h>

void printHello() 
	printf("Hello, world!\\n");

void printGoodbye() 
	printf("Goodbye, world!\\n");


void doTheThing (int type) 
	if (type == 0) 
		printHello();
	 else 
		printGoodbye();
	
	return 0;

如果不考虑“内联”(inline),那么编译器在编译代码的时候就知道程序中有printHelloprintGoodbye这两个函数了,于是会直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中的
若把代码改成这样:

#import <stdio.h>

void printHello() 
	printf("Hello, world!\\n");

void printGoodbye() 
	printf("Goodbye, world!\\n");


void doTheThing (int type) 
	void (*func)();
	if (type == 0) 以上是关于[OC学习笔记]对象消息运行期的主要内容,如果未能解决你的问题,请参考以下文章

-对象消息运行期)第6条:理解“属性”这一概念

efffective Objective-C 学习笔记

JavaScript学习笔记————JavaScript的应用环境

《深入理解 Java 虚拟机》读书笔记:晚期(运行期)优化

ojective-C学习笔记关于面向对象编程

Effective Objective -C 第一章 熟悉iOS