【iOS】Effective Objective-C第二章

【iOS】Effective Objective-C第二章

理解"属性"这一概念

  1. 属性的本质

属性是Objective-C的一项特性,用于封装对象中的数据。Objective-C对象通常会把所需要的数据保存为各种实例变量。实例变量一般通过"存取方法"来访问。其中,"获取方法"(getter)用于读取变量值,而"设置方法"(setter)用于写入变量值。

编写Objective-C代码时,对象布局在编译器已经固定了,只要访问到实例变量的代码,编译器就把其替换为"偏移量",这个偏移量是"硬编码",表示该变量距离存放对象的内存区域的起始地址有多远。

如果代码使用了编译器计算出来的偏移量,那么在修改类定义之后必须重新编译否者就会出错,Objective-C的做法是,把实例变量当作一种存储偏移量所用的"特殊变量",交给"类对象"保管。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样,无论何时访问实例变量,总能找到正确的偏移量。

这个问题的另一个解决办法就是尽量不直接访问实例变量,而通过存取方法来做

在对象接口的定义中,可以使用属性,能够封装在对象里的数据。属性的本质是编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量

objc 复制代码
@interface EOCPerson: NSObject
@property NSString *firstName;
@propertyNSString *lastName;
@end
objc 复制代码
linterface EOCPerson : NSObject
- (NSString*)firstName;
-(void) setFirstName:(NSString*)firstName;
-(NSString*) lastName;
- (void) setLastName: (NSString*)lastName;
@end

这两种代码等效。

  1. 访问属性

要访问属性,可以使用"点语法"。点语法的优势在于:

  • 编译器会把"点语法"转换为存取方法的调用,此过程叫做"自动合成",这个过程有编译器在编译期执行。
  • 编译器自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。

也可以在类的实现代码中通过@synthesize语法来指定实例变量的名字:

objc 复制代码
@implementation EOCPerson
@synthesize firstName = _myFirstName;
@synthesize  lastName  = _myLastName;
@end

这样就会将生成的实例变量命名为_myFirstName和 _myLastName,而不再使用默认名字。

若不想令编译器自动合成存取方法,则可以自己实现:

  • 如果只实现了其中一个存取方法,那么另一个还是会由编译器来合成。
  • 使用@dynamic关键字,它会告诉编译器不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。

而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错,它相信这些方法能在运行期找到。

  1. 属性特质

属性的各种特质设定也会影响编译器所生成的存取方法。属性可以拥有的特质分为四类:

  • 原子性:在默认情况下(不写默认atomic),由编译器所合成的方法通过锁定机制确保其原子性。如果属性具备nonatomic特质,则不使用同步锁。

那么atomic与nonatomic的区别是什么呢?

  • 使用atomic时getter方法会通过锁定机制来确保其操作的原子性。也就是说,如果两个线程读写同一属性,那么不论何时,总能看到有效的属性值。
  • 使用nonatomic,也就是不加锁时,当一个线程正在改写某属性值时,另外一个线程也许会突然闯入,把尚未修改好的属性值读取出来,这样线程读到的属性值就可能不对。

不过我们会发现,平时程序中所有属性都声明为nonatomic。这样做的历史原因是:在iOS中使用同步锁的开销较大,这会带来性能问题。一般情况下并不要求属性必须是"原子的",因为这并不能保证线程安全,若要实现"线程安全"的操作,还需采用更为深层的锁定机制才行。

  • 读写权限:

    • readwrite(读写):属性拥有getter方法与setter方法。若该属性由@synthesize实现,则编译器自动生成这两个方法。
    • readonly(只读):仅拥有getter方法,只有当该属性由@synthesize实现时,编译器才会为其合成setter方法。
  • 内存管理语义:属性用于封装数据,而数据则要有具体的所有权语义。

    • assign:setter方法只会执行针对"纯量类型"(例如CGFloat或NSInteger等)的简单赋值操作。
    • strong:为这种属性设置新值时,setter方法会先报留新值,并释放旧值,然后再将新值设置上去。
    • weak:为这种属性设置新值时,setter方法既不报留新值,也不释放旧值。在属性所指的对象遭到摧毁时,属性值也会清空。
    • unsafe_unretained:此特质语义和assign相同,但适用于"对象类型"。与weak不同的在于,当目标对象遭到摧毁时,属性值不会自动清空。
    • copy:其表达的所属关系与strong类似,但setter方法不保留新值,而是将其拷贝。当属性类型为NSString*时,经常用此特质来保护其封装性,因为传递给setter方法的新值有可能指向一个NSMutableString类的实例。此时若不是拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知道的情况下遭人更改。因此,这时就要拷贝一份不可变的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是可变的,就应该在设置新属性值时拷贝一份
  • 方法名:可通过如下特质指定存取方法名:

    • getter=<name>:如果某属性时BOOL类型,而你想为其getter方法加上is前缀。
    objc 复制代码
    @property (nonatomic, getter=isOn) BOOL on;
    • setter=<name>:指定setter方法方法名,不太常见。

通过以上特质,可以微调由编译器所合成的存取方法。值得注意的是:

  • 若是自己实现这些存取方法,那么应该保证其具备相关属性所声明的特质。例如:如果某个属性声明为copy,那么就应该在setter方法中拷贝相关对象。
  • 如果想在其他方法里设置属性值,那么同样遵守属性定义中所宣称的语义。例如:我们新增一个初始化方法用于设置名和姓的初始值。
objc 复制代码
@property (copy) NSString *firstName;
@property (copy) NSString *lastName;

- (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName;
objc 复制代码
- (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName {
    if (self = [super init]) {
        _firstName = [firstName copy];
        _lastName = [lastName copy];
    }
    return self;
}
  1. 总结一下:
  • 可以用@property语法来定义对象中所封装的数据。
  • 通过"特质"来指定存储数据所需的正确语义。
  • 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
  • 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。

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

在对象之外访问实例变量时,总是应该通过属性来做。然而在对象内部访问实例变量时,笔者的建议是:除了几种特殊情况之外,在读取实例变量时采用直接访问的形式,而在设置时通过属性来做。

直接访问和属性访问的区别:

  • 由于不经过Objective-C的"方法派发"步骤,所以直接访问实例变量比属性访问的速度快。
  • 直接访问实例变量时,不会调用其setter方法,这就饶过了为相关属性所定义的"内存管理语义"。比如,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。
  • 直接访问实例变量不会触发KVO通知。
  • 通过属性访问可给getter方法或setter方法中新值断点,监控该属性的调用者及其访问时机,有助于排查与之相关的错误。

对究竟如何访问实例对象的讨论很多,有一种合理的折中方案。即:在写入实例变量时,通过其setter方法来做,而在读取实例变量时,则直接访问。这样既能提高读取操作的速度,又能控制对属性的写入操作。不过要注意:

  • 在初始化方法中应该如何设置属性值:这种情况下总是应该直接访问实例变量。
    • 在init方法中,优先直接访问实例变量,避免触发子类覆写的setter逻辑。
    • 某些情况下,如果变量在超类中且子类无法直接访问,就需要调用setter方法。
  • 惰性初始化(即懒加载):这种情况下必须通过getter方法来访问属性,否则实例变量永远不会被初始化。一般用于一个属性不常用,而且创建成本高的情况。
objc 复制代码
- (ECOBrain*)brian {
	if (!_brain) {
		_brain = [[Brain alloc] init];
	}
	return _brain;
}

总结一下:

  • 在对象内部读取数据时,应该直接访问实例变量来读;而写入数据时,则应通过属性来写。
  • 在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据。
  • 有时使用惰性初始化配置某份数据时,需要通过属性来读取数据。

理解"对象等同性"这一概念

  1. 对象等同性

因为==比较操作比较的是两个指针本身,而不是其所指对象。所以应该使用NSObject协议中声明的"isEqual:"方法来判断两个对象的等同性。一般来说,两个类型不同的对象总是不相等的。

NSObject协议中有两个用于判断等同性的关键方法:

objc 复制代码
-(BOOL)isEqual:(id)object;
-(NSUInteger)hash;

NSObject类对这两个方法的默认实现是:当且仅当其"指针值"完全相等时,这两个对象才相等。如果"isEqual:"方法判定两个对象相等,那么其hash方法也必须返回同一个值。但是如果两个对象的hash方法返回同一个值,那么"isEqual:"方法未必会认为两者相等

例如:我们认为如果两个EOCPerson的所有字段均相等,那么这两个对象就相等。

objc 复制代码
@interface EOCPerson: NSObject
@property(nonatomic, copy) NSString* firstName;
@property (nonatomic, copy) NSString *lastName;
@property(nonatomic, assign) NSUInteger age;
@end

那么"isEqual:"方法可以写成:

objc 复制代码
一 (BOOL)isEqual:(id)object {
   if (self == object)
      return YES;
   if([self class] !=[object class]) 
      return NO;
   EOCPerson *otherPerson =(EOCPerson*)object;
   if (![_firstName isEqualToString:otherPerson.firstName])    
      return NO;
   if (![_lastNameisEqualToString:otherPerson.lastName])       
      return NO;
   if (_age != otherPerson.age)
      return NO;
   return YES;
}
  • 首先直接判断两个指针是否相等。若相等,则其均指向同一对象,所以受测对象也必定相等。
  • 接下来比较两对象所属的类。若不属于同一类,则两对象不相等。
  • 最后检测每个属性是否相等。只要其中有不相等的属性,就判断两对象不等。
  • 接下来实现hash方法。根据同等性约定:若两对象相等,则其哈希码也相等,但是两个哈希码相同的对象却未必相等。这是能否正确覆写"isEqual:"方法的关键:
objc 复制代码
- (NSUInteger)hash {
	NSUInteger fistNameHash = [_firstName hash];
	NSUInteger lastNameHash = [_lastName hash];
	NNSUInteger ageHash = _age;
	return firstNameHash ^ lastNameHash ^ ageHash;
}

这样既能保持较高效率,又能使生成的哈希码至少位于一定范围之内,而不会过于频繁地重复。

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

NSString类的等同性判定方法为"isEqualToString:",NSArray类的的等同性判定方法为"isEqualToArray:",NSDictionary类的等同性判定方法为"isEqualToDictionary:"。调用此方法比调用"isEqual:"方法快,因为后者不知道受测对象的类型,要执行额外的步骤。

如果经常需要判断等同性,那么可能自己来创建等同性判定方法,因为无须检测参数类型,所以能大大提升检测速度。

在编写判定方法时,也应一并覆写"isEqual:"方法。后者的常见实现方式为:如果受测的参数与接收该消息的对象都属于同一个类,那么就调用自已编写的判定方法,否则就交由超类来判断。

objc 复制代码
- (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];
	}
}
  1. 等同性判定的执行深度

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

  1. 容器中可变类的等同性

在容器中放入可变类对象时,放入collection之后,就不应再改变其哈希码了。这是因为collection会把各个对象按照其哈希码分装到不同的"箱子数组"中。如果某对象在放入箱子之后哈希码又改变,那么现在所处的这个箱子对它来说就是错误的。那么我们需要确保哈希码不是根据对象的可变部分计算出来的,或是保证放入collection之后就不再改变对象内容了

举个例子提示大家要注意这么做的隐患:

现在set里包含一个数组对象,数组中包含两个对象:

objc 复制代码
NSMutableSet *set = [[NSMutableSet alloc] init];
NSMutableArray *arrayA = [@[@1, @2] mutableCopy];
[set addObject:arrayA];
NSLog(@"set = %@", set); 

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

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

此时set里仍然只有一个对象,因为刚才要加入的那个数组对象和set中已有的数组对象相等,所以set并不会改变。

我们添加一个和set中已有对象不同的数组:

objc 复制代码
NSMutableArray *arrayC = [@[@1] mutableCopy];
[set addObject:arrayC];
NSLog(@"set = %@", set);

由于加入的与原来已有的对象不相等,所以现在set里有两个数组了:一个是最早加入的,另一个是刚才新添加的。

最后我们改变arrayC的内容,令其和最早加入set的那个数组相等:

objc 复制代码
[arrayC addObject:@2];
NSLog(@"set = %@", set);

set中居然可以包含两个彼此相等的数组。根据set的语义是不允许出现这种情况的。若是拷贝此set,更糟糕了:

objc 复制代码
NSSet *setB = [set copy];
NSLog(@"setB = %@", setB);

复制过的set中又只剩一个对象了,此set好像是一个空set开始,通过逐个向其中添加新对象而创建出来。

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

以"类族模式"隐藏实现细节

  1. 类族模式

"类族"是一种很有用的模式,可以隐藏"抽象基类"背后的实现细节。Objective-C的系统框架中普遍使用此模式。该模式可以灵活应对多个类,将它们的实现细节隐藏在抽象基类后面,以保持接口简洁。用户无须自己创建子类实例,只需调用基类方法来创建即可。

  1. 创建类族

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

首先要定义抽象基类:

objc 复制代码
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
objc 复制代码
#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

每个"实体子类"都从基类继承而来:

objc 复制代码
#import "EOCEmployeeDeveloper.h"

@implementation EOCEmployeeDeveloper

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

@end

本例中,基类实现了一个"类方法",该方法根据待创建的雇员类别分配好对应的雇员类实例。这种"工厂模式"是创建类族的办法之一。

但是这会出现一个重要陷阱,即类型信息的误导性。结合我们上面的例子:

当调用工厂方法时:

objc 复制代码
EOCEmployee *employee1 = [EOCEmployee employeeWithType:EOCEmployeeTypeDesigner];

在语法和接口上看,你是在创建EOCEmployee。但实际上,因为employeeWithType:方法内是这样写的:

objc 复制代码
case EOCEmployeeTypeDesigner:
    return [[EOCEmployeeDesigner alloc] init]; 

所以,employee1 这个指针的表面类型是 EOCEmployee,但它实际指向的内存对象,是 EOCEmployeeDesigner 的实例。

基于上述原因,如果你用 isMemberOfClass: 检查会返回NO。这是因为 isMemberOfClass: 检查的是是否为这个类的直系实例。因此判断一个对象是否属于这个类族,应该使用 isKindOfClass: 方法

  1. Cocoa里的类族

系统框架中有许多类族,大部分collection类都是类族。

例如NSArray与其可变版本NSMutableArray。这样看来,实际上有两个抽象基类,一个用于不可变数组,另一个用于可变数组。尽管具备公共接口的类有两个,但仍然可以和起来算做一个类族。不可变的类定义了对所有数组都通用的方法,而可变的类则定义了那些只适用于可变类型的方法。两个类属于同一个类族,这意味着二者在实现各自类型的数组可以共用实现代码。此外,还能把可变数组复制为不可变数组,反之亦然。

我们要明白很重要的一点:像NSArray这样的类的背后其实是个类族(对于大部分collection类而言都是这样的)。

objc 复制代码
// 错误示例:试图通过精确的类对象比较来判断
if ([maybeAnArray class] == [NSArray class]) {
    // 这里的代码永远不会执行
}

这段代码的本意是想检查变量 maybeAnArray 是否是一个 NSArray 类的实例。但是NSArray是一个类族。当你通过NSArray的工厂方法或其子类NSMutableArray的初始化方法创建一个数组对象时,返回你的并不是NSArray类或NSMutableArray类的直系实例,而是它们的某个私有子类的实例。这些私有子类继承自NSArray,但对开发者完全隐藏。所以[maybeAnArray class]返回的是这个私有子类的Class对象,它当然永远不等于[NSArray class]。

同样,使用 isKindOfClass: 可以检查,该方法会沿着对象的继承链向上查询。

我们常要向类族中新增实体子类,需要遵守以下几条规则:

  • 子类应该继承自类族中的抽象基类。
  • 子类应该定义自己的数据存储方式。
  • 子类应当覆写超类文档指明要覆写的方法。
  1. 总结一下:
  • 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
  • 系统框架中经常使用类族。
  • 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。

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

有时需要在对象中存放相关信息,这时我们通常会从对象所属的类中继承一个子类,然后改用这个子类对象。然而并非所有情况下都能这么做。Objective-C中有一项强大的特性可以解决此问题,这就是"关联对象"。

可以给某对象关联许多其他对象,这些对象通过"键"区分。存储对象值的时候,可以指明"存储策略",用来维护"内存管理语义"。下表给出了枚举的取值以及与之等效的@property属性:

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

  • 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,但是两者有一个重要差别:设置关联对象时用的键是一个不透明的指针 。如果在两个键上调用"isEqual:"方法的返回值是YES,那么NSDictionary就认为二者相等;然而在设置关联对象值时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。因此,这设置关联对象值时,通常使用静态全局变量做键

不过值得注意的是:只有在其他做法不可行时才选用关联对象,因为这种做法通常会引入难以查找的bug。

理解objc_msgSend的作用

在对象上调用方法是Objective-C中经常使用的功能,这叫做"传递信息"。消息有"名称"或"传递子",可以接受参数,而且可能还有返回值。

在Objective-C中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法完全取决于运行期决定,甚至可以在程序运行时改变。

给对象发消息:

objc 复制代码
id returnValue = [someObject messageName:parameter];

someObject叫做接收者,messageName叫做选择子(selector)。选择子与参数合起来称为消息。编译器看到此消息后,将其转换为一条标准的C语言函数调用(消息传递机制中的核心函数),叫做objc_msgSend:

objc 复制代码
id returnValue = objc_msgSend(someObject, @selector(messageName:),  parameter);

其原型为:

objc 复制代码
void objc_msgSend(id self, SEL cmd, ...)

这是一个参数个数可变的函数,能接受两个及以上的参数:

  • p1:接收者。
  • p2:选择子(类型是SEL),指的就是方法的名字。
  • 后续参数:消息中的那些参数,顺序不变。

objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其方法列表:

  • 如果能找到与选择子名称相符的方法:跳转其代码。
  • 如果找不到:沿着继承体系继续向上查找,等找到合适的方法之后再跳转。
  • 如果最终还是找不到:执行"消息转发"操作。

objc_msgSend将匹配结果缓存在"快速映射表"里面,解决了调用一个方法需要很多步骤的问题。但是这种快速执行路径依旧不如静态绑定的函数调用操作迅速。

以上描述了部分消息的调用过程,其他"边界情况"则需交由另一些函数来处理:

  • objc_msgSend_stret:待发送的消息返回结构体。
  • objc_msgSend_fpret:消息返回的是浮点数。
  • objc_msgSendSuper:要给超类发消息。

原型如下:

objc 复制代码
<return_type> Class_selector(id self, SEL _cmd, ...)

每个类里都有一张表格,其中的指针都会指向这种函数,而选择子的名称是查找表时所用的键。objc_msgSend等函数正是通过这张表来寻找应该执行的方法并跳转实现的。

如果某函数的最后一项操作是调用另一个函数,那么就可以运用"尾调用优化"技术。

在没有优化的情况下,每次函数调用都会在内存中形成一个"栈帧",记录它的执行状态。如果函数A调用了B,A的栈帧就会一直保留在内存中,等待B返回。如果这种调用链很长(比如递归),就会消耗大量内存,甚至导致栈溢出。

尾调用优化的核心就是: 当编译器或运行环境发现一个调用是"尾调用"时,它不会为这个新调用创建新的栈帧,而是直接复用当前函数的栈帧,并"跳转"到新函数的位置。

理解消息转发机制

在编译器向类发送了其无法解读的消息并不会报错,而是启动"消息转发"机制。因为在运行期可以继续向类中添加方法,所以编译器在编译时无法确知类中到底会不会有某个方法实现。

消息转发分为两大阶段:

  • 第一阶段:先征询接收者,所属的类,看是否能动态添加方法以处理当前这个"未知的选择子",这叫做"动态方法解析"。
  • 第二阶段:涉及完整的消息转发机制。

这两个阶段之间又细分为两小步:

  • 首先请接收者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切如常。
  • 若没有"备援的接收者",则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。

下面解释上面提到的几个概念:

  1. 动态方法解析

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

objc 复制代码
+ (BOOL)resolveInstanceMethod:(SEL)selector;

该方法的返回值表示这个类能否新增一个实例方法用于处理该选择子,使用前提是相关方法的代码已经写好,只需要在运行时动态插在类中,内部绑定的是C函数,因为Runtime只能绑定C函数。此方案常用来实现@dynamic属性。

  1. 备援接收者

当接收者还有第二次机会响应未知的选择子,运行期系统会判断是否将消息转给其他接收者来处理,调用方法如下:

objc 复制代码
- (id)forwardTargetForSelector:(SEL)selector;

如果能找到备援对象就将其返回,否则返回nil。

请注意:我们无法操作经由这一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制了。

  1. 完整的消息转发

如果转发算法已经来到这一步的话,那么就只能启用完整的消息转发机制了。此步骤调用:

objc 复制代码
- (void)forwardInvocation:(NSInvocation*)invocation;
  1. 消息转发全流程

接收者在其中的每一步都有机会处理消息,步骤越往后消息处理的代价就越大。

用"方法调配技术"调试"黑盒方法"

通过方法调配技术我们既不需要源代码,也不需要通过继承自类来覆写方法就能改变这个类本身的功能。新功能将在本类的所有实例中生效。

通过下面方法可以实现两个方法实现的交换:

此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:

此函数根据给定的选择从类中取出与之相关的方法。

通过此方案,开发者可以为不知道具体实现的黑盒方法增加日志记录功能,有助于程序调试。

理解类对象的用意

描述Object-C对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也定义在这里:

由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为isa指针。Class对象也定义在运行期程序库的头文件中:

此结构体存放类的元数据。Class本身也是OC对象,其中的super_class定义了本类的超类,类对象所属的类型是元类,用来表述类对象本身所具有的元数据,例如类方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类

可以用类型信息查询方法来检视类继承体系。isMemberOfClass:能够判断对象是否为某个特定类的实例,isKindOfClass:能够判断出对象是否为某类或其派生类的实例。

像这样的类型信息查询方法使用isa指针获取对象所属的类,然后通过super_class指针在继承体系中游走。由于OC使用"动态类型系统",所以用于查询对象所属类的类型信息查询功能非常有用。

此外也可以用比较类对象是否等同的办法来做。若是如此,那就要使用==操作符,而不要用"isEqual:"方法。原因在于类对象是"单例",在应用程序的范围内,每个类的Class仅有一个实例。如:

objc 复制代码
if ([object class] == [SomeClass class]) {
}

即使可以这样做,我们也仅应该尽量使用类型信息查询方法,而不应该直接比较两个类对象是否等同,因为前者可以正确处理那些使用了消息传递机制的对象。注意,对于存在代理的对象,class方法所返回的类表示发起代理的对象,而非接受代理的对象

相关推荐
少云清2 小时前
【UI自动化测试】3_IOS自动化测试 _使用真机
ui·ios
ITKEY_3 小时前
iOS个人开发者账号踩坑The app identifier “com.xxx.aaaaa“ cannot be registered
ios
vx-bot5556663 小时前
企业微信ipad协议的双通道通信机制与架构设计
ios·企业微信·ipad
符哥20089 小时前
新能源智能充电桩与 Android/iOS App 蓝牙通信协议
android·ios
这个人中暑了11 小时前
iOS 查看手机udid
ios·智能手机
Digitally11 小时前
如何通过蓝牙将 iPhone 上的照片传输到 Android
android·ios·iphone
文件夹__iOS11 小时前
iOS 网络安全认证:Token / MD5 / RSA 简明指南
安全·web安全·ios
TMT星球11 小时前
开启iPad专业办公时代 WPS for Pad原生桌面级Office正式上线
ios·ipad·wps
Nefertari_YinC11 小时前
mac搭建基于wda\airtest 的iOS自动化测试环境
软件测试·macos·ios·airtest·ui自动化测试