編寫高質量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運行期系統提供的幾個方法,能夠用來操作這張表??梢孕略鲞x擇子,可以改變某選擇子所對應的方法實現,還可以交換兩個選擇子所映射到的指針。

交換方法實現:

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對象則構成了類的集成體系。
  • 如果對象類型無法在編譯期確定,那么久應該使用類型信息查詢方法來探知。
  • 盡量使用類型信息查詢方法類確定對象類型,而不要直接比較類對象,因為某些類對象可能實現了消息轉發功能。
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容