Effective objective-C 讀書筆記 (第一部分)

_silhouette發表於2017-12-21

第1章 熟悉Objective-C

第1條 瞭解Objective-C語言的起源

  • Objective-C是一種“訊息結構”的語言,而非“函式呼叫”語言。
  • 關鍵區別在於:使用訊息結構的語言,其執行時所執行的程式碼由執行環境來決定;而使用函式呼叫語言,則由編譯器決定。若是函式呼叫語言,若呼叫的函式是多型的,則需要按照“虛方法表”來確定到底應該執行哪個函式實現。(即需要“執行時派發”(runtime method binding)),而“訊息結構語言”無論是否多型,總是在要執行時才會去查所執行的方法,實際上編譯器甚至不關係訊息是何種型別,接收訊息的物件問題也要在執行時處理,這個過程叫做“dynamic binding”。
  • Objective-C的重要工作都是由“執行期元件(runtime component)”完成的,而非編譯器完成的。使用Objective-C的物件導向特性的所需全部資料結構及函式都在執行期元件裡面。舉例:執行期元件含有全部記憶體管理方法。通俗來講:只要重新執行Objective-C工程即可提升應用程式效能,而工作都在“編譯期”完成的語言,如果想獲得效能的提升,必須要重新編譯。
  • Objective-C語言中的指標用來指向物件,這點完全照搬C語言。NSString *string = @"string";它宣告瞭一個指向NSString型別的指標string,這表示了該string指向的物件分配在堆上,在Objective-C中,所有物件都分配在堆上,而string本身分配在棧上。
  • 分配在堆中的記憶體必須直接管理,而分配在棧上的記憶體則會在其棧幀彈出時,自動清理。
  • CGRect rect表示的是C語言中的結構體型別,他們會使用棧空間。因為若整個Objective-C語言都使用物件,則效能會受影響。

第2條 在類的標頭檔案中儘量少引入其他標頭檔案

  • 將引入標頭檔案的時機儘量延後,只在確定有需要時才引入,這樣就可以減少類的使用者所引入的標頭檔案數量。而若是把標頭檔案一股腦的全部引入,會增加很多不必要的編譯時間。若需要在標頭檔案中宣告一個其他類的@property,則可以首先使用向前宣告@class XXX.h這樣就可以告訴編譯器,我先引入這個類,實現的細節以後再告訴你。
  • 使用向前宣告同時也可以解決了兩個類相互引用的問題。
  • 要點:
    • 除非有必要,否則不要引入標頭檔案,一般來說,應在某個類的標頭檔案中儘量使用向前宣告來提及別的類,並在實現檔案中引入那些類的標頭檔案。這樣做可以儘量降低類之間的耦合。
    • 有時無法使用向前宣告,比如要宣告某個類遵守某個協議,這樣的話儘量把“該類所遵守的協議” 這條宣告放在“class-continuation分類”中,如果不行,還可以把分類放在一個單獨的標頭檔案中再引入。

第3條 多用字面量語法,少用與之等價的語法

  1. 字面數值NSNumber
  • 普通方法:NSNumber *someNumber = [NSNumber numberWithInt:1]; 等價的字面量方法:NSNumber *someNumber = @1;能夠以NSNumber型別表示的所有型別都可以使用該語法。字面量語法也可用於下面的表示式:
int x = 5;
int y = 6;
NSNumber *num = @(x * y);
複製程式碼
  1. 字面陣列NSArray
  • 普通方法:NSArray *array = [NSArray arrayWithObjects:@"cat", @"dog", @"pig", nil]; 字面量方法:NSArray *array = @["dog", @"cat", @"pig"];該方法在語義上也是等效的,但是更為簡便。若要取出第1個元素則array[0]
  • 需要注意的是,當使用字面量方式建立陣列時,若陣列元素物件中有nil,則會丟擲異常,因為字面量語法實際上是一種語法糖,其等效於先建立一個陣列,再把所有元素新增到這個陣列中,而使用普通方法建立陣列時,若陣列某個元素為nil,則會直接在該位置完成陣列的建立,nil之後的元素都將被丟棄,並且也不會報錯。所以使用字面量語法更為安全,丟擲異常終止程式總比直接得到錯誤的結果要好。
  1. 字面字典NSDictionary
  • 使用字面量語法建立字典會使得字典更加清晰明瞭。並且與陣列一樣,字面量建立字典時,若遇到nil也會丟擲異常。
  • 字典也可以像陣列那樣用字面量語法訪問。普通方法:[data objectForKey:@"hehe"];等價於字面量方法:data[@"hehe"];
  1. 可變陣列與字典
  • 也可以使用字面量的方式修改其中的元素值:mutableArray[1] = @"gege";
  1. 侷限性
  • 使用字面量語法建立出來的各個Foundation框架中的物件都是不可變型別的,若要將其轉化為可變型別,則需要複製一份NSMutableArray *mutable = [@[@"cat", @"dog", @"pig"] mutableCopy];這樣做會多呼叫一個方法,還要再多建立一個物件,但是好處還是大於這些缺點的。
  • 限制:除了字串外,所建立出來的物件必須屬於Foundation框架才行,即NSArray的子類就不可以使用字面量語法,不過一般也不需要自定義子類。

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

  • 當使用#define預處理指令定義變數時,假設#define ANIMATION_DURATION 0.3時,你以為已經定義好了,實際上當編譯時,會將整個程式所有叫做ANIMATION_DURATION的值都替換為0.3,也就是說假設你在其他檔案也定義了一個ANIMATION_DURATION,它的值也會被改變。要想解決這個問題,則需要充分利用編譯器的特性,比如:static const NSTimeInterval kAnimationDuration = 0.3;這樣就定義了一個名為kAnimationDuration的常量。
  • 若不打算公開某個常量,則應該講它定義在.m檔案中,變數一定要同時用staticconst來定義,使用const宣告的變數如果視試圖修改它的值,編譯器就會報錯。而使用static宣告的變數,表示該變數僅僅在定義此變數的編譯單元中可見(即只在此.m檔案中可見)。假設不為變數新增static修飾符,則編譯器會自動為其建立一個external symbol外部符號此時若另一個.m檔案中也定義了同名變數,則會報錯。
  • 實際上若一個變數既宣告為static又宣告為const,name編譯器會直接像#define一樣,把所有遇到的變數都替換為常量。不過還是有一個區別:用這種方式定義的常量帶有型別資訊。
  • 當需要對外公開某個常量時,可以使用extern修飾符來修飾常值變數。例如在通知中,註冊者無需知道實際字串的具體值,只需要以常值變數來註冊自己想要接收的通知即可。此類變數常放在“全域性符號表”中,以便可以再定義該常量的編譯單元之外使用。例如
// .h
extern NSString *const LYStringConstant;

// .m
NSString *const LYStringConstant = @"VALUE";
複製程式碼
  • 使用上述方式,即可在標頭檔案中宣告,在實現檔案中定義。一旦編譯器看到extern關鍵字,就知道如何在引入此標頭檔案的程式碼中處理常量了。此類常量必須要定義,並且只能定義一次,通常都是在宣告該常量的 .m 檔案中定義該常量。編譯器在此時,會在“data segment”中為字串分配儲存空間。連結器會把此目標檔案與其他目標檔案相連結,生成最終的二進位制檔案。
  • 注意常量的名字,為了避免名稱衝突,一般字首都為與之相關的類。
  • 在實現檔案中使用static const定義“只在編譯單元內可見的常量”,並且通常名稱前加字首k

第5條 用列舉表示狀態,選項,狀態碼

  • 應該用列舉來表示狀態機的狀態,傳遞給方法的選項以及狀態碼等值,給這些值通俗易懂的名字。
  • 如果把傳遞給某個方法的選項表示為列舉型別,而多個選項又可同時使用,應該使用 NS_OPTIONS 通過按位與操作將其組合起來。
  • NS_ENUMNS_OPTIONS 巨集來定義列舉型別,並指明其底層的資料型別,這樣做可確保列舉是用開發者所選的底層資料型別實現的。
  • 在處理列舉型別的 switch 語句中,不要使用 default 分支,這樣加入新列舉之後編譯器便會提示開發者:switch語句還未處理所有的列舉。

##第2章 物件,訊息,執行期

  • 使用 Objective-C 程式設計時,物件就是“基本的構造單元” (buliding block) ,在物件間 傳遞資料 並且 執行任務 的過程就叫做 “訊息傳遞Messaging” 一定要熟悉這兩個特性的工作原理。
  • 當程式執行後,為其提供支援的程式碼叫做:Objective-C執行期環境(Objective-C runtime),它提供了一些使得物件之間能夠傳遞訊息的重要函式,並且包含建立類例項所用的全部邏輯。都是需要理解的。

第6條 理解“屬性”這一概念

  • 當直接在 類介面 中定義 例項變數 時,物件佈局在 “編譯期” 就已經固定了。只要碰到訪問該例項變數的方法,編譯器就自動將其替換為 “偏移量(offset)”,並且這個偏移量是 硬編碼 ,表示該變數距離存放物件的記憶體區域的起始地址有多遠,這樣做一開始沒問題,但是一旦要再新新增一個例項變數,就需要重新編譯了,否則把偏移量硬編碼於其中的那一些程式碼都會讀取到錯誤的值。Objective-C避免這個錯誤的做法是把 例項變數 當做一種儲存 偏移量 所用的 “特殊變數” ,交給 “類物件” 保管。偏移量會在 執行期 runtime 查詢,如果類的定義變了,那麼儲存的偏移量也就變了。這是其中的一種對於硬編碼的解決方案。還有一種解決方案就是儘量 不要直接 訪問例項變數,而是通過 存取方法 來訪問。也就是宣告屬性 @property
  • 在物件介面的定義中,可以使用 屬性 來訪問封裝在物件中的資料。編譯器會自動寫出一套存取方法,用以訪問給定型別中具有給定名稱的變數。此過程叫做 “自動合成” ,這個過程由編譯器在編譯期間執行,所以編譯器裡看不到這些 systhesized method合成方法 的原始碼。編譯器還會自動向類中新增適當型別的例項變數,並在屬性名稱前面加下劃線。
  • 可以使用 @synthesize 語法來指定例項變數的名字 @synthesize firstName = _myFirstName;
  • 如果不想讓編譯器自動合成存取方法,則可以使用 @dynamic 關鍵字來阻止編譯器自動合成存取方法。並且在編譯訪問屬性的程式碼時,編譯器也不會報錯。因為他相信這些程式碼可以在 runtime 時找到。
  • 屬性特質 屬性可以擁有的特質分為四類
    • 原子性
      • 在預設情況下,由編譯器所合成的方法會通過鎖機制保證其原子性,如果屬性具備 nonatomic 特質,則不使用同步鎖,一般情況下在iOS開發中,都將屬性宣告為 nonatomic 修飾的,因為原子性將會耗費大量資源並且也不能保證“執行緒安全”,若要實現“執行緒安全”則需要更深層的鎖機制才行。
      • atomicnonatomic 的區別在於:具備 atomicget 方法會通過鎖機制來確保操作的原子性,也就是如果兩個執行緒同時讀取同一屬性,無論何時總是能看到有效的值。而若不加鎖,當其中一個執行緒在改寫某屬性的值時,另一個執行緒也可以訪問該屬性,會導致資料錯亂。
    • 讀/寫許可權
      • readwrite 特質的屬性,若該屬性由 @synthesize 實現,則編譯器會自動生成這兩個方法。
      • readonly 特質的屬性只擁有讀方法。只有在該屬性由 @synthesize 實現時,編譯器才會為其新增獲取方法。
    • 記憶體管理語義
      • assign:只針對“純量型別”(CGFloatNSInteger 等)
      • strong :表明該屬性定義了一種 “擁有關係” ,即為這種屬性設定新值時,設定方法會__先保留新值,再釋放舊值__,然後再將新值設定上去。
      • weak:表明該屬性定義了一種 “非擁有關係” ,即為這種屬性設定新值時,設定方法會__既不保留新值,也不釋放舊值__,此特質同 assign 類似,然而__在屬性所指的物件遭到摧毀時,該屬性值也會清空(即指向nil)__。
      • copy:此特質所表達的從屬關係同 strong 類似,只是,設定方法並不保留新值,而是將其“拷貝”(copy)。當屬性型別為 NSString* 時,經常使用此特性來保證其封裝性。因為傳遞給 set 方法的新值有可能指向一個可變字串,由於可變字串是字串的子類,所以字串屬性指向他並不會報錯,而此時,一旦可變字串的值改變了,字串的值也會偷偷的跟著改變,會導致在我們不知情的情況下,NSString*屬性的值就改變了,所以應該拷貝一份可變字串的不可變值immutable的字串,確保物件中的字串不會無意間變動。
      • unsafe_unretained :此特質所表達的語義同 assgin 相同,但它適用於物件型別,該特徵表達了一種 “非擁有關係” ,當目標物件遭到摧毀時,不會自動指向nil(不安全)
    • 方法名
      • @property (nonatomic, getter=isOn) BOOL on; 通過如下方式來改變 get 方法的方法名。

第7條 在物件內部儘量直接訪問例項變數

  • 由於不經過 Objective-C 的 “方法派發” ,所以直接訪問例項變數的速度比較快。
  • 直接訪問例項變數時,不會呼叫其 setter 方法,這就繞過了為相關屬性所定義的 “記憶體管理語義” 。比方說:在ARC環境下直接訪問一個宣告為copy的屬性,將不會拷貝該屬性。而是直接丟棄舊值保留新值。
  • 如果直接訪問例項變數,則__不會觸發KVO通知__。這樣做是否產生問題還要看具體的問題。
  • 通過屬性來訪問例項變數有助於排查與之相關的錯誤,因為可以給getter/setter新增斷點,來監控其值。
  • 在物件內部讀取資料時,應該直接通過例項變數來讀取,寫資料時,應該通過屬性來寫。
  • 在初始化或dealloc方法中,總是應該直接通過例項變數來讀寫資料。
  • 當使用懶載入方法載入資料時,需要通過屬性來讀資料。

第8條 理解“物件等同性”這一概念

  • 按照 “ == ” 操作符比較出來的結果未必使我們想要的,因為它實際上是在比較__兩個例項變數所指向的物件是否為同一值__,換句話說,它實際上比較的是例項變數所指向堆記憶體中的物件地址是否為同一個。而當我們要必要兩個物件是否相同時,往往想要比較的是__兩個物件所代表的邏輯意義上的是否相等__。
  • 所以這個時候需要使用 NSObject 協議中宣告的 isEqual 方法來判斷兩個物件的等同性。NSObject 協議中有兩個用於判斷等同性的關鍵方法:- (BOOL)isEqual:(id)object; - (NSInteger)hash;NSObject 類對這兩個方法的預設實現只是簡單的比較兩個物件的地址是否相等。
  • 當自定義相等時,必須理解這兩個方法的使用條件以及意義。
    • isEqual 判定兩個物件相等時,那麼 hash 方法也必須返回同樣的值;
    • hash 方法也返回同樣的值時,isEqual 未必判定兩個物件相等;
// 一種實現hash的比較高效的方法。
- (NSInteger)hash {
  NSInteger firstNameHash = [_firstName hash];
  NSInteger lastNameHash = [_lastName hash];
  Nsinteger age = _age;
  return firstNameHash^ lastNameHash^ age;
}
複製程式碼
  • 當自己實現判斷等同性方法時,當覆寫 isEqual 方法時,有一個邏輯的判斷:如果當前受測的引數與接收該訊息的物件都屬於同一個類,則呼叫自己編寫的方法,否則交給超類來判斷。

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

  • 類簇可以隱藏抽象基類,是一種很有用的設計模式,OC框架中普遍使用此模式。比如 UIButton 類中有一個 + (UIButton)buttonWithType:(UIButtonType)type 類方法。這個方法可以讓你傳遞一個引數給它,然後它會自動根據你傳遞的引數型別自動生成對應的 Button

  • 這個設計模式的在 iOS 中的實現方法就是先定義抽象的基類。在基類的標頭檔案中定義各種 Button 型別。然後使用工廠方法返回使用者所選擇的型別的例項。然後分別實現各個例項。示例如下:


    // 首先定義UIButton型別種類
    typedef NS_ENUM(NSInteger, UIButtonType) {
        UIButtonTypeCustom = 0,                         // no button type
        UIButtonTypeSystem NS_ENUM_AVAILABLE_IOS(7_0),  // standard system button

        UIButtonTypeDetailDisclosure,
        UIButtonTypeInfoLight,
        UIButtonTypeInfoDark,
        UIButtonTypeContactAdd,
        
        UIButtonTypePlain API_AVAILABLE(tvos(11.0)) __IOS_PROHIBITED __WATCHOS_PROHIBITED, // standard system button without the blurred background view
        
        UIButtonTypeRoundedRect = UIButtonTypeSystem   // Deprecated, use UIButtonTypeSystem instead
    };

    // 再實現具體的型別方法,虛擬碼如下
    @interface UIButton : UIControl <NSCoding>  
    @property(nullable, nonatomic,readonly,strong) UILabel     *titleLabel NS_AVAILABLE_IOS(3_0);
    @property(nullable, nonatomic,readonly,strong) UIImageView *imageView  NS_AVAILABLE_IOS(3_0);

    + (UIButton)buttonWithType:(UIButtonType)type; 
    - (void)setTitle:(nullable NSString *)title forState:(UIControlState)state; 
    @end
    @implementation UIButton

    + (UIButton)buttonWithType:(UIButtonType)type {
      switch(type) {
        case 0:
          return [UIButtonCustom new];
          break;
        case 1:
          return [UIButtonSystem new];
          break;
        case 2:
          return [UIButtonDetailDisclosure new];
          break;
          ...
      }  
    }

    - (void)setTitle:(nullable NSString *)title forState:(UIControlState)state {
      // 空實現
    }

    @end
      
    // 然後再具體實現每個"子類"
    @interface UIButtonCustom : UIButton
       
    @end
    @implementation

    - (void)setTitle:(nullable NSString *)title forState:(UIControlState)state {
      // 實現各自不同的程式碼  
    }   
    @end
    複製程式碼

  • 需要注意的是,這種方法下,因為 OC 語言中沒有辦法指名一個基類是抽象的,所以基類介面一般沒有名為 init 的成員方法,這說明該基類並不應該直接被建立。而 UIButton 中實際上擁有這種方法,所以實際上 UIButton也並不完全符合策略模式。

  • 當你所建立的物件位於某個類簇中,你就需要開始當心了。因為你可能覺得自己建立了某個類,實際上建立的確實該類的子類。所以不可以使用 isMemberOfClass 這個方法來判斷你所建立的這個類是否是該類,因為它實際上可能會返回 NO 。所以明智的做法是使用 isKindOfClass 這個方法來判斷。

  • COCOA框架中的類簇:NSArrayNSMutableArray ,不可變類定義了對所有陣列都通用的方法,而可變類定義了值適用於可變陣列的方法。兩個類共同屬於同一個類簇。這意味著兩者在實現各自型別的陣列時,可以共用實現程式碼。並且還能把可變陣列複製成不可變陣列,反之亦然。

  • 我們經常需要向類簇中新增子類,而當我們無法獲取建立這些類的“工廠方法”的原始碼,我們就無法向其中新增子類型別。但是其實如果遵守以下幾種方法,還是可以向其中新增的。

    • 子類應該繼承自類簇的抽象基類
    • 子類應該定義自己的資料儲存方式
    • 子類應該覆寫超累文件中指名需要覆寫的方法

第10條 在既有類中使用關聯物件存放自定義資料

  • 有時需要在物件中存放相關資訊,這是我們通常會從物件所屬的類中繼承一個類,然後改用這個子類物件。但是並非所有情況下都可以這樣做,有時例項可能是由某種特殊機制所建立,而開發者無法令這種機制建立出自己所寫的子類例項。OC中有一項強大的特性可以解決這個問題,那就是關聯物件。

  • 可以給一個物件關聯許多的其他物件,這些物件之間可以用過 key 來進行區分。儲存物件時,可以指明 “儲存策略” ,用以維護相應的 “記憶體管理” 。儲存策略型別如下:(加入關聯物件成為了屬性,那麼它就會具備跟儲存策略相同的語義)

    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
        OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                                *   The association is not made atomically. */
        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                                *   The association is not made atomically. */
        OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                                *   The association is made atomically. */
        OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                                *   The association is made atomically. */
    };
    複製程式碼
  • 下列方法可以管理關聯物件

    // 通過給定的 key 和 value 和 objc_AssociationPolicy policy 為 object 設定 關聯物件 值
    void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                             id _Nullable value, objc_AssociationPolicy policy)
      
    // 通過給定的 key 來從 object 中讀取 關聯物件 的值
    id getAssociatedObject(id object, void *key);

    // 移除指定的 object 的全部 關聯物件
    void objc_removeAssociatedObjects(id object);
    複製程式碼
  • 我們可以把某個物件想象成是某個 NSDictionary 把關聯到該物件的值理解為字典中的條目。 於是,存取相關聯的物件的值就相當於在 NSDictionary 上呼叫 setObject: forKey:objectForKey: 。然而兩者之間有個重要的差別,就是設定關聯物件時,使用的 key指標指向的時不限制型別的指標,而 NSDictionary 當設定時,就知道該物件的型別了。所以一旦在兩個 key上呼叫 isEqual 方法,NSDictionary可以返回YES,就可以認為兩個 key 相等。而 關聯物件 卻不是這樣。 所以我們通常會把 關聯物件 的 key 值設定為 靜態全域性變數。

  • 關聯物件的用法舉例:可以使用關聯物件,給類的分類在Runtime時期動態新增屬性,因為 Category 原本是不支援屬性的。這種方法可以用在夜間模式時,給 UIView 的分類動態新增屬性。

  • 注意:只有在其他做法不可行時才會選用關聯物件,因為這種做法通常會引入難以查詢的bug

第11條 理解objc_msgSend的作用

  • 在物件上呼叫方法是 OC 中經常使用的功能, 用 OC 的術語來說就是 “傳遞訊息” 。訊息有“name” 和 “selector” 可以接受引數, 並且有返回值。

  • C 語言使用 static binding 也就是說,在編譯時就已經確定了執行時所呼叫的函式。於是會直接生成所呼叫函式的指令,而函式指令實際上是硬編碼在指令之中的。只有 C 語言的編寫者使用多型時, C 語言才會在某一個函式上使用 dynamic binding

  • 而在 OC 中, 如果向某物件傳遞訊息,就會使用 dynamic binding 機制來決定需要呼叫的方法。在底層,所有方法都是普通的 C 語言函式,然而物件收到訊息之後,究竟該呼叫那個方法完全取決於執行時期。甚至可以再程式執行時改變,這些特性使得 OC 成為一門真正的動態語言。

    • 給物件傳送訊息可以寫成 id returnValue = [someObject messageName:parameter]; 其中,翻譯成容易理解的語言就是 id returnValue = [receiver(接收者) selector(選擇子):parameter(選擇引數)]; 。編譯器看到這條訊息之後,會將其直接轉化成一條 C 語言函式呼叫,這條函式就是訊息傳遞機制中的核心函式 objc_msgSend, 其原型如下:void objc_msgSend(id self, SEL cmd, ...) 。這是一個引數可變的函式。第二個引數SEL代表選擇子,後續引數是訊息的引數(也就是選擇子的選擇引數)。編譯器會把剛剛的那條訊息轉化成如下函式:

      // 原訊息
      id returnValue = [someObject messageName:parameter];

      /*
       轉化後的訊息 -> 所謂的訊息接受者,也就是說是這個訊息是作用到誰身上的,比如[self method]; 這條訊息啊的接受者就是 self
       **/
      id returnValue = objc_msgSend(someObject, 
                    @selector(messageName:), 
                    parameter);
      複製程式碼

    • objc_msgSend 函式會依據接收者(receiver) 與 選擇子(selector)的型別來呼叫適當的方法。為了完成這個操作:

      • 該方法需要在接收者所屬的類中搜尋其“方法列表” list of methods
      • 如果能找到與選擇子名稱 messageName 相符合的方法的話,就跳至其實現程式碼。並且會將匹配結果快取在“快速對映表” fast map 中,每個類都有一個這樣的快取,如果稍後還向該類傳送與選擇子相同的訊息,那麼執行起來就會很快,直接在 fast map 中找即可。當然,這種方法還是不如“靜態繫結”快速,但是隻要將選擇子 selector 快取起來了,就不會慢很多了。實際上 message dispatch 並不是應用程式的瓶頸所在。
      • 如果找不到的話,就沿著整合體系一路向上找,等找到合適的方法再跳轉。
      • 如果最終還是找不到相符的方法,就執行message forwarding訊息轉發 操作。
    • 前面只講了部分訊息的呼叫過程,其他邊界情況則需要交給 OC 環境中的另一些函式來處理

      // 當待發訊息要返回結構體時,可以交給這個函式來處理。
      objc_msgSend_stret
      // 如果返回是浮點數,這個函式處理
      objc_msgSend_fpret
      // 要給超類傳送訊息時,這個函式處理
      objc_msgSendSuper
      複製程式碼
    • 之所以當 objc_msgSend 函式根據 selectorrecevier 來找到應該呼叫的方法的 實現程式碼 後, 會 “跳轉” 到這個方法的實現, 是因為 OC 物件的每個方法都可以看做是簡單的 C 函式。其 原型 如下:<return_type> Class_selector(id self, SEL _cmd, ...) ,其中,每個 Class 都有一張表格, 其中的指標都會指向這種函式, 而選擇子 selector 的則是查表時所用的 keyobjc_msgSend 函式正是通過這張表格來尋找應該執行的方法並跳轉至它的實現的。

    • 需要注意的是 原型 的樣子和 objc_msgSend 函式很像。這不是巧合,而是為了利用 尾呼叫優化 技術,使得跳轉至指定方法這個操作變得更加簡單些。如果某個函式的最後一項操作是呼叫另一個函式,則就可以運用 尾呼叫優化 技術。編譯器會生成調轉至另一函式所需要的指令碼,而且不會向呼叫堆疊中推入新的“棧幀”frame 。 只有當函式的最後一個操作是呼叫其他函式時,才可以這樣做。這項優化對 OC 十分的關鍵,如果不這樣做,這樣每次呼叫 OC 方法之前,都需要為呼叫 objc_msgSend 準備棧幀,我們可以在 stack trace 中看到這種frame。 此外,如果不優化,還會過早的發生“棧溢位” stack overflow 現象。

  • 訊息有接受者 receiver ,選擇子 selector 及引數 parameter 所構成, 給某物件 “傳送訊息” invork a message 也就是相當於在該物件上 呼叫方法 call a method

  • 發給某個物件的全部訊息都是要由 動態派發系統 dynamic message dispatch system 來處理的,該系統會檢視對應的方法,並執行其程式碼。

第12條 理解訊息轉發機制

  • 上一條講了物件的訊息傳遞機制,這一條將講述當物件無法解讀收到的訊息時的轉發機制。

  • 如果想令類能理解某條訊息,我們必須實現對應的方法才行。但是如果我們向一個類傳送一個我們沒有實現的方法,在編譯器時並不會報錯。因為在執行時可以繼續向類中新增方法,所以編譯器在編譯時還無法通知類中到底有沒有某個方法的實現。當物件接收到無法解讀的訊息後,就會啟動 “ 訊息轉發 message forwarding ” 機制,而我們就應該經由此過程告訴物件應該如何處理未知訊息。而當你沒有告訴物件應該如何處理未知訊息時,物件就會啟動 訊息轉發 機制。最後就會一層層的將訊息轉發給 NSObject 的預設實現。如下表示:

    // 這就是 NSObject 對訊息轉發的預設實現。
    // 訊息的接收者型別是 __NSCFNumber ,但是他並無法理解名為 lowercaseString 的選擇子,就會丟擲異常
    /*
      出現這種情況並不奇怪。因為 __NSCFNumber 實際上是 NSNumber 為了實現 “無縫橋接” 而使用的 內部類
      配置 NSNumber 物件時也會一併建立此物件。
      在本例中,訊息轉發過程以程式崩潰結束。但是實際上,我們在編寫自己的類時,可以在轉發過程中設定掛鉤,就可以當程式執行 訊息轉發 時,處理所轉發的訊息,避免程式的崩潰。
    **/
    2017-12-01 11:30:19.942493+0800 NEUer[17853:2011205] -[__NSCFNumber lowercaseString:]: unrecognized selector sent to instance 0x87
    2017-12-01 11:30:19.964307+0800 NEUer[17853:2011205] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber lowercaseString:]: unrecognized selector sent to instance 0x87'
    複製程式碼
  • 訊息轉發的過程 => 分為兩大階段

    • 第一大階段動態方法解析 : 首先,詢問 接收者 receiver 所屬的類, 能否動態新增方法來處理這個 未知選擇子 unknown selector , 這個過程叫做 動態方法解析
      • 物件當接收無法解讀的訊息時,首先呼叫+ (BOOL)resolveInstanceMethod:(SEL)selector 這個方法的引數就是 未知選擇子。返回值為 BOOL 表示能否在 Runtime 新增一個例項方法來處理這個選擇子。使用這個方法的前提是:這個 未知選擇子 的相關實現程式碼已經寫好了,只等著執行的時候在 Runtime 時期動態插入類中即可。
    • 第二大階段 完整的訊息轉發機制 full forwarding mechanism : 當 接收者 無法解析這個 未知選擇子 時, 詢問 接收者 是否擁有 備援的接收者 replacement receiver ,又分為兩小階段
      • 第一小階段:如果有,則 接收者 就把訊息轉發給它,訊息轉發結束。
        • 這一小階段的過程如下:當前 接收者 還有一次機會來處理 未知選擇子。那就是使用 -(id)forwardingTargetForSelector:(SEL)selctor; 這個方法的參數列示 未知選擇子。 如果當前接收者 能夠找到 備援物件 則可以將 備援物件 返回,如果找不到, 就返回 nil 。 通過這種方案,我們可以使用 __“組合” __ 來模擬 多重繼承 的某些特性。在一個物件的內部, 可能還有一系列其他的物件,而該 物件 可以經過這個方法使得它的內部的某個可以處理這個訊息的物件返回。在外界看來,就好像這個物件自己處理了這個未知方法一樣。
        • 需要注意的是:在這個階段 接收者 沒有權利去操作這一步所轉發的訊息,他只能全盤交給 備援的接收者 來處理這個訊息。
      • 第二小階段:如果沒有 備援的接收者, 則 啟動 完整的訊息轉發機制 。 Runtime 系統會把與訊息有關的全部細節都封裝到 NSInvocation 物件中, 再給接收者最後的一次機會,讓他設法解決當前還未處理的這個訊息。其中,這個 NSInvocation 物件包含 選擇子, 目標, 引數。 在觸發 NSInvocation 物件時, “訊息轉發系統” 將親自出嗎,把訊息轉發給目標物件(也就是目標接收者)。- (void)forwardInvocation:(NSInvocation *)invocation;
        • 當這個方法簡單的實現:例如只是改變接收者目標,那麼它的效果就會跟使用 備援的接收者 效果一樣。
        • 這個方法的比較有意義的實現方式為:在觸發訊息之前,先在 invocation 中改變訊息的內容,不如追加另外一個引數,或切換選擇子。
        • 當在實現這個方法的時候,如果發小某個呼叫不應該由本類處理,則需要呼叫超類的同名方法。這樣的話,繼承體系中每個類都有機會處理此呼叫請求,直到到 NSObject 類。 如果最後呼叫了 NSObject 類的方法,該方法會接著呼叫 doesNotRecognizeSelector 來丟擲異常。如果丟擲了這個異常,就表明在整個訊息轉發的大過程中,沒有人能處理這個訊息!就會使程式崩潰。
      • Effective objective-C 讀書筆記 (第一部分)

  • 接收者 在每個步驟均有機會處理訊息,步驟越往後,處理這個訊息的代價就越大。最好能在第一步就完成,這樣 Runtime 系統就將這個方法快取起來了。回顧 第11條 說道:"當 OC 中某個物件呼叫某個函式實際上就是給該物件傳遞訊息,這是一個使用 動態繫結 的過程。在這個過程中使用 objc_msgSend 這個函式,該函式會依據接收者(receiver) 與 選擇子(selector)的型別來呼叫適當的方法。為了完成這個操作:它需要首先在這個類的 list of method 中找相應的方法,然後如果找到了這個方法,繼而找到它的實現,然後再把這個方法放到 fast map 中。" 這樣就實現了 Runtime 時期的快取。在此之後,如果這個類再次收到了這個選擇子,那麼根本無需啟動訊息轉發機制了。

第13條 用“方法調配技術”除錯“黑盒方法”

  • 我們都知道我們可以在 Runtime 時期,動態選擇要呼叫的方法。實際上我們也可以在 Runtime 時期,動態的把給定選擇子名稱 (SEL) 的方法進行改變。這個功能使我們可以在不使用繼承就可以直接改變這個類本身的功能。這樣一來,新功能就可以在這個類中的所有例項都得到應用。這個功能就叫做 方法調配 method swizzling

  • 類的方法列表會把選擇子名稱對映到相關方法的實現上。使得“動態訊息派發系統”可以據此找到應該呼叫的方法。這種方法以函式指標的形式來表示。這種指標就叫做 IMP 原型如下 id (*IMP)(id, SEL)

  • 原始方法表的佈局

    yuanshifangfabiao.png

  • 當使用 method swizzling 改變記憶體中選擇子與方法實現的對映後,就變成了這樣

    newfangfabiao.png

此時,對於這個類的所有例項,這兩個方法的實現都改變了。

  • // 交換方法實現的方法。
    void method_exchangeImplementation(Method 1, Method 2);

    // 獲取方法的實現。
    Method class_getInstanceMethod(Class aClass, SEL aSelector);
    複製程式碼
  • 在實際應用中,這樣交換兩個方法沒什麼實際用途。method swizzling 主要的作用在於:可以在不知道原本方法的內部具體實現的情況下,為原本的方法新增新的附加功能。示例如下:

    • 新方法可以新增至一個 NSString 的一個 Category 中:

      @interface NSString (SLYMyAdditions)
      - (NSString *)sly_myLowerCaseString;
      @end
        
      @implementation NSString (SLYMyAdditions)
      - (NSString *)sly_myLowerCaseString {
       /*
        在這裡呼叫了 sly_myLowerCaseString 這個方法, 一眼看上去好像是一個遞迴的迴圈呼叫,使這個方法永遠都不會結束,但是實際上,這個方法在 Runtime 時期就已經繫結到 NSString 本身的 lowercaseString 方法上去了。所以這個分類的具體目的就是在實現原本 lowercaseString 功能的同時,列印一些額外資訊。在我們的實際開發中,這也正是 method swizzling 的主要用途。
        **/
        NSString *lowercase = [self sly_myLowerCaseString];
          NSLog(@"%@ --> %@", self, lowercase);
          return lowercase;

      @end
      複製程式碼
    • 具體的交換方法程式碼如下:(一般來說,method swizzling 應該在 load 方法中執行具體的交換)

      // 具體交換兩個方法實現的範例:
      Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
      Method swappedMethod = class_getInstanceMethod([NSString class], @selector(sly_myLowerCaseString));
      method_exchangeImplementation(originalMethod, swappedMethod);
      // 從現在起,這兩個方法的實現與其方法名就互換了。
      複製程式碼
  • 需要注意的是,這個功能雖然強大,但是不能濫用。一般來說都是在開發除錯程式時才需要在 Runtime 時期修改方法實現。

第14條:理解“類物件”的用意

  • 首先來理解 OC 物件的本質:所有 OC 物件的例項都是指向某塊記憶體資料的指標。但是對於通用的物件型別 id 由於其本身已經是指標了,所以我們可以不加 * 。

  • 描述 OC 物件所用的資料結構定義在 Runtime 的標頭檔案裡, id 的定義如下:

    • /*
        每個物件結構體的首個成員是 Class 類的變數,該變數定義了物件所屬的類,通常稱為 is a 指標。
       **/
      typedef struct objc_object {
          Class isa;
      } *id;
      複製程式碼
    • Class 類的實現如下:

      typedef struct objc_class *Class;
      struct objc_class {
        Class isa; // 每個 Class 物件中也定義了一個 is a 指標,這說明 Class 本身也是一個 OC 物件,這個 isa 指標指向的是類物件所屬的型別,是另外一個類,叫做 metaclass, 用來表述類物件所需要具備的後設資料。“類方法”就定義於此處,因為這些方法可以理解成類物件的例項方法。每個類僅有一個“類物件”,而每個“類物件”僅有一個與之相關的“元類”。
        Class super_class; // 指向 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_protpcol_list *protocols;
      }
      複製程式碼
  • 假設有個名為SomeClass的子類從NSObject中繼承而來,則其繼承體系如圖

繼承圖.png

  • 第12條則講述了訊息轉發的原理:如果類無法立即響應某個選擇子,那麼就會啟動訊息轉發流程。然而,訊息的接收者究竟是何物?是物件本身嗎?執行期系統如何知道某個物件的型別呢?物件型別並非在編譯期就繫結好了,而是要在執行期查詢。而且,還有個特殊的型別叫做id,它能指代任意的Objective-C物件型別。一般情況下,應該指明訊息接收者的具體型別,這樣的話,如果向其傳送了無法解讀的訊息,那麼編譯器就會產生警告資訊。而型別為id的物件則不然,編譯器假定它能響應所有訊息。

  • 編譯器無法確定某型別物件到底能解讀多少種選擇子,因為執行期還可向其中動態新增。然而,即便使用了動態新增技術,編譯器也覺得應該能在某個標頭檔案中找到方法原型的定義,據此可瞭解完整的“方法簽名”(method signature),並生成派發訊息所需的正確程式碼。“在執行期檢視物件型別”這一操作也叫做“型別資訊查詢”(introspection,“內省”),這個強大而有用的特性內建於Foundation框架的NSObject協議裡,凡是由公共根類(common root class,即NSObject與NSProxy)繼承而來的物件都要遵從此協議。在程式中不要直接比較物件所屬的類,明智的做法是呼叫“型別資訊查詢方法”。

  • isMemberOfClass: 能夠判斷出物件是否為某個特定類的例項,而 isKindOfClass: 則能夠判斷出物件是否為某類或其派生類的例項.

    // 例如:
    NSMutableDictionary *dict = [NSMutableDictionary new];  
    [dict isMemberOfClass:[NSDictionary class]]; ///< NO 
    [dict isMemberOfClass:[NSMutableDictionary class]]; ///< YES 
    [dict isKindOfClass:[NSDictionary class]]; ///< YES 
    [dict isKindOfClass:[NSArray class]]; ///< NO 
    // 像這樣的型別資訊查詢方法使用isa指標獲取物件所屬的類,然後通過super_class指標在繼承體系中游走。由於物件是動態的,所以此特性顯得極為重要。
    複製程式碼
  • 不可以直接使用兩個物件是否相等來比較

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

    複製程式碼

    因為訊息可能執行了訊息轉發機制,所以不可以這樣對物件的類進行比較。比方說,某個物件可能會把其收到的所有選擇子都轉發給另外一個物件。這樣的物件叫做“代理”(proxy),此種物件均以NSProxy為根類。而如果使用了 isKindOfClass: 這個方法進行比較,則可以比較,因為 isKindOfClass: 這樣的型別資訊查詢方法,那麼代理物件就會把這條訊息轉給“接受代理的物件”(proxied object)。也就是說,這條訊息的返回值與直接在接受代理的物件上面查詢其型別所得的結果相同。也就可以得到正確的結果。

相關文章