• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

[Effective Objective] 对象、消息、运行期

武飞扬头像
瓯海剑
帮助1

对象:“对象”(object)就是“基本构造单元”(building block),开发者可以通过对象来储存并传递数据。

消息:在对象之间传递数据并执行任务的过程就叫做“消息传递”(Messaging)。

运行期:当应用程序运行起来以后,为其提供相关支持代码叫做“Objectivec- C运行期环境”(Objective-C runtime),它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。


属性

“属性”(property)是Objective-C的一项特性,用于封装对象中的数据。Objective-C对象通常会把其所需要的数据保存为各种实例变量

偏移量

“偏移量”(offset)是“硬编码”(hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。

例:

@interface EOCPerson : NSObject {
    @public
    NSString *_firstName;
    NSString *_lastName;
    @private
    NSString *_someInternalData;
}
@end

假设指针为4个字节:
学新通

应用程序二进制接口(Application Binary Interface, ABI)

不兼容现象(incompatibility)

在Java或C 语言里可以定义实例的作用域,然而编写Objective-C代码时却很少这么做。这种写法的问题是:对象布局在编译期(compile time)就已经固定了。此时,如果又加了一个实例变量,那就麻烦了。比如说,假设在_firstName之前又多了一个实例变量:

@interface EOCPerson : NSObject {
    @public
    NSDate *_dateOfBirth;
    NSString *_firstName;
    NSString *_lastName;
    @private
    NSString *_someInternalData;
}
@end

数据布局图就会变成这样:
学新通

原来表示_firstName的偏移量现在却指向_dateOfBirth了。把偏移量硬编码于其中的那些代码都会读取到了错误的值。

如果代码使用了编译期计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。某个代码库中的代码使用了一份旧的类定义。如果和其相链接的代码使用了新的类定义,那么运行时就会出现不兼容现象(incompatibility)。

ABI定义

Objective-C的做法是,把实例变量当作一种储存偏移量所用的“特殊变量”(special variable),交由“类对象”(class object)保管。偏移量会在运行期查找,如果类的定义变了,那么储存的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。这就是稳固的应用程序二进制接口(Application Binary Interface, ABI)。

存取方法

Objective-C对象通常会把所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。

  • 获取方法:getter,用于读取变量值。
  • 设置方法:setter,用于写入变量值。

自动创建存取方法

编译器可以自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。例如下面这个类:

@interface EOCPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end

对于该类的使用者来说,上述代码写出来的类于下面这种写法等效:

@interface EOCPerson : NSObject
- (NSString*)firstName;
- (void)setFirstName:(NSString*)firstName;
- (NSString*)lastName;
- (void)setLastName:(NSString*)lastName;
@end

点语法

此特性引入了一种新的“点语法”(dot syntax),使开发者可以更为容易地依照类对象来访问存放与其中的数据。编译器会把“点语法”转换为对存取方法的调用,使用“点语法”的效果与直接调用存取方法相同。

例:

	EOCPerson* aPerson = [[EOCPerson alloc] init];
    
    aPerson.firstName = @"Bob"; //Same as:
    [aPerson setFirstName:@"Bob"];
    
    NSString* lastName = aPerson.lastName; // Same as:
    NSString* lastName = [aPerson lastName];

属性特质

使用属性时,各种特质(attribute)设定也会影响编译器所生成的存取方法。

详情可参考:属性关键字

要点

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

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

在对象之外访问实例变量时,总是应该通过属性来做,然而在对象内部访问实例变量时又该如何呢?

在实例变量的获取方法与设置方法中,通过存取方法和不经由存取方法访问实例变量有这几个区别:

  • 由于不经过Objective-C的“方法派发”(method dispatch)步骤,所以直接访问实例变量的速度当然比较快。在这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
  • 直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比方说,如果直接访问一个声明为copy的属性,那么并不会拷贝该属性只会保留新值并释放旧值。
  • 如果直接访问实例变量,那么不会触发“键值观测”(Key-Value Observing, KVO)通知。这样做是否会产生问题,还取决于具体的对象行为。
  • 通过属性来访问有助于排查与之相关的错误,因为可以给“获取方法”和/或“设置方法”中新增“断点”(breakpoint),监控该属性的调用者及其访问时机。

折中方案

在写入实例变量时,通过“设置方法”来做,而在读取实例变量时,则直接访问之。

优点:既能提高读取操作的速度,又能控制对属性的写入操作。

需要注意的问题

在初始化方法中应该如何设置属性值

这种情况下总是应该直接访问实例变量,因为子类可能会“覆写”(override)设置方法。

假设EOCPerson有一个子类叫做EOCSmithPerson,这个字类专门表示那些性“Smith”的人。该字类可能会覆写lastName属性所对应的设置方法:

- (void) setFirstName:(NSString *)firstName {
    if (![firstName isEqualToString:@"Smith"]) {
        NSLog(@"llllll");
    }
    self.firstName = firstName;
}

在基类EOCPerson的默认初始化中,可能会将姓氏设为空字符串。此时若是通过“设置方法”来做,那么调用的将会是字类的设置方法,从而抛出异常。

惰性初始化

对某些复杂又不常用,而且创建成本高的属性,我们可能会在“获取方法”中对其执行惰性初始化:

- (EOCBrain*)brain {
	if (!_brain) {
		_brain = [Brain new];
	}
	return _brain;
}

在这种情况下,必须使用存取方法来访问brain属性。

要点

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

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

根据“等同性”(equality)来比较对象是一个非常有用的功能。此时,我们需要使用NSObject协议中声明的“isEqual”:方法来判断两个对象的等同性。一般来说,两个类型不同的对象总是不相等的(unequal)。

以下述代码为例:

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

大家可以看到 == 与等同性判断方法之间的差别。

用于判断等同性的关键方法

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

NSObject类对这两个方法的默认实现是:当且仅当其“指针值”(pointer value)完全相等时,这两个对象才相等。

约定(contract)

  1. 如果“isEqual:”方法判断两个对象相等,那么其hash方法也必须返回同一个值。
  2. 如果两个hash方法返回同一个值,那么“isEqual:”方法未必会认为两者相等。

例:

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

我们认为,如果两个EOCPerson的所有字段均相等,那么这两个对象就相等。于是“isEqual:”方法可以写成:

- (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 (![_lastName isEqualToString:otherPerson.lastName]) return NO;
    if (_age != otherPerson.age) return No;
    
    return YES;
}

hash方法:

- (NSUIngeter)hash {
	return 1337;
}

collection性能问题

如果在collection中使用上面的对象将产生性能问题,因为collection在检索哈希表(hash table)时,会用对象的哈希码做索引。假如某个collection是用set实现的,那么set可能会根据哈希码把对象分装到不同的数组中。在向set中添加新对象时,要根据其哈希码找到与之相关的那个数组,依次检查其中各个元素。由此可知,如果每个对象都返回相同的哈希码,那么在set中已有1000000个对象的情况下,若是继续向其中添加对象,则需将这个1000000个对象全部扫描一遍。

hash方法也可以这样来实现:

- (NSUInteger)hash {
    NSString* stringToHash = [NSString stringWithFormat:@"%@:%@:%lu", _firstName, _lastName, _age];
    
    return [stringToHash hash];
}

这个方法是将NSString对象中的属性都塞入另一个字符串中,然后令hash方法返回该字符串的哈希码。但是这样做还需负担创建字符串的开销,所以返回单一值要慢。因为必须先计算其哈希码,把这种对象添加到collection中也会产生性能问题。

最后一种方法:

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

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

缺点:此算法生成的哈希码还是会碰撞(collision)。

编写hash方法时,应该用当前的对象做做实验,以便在减少碰撞频度与降低运算复杂程度之间取舍。

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

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

在编写判定方法时,也应一并覆写“isEqual:”方法。

例:

- (BOOL)isEqualToPerson:(EOCPerson *)otherPerson {
    if (self == otherPerson) return YES;
    
    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:(EOCPerson*)object];
    } else {
        return [super isEqual:object];
    }
}
学新通

等同性判定的执行深度

创建等同性判定方法时,需要决定是根据整个对象来判断等同性,还是仅根据其中几个字段来判断。

深度等同性判定(deep equality)

例如,NSArray的检测方式为先看两个数组所含对象个数是否相同,若相同,则在每个对应位置地两个对象身上调用其“isEqual:”方法。如果对应位置上的对象均相等,那么这两个数组就相等,这就叫“深度等同性判定”。

唯一标识符(unique identifier)

有些类可能会含有另外一个属性,此属性是“唯一标识符”,在数据库中用作“主键”(primary key):

@property NSUInteger identifier;

在这种情况下,我们也许只会根据标识符来判断等同性。因为只要两者标识符相同,就肯定表示同一个对象,因而必然相等。

容器中可变类的等同性

在容器中放入可变类对象的时候,就不应在改变其哈希值了。

用一个NSMutableSet与几个NSMutableArray对象测试一下,就能发现这个问题。

  1. 先把一个数组加入set中。
	NSMutableSet* set = [NSMutableSet set];
    
    NSMutableArray* arrayA = [@[@1, @2] mutableCopy];
    [set addObject:arrayA];
    NSLog(@"set = %@", set);
    // Output : set = {((1, 2))}

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

  1. 在向set里加入一个与前一个数组相同的数组。
	NSMutableSet* set = [NSMutableSet set];
    
    NSMutableArray* arrayA = [@[@1, @2] mutableCopy];
    [set addObject:arrayA];
    NSLog(@"set = %@", set);
    // Output : set = {((1, 2))}
    
    NSMutableArray* arrayB = [@[@1, @2] mutableCopy];
    [set addObject:arrayB];
    NSLog(@"set = %@", set);
    // Output : set = {((1, 2))}

此时set里仍然只有一个对象:因为数组B与数组A相同,所以set不会改变。
3. 加入一个和set中已有对象不同的数组。

	NSMutableArray* arrayC = [@[@1] mutableCopy];
    [set addObject:arrayC];
    NSLog(@"set = %@", set);
    // Output : set = {((1), (1, 2))}

现在set里有两个数组。
4. 改变数组C的内容,令其和最早加入的那个数组相等。

    [arrayC addObject:@2];
    NSLog(@"set = %@", set);
    // Output : set = {((1, 2), (1, 2))}

set中居然包含了两个相等的数组!这是不允许的。

所以把某对象放入set之后又修改其内容。那么后来的行为将很难预测。

要点

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

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

“类族”(class cluster)是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节。Objective-C的系统框架中普遍使用此模式。

创建类族

现在举例来演示如何创建类族。假设有一个处理雇员的类,每个雇员都有“名字”和“薪水”这两个属性,管理者可以命令其执行日常1工作。但是,各类雇员的工作内容不同。经理在带领雇员做项目时,无须关心每个人如何完成其工作,仅需指示其开工即可。

  1. 定义抽象基类。
typedef NS_ENUM(NSUInteger, EOCEmplyeeType) {
    EOCEmplyeeTypeDeveloper,
    EOCEmplyeeTypeDesigner,
    EOCEmplyeeTypeFinance,
};

@interface EOCEmplyee : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger salary;

// Helper for creating Employee
  (EOCEmplyee*)employeeWithType:(EOCEmplyeeType)type;

// Make Employees do their respective day's work
- (void)doADaysWork;

@end
学新通

@implementation EOCEmplyee

  (EOCEmplyee*)employeeWithType:(EOCEmplyeeType)type {
    switch (type) {
        case EOCEmplyeeTypeDeveloper:
            return [EOCEmplyeeDeveloper new];
            break;
        case EOCEmplyeeTypeDesigner:
            return [EOCEmplyeeDesigner new];
            break;
        case EOCEmplyeeTypeFinance:
            return [EOCEmplyeeFinance new];
            break;
    }
}

- (void)doADaysWork {
    // Subclasses implement this.
}

@end
学新通
  1. 每个实体子类都从基类基础而来。
    例:
@interface EOCEmplyeeDeveloper : EOCEmplyee
@end

@implementation EOCEmplyeeDeveloper

- (void)doADaysWork {
    // ......
}

@end

这种根据待创建的雇员类别分配好对应的雇员类实例叫“工厂模式”。

Coacoa里的类族

系统框架中有许多类族。大部分collection类都是类族,例如NSArray与其可变版本NSMutableArray。这两个类共属于一个类族,这意味着二者在实现各自类型的数组时可以共用实现代码,此外,还能够把可变数组复制为不可变数组,反之亦然。

对于Cocoa中NSArray这样的类族来说,还是有办法新增子类的,但是需要遵守几条规则。这几条规则如下。

  • 子类应该继承自类族中的抽象基类。
    若要编写NSArray类族的子类,则需令其继承自不可变数组的基类或可变数组的基类。
  • 子类应该定义自己的数据储存方式。
    子类必须用一个实例变量来存放数组中的对象。因为NSArray本身只不过是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都需具备的一些接口。对于这个自定义的数组子类来说,可以用NSArray来保存其实例。
  • 子类应当覆写超类文档中需要覆写的方法。
    在每个抽象基类中,都有一些子类必须覆写的方法。

在类族中实现子类时所需要遵循的规范一般都会定义与基类的文档之中,编码前应该先看看。

要点

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

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

有时需要在对象中存放相关信息。这时我们通常会从对象所属的类中继承一个子类,然后改用这个子类对象。然而并非所有情况下都能这么做,有时候类的实例可能是由某种机制所创建的,而开发者无法令这种机制创建出自己所写的子类实例。Objective-C中有一项强大的特性可以解决此问题,这就是“关联对象”(Associated Object)。

关联对象

可以给某对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”(storage policy),用于维护相应的“内存管理语义”。

存储策略

存储策略由名为objc_AssociationPolicy的枚举所定义。

对象关联类型图:
学新通

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

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

关联对象与NSDictionary关于键值的不同

NSDictionary:如果在两个键上调用“isEqual:”方法的返回值是YES,那么NSDictionary就认为二者相等。

关联对象:二者必须是完全相同的指针。

**在设置关联对象时,弱小令两个键匹配到同一个值,则二者必须是完全相同的指针才行。

要点

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

理解objc_msgSend的作用

在对象上调用方法在Objective-C中叫做“传递消息”。消息有“名称”(name)或“选择子”(selector),可以接受参数,而且可能还有返回值。

C语言的函数调用方式使用“动态绑定”(static binding),也就是说,在编译器就能决定运行时所应调用的函数。

在Objectivec-C中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。即在对象收到消息之后,究竟该调用那个方法完全于运行期决定,甚至可以在程序运行时改变。这些特性使得Objective-C成为一门真正的动态语言。

obj_msgSend函数介绍

obj_msgSend函数是消息传递机制中的核心函数,其原型如下:

void objc_msgSend(id self, SEL cmd, ...)

给对象发宋消息可以这样来写:

id rerurnValue = [someObject messageName:parameter];

在本例中,someObject叫做“接收者”(receiver),messageName叫做“选择子”(selector)。选择子与参数合起来称为消息。编译器看到消息后,将其转化成上面的objc_msgSend函数调用。如下:

id returnValue = objc_msgSend(someObject, @selector(messageName), parameter);

objc_msgSend函数是一个参数可变的函数。第一个参数代表接受者,第二个参数代表选择子(SEL是选择子的类型,指的是方法的名字),后续参数就是消息中的那些参数,其顺序不变。

obj_msgSend函数作用

objc_msgSend函数会依据接受者与选择子的类型来调用适当的方法。

该方法会在接受者所属的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”(message forwarding)操作。

objc_msgSend会将匹配结果缓存在“快速映射表”(fast map)里面,每个类都有这样一块缓存,若稍后还向该类发送与选择子相同的消息,那么执行起来就很快了。

其他边界情况时调用的函数

  • objc_msgSend_stret。如果待发送的消息要返回结构体,那么可交由此函数处理。只有当CPU的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若返回值无法容纳与CPU寄存器中(比如说返回的结构体太大了),那么就由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体,
  • objc_msgSend_fpret。如果消息返回的是浮点数,那么可交由此函数处理。在某些架构的CPU中调用函数时,需要对“浮点数寄存器”(floating-point register)做特殊处理,也就是说,通常所用的objc_msgSend在这种情况下并不合适。这个函数是为了处理x86等架构CPU中某些令人稍觉惊讶的奇怪状况。
  • obj_msgSendSuper。如果要给超类发消息,例如[super message:parmeter],那么就交由此函数处理。也有另外两个与objc_msgSend_stret和objc_msgSend_fpret等效的函数,用于处理发给super的相应消息。

尾调用优化

Objective-C对象的每个方法都可以视为简单的C函数,其原型如下:

<return _type> Class _selector(id self, SEL _cmd, ...)

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

请注意,原型的样子和objc_msgSend函数很像。这不是巧合,而是为了利用“尾调用优化”技术,令“跳至方式实现”这一操作变得跟简单。

如果某函数的最后一项操作是调用另外一个函数,那么就可以运用“尾调用优化”技术。编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的“栈桢”(frame stack)。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行“尾调用优化”。这项优化对objc_msgSend非常关键,如果不这么做的话,那么每次调用Objective-C方法之前,都需要为调用objc_msgSend函数准备“栈桢”,大家在“栈踪迹”(stack trace)中可以看到这种“栈桢”。此外,若是不优化,还会过早地发生“栈溢出”(stack overflow)现象。

要点

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

理解消息转发机制

当对象接收到无法解读的消息后,就会启动“消息转发”(message forwarding)机制,程序员可经由此过程告诉对象应该如何处理未知消息。

开发者在编写自己的类时,可于转发过程中设置挂钩,用以执行预定的逻辑,而不使应用程序崩溃。

消息转发分为两大阶段

  1. 征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”(unknown selector),这叫做“动态方法解析”(dynamic method resolution)。
  2. 如果运行期系统已经把第一阶段执行完了,那么接受者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。
    这又细分为两小步:
    1. 请看接收者看看有没有其他对象能处理这条消息。
    2. 若有,则运行期系统会把消息转给那个对象。
    3. 若没有,运行期系统会把与消息有关的全部细节都封装到 NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。

动态方法解析

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

  (BOOL)resolveInstanceMethod:(SEL)selector

该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。

再继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另一个方法,该方法与“resolveInstanceMethod:”类似,叫做“resolveClassMethod:”。

使用这种方法的前提是:相关方法的实现代码已经写好,只等运行的时候动态插在类里面就可以了。

下列代码演示了如何使用“resolveInstanceMethod:”来实现@dynamic属性:
学新通
学新通

  1. 首先将选择子化为字符串。
  2. 检测其是否表示设置方法。
  3. 若前缀为set,则表示设置,否则就是获取方法。
  4. 把处理该选择子的方法加到类里面,所添加的方法是用纯C函数实现的。C函数可能会用代码来操作相关的数据结构,类之中的属性数据就存放在那些数据结构里面。

备援接受者

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

- (id)forwardingTargetForSelector:(SEL)selector;

方法参数代表未知的选择子,若当前接受者能找到备援对象,则将其返回,若找不到,就返回nil。

我们可以用“组合”来模拟出“多重继承”的某些特性。

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

完整的消息转发

  1. 创建NSInvocation对象,把尚未处理的那条消息有关的全部细节都封与其中。此对象包括选择子、目标(target)及参数。在触发NSInvocation对象时,“消息派发系统”将亲自出马,把消息指派给目标对象。

此步骤会调用下列方法来转发消息:

- (void)forwardInvocation:(NSInvocation*)invocation

在触发消息前,可以先以某种方式改变消息内容。

实现此方法时。若发现某调用操作不应由本类处理,则需调用超类的同名方法。如果最后调用了NSObjective类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,此异常表明选择子最终未能得到处理。

消息转发全流程

学新通

要点

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

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

在Objective-C中,与给定的选择子名称相对应的方法也可以在运行期改变。若能善用此特性,则可以发挥出巨大优势,因为我们既不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能。此方案经常称为“方法调配”。

类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP,其原型如下:

id (*IMP)(id, SEL, ...)

NSString类可以响应lowercaseString、uppercaseString、capitalizedString等选择子。
学新通

Objective-C运行期系统提供的几个方法都能够用来操作这张表。开发者可以向其中新增选择子,也可以改变某选择子所对应的方法实现,还可以交换两个选择子所映射到的指针。

互换两个方法实现

想交换方法实现,可用下列函数:

void method_exchangeImplementations(Method m1, Method m2)

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

Method class_getInstanceMethod(Class aClass, SEL aSelector)

此函数根据给定的选择从类中取出与之相关的方法。执行下列代码,即可交换前面提到的lowercaseString与uppercaseString方法实现:

    Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
    
    Method swqppedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));

    method_exchangeImplementations(originalMethod, swqppedMethod);

从现在开始,如果在NSString实例上调用lowercaseString,那么指向的将是uppercaseString,反之亦然。

为既有方法增添新功能

例,调用lowercaseString时记录某些信息,这时就可以通过交换方法实现来达成此目标。

思路:新编写一个方法,在此方法实现所需的附加功能,并调用原有实现。

  1. 新方法可以添加至NSString的一个“分类”(category)中:
@interface NSString (EOCMyAdditions)
- (NSString*)eoc_myLowercaseString;
@end
  1. 实现代码:
@implementation NSString (EOCMyAdditions)

- (NSString*)eoc_myLowercaseString {
    NSString *lowercase = [self eoc_myLowercaseString];
    NSLog(@"%@ => %@", self, lowercase);
    return lowercase;
}

@end
  1. 将上述方法与原有方法互换:
    Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
    
    Method swqppedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
    
    method_exchangeImplementations(originalMethod, swqppedMethod);

交换之后的方法表如图:
学新通

  1. 在NSString实例上调用lowercaseString方法,就会输出一行记录消息:
NSString* string = @"String";
NSString* lowercaseString = [string lowercaseString];

输出:
学新通

若通过此方案,开发者可以为那些“完全不知道其具体实现的”黑盒方法增加日志记录功能,这非常有助于程序调试。

要点

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

理解“类对象”的用意

每个Objective-C对象实例都是指向某块内存数据的指针。所以在声明变量时,类型后面要跟一个“*”字符:

NSString* pointerVariable = @"Some string";

因此可以说,该变量“指向”NSString实例,所有Objective-C对象都是如此,若是想把对象所需的内存分配在栈上,编译器则会报错:

String stackVariable = @"Some String";
// error: interface type cannot be statically allocted

对于通用的对象类型id,由于其本身已经是指针了,所以我们能够这样写:

id genericTypedString = @"Some String";

上面这种定义方式与用NSString*来定义,其语法意义相同。唯一区别在于,如果声明指定了具体类型,那么在该类实例上调用其所没有的方法时,编译器会探知此情况,并发出警告信息。

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

typedef struct objc_object {
	Class isa;
} *id;

由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为“isa”指针。

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_protoll_list *protocols;
};

此结构体存放类的“元数据”,例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身亦为Objective-C对象。结构体里还有个变量叫做super_class,他定义了本类的超类。

类对象本身所属的类型是另外一个类,叫做“元类”,用来表述类对象本身所具有的元数据。“类方法”就定义于此处,因为这些方法可以理解成类对象的实例方法。

每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。

继承体系图

学新通

要点

  • 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则成了类的继承体系。
  • 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知。
  • 尽量使用了类型信息查询方法来确定对象类型,而不要直接比对类对象,因为某些对象可能实现了消息转发功能。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgabacb
系列文章
更多 icon
同类精品
更多 icon
继续加载