編寫高質量iOS與OS X代碼的52個有效方法 - 學習筆記 一、2



第一章   熟悉 Objective-C

一、瞭解 Objective-C 語言的起源

  • OC是「消息結構」(message structure)而非「函數調用」(function caling)。
  • OC在運行時纔會檢查對象類型,接收一條消息以後,執行的代碼由運行期環境來決定。
  • 掌握C語言的內存模型與指針很重要。對象所佔內存老是分配在「堆空間」中。


二、在類的頭文件中儘可能少引入其餘頭文件

將引入頭文件的時機儘可能延後。
  • 向前聲明:@class SomeClass;  
  • 除非確有必要,不然不要引入頭文件。通常來講,應在某個類的頭文件中使用向前聲明來說起別的類,並在實現文件中引入那些類的頭文件。這樣作能夠下降耦合。
  • 有時沒法使用向前聲明,如要聲明某個類遵循某協議。此時儘可能把「該類遵循某協議」的這條聲明移至分類中。若是不行,就把協議單獨放在一個頭文件中再引入。


 三、多用字面量語法,少用與之等價的方法

  • 應用字面量語法建立字符串、數值、數組、字典。與建立此類對象的常規方法相比,更簡明扼要。
  • 應該經過取下標操做來訪問數組下標或字典中的鍵所對應的元素。
  • 用字面量語法建立數組或字典時,務必確保值不含nil。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSNumber *someNumber1 = [NSNumber numberWithInt:1];
    NSNumber *someNumber2 = @1;
    
    NSArray *animals1 = [NSArray arrayWithObjects:@"cat", @"dog", @"mouse", nil];
    NSArray *animals2 = @[@"cat", @"dog", @"mouse"];
    /* 用字面兩語法建立數組時,若數組元素對象中有nil,則會拋出異常。 「arrayWithObjects:」方法會一次處理各個參數,直到發現nil爲止。 使用字面量語法更安全。拋出異常比建立數組以後發現元素個數少了要好。 */
    
    NSDictionary *dictA = [NSDictionary dictionaryWithObjectsAndKeys:
                           @"value1", @"key1",
                           @"value2", @"key2",
                           @"value3", @"key3", nil];
    
    NSDictionary *dictB = @{@"key1" : @"value1",
                            @"key2" : @"value2",
                            @"key3" : @"value3"};
    /* 字典中的 Key 跟 Value 都必須是OC對象 字典跟數組同樣,存在nil的問題 */
    
}
複製代碼


四、多用類型常量,少用 #define 預處理指令

總之,勿使用預處理指令定義常量,應藉助編譯器來確保常量正確
  • 不要用預處理指令定義常量。這樣定義出來的常量不含類型信息,編譯器只是會在編譯前據此執行查找與替換操做。
  • 在實現文件(.m文件)中使用 static const 來定義「只在編譯單元內可見的常量」。此類常量不在全局符號表中,因此無須爲其名稱加前綴。
  • 在頭文件中使用 extern 來聲明全局常量,並在相關實現文件中定義其值。這種常量要出如今全局符號表中,因此其名稱應加以區隔,一般用與之相關的類名作前綴。 

#define ANIMATION_DURATION 0.3

static const NSTimeInterval kAnimationDuration = 0.3;

//In the .h
extern NSString *const EOCStringConstant;
//In the .m
NSString *const EOCStringConstant = @"VALUE";
複製代碼


五、用枚舉表示狀態、選項、狀態碼

  • 應用枚舉來表示狀態、傳遞給方法的選項以及狀態碼等值。
  • 若是傳遞給某個方法的選項爲枚舉類型,且此選項可多個同時使用。那麼將個選項值定覺得2的冪,以便經過按位或操做將其組合起來。
  • 在處理枚舉類型的 switch 語句中不要實現 default 分支


第二章   對象、消息、運行時

在對象之間傳遞數據並執行任務的過程叫作「消息傳遞」
當應用程序運行起來之後,爲其提供相關支持的代碼叫作「Objective-C 運行時環境」

六、理解「屬性」這一律念

「屬性」(property)是OC的一項特性,用於封裝對象中的數據。OC對象一般會把其所須要的數據保存爲各類實例變量。實例變量通常經過「存取方法」來訪問。其中 getter方法 用於讀取變量值,而 setter方法 用於寫入變量值。
php

6.一、@property

property = ivar(實例變量) + getter + setter (存取方法)

在正規的OC編碼風格中,存取方法有着嚴格的命名規範。所以,OC這門語言才能根據名稱自動建立出存取方法。數組

在對象接口的定義中,可使用屬性,這是一種標準的寫法,可以訪問封裝在對象裏的數據。所以,也能夠把屬性理解爲:編譯器會自動寫出一套存取方法,用以訪問給定類型中具備給定名稱的變量。以下的這個類:緩存

@interface Person : NSObject

@property NSString *firstName;
@property NSString *lastName;

@end複製代碼

上述代碼寫出來的類與下面這種寫法等效:安全

@interface Person : NSObject

- (NSString *)firstName;
- (void)setFirstName:(NSString *)firstName;

- (NSString *)lastName;
- (void)setLastName:(NSString *)lastName;

@end複製代碼

要訪問屬性,可使用「點語法「。編譯器會把」點語法「轉換爲對存取方法的調用


6.二、autosynthesis(自動合成)

若是使用了屬性,那麼編譯器會自動編寫訪問這些屬性所需的方法,此過程叫作「自動合成」。除了生成方法代碼以外,編譯器還要自動向類中添加適當類型的實例變量,而且在屬性名前面加下劃線,以此做爲實例變量的名字。也能夠在類的實現代碼裏經過 @synthesize 語法來指定實例變量的名字:bash

@implementation Person

@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;

@end複製代碼

若不想令編譯器自動合成存取方法,則能夠本身實現。但你不能同時本身實現 getter 和 setter。還有一種辦法能阻止編譯器自動合成存取方法,就是使用 @dynamic 關鍵字,它會告訴編譯器:不要自動建立實現屬性所用的實例變量,也不要爲其建立存取方法。框架

//In the .h
@interface ViewController : UIViewController

@property NSString *firstName;
@property NSString *lastName;

@end

//In the .m
@implementation ViewController

@dynamic firstName, lastName;

@end

/* 假如一個屬性被聲明爲 @dynamic var,而後你沒有提供 @setter方法和 @getter 方法,編譯的時候 沒問題,但程序運行到instance.var = someVar, 或者 somevar = var時, 因爲缺乏 setter 和 getter,程序會崩潰。 編譯時沒問題,運行時才執行相應的方法,這就是所謂的動態綁定。 */複製代碼


6.三、屬性特質

使用屬性時需注意:各類特質設定也會影響編譯器所生成的存取方法。

屬性能夠擁有的特質分爲四類:函數

  • 原子性
  • 讀 / 寫權限
  • 內存管理語義
  • 方法名

6.3.一、原子性ui

在默認狀況下,由編譯器所合成的方法會經過鎖定機制確保其原子性(atomicity)。若是屬性具有 nonatomic 特質,則不使用同步鎖。編碼

6.3.二、讀 / 寫權限atom

具有 readwrite(讀寫)特質的屬性擁有 getter 和 setter。

具有 readonly(只讀)特質的屬性僅有獲取方法。

6.3.三、內存管理語義

屬性用於封裝數據,而數據則要有「具體的全部權語義」。  

下面這一組特質僅會影響 setter方法:

  • assign。setter方法只會執行鍼對「純量類型」(如 CGFloat 或 NSInteger等)的簡單賦值操做。
  • strong。此特質代表該屬性定義了一種「擁有關係」,爲這種屬性設置新值時,設置方法會先保留新值,並釋放舊值,而後再將新值設置上去。
  • weak。此特質代表該屬性定義了一種「非擁有關係」,爲這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同 assign 相似,然而在屬性所指的對象遭到摧毀時,屬性值會置空(nil out)。
  • unsafe_unretained。此特質的語義和 assign 相同,可是它適用於「對象類型」,該特質表達一種「非擁有關係」,當目標對象遭到摧毀時,屬性值不會自動清空,這一點與weak有區別。
  • copy。此特質所表達的所屬關係與 strong 相似,然而設置方法並不保留新值,而是將其 copy。當屬性類型爲 NSString* 時,用此特質來保護其封裝性。

6.3.五、方法名                                      

一、getter=<name> 指定「獲取方法」的方法名。 

@property (nonatomic, getter=isOn) BOOL on;複製代碼

二、setter=<name> 指定「設置方法」的方法名,這種用法不太常見


七、在對象內部儘可能直接訪問實例變量

直接訪問實例變量 跟 經過屬性來訪問 的區別:

  • 因爲不通過OC的「方法派發」,因此直接訪問實例變量的速度更快。
  • 直接訪問實例變量時,不會調用其「設置方法」,這就繞過了爲相關屬性所定義的「內存管理語義」。
  • 若是直接訪問實例變量,那麼不會觸發 KVO
  • 經過屬性來訪問有助於排查與之相關的錯誤。

要點:

  • 在對象內部讀取數據時,應該直接經過實例變量來讀,而寫入數據時,則應經過屬性來寫。
  • 在初始戶方法及 dealloc 方法中,老是應該直接經過實例變量來讀寫數據。
  • 有時會使用懶加載技術配置某份數據,此時須要經過屬性來讀取數據。


八、理解「對象等同性」這一律念

使用 NSObject 協議中聲明的 「isEqual:」 方法來判斷兩個對象的等同性


九、以「類族模式「隱藏實現細節

「類族」能夠隱藏「抽象基類」背後的實現細節。OC的系統框架中廣泛使用此模式,如 UIKit 中的UIButton類,想建立按鈕,只需調用下面這個「類方法」。使用者無需關心建立的按鈕屬於哪一個類,也不用考慮按鈕的實現細節。

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


十、在既有類中使用關聯對象存放自定義數據

有時候類的實例多是由某種機制所建立的,而開發者沒法令這種機制建立出本身所寫的自類實例。OC經過 關聯對象 這一特性解決此問題。

能夠給某對象關聯許多其餘對象,這些對象經過「鍵」來區分。存儲對象值的時候,能夠指明「存儲策略」,用以維護相應的「內存管理語義」。

10.一、存儲策略由名爲 objc_AssociationPolicy 的枚舉所定義


10.二、管理關聯對象的方法

//此方法以給定的鍵和策略爲某對象設置關聯對象值
objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
    
//此方法根據給定的鍵從某對象中獲取相應的關聯對象值
objc_getAssociatedObject(id object, void *key)
    
//此方法移除指定對象的所有關聯對象
objc_removeAssociatedObjects(id object)複製代碼

關聯對象保存在一個全局的AssociationsManager裏邊,原理在此


十一、理解 objc_msgSend 的做用

在對象上調用方法是OC常用的功能,用哪一個OC的術語來講,這叫作「傳遞消息」。消息有 「名稱」 和 「選擇子」,能夠接受參數,並且可能還有返回值。
C語言的函數調用方式: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 函數會根據接收者與選擇子的類型來調用適當的方法。爲了完成此操做,該方法須要在接收者所屬的類中搜尋其「方法列表」,若是找到了與選擇子名稱相符的方法,就跳轉至其實現代碼。
  • 若找不到,就沿着繼承體系繼續向上查找,等找到合適的方法以後再跳轉。
  • 若是最終仍是找不到相符的方法,那就執行」消息轉發「操做。
  • objc_message 會將匹配結果緩存在「快速映射表」裏面,每一個類都有這樣一塊緩存。


十二、理解消息轉發機制

對象在收到沒法解讀的消息以後會發生什麼狀況?-- 會啓動「消息轉發」機制

消息轉發過程,分爲兩大階段:

第一階段:先徵詢接收者所屬的類,看其是否能動態添加方法,以處理當前這個「未知的選擇子」,這叫作「動態方法解析」。

若是第一階段執行完,那麼接收者本身就沒法再以動態新增方法的手段來響應包含該選擇子的消息。此時來到第二階段,這裏涉及「完整的消息轉發機制」。

第二階段:會先讓接收者看看有沒有其餘對象能處理這條消息,如有,則運行時系統會把消息轉發給那個對象,因而消息轉發過程結束,一切正常。

第三階段:若沒有「被援的接收者」,則啓動完整的消息轉發機制,運行時系統會把與消息有關的所有細節都封裝到 NSInvocation 對象中,再給接收者最後一次機會。

12.一、動態方法解析

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

+ (BOOL)resolveInstanceMethod:(SEL)selector

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

在繼續往下執行轉發機制以前,奔雷有機會新增一個處理此選擇子的方法。

假設還沒有實現的方法不是實例方法而是類方法,那麼運行時系統會調用另一個方法,該方法是:resovleClassMethod:

調用 class_addMethod: 來新增一個實例方法

12.二、備援接收者

當前接收者還有第二次機會能處理未知的選擇子。在此,運行時系統會問它:能不能把這條消息轉給其餘接收者來處理。會調用下列方法:

- (id)forwardingTargetForSelector:(SEL)selector

該方法的參數就是那個未知的選擇子,若當前接收者能找到被援對象,則將其返回。若找不到,就返回 nil。

咱們沒法操做經由這一步所轉發的消息。若想在發送給被援接收者以前先修改消息內容,那就的經過完整的消息轉發機制來作了。

12.三、完整的消息轉發

首先建立 NSInvocation 對象,把與還沒有處理的那條消息有關的所有細節都封於其中。在觸發 NSInvocation 對象時,「消息派發系統」將親自出馬,把消息指派給目標對象。會調用下列方法:

- (void)forwardInvocation:(NSInvocation *)invocation

實現此方法時,若發現某調用操做不該由本類處理,則需調用超類的同名方法。這樣的話,繼承體系中的每一個類都有機會處理此調用請求,直至 NSObject。

若是最後調用了 NSObject 類的方法,那麼該方法會調用 「doesNotRecognizeSelector:」  以拋出異常,此異常代表選擇子最終未能獲得處理。

消息轉發全流程:



1三、用 「方法調配技術」 調試 「黑盒方法」

給定的選擇子名稱相對應的方法能夠在運行期改變,此方案成爲「方法調配」。

類的方法列表會把選擇的名稱映射到相關的方法實現之上,使得「動態消息派發系統」可以據此找到應該調用的方法。這些方法均爲函數指針的形式來表示,這種指針叫作IMP,原型:

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

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

void method_exchangeImplementaions(Method m1, Method m2)

此函數的兩個參數表示待交換的兩個方法實現,而方法實現則可經過下列函數得到:

Method class_getInstanceMethod(Class aClass, SEL aSelector)

舉個例子:


假設在調用 lowercaseString 時記錄某些信息,這時就能夠經過交換方法實現來達成此目標。咱們新編寫一個方法,在此方法中實現所需的附加功能,並調用原有實現。

新增的方法添加至NSString的一個分類中:

//In the .h
@interface NSString (Category)

- (NSString *)eoc_myLowercaseString;

@end

//In the .m
@implementcation NSString (Category)

- (NSString *)eoc_myLowercaseString {
    NSString *lowercase = [self eoc_myLowercaseString];
    NSLog(@"%@ => %@", self, lowercase);
    return lowercase;
}

/* 這段代碼看上去好像會陷入遞歸調用的死循環,不過你們要記住, 此方法是準備和 lowercaseString 方法互換的,因此不會有問題 */

@end複製代碼

最後經過下列代碼交換這兩個方法實現:

Method oMethod = class_getClassMethod([NSString class], @selector(lowercaseString));
    
Method sMethod = class_getClassMethod([NSString class], @selector(eoc_myLowercaseString));
    
method_exchangeImplementations(oMethod, sMethod);複製代碼

經過此方案,開發者能夠爲那些黑盒方法增長日誌記錄功能。


1五、理解 「類對象」 的用意

  • 對象類型並不是在編譯期就綁定的,是在運行期去查找。
  • 有一個特殊的類型叫作 id,它能指代任意的 Objective-C 對象類型。
  • 每一個OC對象實例都是指向某塊內存數據的指針。

15.一、id 類型定義,它是 objc_object 結構體

typedef struct objc_object {
    Class isa;
} *id;複製代碼

  • 每一個對象結構體的首個成員是 Class 類的變量。該變量定義了對象所屬的類,一般成爲「isa」指針。
  • 舉個例子:NSString *pointerVariable = @"Some string"; 這個例子中,所用的對象「是一個」 (isa)NSString,因此其 「isa」 指針就指向 NSString

15.二、Class 類型定義,它是 objc_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 obje_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
};複製代碼

  • 此結構體存放類的「元數據」(metadata),例如類的實例實現了幾個方法,具有多少個實例變量。
  • 此結構體的首個變量也是 isa指針,這說明 Class 自己亦爲 OC 對象。
  • 類對象所屬的類型(就是 isa指針 所指向的類型),叫作「元類」(metaclass),用來表述類對象自己所具有的元數據。「類方法」就定義於此處,這些方法能夠理解爲對象的實例方法。每一個類僅有一個「類對象」,僅有一個與之相關的「元類」。
  • 結構體的 super_class 定義了本類的超類,它確立了繼承關係。而 isa指針 描述了實例所屬的類。


15.三、再累繼承體系中查詢類型信息

  • 「isMemberOfClass:」 可以判斷出對象是否爲某個特定類的實例。
  • "isKindOfClass:" 可以判斷出對象是否爲某類或其派生類的實例。
相關文章
相關標籤/搜索