日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Effective objective-C 读书笔记 (第一部分)

發布時間:2025/3/15 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Effective objective-C 读书笔记 (第一部分) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

第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條 多用字面量語法,少用與之等價的語法

  • 字面數值NSNumber
    • 普通方法:NSNumber *someNumber = [NSNumber numberWithInt:1]; 等價的字面量方法:NSNumber *someNumber = @1;能夠以NSNumber類型表示的所有類型都可以使用該語法。字面量語法也可用于下面的表達式:
    int?x?=?5;
    int?y?=?6;
    NSNumber?*num?=?@(x?*?y);
    復制代碼
  • 字面數組NSArray
    • 普通方法:NSArray *array = [NSArray arrayWithObjects:@"cat", @"dog", @"pig", nil]; 字面量方法:NSArray *array = @["dog", @"cat", @"pig"];該方法在語義上也是等效的,但是更為簡便。若要取出第1個元素則array[0]
    • 需要注意的是,當使用字面量方式創建數組時,若數組元素對象中有nil,則會拋出異常,因為字面量語法實際上是一種語法糖,其等效于先創建一個數組,再把所有元素添加到這個數組中,而使用普通方法創建數組時,若數組某個元素為nil,則會直接在該位置完成數組的創建,nil之后的元素都將被丟棄,并且也不會報錯。所以使用字面量語法更為安全,拋出異常終止程序總比直接得到錯誤的結果要好。
  • 字面字典NSDictionary
    • 使用字面量語法創建字典會使得字典更加清晰明了。并且與數組一樣,字面量創建字典時,若遇到nil也會拋出異常。
    • 字典也可以像數組那樣用字面量語法訪問。普通方法:[data objectForKey:@"hehe"];等價于字面量方法:data[@"hehe"];
  • 可變數組與字典
    • 也可以使用字面量的方式修改其中的元素值:mutableArray[1] = @"gege";
  • 局限性
    • 使用字面量語法創建出來的各個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文件中,變量一定要同時用static和const來定義,使用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_ENUM 與 NS_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 修飾的,因為原子性將會耗費大量資源并且也不能保證“線程安全”,若要實現“線程安全”則需要更深層的鎖機制才行。
        • atomic與 nonatomic 的區別在于:具備 atomic 的 get 方法會通過鎖機制來確保操作的原子性,也就是如果兩個線程同時讀取同一屬性,無論何時總是能看到有效的值。而若不加鎖,當其中一個線程在改寫某屬性的值時,另一個線程也可以訪問該屬性,會導致數據錯亂。
      • 讀/寫權限
        • readwrite 特質的屬性,若該屬性由 @synthesize 實現,則編譯器會自動生成這兩個方法。
        • readonly 特質的屬性只擁有讀方法。只有在該屬性由 @synthesize 實現時,編譯器才會為其添加獲取方法。
      • 內存管理語義
        • assign:只針對“純量類型”(CGFloat 或 NSInteger 等)
        • 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框架中的類簇:NSArray和 NSMutableArray ,不可變類定義了對所有數組都通用的方法,而可變類定義了值適用于可變數組的方法。兩個類共同屬于同一個類簇。這意味著兩者在實現各自類型的數組時,可以共用實現代碼。并且還能把可變數組復制成不可變數組,反之亦然。

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

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

    第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 函數根據 selector 和 recevier 來找到應該調用的方法的 實現代碼 后, 會 “跳轉” 到這個方法的實現, 是因為 OC 對象的每個方法都可以看做是簡單的 C 函數。其 原型 如下:<return_type> Class_selector(id self, SEL _cmd, ...) ,其中,每個 Class 都有一張表格, 其中的指針都會指向這種函數, 而選擇子 selector 的則是查表時所用的 key 。 objc_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 來拋出異常。如果拋出了這個異常,就表明在整個消息轉發的大過程中,沒有人能處理這個消息!就會使程序崩潰。

      ?

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

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

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

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

    • 原始方法表的布局

    • 當使用 method swizzling 改變內存中選擇子與方法實現的映射后,就變成了這樣

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

    • //?交換方法實現的方法。
      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中繼承而來,則其繼承體系如圖

    • 第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)。也就是說,這條消息的返回值與直接在接受代理的對象上面查詢其類型所得的結果相同。也就可以得到正確的結果。

    總結

    以上是生活随笔為你收集整理的Effective objective-C 读书笔记 (第一部分)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。