编写高质量iOS与OSX代码的52个有效方法-第二章:对象、消息、运行期

用OC等面向对象语言编程时,对象(object)就是基本构造单元(building block),开发者可以通过对象来存储并传递数据。

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

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

一定要理解运行期中各个部分协同工作的原理。

6、“属性”概念

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

@property 语法。对象接口的定义中,可以使用属性,这是一种标准的写法,能够访问封装在对象中的数据。因此,也可以把属性当成一种简称,意思是:编译器会自动写出一套存取方法,可以访问给定类型中具有给定名称的变量。

要访问属性,可以使用点语法。

如果使用了属性,编译器就会自动编写访问这些属性所需的方法,此过程叫做自动合成(autosynthesis)。这个过程又编译器在编译期执行,所以编辑器中看不到这些合成方法(synthesized method)的源代码。除了生成方法代码之外,编译器还要自动向类中添加适当类型的实例变脸个,并且在属性名前加下划线,以此作为实例变量的名字。

也可以在实现文件中通过@synthesize语法来指定智力变量的名字

@implementation DogObject
@synthesize dogName = _dogName;
@end

若不想令编译器自动合成取出方法,可以自己实现。如果只显示一个存取方法,那么另外一个还是由编译器来合成。

还有一种方法都能阻止编译器自动合成存取方法,就是使用@dynamic关键字,他告诉编译器:不要自动创建实现属性所需要的实例变量,也不要为其创建存取方法。并且在编译访问属性代码时,即使编译器发现没有定义存取方法,也不会报错。

属性特质

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

1、 原子性

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

如果属性不具备nonatomic特质,就是atomic的。

如果是自己定义存取方法,就应该遵从与属性特质相符的原子性。

2、 读/写权限
  • readwrite(读写),拥有获取方法(getter)和设置方法(setter),若该属性由@synthesize实现,则编译器会自动生成两个方法。
  • readonly(只读),仅拥有获取方法。只有当该属性由@synthesize实现时,编译器才会为其合成获取方法。可以用此特质把某个属性对外公开为只读属性,然后再class-continuation分类中将其重新定义为读写属性。
3、内存管理语义

属性用于封装数据,而数据要有具体的所有权语义(concrete ownership semantic)。

  • assign 只会执行针对纯量类型(scalar type,如CFLoat或NSInteger)的简单赋值操作。
  • strong 表明该属性定义一种拥有关系(owning relationship)。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。
  • weak 表明该属性定义一种非拥有关系(nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似,然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。
  • unsafe_unretained 同assign相同。但是它是哟用于对象类型(object type),表达一种非拥有关系(不保留,unretained),当目标对象遭到摧毁时,属性值不会自动清空(不安全,unsafe),这一点与weak有区别
  • copy 所属关系与strong类似。然而设置方法并不保留新值,而是将其拷贝(copy)。当属性值类型为NSString*时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个NSMutableString类的实例。若是不拷贝字符串,那么设置完属性后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,要拷贝一份不可变的字符串,确保对象中的字符串不会无意间变动。只要实现属性所用的对象是可变的,就应该在设置新属性时拷贝一份。
4、方法名

指定存取方法的方法名

  • getter=<name> 指定获取方法名
    @property (nonatomic,getter=isOn) BOOL on;

  • setter=<name> 指定设置方法名,少见。

通过以上特质,可以微调有编译器所合成的存取方法。如果是自己来实现这些方法,要保证其具备相关属性所声明的特质。

如果想在其他方法里设置属性值,同样要遵守属性定义中所宣称的语义。


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

具备atomic特性的获取方法会通过锁定机制来确保其操作的原子性。在iOS中使用同步锁的开销较大,会带来性能问题。一般情况下并不要求属性必须是原子的。因为这并不能保证线程安全(Thread safety),若要实现线程安全的操作,还需采用更为深层的锁定机制才行。

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

- (NSString *)stringOfDogInfomation {
    return [NSString stringWithFormat:@"Dog: \n name:%@ \n age:%zd",_dogName,_dogAge];
    //直接访问实例变量
}

- (NSString *)stringOfDogInfomation {
    return [NSString stringWithFormat:@"Dog: \n name:%@ \n age:%zd",self.dogName,self.dogAge];
    //通过属性访问变量
}

两种方法的区别:

  • 不经过OC的方法派发(method dispatch)步骤,所以直接访问实例变量的速度比较快。这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
  • 直接访问实例变量时,不会调用其设置方法,绕过了为相关属性所定义的内存管理语义。
  • 直接访问实例变量,不会触发键值观察(Key-Value Observing)通知。
  • 通过属性访问有助于排查与之相关的错误,因为可以个获取方法和设置方法增加断点。进行调试。

合理的这种方案是,在写入实例变量时,通过设置方法,在读取实例变量时,直接访问。此办法技能提高读取操作的速度,又能控制对属性的写入操作。

用这种方法有几个问题:

  • 初始化方法中如何设置属性值。这种情况下,直接访问实例变量,因为子类可能会覆写设置方法。在某些情况下必须在初始化方法中调用设置方法:如果待初始化的实例变量声明在超类中,而我们又无法在子类中直接访问次实例标量的话,就需要调用设置方法。
  • 惰性初始化(lazy initialization,懒加载)。这种情况下,必须通过获取方法访问属性,否则实例变量永远不会被初始化。
- (NSString *)dogOwner {
    if (!_dogOwner) {
        _dogOwner = [NSString stringWithFormat:@"Smith"];
    }
    return _dogOwner;
}


  • 在对象内部读取数据是,应该直接通过实例变量,而写入数据时,应通过属性来写。
  • 在初始化方法及dealloc方法中,总是应该通过实例变量来读写数据。
  • 在使用惰性初始化数据的情况下,要通过属性来读取数据。

8、理解“对象同等性”概念

==比较两个指针本身,而不是其所指的对象。

根据等同性来比较对象,一般使用NSObject协议中声明的isEqual:方法来判断两个对象的等同性。一般来说,两个不同类型的对象总是不相等的。某些类对象提供了特殊的等同性判定方法(equality-checking method),如果已经知道两个受测对象都属于同一个类,可以使用这种方法。

NSString *stringA = @"string 123";
NSString *stringB = [NSString stringWithFormat:@"string %d",123];
BOOL equalA = stringA == stringB;
BOOL equalB = [stringA isEqual:stringB];
BOOL equalC = [stringA isEqualToString:stringB];
    
NSLog(@"value:%d %d %d",equalA,equalB,equalC);
// value:0 1 1
    

NSString实现了一个自己独有的等同性判断方法,叫做isEqualToString:。传递给该方法的对象必须是NSString,否则结果undefined。调用该方法比isEqual:快,后者还要执行额外的步骤,因为它不知道受测对象的类型。

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

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

NSObject对两个方法的默认实现是:当且仅当去指针值完全相等时,这两个对象才相等。

若想在自定义的对象中正确覆写这些方法,必须先理解其约定。如果isEqual:方法判定两个对象相等,那么其hash方法也必须返回同一个值。如果两个对象的hash方法返回同一个值,那么isEqual:未必会认为两者相等。

假定实现一个自定义判断方法。

- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    }
    if ([self class] != [object class]) {
        return NO ;
    }
    
    DogObject *oDog = (DogObject *)object;
    if (![_dogName isEqualToString:oDog.dogName]) {
        return NO ;
    }
    if (_dogAge != oDog.dogAge) {
        return NO ;
    }
    
    return YES;
}

下面是hash方法,规则:若两对象相等,则hash码相等,但是两个hash码相等的对象未必相等。所以,hash方法与isEqual:关联。假定hash算法为:

- (NSUInteger)hash {
    NSUInteger dognameHash = [_dogName hash];
    NSUInteger dogAgeHash = _dogAge;
    return dognameHash ^ dogAgeHash;
}

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

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

除了NSString,NSArray和NSDictionary也具有特殊的等同性判定方法。isEqualToArray:isEqualToDictionary:。如果比较的对象不是数组或字典,就会抛出异常。

如果经常需要判断等同性,需要自己来创建等同性判断方法,因为无需检测参数类型,能大大提升检测速度。自己编写判定方法的另一个原因是,代码更易读、更美观。

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

- (BOOL)isEqualToDog:(DogObject *)oDog{
    if (self == oDog) {
        return YES;
    }
    if (![_dogName isEqualToString:oDog.dogName]) {
        return NO ;
    }
    if (_dogAge != oDog.dogAge) {
        return NO ;
    }
    return YES;
}

- (BOOL)isEqual:(id)object {
    if ([self class] == [object class]) {
        return [self isEqual:(DogObject *)object];
    } else {
        return [super isEqual:object];
    }
}

等同性判定的执行深度

确定等同性比较的因素,是否需要在等同性判定方法中检测全部字段,取决于受测对象。只有类的编写者才可以确定两个对象实例在何种情况下判定为相等。


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

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

类族(class cluster)是一种很有用的模式,可以隐藏抽象基类(abstract base class)背后的实现细节。OC系统框架中普遍使用此模式。比如UIButton,类方法buttonWithType:所返回的对象,其类型取决于输入的按钮类型。然而不管是什么类型对象,它们都继承自同一个基类:UIButon。

创建类族

首先定义抽象基类,在从基类中集成实体子类(concrete subclass)。并通过类方法,通过不同类型,创建不同的实例对象。

typedef NS_ENUM(NSUInteger, ZYDEmployeeType) {
    ZYDEmployeeTypeDeveloper,
    ZYDEmployeeTypeDesigner, 
};

@interface ZYDEmployee : NSObject
@property (nonatomic,copy) NSString *name;

+ (ZYDEmployee *)employeeWithType:(ZYDEmployeeType )type;

- (void)doADaysWork;

@end


@implementation ZYDEmployee

+ (ZYDEmployee *)employeeWithType:(ZYDEmployeeType)type {
    switch (type) {
        case ZYDEmployeeTypeDeveloper:
            return [ZYDEmployeeDeveloper new];
            break;
        case ZYDEmployeeTypeDesigner:
            return [ZYDEmoloyeeDesigner new];
            break;
    }
}

- (void)doADaysWork {
    
}
@end

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

@interface ZYDEmployeeDeveloper : ZYDEmployee

@end


@implementation ZYDEmployeeDeveloper

- (void)doADaysWork {
    NSLog(@"write code");
}

@end

OC中没有办法指明某个基类是抽象的,所以一般是在文档中写明类的用法。这种情况下一般没有名为init的成员方法,这暗示该类的实例不应该由用户直接创建。

如果对象所属的类位与某个类族中,那么在查询类型信息时就要当心。可能创建了某个类的实例,但是实际上创建的确实其子类的实例。

Cocoa里的类族

系统框架中有很多类族,大部分的collection类都是类族。例如NSArray与NSMutableArray。

若要判断某对象是否位于类族中,不要直接检测两个类对象是否等同,而应通过isKindOfClass:方法判断。


  • 类族模式可以把实现细节隐藏在一套简单的公共接口后面
  • 从类族的公共抽象基类中集成子类时要当心,若有开发文档,首先阅读。

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

关联对象

有时需要在对象中存放相关信息。可以从对象所属的类中继承一个子类,然后改用子类对象。

但是并非所有情况都能这么做,有时候类的实例可能是由某种机制所创建的,无法令这种机制创建出自己缩写的子类实例。(例如,如果要个NSArray添加一个属性(不能继承),分类只能添加方法不能添加属性(分类可以添加属性,同样适用@property大师不会生成带下划线变量,也不会生成getter和setter方法,即,即使添加了属性,也无法用钓点语法调用getter和setter方法。))就可以使用OC的一项特性解决这个问题,就是关联对象(Associated Object)。

可以给对象关联其他对象,这些对象通过“键”来区分。通过键来object绑定对象,也可以通过键获取object绑定的对象。

可以把对象相像成一个NSDictionary,键对应key,关联对象对应value。存取关联对象就相当于在字典对象上调用[object setObject: forKey:];[object objectForKey:];。不同的是:

  • 设置关联对象时用的键是不透明的指针(opaque pointer)。
  • 如果在两个键上调用isEqual:方法的返回值是YES(key内容相同,不论指针),那么Dictionary就认为二者相等;但设置关联对象时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,设置关联对象值时,通常使用静态全局变量做键。

使用关联对象引入头文件#import <objc/runtime.h>

存储对象值的时候,可以指明存储策略,用以维护相应的内存管理语义。

  • 存储策略

存储策略由枚举objc_AssociationPolicy定义,与@property类似。

/* Associative References */

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,          // assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // nonatomic,retain
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   // nonatomic copy
    OBJC_ASSOCIATION_RETAIN = 01401,       // retain
    OBJC_ASSOCIATION_COPY = 01403          // copy
};


  • 关联对象的管理方法
//以给定的键和策略为某对象设置关联对象值。传入nil可达到移除某个关联对象的效果。
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
   
// 根据给定的键从某对象中获取相应的关联对象值。 
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    

// 移除指定对象的全部关联对象
OBJC_EXPORT void
objc_removeAssociatedObjects(id _Nonnull object)

示例

自定义DogObject类代表狗,又添加类别(category)田园犬特殊属性DogObject+TianYuanDog

#import "DogObject.h"

@interface DogObject (TianYuanDog)

// 为类别添加属性,但是类别不会生成_tianYuanHome。
// 不会生成setter和getter方法,使用这两个方法就会崩溃
@property (nonatomic,copy) NSString *tianYuanHome;

@end

调用settergetter方法报错

DogObject *littleDog = [[DogObject alloc] initWithDogName:@"John" age:12];   
ittleDog.tianYuanHome = @"The Great Wall";
NSLog(@"%@",littleDog.tianYuanHome);


-[DogObject setTianYuanHome:]: unrecognized selector sent to instance 0x604000269840

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[DogObject setTianYuanHome:]: unrecognized selector sent to instance 0x604000269840'

在类别中通过关联对象,实现对应setter和getter方法。

#import "DogObject+TianYuanDog.h"
#import <objc/runtime.h>

static char * const kDogObject_TianYuan = "kDogObject_TianYuan";

@implementation DogObject (TianYuanDog)

// 实现getter方法,获取key对应的对象
- (NSString *)tianYuanHome {
    return objc_getAssociatedObject(self, kDogObject_TianYuan);
}

// 实现setter方法,设置key对应的对象
- (void)setTianYuanHome:(NSString *)tianYuanHome {
    objc_setAssociatedObject(self, kDogObject_TianYuan, tianYuanHome, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

The Great Wall 一切顺利执行,也实现了为类别添加属性的目的。


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

11、理解objc_msgSend的作用

在对象上调用方法,用OC术语来说,叫做传递消息(pass a message)。消息有名称(name)或选择子(selector)。可以接受参数,而且可能还有返回值。

C语言函数调用方式

C语言使用静态绑定(static building),也就是说,在编译期就能决定运行时所应调用的函数。

void printHello() {
    printf("hello world \n");
}

void printGoodbye() {
    printf("Goodbye world~ \n");
}

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

不考虑内联(inline)编译器在编译代码的时候,就已经知道函数printHelloprintGoodbye存在,并直接生成调用这些函数的指令。而函数地址实际是硬编码在指令之中。

void printHello() {
    printf("hello world \n");
}

void printGoodbye() {
    printf("Goodbye world~ \n");
}

void doTheThing(int type) {
    void (*func)(void);
    if (type == 0) {
        func = printHello;
    } else {
        func = printGoodbye;
    }
    func();
}

如果是第二种方式,就要使用动态绑定(dynamic binding)。因为所要调用的函数之道运行期才能确定。待调用的函数地址无法硬编码在指令之中,而是要在运行期读取出来。

OC的消息传递

OC中,如果向某对象传递消息,就会使用动态绑定机制来决定需要调用的方法。

在底层,所有方法都是普通的C函数,然而对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行期改变,这些特性是的OC成为一门真正的动态语言。

id returnValue = [littleDog changeDogAgeWithSpecialMethod:age];

littleDog是接收者,changeDogAgeWithSpecialMethod:叫做选择子,选择子和参数合起来称为消息。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中心的核心函数,叫做objc_msgSend,原型如下:

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

这个参数个数可变的函数,能接受两个或两个以上的参数,第一个参数表示接收者,第二个表示选择子,后续参数就是消息中的参数,顺序不变。选择子就是方法的名字。

编译器会把刚刚的消息转换为如下函数:

id returnValue = objc_msgSend(littleDog, @selector(changeDogAgeWithSpecialMethod:),age);

objc_msgSend函数会根据接收者与选择子的类型来调用适当的方法。为完成此操作,该方法需要在接收者所属的类中搜寻其方法列表(list of methods),如果能找到与选择子相符的方法,就调至其实现代码。若找不到,就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终找不到相符的方法,就执行消息转发(message forwarding)。

objc_msgSend会将匹配结果缓存在快速映射表里(fast map)。每个类都有一块缓存,若是稍后还向该类发送与选择子相同的消息,执行速度就会提升。

边界情况(edge case)需要OC中另一些函数来处理:

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

objc_msgSend等函数一旦找到应该调用的方法实现之后,就会跳转。之所以能这样做,因为OC对象的每个方法都可以视为简单的C函数。

尾调用优化(tail-call optimization)

如果某函数最后一项操作时调用另外一个函数,就可以运用尾调用优化技术,编译器会生成掉专职另一函数所需的指令码,而且不会向调用堆栈中推入新的栈帧(frame stack)。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值用作他用时,才能执行尾调用优化。

这项优化对objc_msgSend非常关键,不这么做,那么每次调用OC方法之前,都需要为调用objc_msgSend函数准备栈帧,在栈踪迹(stack trace)中可以卡到这种栈帧。此外,若不优化,会过早地发生栈溢出(stack overflow)现象。


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

12、理解消息转发机制

在编译期向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译器在编译期时还无法确知类中到底会不会有某个方法实现。当对象接收到无法解决的消息后,就会启动消息转发(message forwarding)机制,在此过程中告诉对象应该如何处理未知消息。

如下面错误日志:

-[__NSCFNumber lowercaseString]: unrecognized selector sent to instance 0xb000000000000022
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber lowercaseString]: unrecognized selector sent to instance 0xb000000000000022'
*** First throw call stack:

就是想某个对象发送过一条其无法解读的消息,从而启动了消息转发机制,并将此消息转发给NSObject的默认实现。

上面错误中__NSCFNumber是为了实现无缝桥接(toll-free bridging)而是用的内部类(internal class),配置NSNumber对象时,也会一并创建此对象。

消息转发两个阶段

  • 正序接收者,所属的类,是否能动态添加方法,已处理当前这个未知的选择子(unknown selector),这叫做动态方法解析(dynamic method resolution)。
  • 第二阶段,完整的消息转发机制(full forwarding mechanism)

如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来相应包含该选择子的消息了。此时,运行期系统会请求接收者以看看有没有其他对象能处理这条消息。如果则运行期系统会把消息转给那个对象,于是消息转发过程结束。若没有备援的接收者(replacement receiver)则启动完整的消息转发机制,运行期会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的消息。

动态方法解析

对象在收到无法解读的消息后,首先将调用其所属类的方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel;
该方法的参数就是未知的选择子,返回值为布尔类型。表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制之前,奔雷有机会新增一个处理此选择子的方法。

加入尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另外一个方法+ (BOOL)resolveClassMethod:(SEL)sel;.

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

此方案常用来实现@dynamic属性。

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

备援接收者

当接收者还有第二次机会能处理未知的选择子,在这一步中运行期系统会问他:能不能吧这条消息转给其他接收者来处理。对应的处理方法
- (id)forwardingTargetForSelector:(SEL)aSelector;

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

通过此方案,可以用组合来模拟出多重继承(multiple inheritance)的某些特性。

在一个对象内部,可能还有一些列其他对象,该对象经由此方法将能够处理某选择子的相关内部对象返回,这样,在外界卡奈,好像是该对象亲自处理了这些消息。

这一步所转发的消息,无法操作。若是箱子啊发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做。

完整的消息转发

创建NSInvocation对象,把尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子、目标及参数。在触发NSInvocation对象时,消息派发系统(message-dispatch system)把消息指派给目标对象。

调用方法- (void)forwardInvocation:(NSInvocation *)anInvocation;转发消息。

这个方法可以实现得很简单:只需要改变调用目标,使消息在新目标上得以调用即可(与备援接收者实现的方法等效,很少人使用这么简单的实现方式)。

比较有用的方式:在触发消息前,先以某种方式改变消息内容,不如追加另外一个参数,或者改换选择子,等等。

如果发现调用操作不应该由本类处理,则需要调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。

消息转发全流程

resolveInstanceMethod (返回NO)-> forwardingTargetForSelector (返回nil)—> forwardInvocation(无法处理) -> 消息未能处理。

接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来。如果这类的实例稍后还收到同名选择子,那么根本无需启动消息转发流程。若想在第三步把消息转给备援的接收者,那不如把转发操作提前第二步。因为第三步只是修改了调用目标,这项改动放在第二步执行会更为简单,不然的话,还得创建并处理完整的NSInvocation。


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

13、用方法调配技术调试黑盒方法

OC对象接收消息后,究竟会调用何种方法需要在运行期才能解析出来。给定的选择子名称相对应的方法可以再运行期改变,这样我们既不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有示例中生效,而不是仅限于覆写了相关方法的那些子类实例。此方案经常称为方法调配(method swizzling)。

类的方法列表会把选择子的名称映射到相关的方法实现之上,使的动态消息派发系统能够据此找到应该调用的方法。这些方法均已函数指针的形式来表示,这种指针叫做IMP。id (*IMP)(id,SLE,...)

OC运行期系统提供的几个方法,能够用来操作这张表。可以新增选择子,可以改变某选择子所对应的方法实现,还可以交换两个选择子所映射到的指针。

交换方法实现:

void method_exchangeImplementations(<#Method _Nonnull m1#>, <#Method _Nonnull m2#>)

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

Method class_getInstanceMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>)

  • 交换方法实例
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
    
NSString *sstring = @"This is a Cat.";
NSLog(@"lowercaseString :%@",[sstring lowercaseString]);
NSLog(@"uppercaseString :%@",[sstring uppercaseString]);

// 打印结果
// lowercaseString : THIS IS A CAT.
// uppercaseString : this is a cat.

不过这种交换没什么意义,因为两种方法已经实现得很好。

  • 为既有方法添加新功能

给NSString的lowercaseString方法添加一个日志功能。

#import <Foundation/Foundation.h>
@interface NSString (Http)

- (NSString *)zyd_myLowercaseString;
@end
#import "NSString+Http.h"
@implementation NSString (Http)
- (NSString *)zyd_myLowercaseString {
    NSString *lowercase = [self zyd_myLowercaseString];
    NSLog(@"%@ => %@",self,lowercase);
    return lowercase;
}

@end
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(zyd_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
    
NSString *sstring = @"This is a Cat.";
NSLog(@"lowercaseString : %@",[sstring lowercaseString]);

在执行lowercaseString方法的时候,就会打印一行记录消息

This is a Cat. => this is a cat.

lowercaseString : this is a cat.

通过此方法,可以为那些完全不知道其具体实现的(completely opaque 完全不透明的)黑盒方法增加日志记录功能,有助于程序调试。

此做法只在调试程序时游泳,很少有人在调试程序之外的场合用方法调配技术来永久改动某个类的功能。不能仅仅因为OC有这个特定就一定要用它,若是滥用反而会另代码变得不易读懂且难于维护。


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

14、理解类对象的用意

OC是一门极其动态的语言。对象类型并非在编译期就绑定好了,而是要在运行期查找。而且还有个特殊的类型id,他能指代任意的OC对象类型。

一般情况下,要指明消息接收者的具体类型,如果向其发送了无法解读的消息,那么编译器就产生警告信息。而类型为id的对象,编译器假定它能响应所有消息。

编译器无法确定某类型对象到底能解读多少种选择子,因为运行期还可以向其中动态新增。然而,几遍使用了动态新增技术,编译器也觉得应该能在头文件中找到方法原型的定义,据此可了解完整的方法签名,并生成拍发消息所需的正确代码。

OC对象的本质

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

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

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;

没个结构体的首个成员是Class类的变量。该变量定义了对象所述的类,通常称为is a指针。

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

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

这样的类型信息查询方法使用isa指针获取对象所属的类,然后通过super_class指针在继承体系中游走。由于对象是动态的,所以cite习惯显得极为重要。OC中,必须查询类型信息,方能完全了解对象的真实类型。

在程序中不要直接比较对象所属的类,明智的做法是调动类型查询方法。

OC使用动态类型系统(dynamic typing),所以用于查询对象所属类的类型信息查询功能非常有用。从collection中获取对象时,通常会查询类型信息,这些对象不是强类型的,从collection中取出来时,其类型通常是id。如果想知道具体类型就可以使用类型信息查询方法。

NSNumber *ageNumber = @2;
NSArray *array = @[ageNumber];

if ([array[0] isKindOfClass:[NSString class]]) {
    NSLog(@"string :%@",[array[0] lowercaseString]);
} else if ([array[0] isKindOfClass:[NSNumber class]]) {
    NSLog(@"number :%@",array[0]);
}

// 打印结果:
// number :2

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

推荐阅读更多精彩内容