(iOS)Effective Objective-C 2.0 讀書筆記 (一)

第一章 熟悉Objective-C

1.OC的起源

oc使用了消息結(jié)構(gòu)而非函數(shù)調(diào)用。使用消息結(jié)構(gòu)的語言,其運行時所執(zhí)行的代碼由運行環(huán)境決定,而使用函數(shù)調(diào)用的語言,則由編譯器決定。

OC對象所占內(nèi)存總是分配在“堆空間”,而不會分配在“棧”上。分配在堆中的內(nèi)存必須直接管理,而分配棧上的用于保存變量的內(nèi)存 則會在其棧幀彈出時自動清理。

當(dāng)我們看到一個變量類型是已知的,就分配在棧里面,比如,int、double等。其他未知的類型,比如自定義的類型,因為系統(tǒng)不知道需要多大,所以程序自己申請,這樣就分配在堆里面百度文庫

466833EB-AF2E-4735-9606-7956183155CE.png

2.在類的頭文件中盡量少引入其他頭文件

  • 除非有必要,否則不要引入頭文件。一般來說,應(yīng)在某個類的頭文件里使用向前聲明來提及別的類,并在實現(xiàn)文件中引入哪些類的頭文件。這樣做可以盡量降低類之間的耦合。
  • 有時無法使用向前聲明,比如要聲明某個類遵循一項協(xié)議。這種情況下,盡量把“該類遵循某協(xié)議”的這條聲明移至“class-continuation”分類中。如果不行的話,就把協(xié)議單獨放在一個頭文件中,然后將其引入。

3.多用字面量語法,少用與之等價的方法

  • 應(yīng)該使用字面量語法來創(chuàng)建字符串、數(shù)值、數(shù)組、字典。與創(chuàng)建此類對象的常規(guī)方法相比,這么做更加簡明扼要。(即使用NSStringstr = @"string"*這種方式來創(chuàng)建)
  • 應(yīng)該通過取下標操作來訪問數(shù)組下標或字典中的鍵值所對應(yīng)的元素。(即使用array[1]這種方式)
  • 用字面量語法創(chuàng)建數(shù)組或字典時,若值中有nil,則會拋出異常。因此,務(wù)必確保值里不含nil。

4.多用類型常量,少用#define預(yù)處理指令

應(yīng)該使用

static constant NSTimeInterval kAnimationDuration = 0.3;

而不要使用
#define Animation_duration 0.3
如果想要在該常量的編譯單元之外使用,那么使用這種形式

//In the header file
 extern NSString *constant EOCStringConstant;//這里用UIKIT_EXTERN會比較好
//In the implementation file
NSString *const EOCStringConstant = @"Value";`

常量定義應(yīng)從右至左解讀,EOCStringConstant就是一個常量,而這個常量是指針,指向NSString對象。(大概意思應(yīng)該是,const修飾的是常量,那么EOCStringConstant是一個常量,而*是一個指針,最后,它的類型是NSString。)

  • 不要用預(yù)處理指令定義常量。這樣定義出來的常量不含類型信息,編譯器只是會在編譯前據(jù)此執(zhí)行查找與替換操作。即時有人重新定義了常量值,編譯器也不會產(chǎn)生警告信息,這將導(dǎo)致應(yīng)用程序中的常量值不一致。
  • 在實現(xiàn)文件中使用static const來定義“只有在編譯單元內(nèi)可見的常量”。由于此類常量不在全局符號表中,所以無須為其名稱加前綴。
  • 在頭文件中使用extern來聲明全局常量,并在相關(guān)實現(xiàn)文件中定義其值。這種常量要出現(xiàn)在全局符號表中,所以其名稱應(yīng)加以區(qū)隔,通常用與之相關(guān)的類名做前綴。

5.用枚舉表示狀態(tài)、選項、狀態(tài)碼

  • 應(yīng)該用枚舉來表示狀態(tài)機的狀態(tài),傳遞給方法的選項以及狀態(tài)碼等值,給這些值起個易懂的名字。
  • 如果把傳遞給某個方法的選項表示為枚舉類型,而多個選項又可同時使用,那么就將各個選項值為2的冪,以便通過按位或操作將其組合起來。
  • 用NS_ENUM與NS_OPTIONS宏來定義枚舉類型,并指明其底層數(shù)據(jù)類型。這樣做可以確保枚舉是用開發(fā)者所選的底層數(shù)據(jù)類型實現(xiàn)出來的,而不會采用編譯器所選的類型。
  • 在處理枚舉類型的switch語句中不要實現(xiàn)default分支。這樣的話,加入新枚舉之后,編譯器就會產(chǎn)生還有未處理的枚舉。(xcode7.3實踐,反正不加default會有警告。。。這可能是版本問題?)

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

6.理解“屬性“這一概念

  • 可以使用@property語法來定義對象中所封裝的數(shù)據(jù)
  • 通過“特性”來指定存儲數(shù)據(jù)所需的正確語義(即copy,atomic等)
  • 在設(shè)置屬性所對應(yīng)的實例變量時,一定要遵從該屬性所聲明的語義(比如,NSString使用copy修飾的時候,那么在set方法內(nèi)部應(yīng)該使用[string copy])
  • 開發(fā)iOS程序時應(yīng)該使用nonatomic屬性,因為atomic屬性會嚴重影響性能

7.在對象內(nèi)部盡量直接范文實例變量

  • 在對象內(nèi)部讀取數(shù)據(jù)時,應(yīng)該直接通過實例變量來讀,而寫入數(shù)據(jù)時,則應(yīng)通過屬性來寫。(直接訪問實例變量比較快,但是使用屬性會遵循屬性的內(nèi)存管理語義,會觸發(fā)鍵值觀察,出錯的時候通過斷點比較好排查)
  • 在初始化方法及dealloc方法中,總是應(yīng)該直接通過實例變量來讀寫數(shù)據(jù)。
  • 有時會使用懶加載的初始化方法來配置某份數(shù)據(jù),這種情況下,需要通過屬性來讀取數(shù)據(jù)

8.理解“對象等同性”這一概念

  • 若想檢測對象的等同性,請?zhí)峁癷sEqual”與hash方法
  • 相同的對象必須具有相同的哈希碼,但是兩個哈希碼相同的對象卻未必相同
  • 不要盲目地逐個檢測每條屬性,而是應(yīng)該依照具體需求來指定檢測方案
  • 編寫hash方法時,應(yīng)該使用計算速度快而且哈希碼碰撞幾率低的算法

9. 以“類簇模式”隱藏實現(xiàn)細節(jié)

  • 子類應(yīng)該繼承自類簇中的抽象基類
    若要編寫NSArray類簇的子類,則需令其繼承自不可變數(shù)組的基類或可變數(shù)組的基類。
  • 子類應(yīng)該定義自己的數(shù)據(jù)存儲方式。
    開發(fā)編寫NSArray子類時,經(jīng)常在這個問題上受阻。子類必須用一個實例變量來存放數(shù)組中的對象。這似乎與大家預(yù)想的不同,我們以為NSArray自己肯定會保存那些對象,所以在子類中就無須再存一份了。但是大家要記住,NSArray本身只不過是包在其他隱藏對象外面的殼,它僅僅定義了所有數(shù)組都需具備的一些接口。對于這個自定義的數(shù)組子類來說,可以用NSArray來保存其實例
  • 子類應(yīng)該覆寫超類文檔中指明需要覆寫的方法。
    在每個抽象基類中,都有一些子類必須覆寫的方法。比如說,想要編寫NSArray的子類,就需要實現(xiàn)count及其“objectAtIndex:”方法。像lastObject這種方法則無須實現(xiàn),因為基類可以根據(jù)前兩個方法實現(xiàn)出這個方法。
    在類簇中實現(xiàn)子類所需遵循的規(guī)范一般都會定義于基類的文檔之中,編碼前應(yīng)該先看看。
  • 類簇模式可以把實現(xiàn)細節(jié)隱藏在一套簡單的公共接口后面(有點像協(xié)議)
  • 系統(tǒng)框架中經(jīng)常使用類簇
  • 從類簇的公共抽象基類中繼承子類時要小心,若有開發(fā)文檔,則應(yīng)先閱讀

10. 在既有類中使用關(guān)聯(lián)對象存放自定義數(shù)據(jù)

  • 可以通過“關(guān)聯(lián)對象”機制來把兩個對象連起來(就是runtime里面的objc_getAssociateObject與objc_getAssociatedObject關(guān)聯(lián)的參考)
  • 定義關(guān)聯(lián)對象時可指定內(nèi)存管理語義,用以模仿定義屬性時所采用的“擁有關(guān)系”與“非擁有關(guān)系”。
  • 只有在其他做法不可行時才應(yīng)選用非關(guān)聯(lián)對象,因為這種做法通常會引入難以查找的bug。

11. 理解objc_msgSend的作用

C語言的函數(shù)調(diào)用方式:C語言使用“靜態(tài)綁定”,也就是說,在編譯期就能決定運行時所應(yīng)調(diào)用的函數(shù)。編譯器在編譯代碼的時候就已經(jīng)知道程序中有哪些函數(shù)了。于是會直接生成調(diào)用這些函數(shù)的命令。
??動態(tài)綁定:在OC中,如果向某對象傳遞消息,那就會使用動態(tài)綁定機制來決定需要調(diào)用的方法。在底層,所有的方法都是普通的C語言函數(shù),然而對象收到消息之后,究竟該調(diào)用哪個方法則取決于運行期決定,甚至可以在程序運行時改變,這些特性使得OC成為一門真正的動態(tài)語言,類似如下

void printHello(){
    printf("Hello, world!");
}
void printGoodbye(){
    print("goodbye,world!\n");
}
void doTheThing(int type){
void (*fnc)();
if(type == 0){
    fnc = printHello;
}else{
    fnc = printGoodby;
}
}

一般的方法調(diào)用:

 id returnValue = [someObject messageName:parameter] 

本例中,somObject叫做“接收者”(receiver),messageName叫做“選取器”(selector)。選取器與參數(shù)合起來稱為“消息”(message)。編譯器看到此消息后,將其轉(zhuǎn)換為一條標準的C語言函數(shù)調(diào)用,所調(diào)用的函數(shù)乃是消息傳遞機制中的核心函數(shù),叫做objc_msgSend,“原型”如下
void objc_msgSend(id self, SEL cmd,...)
??這是個“參數(shù)個數(shù)可變的函數(shù),能接受兩個或兩個以上的參數(shù)。第一個參數(shù)代表接收者,第二個代碼選取器,后續(xù)參數(shù)就是消息中的那些參數(shù),其順序不變。選取器指的就是方法名。“選取器”與“方法”這兩個詞經(jīng)常交替使用。編譯器會把剛才那個例子中的消息轉(zhuǎn)換為如下函數(shù):

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

objc_msgSend函數(shù)會一句接收者與選取器的類型來調(diào)用適當(dāng)?shù)姆椒ā榱送瓿纱瞬僮鳎摲椒ㄐ枰诮邮照咚鶎俚念愔兴褜て洹胺椒斜怼保绻苷业脚c選取器名稱相符的方法,就調(diào)至其實現(xiàn)代碼。沒有就沿著繼承體系向上查找。如果最終找不到,就執(zhí)行“消息轉(zhuǎn)發(fā)”操作。
還有另外一些邊界情況,將交由oc運行環(huán)境中的另一些函數(shù)來處理:

  • objc_msgSend_stret:如果待發(fā)送消息要返回結(jié)構(gòu)體,那么可交由此函數(shù)處理。

  • objc_msgSend_fpret:如果消息返回的是浮點數(shù),那么可交由此函數(shù)處理。

  • objc_msgSendSuper:如果需要給超類發(fā)消息,例如[super message:parameter],那么就交由此函數(shù)處理。

  • 消息接收者、選取器及參數(shù)構(gòu)成。給某對象“發(fā)送消息”也就相當(dāng)于在該對象上“調(diào)用方法”

  • 發(fā)給某對象的全部消息都要由“動態(tài)消息派發(fā)系統(tǒng)”來處理,該系統(tǒng)會查出對應(yīng)的方法,并執(zhí)行其代碼

12. 理解消息轉(zhuǎn)發(fā)機制

消息轉(zhuǎn)發(fā)分為兩大階段,第一階段先征詢接收者所屬的類,看其是否能動態(tài)添加方法,以處理當(dāng)前這個“未知的選取器”,這叫做“動態(tài)方法解析"。第二階段涉及“完整的消息轉(zhuǎn)發(fā)機制”,如果runtime已經(jīng)把第一階段執(zhí)行完了,那么接收者自己無法再以動態(tài)添加的方法來相應(yīng)包含該選取器的消息了。此時,運行期系統(tǒng)會請求接收者以其他手段來處理與消息相關(guān)的方法調(diào)用。這又細分為兩部,首先,看有沒其他對象處理。有就給它。即“備援的接收者”。沒有,則啟動“完整的消息轉(zhuǎn)發(fā)機制”。runtime會把與消息有關(guān)的全部細節(jié)都封裝到NSInvocation對象,再給接收者最后一次機會。

動態(tài)方法解析:

對象在收到無法解讀的消息后,首先將調(diào)用其所屬類的下列表方法
+ (BOOL)resolveInstanceMethod:(SEL)selecotor
??該方法的參數(shù)就是那個未知的選取器,其返回值表示這個類能否新新增一個實例方法用以處理這個選取器。在繼續(xù)往下執(zhí)行轉(zhuǎn)發(fā)機制之前,本類有機會新增一個處理此選取器的方法。加入尚未實現(xiàn)的方法不是實例方法,而是類方法,那么會調(diào)用另一個叫resolveClassMethod:的方法
??使用這種方法的前提是,相關(guān)方法的實現(xiàn)代碼已經(jīng)寫好,只等著運行時,動態(tài)插在類里就可以了。此方法常用來實現(xiàn)@dynamic屬性。

備援接收者

當(dāng)前接收者還有第二次機會能處理未知的選取器,在這一步中,runtime系統(tǒng)會問它,能不能把這條消息轉(zhuǎn)給其他接收者來處理。其對應(yīng)處理方法:
- (id)forwardingTargetForSelector:(SEL)selector
??方法參數(shù)代表未知的選取器,若當(dāng)前接收者能找到備援對象,則將其返回,找不到,就返回nil。通過此方案,我們可以用“組合”來模擬出“多重集成”的某些特性。在一個對象內(nèi)部,可以還有其他對象,改對象經(jīng)由此方法將能夠處理某選取器的相關(guān)內(nèi)部對象返回,這樣的話,看起來就是該對象處理的。
注意,我們無法操作經(jīng)由這一步所轉(zhuǎn)發(fā)的消息,如果想在發(fā)送給備援接收者之前修改消息內(nèi)容,那就得通過完整的消息轉(zhuǎn)發(fā)機制來做。

完整的消息轉(zhuǎn)發(fā)

如果轉(zhuǎn)發(fā)算法已經(jīng)來到這一步的話,那么唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機制。首先創(chuàng)建NSInvocation對象,把尚未處理的那條消息有關(guān)的全部細節(jié)全部封裝與其中。此對象包含選取器,目標(target)及參數(shù)。在觸發(fā)NSInvocation對象時,“消息派發(fā)系統(tǒng)”將親自出馬,把消息指派給目標對象。
此步驟會調(diào)用下列方法來轉(zhuǎn)發(fā)消息:

 - (void)forwardInvocation:(NSInvocation *)invocation 

這個方法可以實現(xiàn)的很簡單:只需改變調(diào)用目標,使消息在新目標上得以調(diào)用即可。然而這樣實現(xiàn)出來的方法與“備援接收者”方案所實現(xiàn)的方法等效。比較有用的實現(xiàn)方法:在觸發(fā)消息前,先以某種方式改變消息內(nèi)容,比如追加另一個參數(shù),或者改換選取器等。
實現(xiàn)此方法時,若發(fā)現(xiàn)某調(diào)用操作不應(yīng)由本類處理,則需調(diào)用超類的同名方法。這樣的話,繼承體系中的每個類都有機會處理此調(diào)用請求,直至NSObject。如果最后調(diào)用了NSObject類方法,那么該方法還會繼而調(diào)用doesNotRecognizeSelector:以拋出異常,此異常表明選取器最終未能得到處理。

消息轉(zhuǎn)發(fā)流程
  • 若對象無法響應(yīng)某個選取器,則進入消息轉(zhuǎn)發(fā)流程
  • 通過運行期的動態(tài)方法解析功能,我們可以在需要用到某個方法時再將其加入類中
  • 對象可以把其無法解讀的某些選取器交給其他對象來處理
  • 經(jīng)過上述兩部之后,如果還是沒辦法處理,那就只能啟動完整的消息轉(zhuǎn)發(fā)機制(目前能看出的用處:1. 理解了錯誤的時候拋出異常的地方,2. 用于實現(xiàn)類似多繼承的效果)

13. 用"方法轉(zhuǎn)換“調(diào)試”黑盒方法“

使用** 方法轉(zhuǎn)換 **可以讓我們既不需要源代碼,也不需要通過繼承子類來覆寫方法就能改變這個類本身的功能。這樣一來,新功能將在本類的所有實例中生效,而不是僅限于腹寫了相關(guān)方法的那些子類實例。
??類的方法列表會把選取器的名稱映射到相關(guān)的方法實現(xiàn)之上,使得“動態(tài)消息派發(fā)系統(tǒng)”能夠據(jù)此找到應(yīng)該調(diào)用的方法。這些方法均以函數(shù)指針的形式來表示,這種指針叫IMP,原型如下

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

NSString類可以響應(yīng)uppercaseString等選取器。

NSString選取器映射表

?#emsp;想交換方法實現(xiàn),可用下列函數(shù):

void method_exchangeImplementations(Method m1, Method m2)

此函數(shù)的兩個參數(shù)表示待交換的兩個方法實現(xiàn),而方法實現(xiàn)則可通過下列函數(shù)獲得:

Method class_getInstanceMethod(Class aClass, SEL aSelector)

此函數(shù)根據(jù)給定的選擇從類中去除與之相關(guān)的方法。執(zhí)行下列代碼,即可交換比如lowercaseString和uppercaseString方法實現(xiàn):

Method originalMethod = class_getInstanceMethod([NSStringclass], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSStringclass], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);

如果想要在調(diào)用lowercaseString時記錄某些信息,這時可以通過交換方法來實現(xiàn)
??新方法可以添加到NSString的一個category鐘:

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

上述新方法將與原有的lowercaseString方法呼喚
??新方法的實現(xiàn)可以這樣寫:

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

這段代碼看上去好像會是死循環(huán),不過要記住,此方法是準備和lowercaseString方法呼喚的。所以,在運行期,eoc_myLowercaseString選取器實際上對應(yīng)于原有的lowercaseString方法實現(xiàn)。最后通過以下來交換實現(xiàn):

Method originalMethod = class_getInstanceMethod([NSStringclass], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSStringclass], @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
  • 在運行期,可以向類中新增或替換選取器所對應(yīng)的方法實現(xiàn)
  • 使用另一份實現(xiàn)來替換原有的方法實現(xiàn),這道工序叫做** 方法轉(zhuǎn)換 **,開發(fā)者常用詞技術(shù)向原有實現(xiàn)中添加新功能
  • 一般來說,只有調(diào)試程序的時候才需要在運行期修改方法實現(xiàn),這種做法不宜濫用

14. 理解類對象的用意

元類繼承
  • 每個實例都有一個指向Class對象的指針,用以表明其類型,而這些Class對象則構(gòu)成了類的繼承體系
  • 如果對象類型無法再編譯期確定,那么就應(yīng)該使用類型信息來查詢方法探知(isEquelto)
  • 盡量使用類型信息查詢方法來確定對象類型,而不要直接比較類的對象,因為某些對象可能實現(xiàn)了消息轉(zhuǎn)發(fā)功能
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容