編寫高質量iOS與OS X代碼的52個有效方-Effective Objective-C 2.0閱讀筆記

第1條:瞭解基本OC對象

NSString *someString = @"The String";

這種語法基本上是辦照C語言的,它聲明一個名爲 someString的變量,類型爲NSString* 。也就是說此變量指向 NSString的指針。所有Objective-C語言的對象都必須這樣聲明,因爲對象所佔內存總是分配在”“堆空間”(heap space)中, 而絕不會分配在”“棧”(stack)上。不能在棧中分配 Objective-C 對象;

someString 變量指向分配在堆裏的某個內存區域,其中包含一個NSString對象。也就是說,如果再創建一個變量,另其指向同一個地址,那麼並不會拷貝該對象,只是這兩個變量會同時指向此對象:

NSString *someString = @"The String";
NSString *anotherString = someString;

只有一個 NSString 實例,然而有兩個變量指向此實例。兩個變量都是 NSString* 類型,這說明當前”“棧幀”(stack frame)裏分配了兩塊內存,每塊內存的大小都能容下一枚指針(在32位架構的計算機上是4字節,64位計算機上是8字節)。這兩塊內存裏的值都一樣,就是NSString實例的地址。

圖1-1

圖1-1 此內存佈局圖演示了一個分配在堆中的NSSting實例,有兩個分配在棧上的指針指向該實例。

分配在堆中的內存必須直接管理,而分配在棧上用於保存變量的內存則會在其棧幀彈出時自動清理。

OC將堆內存管理抽象出來了。不需要malloc以及free來分配或者釋放內存。OC運行期環境把這部分工作抽象爲一套內存管理架構,名爲引用計數器

在OC代碼中,有時會遇到定義不含*的變量,它們可能會使用”“棧控件”(stack space)。這些變量所保存的不是OC對象。比如CoreGraphice框架中的CGRect:

CGRect frame;
frame.origin.x = .0f;
frame.origin.y = .0f;
frame.size.width = 100.0f;
frame.size.height = 200.0f;

CGRect 是 C 結構體,其定義是:

struct CGRect {
    CGPoint origin;
    CGSiez size;
};
typedef struct CGRect CGRect;

整個系統框架都在使用這種結構體,因爲如果改用OC對象來做的話,性能會受影響。與創建結構體相比,創建對象還需要額外開銷,例如分配及釋放內存等。如果只需保存 int、float、double、char等”“非對象類型”(non object type),那麼使用CGRect 這種結構體就可以了。

  • 我們可以利用這一特點、運用到開發中。

第2條: 在類的頭文件中儘量少引入其他頭文件


在編譯一個MSPerson類的文件時,不要知道 MSEmployer 類的全部細節,只需要知道有一個類名叫 MSEmployer 就好:

@Class MSEmployer
這叫做向前聲明(forward declaring)該類。這樣 MSPerson 就能設置 MSEmployer 類型的屬性

@Class MSEmployer
@Interface MSPerson : NSObject
@porperty (nonatomic, strong) MSEmployer *employer;
@porperty (nonatomic, copy) NSSting *name;
@end

如果MSPerson類需要知道B類頭文件的所有細節,那麼通過使用#improt "MSEmployer"替換@Class MSEmployer

將引入頭文件的時機儘量延後,只在卻有需要時才引入,這樣就可以減少類的使用者所需引入的頭文件的數量。

假設將 MSEmployer.h 引入到 MSPerson.h 中,那麼其他的類引入 MSPerson.h ,就一併會引入MSEmployer.h的所有內容。此過程若持續下去,則要引入許多根本用不到的內容,這當然會增加編譯時間。

向前聲明(forward declaring)也解決了兩個類互相引用的問題。假設要爲MSEmployer類
加入新增及刪除僱員的方法,那麼其頭文件會加入下述定義:

- (void)addEmployee:(MSPerson *)person;
- (void)remvoeEmployee:(MSPerson *)person;

此時需要編輯 MSEmployer,則編輯器必須知道 MSPerson 這個類,而編譯 MSPerson,則又必須知道 MSEmployer。如果在各自的頭文件引入對方的頭文件,則導致”“循環引用”。

當解析其中一個頭文件時,編輯器會發現它引入了另一個頭文件,而那個頭文件又回過頭來引用第一個頭文件。使用#Import而非 #include指令雖然不會導致死循環,但卻這意味着兩個類有一個無法被正確編譯。

向前聲明只能告訴編輯器有某個類,某個協議。

必須引入頭文件

  • 繼承
  • 遵守某個協議,且知道協議方法

第二條的#import 是難免的。鑑於此,最好是把協議單獨放在一個頭文件中。


第4條: 多用類型常量,少用#define預處理指令


編碼時經常要定義常量。例如,要寫一個UI視圖類,此視圖顯示出來就播放,然後消失。你可能想把播放動畫的時間提取爲常量。掌握了OC與C語言基礎的人,也許會用這種方法來做:

#define ANIMATION_DURATION 0.3

上述預處理指令會把源代碼中的 ANNIMATION_DURATION 字符串替換爲0.3。這可能就是你想要的效果,不過這樣定義出來的常量沒有類型信息。”持續”(dutation)這個詞看上去應該與時間有關,但是代碼中又未明確指出。此外,預處理過程會把碰到的所有 ANIMATION_DURATION 一律替換成0.3, 這樣的話,假設此指令在某個頭文件中,那麼所有引入了這個頭文件的代碼,其ANMIATION_DUTATION都會被替換。


第9條:以”類族模式” 隱藏實現細節


“類族”是一中很有用的模式,可以隱藏”抽象基類”,背後的實現實現細節。OC的框架中普遍使用此模式。比如,iOS 的用戶界面框架 UIKit 中就有一個名爲 UIButton 的類。想創建按妞,需要調用下面這個 “類方法”:

+ (UIButton *)buttionWtihType:(UIButtonType)type;

該方法返回的對象,其類型取決於傳入的按鈕類型(Button type)。然而,不管返回什麼類型的對象,它們都繼承自同一個基類: UIButton。這麼做的意義在於: UIButton 類的使用者無需關心創建出來的按鈕具體屬於哪個子類,也不用考慮按鈕的繪製方式等實現細節。使用者只需要明白如何創建按鈕。如何設置項想 “標題(title)” 這樣的屬性,如何增加觸摸動作的目標對象等問題就好。

回到開頭說的那個問題上,我們可以把各種按鈕的繪製邏輯都放在一個類裏,並根據按鈕類型來切換:

- (void)drawRect:(CGRect)rect {
    if  (_type = TypeA){
        //Draw 按鈕A
    }else if (_type = TypeB){
        //Draw 按鈕B
    }/* ... */
}

這樣寫現在看上去還算簡單,然而,若是需要依按鈕類型來切換的繪製方法有許多種,那麼就會變得很麻煩了。優秀的程序員會將這種代碼重構爲多個子類,把各種按鈕所用的繪製方式放到相關子類中去。不過,這麼做需要用戶知道各種子類才行。此時應該使用 “類族模式”,該模式可以靈活應對多個類。將它們的實現細節隱藏在抽象基類後面,以保持接口簡潔。用戶無需自己創建子類實例,只需要調用基類方法來創建即可。

創建類族

現在舉例來演示如果創建類族。假設有一個處理僱員的類,每個僱員都有 “名字” 和 “薪水” 這個兩個屬性,管理者可以命令其執行日常工作。但是,各種僱員的工作內容卻不同。經理在帶領僱員做項目時,無需關係每個人如何完成其工作,僅需指示其開工即可。

下面看代碼:

typedef NS_ENUM(NSUInteger, MSEmployeeType) {
    MSEmployeeTypeDeveloper,
    MSEmployeeTypeDesigner,
    MSEmployeeTypeFinance,
}

@interface  MSEmployee : NSObject
@property(copy) NSStirng *name;
@property NSUInterger salary;

///類方法創建MSEmployee實例
+ (MSEmployee *)employeeWhihType:(MSEmployeeType)type;

- (void)doADaysWork;

@end

@implementation MSEmployee

///類方法創建MSEmployee實例
+ (MSEmployee *)employeeWhihType:(MSEmployeeType)type {
    swicth(type){
          case MSEmployeeTypeDeveloper:
        return [MSEmployeeTypeDeveloper new];
        break;
           case MSEmployeeTypeDesigner:
        return [MSEmployeeTypeDesigner new];
        break;
           case MSEmployeeTypeFinance:
        return [MSEmployeeTypeFinance new];
        break;
    }
}
- (void)doADaysWork {
    // Subcalasses implement this.
}

@end

每個 “實體子類” 都是繼承基類而來。例如:

@interface MSEmployeeDeveloper : MSEmployee
@end

@inplementation MSEmployeeDeveloper
- (void)doADaysWork {
    [self writeCode];
}
@end

在本例中,基類實現了一個 “類方法”, 該方法根據創建的僱員類別分配好對應的僱員實類實例。這種 “工廠模式” 是創建類族的辦法之一。

可惜 OC 這門語言沒辦法指明某個基類是 “抽象的”。於是,開發者通常會在文檔中寫明類的用法。這種情況下,基類接口一般都沒有 “init” 的初始化方法, 這暗示該類的實例也許不會由用戶直接創建。還有一種辦法可以確保用戶不會使用基類實例,那就是在基類的 doADaysWrok 方法中拋出異常。然而這種方法相當極端,很少人用。(個人覺得一定程度上違反了 OC 對象基本架構)。

在 MSEmployee 這個例子中,【employee isMemberOfClass:[MSEmployee class]】似乎會返回YES,但實際上返回確是NO, 因爲 employee 並非 MSEmployee 類的示例,而是其某個子類的示例。

Cocoa 裏的類族

系統框架中有許多類族。大部分 collection 類都是類族。例如 NSArray 與其可變版本 NSMutableArray 。這樣看來,實際上有兩個抽象基類,一個用於不可變數組,另一個用於可變數組。儘管具備公共接口的類有兩個,但仍然可以合起來算作一個類族。不可變的類定義了對所有數組都通用的方法,而可變的類則定義了那些只適用於可變數組的方法。兩個類共同屬於一個類族,這意味着二者在實現各自類型的數組時可以共用實現代碼,此外,還能夠把可變數組複製爲不可變數組,反之亦然。


第10條


第11條:理解objc_msgSend的作用


在對象上調用方法是OC中常用的功能。用OC的術語來說,這叫傳遞消息。消息有 “名稱” 或者 “選擇器(selector)”,可以接受參數,而且可能還有返回值。

由於OC是C的超集,所以最好理解C的函數調用方式。C使用 “靜態綁定”,也就是說,在編譯期就能決定運行時所對應調用的函數。以下代碼爲例:

#import <stdio.h>

void printHello(){
    printf("Hello,world\n");
}
void printGoodbye(){
    printf("Goodbye,world\n");
}
void doTheThing(int type) {
    if (type ==0) {
        printHello();
    } else {
        printGoodbye();
    }
    return 0;
}

如果不考慮 “內聯”,那麼編譯器在編譯代碼的時候就已經知道程序中有 printGoodbye 和 printHello 兩個函數了,於是會直接生成調用這些函數的指令。而函數地址實際上是硬編碼指令之中的。若是將剛纔的代碼寫成下面這樣,會如何呢?

#import <stdio.h>

void printHello(){
    printf("Hello,world\n");
}
void printGoodbye(){
    printf("Goodbye,world\n");
}
void doTheThing(int type) {
   void (* func)();
    if (type ==0) {
        func = printHello;
    } else {
       func = printGoodbye;
    }
    return 0;
}

這時就是使用 “動態綁定” 了,因爲所要調用的函數直到運行期才能確定。編譯器在這種情況下生成的指令與剛纔那個例子不同,在第一個例子中,if與else語句裏都有函數調用指令。而在第二個例子中,只有一個函數調用指令,不過待調用的函數地址無法硬編碼在指令之中,而是要運行期讀取出來。

在OC中,如果向對象傳遞消息,那就會使用動態機制來決定訣要調用的方法。在底層,所有的方法都是普通的C語言函數,然而對象收到消息後,究竟該調用哪個方法則完全於運行期決定,甚至可以再程序運行時改變,這些特性使得OC成爲一門真正的動態語言。

給對象發送消息可以這樣來寫:

id returnValue = [someObject messageName:parameter];

在本例中,someObject 叫做 “接收者(receiver)”,messageName 叫做 “選擇器(selector)”。選擇器與參數合起來稱爲 “消息(message)”。編譯器看到消息後,將其轉換一條標準的 C 語言函數調用,所調用的函數乃是消息傳遞機制中的核心函數,叫做 objc_msgSend,其 “原型(prototype)”如下:

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

這是個 “參數個數可變的函數”,能接受兩個或者兩個以上的參數。第一個參數代表接收者,第二個參數代表選擇器(SEL是選擇器的類型),後續參數就是消息中的那些參數,其順序不變。選擇器值得就是方法的名字。”選擇器” 與 “方法” 這兩個詞經常交替使用。上述例子轉換成C函數如下:

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

objc_msgSend 函數會依據接收者與選擇器的類型來調用適當的方法。爲了完成此操作,該方法需要在接收者所屬的類中搜尋其 “方法列表”,如果能找到與選擇器名稱相符的方法,就跳至其實現代碼。若是找不到,那就沿着繼承體系繼續向上查找,等找到合適的方法之後再跳轉。如果最終還是找不到相符的方法,那就執行 “消息轉發” 操作。

這麼說來,想調用一個方法似乎需要很多步驟。所幸 objc_msgSend 會將匹配結果緩存在 “快速映射表”裏面,每個類都有這樣一塊緩存,若是稍後還向該類發送與選擇器相同的消息,那麼執行起來就很快了。當然,這種 “快速執行路徑” 還是不如 “靜態綁定的函數調用操作”那樣迅速,不過只要把選擇器緩存起來,也不會慢很多,實際上,消息派發並非用應用程序的瓶頸所在。

小結:

  • 消息由接收者、選擇器以及參數構成。給某對象 “發送消息” 也就相當於給在該對象上 “調用方法”。
  • 發給某對象的全部消息都要由 “動態消息派發系統”來處理,給系統會查出對應的方法,並執行其代碼。

第 12 條: 理解消息轉發機制


第 11 條講解了對象的消息傳遞機制,並強強調了其重要性。第12條則要講解另外一個重要的問題,就是對象在收到無法解讀的消息之後會發什麼什麼情況。

若想令類能理解某條消息,我們必須以程序碼實現出對應的方法才行。但是,在編輯期向類發送其無法解讀的消息並不會報錯,因爲運行期可以繼續向類中添加方法,所以編譯器在編譯時還無法確知類中到底會不會有某個方法實現。當對象接收到無法解讀的消息後,就會啓動 “消息轉發”機制,程序員可經由此過程告訴對象應該如何處理未知消息。

你可能早就遇到過經由消息轉發流程處理的消息,只是未加留意。如果在控制檯中看到下面這種提示信息,那就說明你曾像某個對象發送一條無法解讀的消息,從而啓動了消息轉發機制,並將此消息轉發給了NSObject的默認實現。

 +[__NSCFNumber lowercaseString]: unrecognized selector sent to class 0x10c0e4600

 *** Terminating app due to uncaught exception 'NSInvalidArgumentException', 

 reason: '+[__NSCFNumber lowercaseString]: unrecognized selector sent to class 0x10c0e4600'

上面這段異常信息是由 NSObject 的 “doesNotRecognizeSelector:” 方法所拋出的,此異常表明: 消息接收者類型是__NSCFNumber, 而該接收者無法理解名爲 lowercaseString的選擇器。

消息轉發分爲兩大階段。第一階段先徵詢接收者,所謂的類,看其是否能動態添加方法,以處理當前這個 “未知的選擇器”,這叫做 “動態方法解析”。第二階段涉及 “完整的消息轉發機制”。 如果運行期系統已經把第一階段執行完了,那麼接收者自己就無法再以動態的新增方法的手段來響應包含該選擇器的消息了。此時,運行期系統會請求接收者以其他手段來處理與消息相關的方法調用。這又細分爲兩小步。首先,請接收者看看有沒有其他對象能處理這條消息。若有,則運行期系統則會把消息轉給那個對象,於是消息轉發過程結束,一切如常。若沒有 “備援的接收者”,則啓動完整的消息轉發機制,運行期系統會把消息有關的所有細節都封裝到 NSIvocation 對象中,再給接收者最後一次機會,另其設法解決當前還未出來的這條消息。

動態方法分析

對象在收到無法解讀的消息後,首先調用其所屬類的下列類方法。


+ (BOOL)resolveInstanceMethod:(SEL)selector

該方法的參數就是那個未知的選擇器,其返回值是 Boolean 類型,表示這個類是否能新增一個實例方法用以處理此選擇器。

加入尚未實現的方法不是實例方法而是類方法,那麼運行期系統就會調用另一個方法,該方法與 “resolveInstanceMethod:”類似,叫做 “resolveClassMethod:”。

使用這種方法的前提是: 相關方法的實現代碼已經寫好,只等着運行的時候動態插在類裏面就可以了。

此方案常用來實現@dynamic屬性,比如說,要訪問CoreData框架中的NSManagerObjects 對象的屬性時就可以這麼做,因爲實現這些屬性所需的存取方法在編譯期就能確定。

id autoDictionaryGetter(id slef, 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*/ ){
        if ([selectorString hasPrefix:@"set"]){
            class_addMethod(self, selector, (IMP)autoDictionarySetter,"v@:@");
        }else{
            class_addMethod(self, selector,(IMP)autoDictionaryGetter,"@@:")
        }
        return YES;
    }
        return [super resolveInstanceMehtod:selector];
}

首選將選擇器化爲字符串,然後檢車其是否表示設置方法。若前綴爲set,則表示設置方法,否則就是獲取方法。不管哪種情況,都會把處理該選擇器的方法夾加到類裏面,所添加的方法使用純C函數實現的。C函數可能會用代碼來操作相關的數據結構,類之中的屬性數據就存放在那些數據結構裏面。

備援接收者

當接收者還有第二次機會能處理未知的選擇器,在這一步中,運行期系統會問它:能不能把這條消息轉給其他接收者來處理。與該步驟對應的處理方法如下:

- (id)forwardingTargerForSelector:(SEL)selector

若當前接收者能找到備援對象,有則將其返回,否則返回nil。

通過此方案,我們可以用 “組合” 來模擬出 “多繼承” 的某些特性。在一個對象內部,可能還有一些列的其他對象,該對象可經由此方法將能夠處理某些選擇器的相關內部對象返回,這樣的話,在外界看來,好像是該對象親自處理了這些消息似的。

在我們看完這裏,反正我是比較蒙的。下面給大家寫個具體實現,大家也就明白了。

//消息轉發第二步 備選接收者
- (id)forwardingTargetForSelector:(SEL)aSelector{
    Developer *dev = [[Developer alloc] init:@"Huang"];
    if ([dev respondsToSelector:aSelector]) {
        return dev;
    }
    return [super forwardingTargetForSelector:aSelector];
}

示例中創建了Developer示例對象,把消息轉由Developer對象處理。既然我們創建了Developer對象,那麼我們就獲得Developer對象相關的屬性已經方法,就能模擬出 “多繼承”了。

請注意: 我們無法操作經由這一步所轉發的消息。若是想在發送給備援接收者之前修改消息內容,那就得通過完整的消息轉發機制了。

完整的消息轉發

如果轉發算法已經來到這一步的話,那麼唯一的能做的就是啓動完整的消息轉發機制。

首先創建 NSInvocation 對象,把尚未處理的那條消息有關的全部細節都封於其中。此對象包含選擇器、目標以及參數。

在觸發 NSInvocation 對象時,”消息派發系統” 將親自出馬,把消息指派給目標對象。

完整的消息轉發步驟會調用下列方法來轉發消息:

- (void)forwardInvocation:(NSInvocation *)invocation;

這個方法可以實現得很簡單:

  • 只需要改變調用目標,使消息在新目標上得以調用即可。然而這樣實現出來的方式與 “備援接收者” 方案所實現的方法等效,所以很少有人採用這麼簡單的實現方式。

  • 比較有用的實現方式爲: 在觸發消息前,先以某種方式改變消息內容,比如追加另外一個參數,或是改換選擇器等等。

實現此方法時,若發現某調用操作不應由本類處理,則需調用超類的同名方法。這樣的話,繼承體系中的每個子類都會有機會處理此調用請求,直至 NSObject。如果最後調用了 NSObject類的方法,那麼該方法還會繼而調用 “doesNotRecognizeSelector:” 以拋出異常,表明選擇器最終未得到處理。

消息轉發全流程

消息轉發

接收者在每一步驟中均有機會處理消息。步驟越往後,處理消息的代價就越大。最好是在第一步就處理完,這樣的話,運行期系統就可以將此方法緩存起來了。如果這個類的實例稍後還收到同名選擇器,那麼根本無須啓動消息轉發流程。

以完整的例子演示動態方法分析

爲了說明消息轉發機制的意義,下面示範如何解析來實現 @dynamic 屬性。假設要編寫一個類似於 “字典” 的對象,它裏面可以容納其他對象,只不過開發者要直接通過屬性來存取其中的數據。這個類的設計思路是: 由開發者來添加屬性定義,並將其聲明爲 @dynamic,而類則會自動處理相關屬性值的存放與獲取操作。

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

基礎實現部分

#improt "EOCAutoDictionary.h"
#improt <objc/runtime.h>

@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end

@implementation EOCAutoDictionary
@dynamic string, number, date, opaqueObject;

- (id)init {
    self = [super init];
    if (self){
        _backingStore = [NSMutableDictionary new];
    }
    return self;
}

@end

本例的關鍵在於 resolveInstanceMethod: 方法的實現代碼:

+ (BOOL)resolveInstanceMethod:(SEL)selector {
    NSString *selectorString = NSStringFromSelector(selector);
    if ([selectorString hasPrefix:@"set"]){
        class_addMethod(self, selector, (IMP)autoDictionarySetter,"v@:@");
    }else{
         class_addMethod(self, selector, (IMP)autoDictionaryGetter,"@@:");
    }
    return YES;
}

當開發者首次在 EOCAutoDictionary 實例上訪問某個屬性時,運行期系統還找不到對應的選擇器,因爲所需的選擇器既沒有直接實現,也沒有合成出來。現在假設要寫入 opaqueObject 屬性,那麼系統就會以 “setOpaqueObject:” 爲選擇器來調用上面這個方法。同理,在讀取該屬性時,系統也會調用上述方法,只不過傳入的選擇器是 opaqueObject。

resolveInstanMethod 方法會判斷選擇器的前綴是否爲set,以此分辨是 set 選擇器還是 get 選擇器。在這兩種情況下,都要向類中新增一個處理該選擇器所用的方法,這兩個方法分別以 autoDictionarySetter 及 autoDictionaryGetter 函數指針的形式出現。此時就用到了 class_addMethod 方法,它可以向類中動態的添加方法,用以處理給定的選擇器。第三個參數爲函數指針,指向待添加的方法。而最後一個參數則表示待添加方法的 “類型編碼”。在本例中,編碼開頭的字符標識方法的返回值類型,後續字符表示其所接受的各個參數。

getter 函數可以用下列代碼實現:

id autoDictionaryGetter(id self, SEL _cmd) {
    // Get the backing store from the object
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;

    // The key is simply the slector name
    NSString *key = NSStringFromSelector(_cmd);

    // Return the value
    return [backingStore objectForKey:key];
}

而 setter 函數則可以這麼寫:

void autoDictionarySetter(id self, SEL _cmd, id value) {
    // Get the backing store from the object
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;

    NSMutableDictionary *backingStore = typedSelf.backingStore;

    NSString *selectorString = NSStringFromSelector(_cmd);

    NSMutableString *key = [selectorString mutableCopy];

    // Remove the ':' at the end
    [key deleteCharatersInRange:NSMakeRange(key.length - 1, 1)];

    // Remove the 'set' prefix 
    [key deleteCharactersInRange:NSMakeRange(0,3)];

    //Lowercase the firset character
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowecaseString];

    [key replaceCharatersInRange:NSMakeRnage(0,1) withString:lowercaseFirstChar];

    if (value) {
        [backingStore setObject:value forKey:key];
    }else{
        [backingStore removeObjectForKey:key];
    }
}

EOCAutoDictionary 的用戶很簡單:

EOCAutoDictionary *dict = [EOCAutoDictionary new];
dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];
NSLog(@"dict.date = %@", dict.date);
//Output : dict.date = 1985-01-24 00:00:00 +000

小結

  • 若對象無法響應某個子選擇器,則進入消息轉發流程。
  • 通過運行期的動態方法解析功能,我們可以需要在某個方法時再講其加入類中。
  • 對象可以把其無法解讀的某些選擇器轉交給其他對象來處理。
  • 經過動態分析,備援接收者之後,如果還是沒有辦法處理選擇器,那麼久啓動完整的消息轉發機制。

第 13 條: 用 “方法調配技術” 調試 “黑盒方法” - 黑魔法(method swizzling)


  • 不急不燥腳踏實地

在第 11 條中解釋過: OC 對象收到消息後,究竟會調用何種方法需要在運行期才能解析出來。那麼你也許會問: 與給定的選擇器名稱相應的方法是不是也可以在運行期改變呢? 沒錯,就是這樣。若能善用此特性,則可發揮出巨大優勢,因爲我們既不需要源代碼,也不需要通過繼承子類來覆寫方法就能改變這個類本身的功能。這樣一來,新功能將在本類的所有實例中生效,而不是僅限覆寫了相關方法的那些子類實例。此方法經常稱爲 “方法調配” 也叫 “黑魔法”(method swizzling)。

類的方法列表會把選擇器的名稱映射到相關的方法實現之上,使得 “動態消息派發系統” 能夠據此找到調用的方法。這些方法均以函數指針的形式來表示,這種指針叫做 IMP, 其原型如下:

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

NSString 類可以響應 lowercaseString、uppercaseString、capitalizedString等選擇器。這張映射表中的每個選擇器都映射到了不同的IMP之上,如下圖。

image

OC 運行期系統提供的幾個方法都能夠用來操作這張表。開發者可以向其中新增選擇器,也可以改變某個選擇器所對應的方法實現,還可以交換兩個選擇器所映射到的指針。經過幾次操作之後,類的方法就會變成圖下圖這個樣子。

image

在新的映射表中,多了一個名爲 newSelector 的選擇器,lowercaseString 與 uppercaseString 的實現則互換了。上述修改均無須編寫子類,只要修改了 “方法表” 的佈局,就會反映到程序中所有的 NSString 實例之上。

想要交換方法實現,可以用下列函數:

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 swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));

method_exchangeImplementations(originalMethod, swappedMethod);

從現在開始,如果在 NSSring 實例上調用lowercaseString,那麼執行的將是 uppercaseString 的原有實現,反之亦然。

那麼在實際開發中,我們可以通過這一手段來爲既有的方法實現增添新功能。比方說, 想要在調用 lowercaseString 時記錄某些信息,這時就可以通過交換方法來達成此目標。看下面案例:

@interface NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString;
@end

將上述新方法 eoc_myLowercaseString 與 lowercaseString 方法交換。

image

新方法的實現代碼可以這樣寫:

@implementation NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString {
    NSString *lowercase = [self eoc_myLowercaseString];
    return lowercase;
}
@end

這段代碼看上去會陷入遞歸調用的死循環,不過大家要記住,此方法是準備和 lowercaseString 方法互換的。所以,在運行期,eoc_myLowercaseString 選擇器實際上對應於原有的 lowercaseString 方法實現。

示例看上述  lowercaseString 與 uppercaseString 方法實現, 把uppercaseString替換成eoc_myLowercaseString即可。

通過此方案,開發者可以爲那些 “完全不知道其具體實現的” 的黑盒方法增加日誌功能,這非常有助於程序調試。然而,此做法只在調試程序時有用。很少人在調試程序之外的場合用上述 “方法調配技術” 來永久改動某個類的功能。不能僅僅因爲 OC 語言有這個特性就一定要用它。若是濫用,反而會令代碼變得不易讀懂且難於維護。

小結

  • 在運行期,可以向類中新增或者替換選擇器所對應的方法實現。
  • 使用另一份實現來替換原有的方法實現,這道工序叫做 “方法調配”,開發者常用此技術想原有的實現中添加新功能。
  • 一般來說,只有調試程序的時候才需要在運行期修改方法實現,這種做法不宜濫用。

第 14 條: 理解 “類對象” 的用意


OC 實際上是一門極其動態的語言。

  • 第 11 條講解了運行期系統如何查找並調用某方法的實現代碼。
  • 第 12 條講解了消息轉發原理: 如果類無法立即響應某個選擇器,那麼就會啓動消息轉發流程。

然而,消息的接收者究竟爲何物?我們只知道,這個接收者的類,會逐步調用一些相關消息流程的方法。那麼這個接收者是對象本身嗎? 運行期如何知道某個對象的類型呢? 對象類型並非在編譯期就綁定好了,而是在運行期查找。而且,還有個特性的類型叫做 id,它能代指任意 OC 對象類型。一般情況下,應該指明消息接收者的類型,這樣的話,如果向其發送了無法解讀的消息,那麼編譯器就會產生警告信息。而類型爲 id 的對象則不然,編輯器假設它能響應所有消息。

如果看過 12 條,你就會明白,編譯器無法確定某類型對象到底能解讀多少種選擇器,因爲運行期還能動態新增。然而,即使使用了動態新增技術,編譯器也覺得應該能在某個頭文件找到方法原型的定義,據此可瞭解完整的 “方法簽名”,並生成派發消息所需的正確代碼。

“在運行期檢視對象類型” 這一操作也叫做 “類型信息查詢”(introspection, “內省”), 這個強大而有用的特性內置於 Foundation 框架的 NSObject 協議裏,凡是由公共根類(common root class, 即 NSObject 與 NSProxy),繼承而來的對象都需要遵從此協議。在程序中不要直接比較對象所屬的類,明智的做法是調用 “類型信息查詢方法”,其原因筆者稍後解釋。不過在介紹類型信息查詢技術之前,我們先講一些基礎知識,看看 OC 對象的本質是什麼。

每個 OC 對象實例都是指向某塊內存數據的指針。所以在聲明變量時,類型後面要跟一個 “*” 字符:

NSString *pointerVarible =  @"Some string";

描述 OC 對象所用的數據結構定義在運行期程序庫的頭文件裏,id類型本身也定義在這裏:

typedef struct objc_object {
    Class isa;
} *id;

由此可見,每個對象結構體的首個成員是Class類的變量。該變量定義了對象所屬的類,通常稱爲 “is a”指針。例如,剛纔的例子中所用的對象 “是一個”(is a)NSSring, 所以其 “is a”指針就指向 NSString。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_mehtod_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
}

此結構體存放類的 “元數據”,例如類的實例實現了幾個方法,具備多少個實例變量等信息。此結構體的首個變量也是 isa 指針,這說明 Class 本身亦爲 OC 對象。結構體裏面還有個變量叫做super_class, 它定義了本類的超類。類對象所屬的類型(也就是 isa指針所指向的類型)是另外一個類,叫做 “元類”,用來描述對象本身所具備的元數據。”類方法” 就定義於此處,因爲這些方法可以理解成類對象的實例方法。每個類僅有一個 “類對象”,而每個 “類對象” 僅有一個與之相關的 “元類”。

假設有個名爲 SomeClass 的子類從 NSObject 中繼承而來,則其繼承體系如下圖所示。

這裏寫圖片描述

super_class 指針確立了繼承關係,而 isa 指針描述了實例所屬的類。通過這張佈局關係圖即可執行 “類型信息查詢”。我們可以查出對象是否能響應某個選擇器,是否遵從某項協議,並且能看出此對象位於 “類繼承體系”(class hierarchy) 的哪一部分。

在類繼承體系中查詢類型信息

可以用類型信息查詢方法來檢視類繼承體系。”isMemberOfClass:” 能夠判讀出對象是否爲某個特定類的實例,而 “isKandOfClass:” 則能夠判讀出對象是否爲某類或其派生類的實例。

NSMutableDictionary *dict = [NSMutableDictionary new];
[dict isMemberOfClass:[NSDictionary class]]; //<NO
[dict isMemberOfClass:[NSMutableDictionary class]]; //<YES
[dict isKandOfClass:[NSDictionary class]]; //<YES
[dict isKandOfClass:[NSArray class]]; //<NO

像這樣的類型信息查詢方法使用 isa 指針獲取對象所屬的類,然後通過 super_class 指針在繼承體系中游走。由於對象是動態的,所以此特性顯得極爲重要。OC 與你可能屬性的其他語言不同,在此語言中,必須查詢類型信息,方能完全瞭解對象的真是類型。

由於 OC 使用 “動態類型系統”, 所以用於查詢對象所屬類的類型查詢功能非常有用。從 collection 中獲取對象,通常會查詢類型信息,這些對象不是 “強類型的”(strongly typed), 把它們從 colletion 中取出來,其類型通常是id。如果想知道具體類型,那就可以使用類型信息查詢方法。例如,想根據數組中存儲的對象生成以逗號分隔的字符串(comma-separated string), 並將其存至文本文件,就可以使用下列代碼:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章