对象、消息、运行期

Objective-C作为面向对象编程,“对象”(object)就是“基本构造单元”(building block),开发者可以通过对象来存储并传递数据。

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

第6条:理解“属性”这一概念

  • “属性”(property)是Objective-C的一项特性,用于封装对象中的数据。

    通过使用属性这一写法,我们可以从中获取很多的优势,其中就包括编译器会自动编写这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。而且需要强调的是,这个过程由编译器在编译期执行,所以编辑器里是看不到这些“合成方法”(syntheiszed method)的源代码。

    除了生成方法代码以外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。而一般情况下,不太建议大家去修改默认的实例变量名,但如果由于个人原因不能接受下划线命名方案的话,也可以改为自己想要的写法,如下:

    @synthesize firstName = myFirstName;
    @synthesize lastName = myLastName;
    
  • 若不想令编译器自动合成存取方法,则可以自己实现。如果你只实现了其中一个存取方法,那么另外一个还是会由编译器来合成,除非你使用@dynamic关键字来告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译器访问属性的代码时,及时编译器发现没有定义存取方法,也不会报错,因为它相信这些方法会在运行期找到。

    @interface MarkAnimatedView ()
    
    @property NSString *firstName;
    @property NSString *lastName;
    
    @end
    
    @implementation MarkAnimatedView
    @dynamic firstName, lastName;
    

    编译器不会为上面这个类自动合成存取方法或实例变量,使用代码访问其中的属性,编译器也不会发出警示信息。

  • 内存管理语义

    属性用于封装数据,而数据则要有“具体的所有权语义”(concrete ownership semantic)。如果自己编写存取方法,那么就必须与有关属性所具备的特质相符。

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

    @property (nonatomic, getter=isOn) BOOL on;
    
  • atomic与nonatomic的区别

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

    但是在iOS开发中,基本所有的属性都声明为nonatomic,这样做的历史原因就是:在iOS中使用同步锁的开销较大,这会带来性能问题。但是在Mac OS程序开发时,使用atomic属性通常都不会有性能瓶颈。

第7条:在对象内部尽量直接访问实例变量

  • 首先,我们需要先清楚,什么是【通过属性访问】?什么又是【直接访问】?

    其实用最简单的话来说就是:使用点语法来访问的就是【通过属性访问】,如: self.firstName = [components objectAtIndex: 0];
    而【直接访问】就是使用下划线语法访问,如: _firstName = [components objectAtIndex: 0];

    这两种写法有几个区别:

    1. 由于不经过Objective-C的“方法派发”(method dispatch)步骤,所以直接访问实例变量的速度当然会比较快。在这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
    2. 直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。(如:在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。)
    3. 如果直接访问实例变量,那就不会触发“键值观察”(Key-Value Observing,KVO)通知。(所以是否使用需要根据实际的项目需求而定)
    4. 通过属性来访问则有助于排查与之相关的错误,因为可以给“获取方法”和/或“设置方法”中新增“断点”(breakpoint)。

    所以总的来说,在对象内部读取数据的时候,我们更推荐通过直接访问实例变量的方式进行,而在写入数据的时候,则应通过属性访问的方式来写。在初始化方法及delloc方法中,就只应该使用【直接访问】来读写数据了!

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

  • 按照==操作符比较出来的结果未必是我们想要的,因为该操作比较的是两个指针本身,而不是其所指的对象。应该使用NSObject协议中声明的“isEqual:”方法来判断两个对象的等同性。

    NSString *foo = @"Badger 123";
    NSString *bar = [NSString stringWithFormat:@"Badger %i",123];
    BOOL equalA = (foo == bar);
    BOOL equalB = [foo isEqual:bar];
    BOOL equalC = [foo isEqualToString:bar];
    NSLog(@"equalA : %d\nequalB : %d\nequalC : %d",equalA,equalB,equalC);
    

    Output:
    equalA : 0
    equalB : 1
    equalC : 1

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

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

  • 若想在自定义的对象中正确覆写判断等同性的方法,就必须先理解其约定。但是,如果两个对象的hash方法返回同一个值,那么“isEqual:”方法未必会认为两者想等。

==大概的判断逻辑思路如下:==

  1. 首先,直接判断两个指针是否想等(若相等,则其均指向同一个对象,所以受测的对象也必定相等。);
  2. 接下来,比较两对象所属的类(若不属于同一个类,则两对象不相等。);
  3. 但是在继承体系中判断等同性时,经常会遭遇A实例与其子类实例想等,所以实现“isEqual:”方法要考虑到这种情况!
  4. 最后,检测每个属性是否相等。(只要其中有不相等的属性,就判定两对象不等,否则两对象相等。)
  接下来该实现hash方法了,根据等同性约定:若两对象相等,则其哈希码也相等,但是两个哈希码相同的对象却未必相等。这就是能否正确覆写“isEqual:”方法的关键所在。下面这种写法完全可行:

```
- (NSUInteger)hash{
        return 1337;
    }
```

不过若是这么写的话,在collection中使用这种对象将会产生性能问题,因为collection在检索哈希表时,会用对象的哈希码做引索。假如某个collection是用set集合实现的,那么set可能会根据哈希码把对象分装到不同的数组中。在向set中添加新对象的时候,要根据其哈希码找到与之相关的那个数组,依次检查其中各个元素,看数组中已有的对象是否和将要添加的新对象相等。

由此可知,如果令每个对象都返回相同的哈希码,那么在set中已有1 000 000个对象的情况下,若是继续向其中添加对象,则需将这1 000 000个对象全部扫描一遍。

  下面给大家推荐一种技能保持较高效率,又能使生成的哈希码至少位于一定范围内,而不会过于频繁地重复。当然,此算法生成的哈希码还是会碰撞,不过至少可以保证哈希码有多种可能的取值。编写hash方法时,应该用当前的对象做做实验,以便在减少碰撞频度与降低运算复杂程度之间取舍。

```
- (NSUInteger)hash{
    
    NSUInteger firstNameHash = [_firstName hash];
    NSUInteger lastNameHash = [_lastName hash];
    NSUInteger ageHash = _age;
    
    return firstNameHash ^ lastNameHash ^ ageHash;
}
```
  • 特定类所具有的等同性判定方法

    除了刚刚上面提到的NSString之外,NSArray与NSDictionary类也具有特殊的等同性判定方法,由于使用这些特殊的等同性判定方法在编译期是不做强类型检查(strong type checking),所以判定的速度会比较快,但是开发者要保证所传对象的类型是正确的。

  • 等同性判定的执行深度

    NSArray的检测方式为先看两个数组所含对象个数是否相同,若相同,则在每个对应位置的两个对象身上调用其“isEqual:”方法。如果对应位置上的对象均相等,那么这两个数组就相等,这叫做“深度等同性判定”(deep equality)。
      不过有时候无须将所有数据逐个比较,只根据其中部分数据即可判明二者是否等同。(如:A类的实例是根据数据库里面的数据创建而来,那么其中就可能含有另外一个属性,此属性是“唯一标识符”,在这种情况下,我们也许只会根据标识符来判断等同性,尤其是在此属性声明为readonly时更应该如此。)

  • 容器中可变类的等同性

    现在跟大家分享一种情况:用一个NSMutableSet与几个NSMutableArray对象测试一下,就能让大家发现问题了。

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

    现在set里含有一个数组对象,数组包含两个对象。如果再向set中加入一个与现有数组一模一样的数组对象的话,那set里面的对象将不会有变化,所以我们接下来添加一个和set已有对象不同的数组:

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

    正如大家所料,由于arrayC与set里已有的对象不相等,所以现在set里有两个数组了。最后,我们来改变一下arrayC的内容,令到其和arrayA相等

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

set中居然可以包含两个彼此相等的数组!!!根据set语义是不允许出现这种情况的,然而现在却无法保证这一点了,因为我们修改了set中的已有对象。所以,我们得出一个结论:==如果把某对象放入set之后,又修改其内容,那么后面的行为将很难预料==

当然,举这个例子只是为了提醒大家,把某对象加入collection之后,改变其内容将会带来的后果,并没有说绝对不要这么做,而是让大家注意在这样做的时候,要用相应的代码去处理可能出现的问题。

==**要点概括**==
1. 若想检测对象的等同性,请提供“isEqual:”与hash方法;
2. 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同;
3. 不要盲目地逐个检测每天属性,而是应该依照具体需求来制定检测方法;
4. 编写hash方法时,应该使用计算速度快且哈希码碰撞几率低的算法;

第9条:以“类族模式”隐藏实现细节

  • 类族模式

    “类族”(class cluster)是一种模式(pattern)。该模式可以灵活应对多个类,将它们的实现细节隐藏在“抽象基类”(abstract base class)后面,以保持接口简洁。开发者无须自己创建子类实例,只需调用基类方法来创建即可。

  • 创建类族

    MarkEmployee头文件:

    //
    //  MarkEmployee.h
    #import <Foundation/Foundation.h>
    /* 抽象基类  */
    typedef NS_ENUM(NSUInteger, MarkEmployeeType) {
    MarkEmployeeTypeDeveloper,
    MarkEmployeeTypeDesigner,
    MarkEmployeeTypeFinance,};
    
    @interface MarkEmployee : NSObject
    /** 名字 */
    @property(nonatomic,copy) NSString *name;
    /** 薪水 */
    @property(nonatomic,assign) NSUInteger salary;
    
    // 工厂方法(类方法):创建雇员对象
    + (MarkEmployee *)employeeWithType:(MarkEmployeeType)type;;
    
    // 职员干自己的工作
    - (void)doADaysWork;
    
    @end
    

    MarkEmployee实现文件:

    //
    //  MarkEmployee.m
    //
    
    #import "MarkEmployee.h"
    
    @implementation MarkEmployee
    
    /*  将子类的实例的创建隐藏在基类的实现方法中 */
    // 根据职员类型创建子类各自的实例
    +(MarkEmployee *)employeeWithType:(MarkEmployeeType)type
    {
        switch (type) {
            case MarkEmployeeTypeDeveloper:
                return [MarkEmployeeDeveloper new];
                break;
                
            case MarkEmployeeTypeDesigner:
                return [MarkEmployeeDesigner new];
                break;
                
            case MarkEmployeeTypeFinance:
                return [MarkEmployeeFinance new];
                break;
                
            default:
                break;
        }
    }
    
    //Make Employees do their respective day's work
    -(void)doADaysWork
    {
        // 在子类的实现文件中各自实现其工作
    }
    
    @end
    

    MarkEmployee的子类MarkEmployeeDeveloper的实现文件:

    #import "MarkEmployeeDeveloper.h"
    
    @implementation MarkEmployeeDeveloper
    
    - (void)doADaysWork{
        // 子类其工作的实现细节
        [self writeCode];
    }
    
    - (void)writeCode{
        NSLog(@"writeCode");
    }
    
    @end
    

    main函数:

    #import "MarkEmployee.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            MarkEmployee *developer = [MarkEmployee emplyeeWithType:MarkEmployeeTypeDeveloper];
            NSLog(@"%@",[developer class]);// output MarkEmployeeDeveloper
    
            /*
             * 总结:
             * 工厂模式
             * 通过MarkEmployee类的工厂方法创建出来的实例是MarkEmployee类的子类的实例
             */
        }
        return 0;
    }
    
  • Cocoa里的族类

    系统框架中有许多类族。大部分collection类都是某个类族中的抽象基类。NSArray与NSMutableArray实际上有两个抽象基类,但是仍然算是一个类族,意味着两者在实现各自类型的数组时可以共用实现代码,此外,还能把可变数组复制为不可变数组,反之亦然。

    id maybeAnArray = /* ... */;
    if ([maybeAnArray class] == [NSArray class]){// 永远不可为真
        // will nerver be hit
    }
    

    注意 : [maybeAnArray class] 所返回的类绝不可能是NSArray类本身,因为由NSArray的初始化方法所返回的那个实例所属的类型是“隐藏在类族公共接口后面的那个内部类型”。要判断出某个实例所属的类是否位于类族之中需要用类型信息查询方法。

    id maybeAnArray = /* ... */;
    if (maybeAnArray isKindOfClass:[NSArray class]){
        // will be hit
    }
    

    向Cocoa中NSArray这样的类族新增子类所要遵守的规则

    • 子类应该继承自类族中的抽象基类。
    • 子类应该定义自己的数据存储方式。
      子类必须用一个实例变量来存放数组中的对象,而NSArray本身只不过是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都需具备的一些接口。对于这个自定义的数组子类来说,可以用NSArray来保存其实例。
    • 子类应当覆写超类文档中指明需要覆写的方法。
      在类族中实现子类时所需遵循的规范一般都会定义基类的文档之中,编码前应该先看看。

    要点

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

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

  • 有时候我们需要在对象中存放相关的信息,此时,我们通常的做法是会从对象所属的类中去继承一个子类,然后改用这个子类对象。然而,并非所有情况下都能这么做,有时候类的实例可能是由某种机制所创建的,而开发者无法令这种机制创建出自己所写的子类实例。
  • 但是,Objective-C中有一项强大的特性可以解决此问题,这就是“关联对象(Associated Object)”。
  • 可以给某对象关联许多其他对象,这些对象通过“键”来区分,这就是关联对象。存储对象值的时候,可以指明“存储策略”(storage policy),用以维护相应的“内存管理语义”
| 关联类型 | 等效的@property属性 |
| --- | --- |
| OBJC_ASSOCIATION_ASSIGN | assign |
| OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic,retain |
| OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic, copy |
| OBJC_ASSOCIATION_RETAIN | retain |
| OBJC_ASSOCIATION_COPY | copy |

**下列方法可以管理管理对象:**
  • void objc_setAssociatedObject(id object, void*key, id value, objc_AssociationPolicy)

    此方法以给定的键和策略为某对象设置关联对象值。

  • id objc_getAssociatedObject(id object, void*key)

    此方法根据给定的键从某对象中获取相应的关联对象值。

  • void objc_removeAssociatedObjects(id object)

    此方法移除指定对象的全部关联对象。

    由于设置关联对象时所用的键是个“不透明的指针”(opaque pointer),所以在设置关联对象值时,若想令两个键匹配到同一个值,则两者必须是完全相同的指针才行。鉴于此,在设置关联对象值时,通常使用静态全局变量做键。

  • 关联对象用法举例

    以我们最常用的UIAlertView为例,给大家讲解一下关联对象的用法:

    这是我们常用于处理一些向用户提示某些消息,并根据用户点击的按钮来处理下一步动作的代码:

    - (void)askUserAQuestion{
        
        UIAlertView *alert = [[UIAlertView alloc]
                              initWithTitle:@"Question"
                              message:@"What do you want to do ?"
                              delegate:self
                              cancelButtonTitle:@"Cancel"
                              otherButtonTitles:@"Continue", nil];
        [alert show];
    }
    
    //UIAlertView Delegate
    -(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
    {
        if (buttonIndex == 0) {
            [self doCancel];
        }else{
            [self doContinue];
        }
    }
    

    如果想在同一个类里处理多个AlertView的话,代码就会变得更为复杂,我们必须在delegate方法中检查传入的alertView参数,并据此选用相应对的逻辑。要是能在创建AlertView的时候直接把处理每个按钮的逻辑都写好,那岂不是简单多了!
      接下来我们就用关联对象的方式来实现同样的功能:

    #import <objc/runtime.h>
        
    static void *MarkMyAlertViewKey = "MarkMyAlertViewKey";
        
    - (void)askUserAQuestion{
       
       UIAlertView *alert = [[UIAlertView alloc]
                             initWithTitle:@"Question"
                             message:@"What do you want to do ?"
                             delegate:self
                             cancelButtonTitle:@"Cancel"
                             otherButtonTitles:@"Continue", nil];
       
       void(^block)(NSInteger) = ^(NSInteger buttonIndex){
           
           if (buttonIndex == 0) {
               [self doCancel];
           }else{
               [self doContinue];
           }
       };
       
       objc_setAssociatedObject(alert,
                            MarkMyAlertViewKey,
                            block,
                            OBJC_ASSOCIATION_COPY);
       [alert show];
    }
        
       //UIAlertView Delegate
    -(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
    {
       void (^block)(NSInteger) = objc_getAssociatedObject(alertView, MarkMyAlertViewKey);
       block(buttonIndex);
    }
    

    在一个类里创建一个警告视图后,设定一个与之关联的“块”并将它们放在同一作用域里,等到执行delegate方法时再将其读出来。这种方式创建的UIAlertView与处理操作结果的代码都放在一起,更易读懂。==但是,由于块可能要捕获某些变量,也会造成“保留环”==
      所以,这种做法只应该在其他办法行不通时才去考虑用它。而对于多次用到UIAlertView视图,有个更好的办法,那就是从中继承子类,把块保存为子类中的属性。

    要点

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

第11条:理解objc_msgSend的作用

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

  • 消息派发的一般过程

    id returnValue  = [someObject messageName:parameter];
    

    解释:someObject是“接收者”,messageName是“选择器”,选择器与参数合起来成为“消息”。

    编译器在看到此消息后,将其转换为一条标准的C语言函数调用objc_msgSend,其原型如下:

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

    解释:这是个参数可变的函数。第一个参数代表接收者,第二个参数代表选择器,后续参数就是消息中的那些参数,其顺序不变。

    对先前例子中的消息进行转换,如下:

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

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

  • 消息派发的特殊情况

    • objc_msgSend_stret:如果待发送的消息要返回结构体,那么可交由此函数处理。
    • objc_msgSend_fpret:如果消息返回的是浮点数,那么交由此函数处理。
    • objc_msgSendSuper:如果要给超类发消息,例如[super message:parameter],那么就交由此函数处理。
  • 尾调用优化

    objc_msgSend等函数一旦找到应该调用的方法实现之后,就会“跳转过去”。之所以能这么做,是因为Objective-C对象的每个方法都可以视为简单的C函数,其原型如下:

    <return_type> Class_selector(id self, SEL _cmd, ...)
    

    解释:每个类里都有一张表格,其中的指针都会指向这种函数,而选择器的名称则是查表时所用的“键”。objc_msgSend等函数正是通过这张表格来寻找应该执行的方法并跳至其实现的。其中,所使用的“尾调用优化”(tail-call optimization)技术,可以让“跳至方法实现”这一操作变得更简单些。但是,只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行“尾调用优化”。

    要点

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

第12条:理解消息转发机制

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

  • 消息转发分为两大阶段

    第一阶段:先征询接受者所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”,这叫做“动态方法解析”。

    第二阶段:涉及“完整的消息转发机制”。
    运行时系统会请求接收者以“动态新增方法”之外的手段来处理与消息相关的方法调用,这又细分为两小步。
      首先,请接收者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束。
      若没有“备援的接收者”,则启动完整的消息转发机制,运行时系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还没处理的这条消息。

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

    + (BOOL)resolveInstanceMethod:(SEL)selector
    

    解释:selector是未知的选择器,返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择器。

    在继续往下执行转发之前,本类有机会新增一个处理未知选择器的方法,便是通过调用“resolveInstanceMethod:”或“resolveClassMethod:”方法来实现的。

    但是,使用这种办法有个前提:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。此方案常用来实现@dynamic属性。

    id autoDictionaryGetter(id self, SEL _cmd);
    void autoDictionarySetter(id self, SEL _cmd, id value);
    
    + (BOOL)resolveInstanceMethod:(SEL)selector{
        NSString *selectorString = NSStringFromSelector(selector);// 将选择器转换为字符串
        if(/* selector is from a @dynamic property */){// 使用了@dynamic属性
            if([selectorString hasPrefix:@"set"]){
                class_addMethod(self,
                                selector,
                                (IMP)autoDictionarySetter,
                                "V@:@");
            }else{
                class_addMethod(self,
                                selector,
                                (IMP)autoDictionaryGetter,
                                "@@:");
            }
            return YES;
        }
        return [super resolveInstanceMethod:selector];
    }
    
    
  • 备援接受者

    在第二阶段的第一小步中,运行期系统会问未知的选择器能不能把这条消息转发给其他接收者来处理。与该步骤对应的处理方法如下:

    - (id)forwardingTargetForSelector:(SEL)selector
    

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

  • 完整的消息转发

    若没有“备援的接收者”,则启动完整的消息转发机制,运行时系统会把与尚未处理的那条消息有关的全部细节都封装到NSInvocation对象中。

    在触发NSInvocation对象时,“消息派发系统”(message-dispatch system)将亲自出马,把消息指派给目标对象。如下:

    - (void)forwardInvocation:(NSInvocation*)invacation
    

    此方法比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择器,等等。

  • 消息转发全流程

  • 以完整的例子演示动态方法解析
    MarkAutoDictionary头文件

    #import <Foundation/Foundation.h>
    
    @interface MarkAutoDictionary : NSObject
    
    @property(nonatomic,strong) NSString *string;
    @property(nonatomic,strong) NSNumber *number;
    @property(nonatomic,strong) NSDate *date;
    @property(nonatomic,strong) id opaqueObject;
    
    @end
    

    MarkAutoDictionary实现文件

    #import "MarkAutoDictionary.h"
    #import <objc/runtime.h>
    
    @interface MarkAutoDictionary ()
    @property(nonatomic,strong) NSMutableDictionary *backingStore;
    @end
    
    @implementation MarkAutoDictionary
    
    // @dynamic会阻止编译器自动生成相关的存取方法,而由开发者自己创建存取方法
    @dynamic string, number, date, opaqueObject;
    
    - (id)init{
        if (self = [super init]) {
            _backingStore = [NSMutableDictionary new]; // 延迟加载
        }
        return self;
    }
    
    // 动态添加新方法
    + (BOOL)resolveInstanceMethod:(SEL)sel{
        NSString *selectorString = NSStringFromSelector(sel);
        if ([selectorString hasPrefix:@"set"]) {
            class_addMethod(self,
                            sel,
                            (IMP)autoDictionarySetter,
                            "v@:@");
        }else{
            class_addMethod(self,
                            sel,
                            (IMP)autoDictionaryGetter,
                            "@@:");
        }
        return YES;
    }
    
    // getter函数
    id autoDictionaryGetter(id self, SEL _cmd){
        // 从MarkAutoDictionary对象获取backingStore字典
        EOCAutoDictionary *typedSelf = (MarkAutoDictionary*)self;
        NSMutableDictionary *backingStore = typedSelf.backingStore;
    
        // 将选择器转换为字符串,并将其设为key
        NSString *key = NSStringFromSelector(_cmd);
    
        // 返回backingStore字典中key所对应的值
        return [backingStore objectForKey:key];
    }
    
    // setter函数
    void autoDictionarySetter(id self, SEL _cmd, id value){
        // 从MarkAutoDictionary对象获取backingStore字典
        MarkAutoDictionary *typedSelf = (MarkAutoDictionary*)self;
        NSMutableDictionary *backingStore = typedSelf.backingStore;
    
        // 将选择器转换为字符串,并将其拷贝为可变字符串
        NSString *selectorString = NSStringFromSelector(_cmd);
        NSMutableString *key = [selectorString mutableCopy];
    
        // 移除key中尾部的“:”
        [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
    
        // 移除key中前面的“set”
        [key deleteCharactersInRange:NSMakeRange(0, 3)];
    
        // 取出现有的key中的首字母,将其小写化并替代掉原来的首字母
        NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
        [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
    
        // 根据key给backingStore存储相关的值
        if (value) {
            [backingStore setObject:value forKey:key];
        }else{
            [backingStore removeObjectForKey:key];
        }
    
    }
    @end
    

    main函数:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            EOCAutoDictionary *autoDict = [EOCAutoDictionary new];
            // autoDict.date == [autoDict setDate]
            // 由于接收者没有相应的方法可调用,因为@dynamic特性,所以可以动态新增方法
            autoDict.date = [NSDate dateWithTimeIntervalSince1970:3140907998];
            NSLog(@"%@",autoDict.date);
        }
        return 0;
    }
    

    输出结果为:

    2016-03-09 20:29:25.552 第12条.演示动态方法解析[7000:347059] 2069-07-13 02:26:38 +0000
    Program ended with exit code: 0
    

    总结:要想添加新属性,只需要用@property来定义,并将其声明为@dynamic即可。

    要点

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

第13条:用“方法调配技术”调试“黑盒方法”

  • 在给定的选择子名称相对应的方法也可以在运行期改变,若能善用此特性,则可发挥巨大优势,因为我们既不需要源代码也不需要通过继承子类来覆写方法就能改变这个类本身的功能。这么一来,新功能将在本类的所有实例中生效,而不是仅限于覆写了相关方法的那些子类实例。此方案经常称为“方法调配”(method swizzling)。

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

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

    以NSString类的选择器映射表为例 图例如下:

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

  • Method class_getInstanceMethod(Class aClass, SEL aSelector) :根据给定的选择器从类中取出与之相关的方法。

  • void method_exchangeImplementations( Method m1, Method m2):可以交换两个方法实现。

  • 应用案例

    编写一个方法,在此方法中实现所需的附加功能,并调用原有实现。
    分类 NSString (MarkMyAdditions)头文件:

    @interface NSString (MarkMyAdditions)
    - (NSString*)mark_myLowercaseString;
    @end
    

    分类 NSString (MarkMyAdditions)实现文件:

    @implementation NSString (MarkMyAdditions)
    // 新增一个方法,实现附加功能
    - (NSString *)mark_myLowercaseString{
        NSString *lowercase = [self eoc_myLowercaseString];
        NSLog(@"%@ => %@", self, lowercase);
        return lowercase;
    }
    @end
    

    main函数:

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    #import "NSString+MarkMyAdditions.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // 1.交换方法
            Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
            Method swappedMethod = class_getInstanceMethod([NSString class], @selector(mark_myLowercaseString));
            // 2.交换方法
            method_exchangeImplementations(originalMethod, swappedMethod);
    
            // 3.调用lowercaseString方法,但实现的功能却是分类中新增方法的附加功能
            NSString *string = @"THIs is tHe StRiNg";
            NSString *lowercaseString = [string lowercaseString];
    
        }
        return 0;
    }
    

    输出结果:

     THIs is tHe StRiNg => this is the string
    

    总结:通过此方案,开发者可以为那些“完全不知道其具体实现的”黑盒方法增加日志记录功能,这非常有助于程序调试。然而,此做法只在调试程序时有用,禁止滥用。

    要点

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

第14条:理解“类对象”的用意

  • 运行期检视对象类型的操作叫做“类型信息查询”(introspection,内省)。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息查询方法”。

    typedef struct objc_object *id;
    struct objc_object { Class isa; };
    
    // 等价于
    typedef sturct objc_object {
        Class isa;
    } *id;
    

    解释:该结构体描述了Objective-C对象所用的数据结构。其中,isa指针定义了对象所属的类。

    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;
    
    #if defined(Release3CompatibilityBuild)
        struct objc_method_list *methods;
    #else
        struct objc_method_list **methodLists;
    #endif
    
        struct objc_cache *cache;
        struct objc_protocol_list *protocols;
    };
    

    解释:该结构体存放类的“元数据”。其中,isa指针定义了另外一个类——元类(metaclass),用来表述类对象本身所具备的元数据。super_class定义了本类的超类。
    类方法 :类方法可以理解成类对象的实例方法,每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。

  • 类继承体系 图例

    总结:通过这张布局关系图即可执行“类型信息查询”。开发者可以查出对象是否能响应某个选择器,是否遵从某项协议,并且能看出此对象位于“类继承体系”(class hierarchy)的哪一部分。

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

    1. 类型信息查询方法

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

    2. 等同性判断方法

      使用“==”操作符来比较类对象是否等同。原因在于,类对象是“单例”(singleton),在应用程序范围内,每个类的Class仅有一个实例,也就是说,借助“==”操作符可以精确判断出对象是否为某类实例。如:

      id object = /* ... */;
      if ([object class] == [EOCSomeClass class]){
          // 'object' is an instance of EOCSomeClass
      }
      

      总结:应该尽量使用类型信息查询方法,而不应该直接比较两个类对象是否等同。

    要点

    • 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系
    • 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法(内省)来探知。
    • 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容