Effective Objective-C 2.0 敲門(mén)磚

Effective Objective-C 2.0 編寫(xiě)高質(zhì)量iOS和OS X代碼的52個(gè)有效方法

前言

這本書(shū)和Objective-C高級(jí)編程-iOS和OS X多線程和內(nèi)存管理實(shí)在是iOS開(kāi)發(fā)人員必讀書(shū). 實(shí)在是太經(jīng)典了. 相信懂的人自然懂~

Objective-C高級(jí)編程的讀書(shū)筆記我已經(jīng)整理好發(fā)布了, 大家感興趣的話可以去看看, 不感興趣就直接略過(guò)吧~
Objective-C高級(jí)編程讀書(shū)筆記之內(nèi)存管理
Objective-C高級(jí)編程讀書(shū)筆記之blocks
Objective-C高級(jí)編程讀書(shū)筆記之GCD

這篇文章只是一個(gè)敲門(mén)磚, 大家不要指望看了這篇文章就不用去看書(shū)了, 那是不可能的, 也是遠(yuǎn)遠(yuǎn)不夠的, 只是希望各位能借助我這篇文章, 留個(gè)整體的印象, 然后再帶著問(wèn)題去研讀這本書(shū). 那才能達(dá)到最好的效果.


目錄

第1章 : 熟悉Objective-C
第2章 : 對(duì)象, 消息, 運(yùn)行時(shí)
第3章 : 接口與API設(shè)計(jì)
第4章 : 協(xié)議和分類(lèi)
第5章 : 內(nèi)存管理
第6章 : 塊與大中樞派發(fā)(也就是Block與GCD)
第7章 : 系統(tǒng)框架


第1章 : 熟悉Objective-C

1. Objective-C是一門(mén)動(dòng)態(tài)語(yǔ)言, 該語(yǔ)言使用的是"消息結(jié)構(gòu)"而非"函數(shù)調(diào)用".
  • 消息結(jié)構(gòu) : 運(yùn)行時(shí)所執(zhí)行的代碼由運(yùn)行時(shí)環(huán)境決定
  • 函數(shù)調(diào)用 : 運(yùn)行時(shí)所執(zhí)行的代碼由編譯期決定.

也就是說(shuō)[person run];

給person對(duì)象發(fā)送一條run消息 : 不到程序運(yùn)行的時(shí)候你都不知道他究竟會(huì)執(zhí)行什么代碼. 而且, person這個(gè)對(duì)象究竟是Person類(lèi)的對(duì)象, 還是其他類(lèi)的對(duì)象, 也要到運(yùn)行時(shí)才能確定, 這個(gè)過(guò)程叫動(dòng)態(tài)綁定.

2. 堆空間

對(duì)象所占內(nèi)存總是分配在堆空間中. 不能在棧中分配Objective-C對(duì)象.

  • 椔ⅲ空間 : 椉钇ǎ空間的內(nèi)存不用程序員管理.
  • 堆空間 : 堆空間的內(nèi)存需要程序員管理.
NSString *anString = @"Jerry";
NSString *anotherString = anString;

以上代碼的意思是, 在堆空間中創(chuàng)建一個(gè)NSString實(shí)例對(duì)象, 然而棧空間中分配兩個(gè)指針?lè)謩e指向該實(shí)例. 如圖,


堆和棧
在類(lèi)的頭文件中盡量少引入其他文件

在類(lèi)的頭文件中用到某個(gè)類(lèi), 如果沒(méi)有涉及到其類(lèi)的細(xì)節(jié), 盡量用@class向前聲明該類(lèi)(等于告訴編譯器這是一個(gè)類(lèi), 其他你先別管)而不導(dǎo)入該類(lèi)的頭文件以避免循環(huán)引用和減少編譯時(shí)間.

多用字面量語(yǔ)法, 少用與之等價(jià)的方法

我們知道, 現(xiàn)在我們創(chuàng)建Foundation框架的類(lèi)時(shí)有許多便捷的方法, 如

NSString *string = @"Jerry";
NSNumber *number = @10;
NSArray *array = @[obj, obj1, obj2];
NSDictionary *dict = @{
                     @"key1" : obj1,
                     @"key2" : obj2,
                     @"key3" : obj3 };

我用們字面量語(yǔ)法替代傳統(tǒng)的alloc-init來(lái)創(chuàng)建對(duì)象的好處 :

  • 方便直觀
  • 更加安全
  • 更利于debug

局限性 :

  • 只有NSString, NSArray, NSDictionary, NSNumber支持字面量語(yǔ)法
  • 若想用字面量語(yǔ)法創(chuàng)建出可變對(duì)象, 則需要再次調(diào)用mutableCopy方法復(fù)制多一份(多調(diào)用了一個(gè)方法, 多創(chuàng)建了一個(gè)對(duì)象. 不必要)

關(guān)于字面量語(yǔ)法, 有位哥們寫(xiě)得很通俗易懂, 可以去移步到淺談OC字面量語(yǔ)法這里看看.

多用類(lèi)型常量, 少用#define預(yù)處理指令

為什么少用#define預(yù)處理指令?

  • 用預(yù)處理指令定義的常量不含類(lèi)型信息
  • 編譯時(shí)只會(huì)進(jìn)行簡(jiǎn)單查找與替代操作, 會(huì)分配多次內(nèi)存
  • 如果有人重新定義了常量值, 則會(huì)導(dǎo)致程序中常量值不一致

為什么多用類(lèi)型常量?

  • 在實(shí)現(xiàn)文件中使用static const定義只在該文件內(nèi)可見(jiàn)的常量, 其他文件無(wú)法使用(無(wú)需給常量名稱加前綴)
  • 在頭文件中使用extern來(lái)聲明全局常量, 并在實(shí)現(xiàn)文件中定義其值, 可以供整個(gè)程序使用(需要給常量名稱加前綴)

針對(duì)const#define的優(yōu)劣, 可參考我之前寫(xiě)過(guò)的一篇文章15分鐘弄懂 const 和 #define

用枚舉來(lái)表示狀態(tài), 選項(xiàng), 狀態(tài)碼

相對(duì)于魔法數(shù)字(Magic Number), 使用枚舉的好處不言而喻. 這里只說(shuō)兩個(gè).

  1. 如果枚舉類(lèi)型的多個(gè)選項(xiàng)不需要組合使用, 則用NS_ENUM
typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
     UIViewAnimationTransitionNone,
     UIViewAnimationTransitionFlipFromLeft,
     UIViewAnimationTransitionFlipFromRight,
     UIViewAnimationTransitionCurlUp,
     UIViewAnimationTransitionCurlDown,
};
  1. 如果枚舉類(lèi)型的多個(gè)選項(xiàng)可能組合使用, 則用NS_OPTIONS
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
       UIViewAutoresizingNone                 = 0,
       UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
       UIViewAutoresizingFlexibleWidth        = 1 << 1,
       UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
       UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
       UIViewAutoresizingFlexibleHeight       = 1 << 4,
       UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

以上代碼為蘋(píng)果源碼.
使用NS_ENUM和NS_OPTIONS來(lái)替代C語(yǔ)言的enum的好處

  • 可以自定義枚舉的底層數(shù)據(jù)類(lèi)型
  • 在C中使用C的語(yǔ)法, 在OC中使用OC的語(yǔ)法, 保持語(yǔ)法的統(tǒng)一

另外, 在處理枚舉的switch語(yǔ)句中, 不要使用default分支, 因?yàn)橐院竽慵尤胄旅杜e之后, 編譯器會(huì)提示開(kāi)發(fā)者 : switch語(yǔ)句沒(méi)有處理所有枚舉(沒(méi)使用default的情況下).


第2章 : 對(duì)象, 消息, 運(yùn)行時(shí)

上一章我們說(shuō)到, Objective-C是一門(mén)動(dòng)態(tài)語(yǔ)言, 其動(dòng)態(tài)性就由這一章來(lái)說(shuō)明.

理解"屬性"這一概念
@interface Person : NSObject {
@public
    NSString *_firstName;
    NSString *_lastName;
@private
    NSString *_address;
}

編寫(xiě)過(guò)Java或C++的人應(yīng)該比較熟悉這種寫(xiě)法, 但是這種寫(xiě)法問(wèn)題很大!!!
對(duì)象布局在編譯器就已經(jīng)固定了. 只要碰到訪問(wèn)_firstName變量的代碼, 編譯器就把其替換為"偏移量", 這個(gè)偏移量是"硬編碼", 表示該變量距離存放對(duì)象的內(nèi)存區(qū)域的起始地址有多遠(yuǎn).

目前這樣看沒(méi)有問(wèn)題, 但是只要在_firstName前面再加一個(gè)實(shí)例變量就能說(shuō)明問(wèn)題了.

@interface Person : NSObject {
@public
    NSDate *_birthday;
    NSString *_firstName;
    NSString *_lastName;
@private
    NSString *_address;
}

原來(lái)表示_firstName的偏移量現(xiàn)在卻指向_birthday了. 如圖

在類(lèi)中新增另一個(gè)實(shí)例變量前后的數(shù)據(jù)布局圖

有人可能會(huì)有疑問(wèn), 新增實(shí)例變量不是要寫(xiě)代碼然后編譯運(yùn)行程序嗎? 重新編譯后對(duì)象布局不就又變正確了嗎? 錯(cuò)誤! 正是因?yàn)镺bjective-C是動(dòng)態(tài)語(yǔ)言, 他可以在運(yùn)行時(shí)動(dòng)態(tài)添加實(shí)例變量, 那時(shí)對(duì)象布局早就已固定不能再更改了.

那么Objective-C是怎么避免這種情況的呢? 它把實(shí)例變量當(dāng)做一種存儲(chǔ)偏移量所用的"特殊變量", 交由"類(lèi)對(duì)象"保管(類(lèi)對(duì)象將會(huì)在本章后面說(shuō)明). 此時(shí), 偏移量會(huì)在運(yùn)行時(shí)進(jìn)行查找, 如果類(lèi)的定義變了, 那么存儲(chǔ)的偏移量也會(huì)改變, 這樣在運(yùn)行時(shí)無(wú)論何時(shí)訪問(wèn)實(shí)例變量, 都能使用正確的偏移量. 有了這種穩(wěn)固的ABI(Application Binary Interface), OC就能在運(yùn)行時(shí)給類(lèi)動(dòng)態(tài)添加實(shí)例變量而不會(huì)發(fā)生訪問(wèn)錯(cuò)誤了.

@property, @synthesize, @dynamic

這是本節(jié)的重中之重. 我們必須要搞清楚使用@property, @synthesize, @dynamic關(guān)鍵字, 編譯器會(huì)幫我們做了什么, 才能更好地掌握使用屬性.

  • @property
@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end

以上代碼編譯器會(huì)幫我們分解成setter和getter方法聲明, 以上代碼與以下代碼等效

@interface Person : NSObject
- (NSString *)firstName;
- (void)setFirstName:(NSString *)firstName;
@end
  • @synthesize
@implementation Person
@synthesize firstName;
@end

以上代碼相當(dāng)于給Person類(lèi)添加一個(gè)_firstName的實(shí)例變量并為該實(shí)例變量生成setter和getter方法的實(shí)現(xiàn)(存取方法).

可以利用@synthesize給實(shí)例變量取名字(默認(rèn)為_(kāi)xxx, 例如@property聲明的是name, 則生成的是_name的實(shí)例變量)

@implementation Person
@synthesize firstName = myFirstName;
@end

以上代碼就是生成myFirstName的實(shí)例變量了. 由于OC的命名規(guī)范, 不推薦這么做. 沒(méi)必要給實(shí)例變量取另一個(gè)名字.

  • @dynamic
@implementation Person
@dynamic firstName;
@end

該代碼會(huì)告訴編譯器 : 不要自動(dòng)創(chuàng)建實(shí)現(xiàn)屬性(property)所用的實(shí)例變量(_property)和存取方法實(shí)現(xiàn)(setter和getter).

也就是說(shuō), 實(shí)例變量不存在了, 因?yàn)榫幾g器不會(huì)自動(dòng)幫你創(chuàng)建了. 而且如果你不手動(dòng)實(shí)現(xiàn)setter和getter, 使用者用點(diǎn)語(yǔ)法或者對(duì)象方法調(diào)用setter和getter時(shí), 程序會(huì)直接崩潰, 崩潰原因很簡(jiǎn)單 : unrecognized selector sent to instance

上代碼

// Person.h
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
-------------------------------------------
// Person.m
@implementation Person
@dynamic name;
@end
-------------------------------------------
// main.m
int main(int argc, const char * argv[]) {
    Person *p = [[Person alloc] init];
    p.name = @"Jerry";
    return 0;
}
-------------------------------------------
// 程序崩潰, 控制臺(tái)輸出
-[Person setName:]: unrecognized selector sent to instance 

原因很簡(jiǎn)單, 我用@dynamic騙編譯器, 你不用幫我生成實(shí)例變量跟方法實(shí)現(xiàn)啦, 我自己來(lái). 結(jié)果運(yùn)行的時(shí)候卻發(fā)現(xiàn)你丫的根本找不到實(shí)現(xiàn)方法, 所以崩潰了唄~

總結(jié)下

在現(xiàn)在的編譯器下,

  1. @property會(huì)為屬性生成setter和getter的方法聲明, 同時(shí)調(diào)用@synthesize ivar = _ivar生成_ivar實(shí)例變量和存取方法的實(shí)現(xiàn)
  2. 手動(dòng)調(diào)用@synthesize可以用來(lái)修改實(shí)例變量的名稱
  3. 手動(dòng)調(diào)用@dynamic可以告訴編譯器: 不要自動(dòng)創(chuàng)建實(shí)現(xiàn)屬性所用的實(shí)例變量, 也不要為其創(chuàng)建實(shí)例變量的存取方法聲明與實(shí)現(xiàn).
readonly與readwrite

以上文檔說(shuō)明, 就算你沒(méi)有用@dynamic, 只要你手動(dòng)實(shí)現(xiàn)了setter和getter方法(屬性為readwrite情況下)或者手動(dòng)實(shí)現(xiàn)getter方法(屬性為readonly情況下), @property關(guān)鍵字也不會(huì)自動(dòng)調(diào)用@synthesize來(lái)幫你合成實(shí)例變量了.

以上特性均可以使用runtime打印類(lèi)的實(shí)例變量列表來(lái)印證.

在對(duì)象內(nèi)部盡量直接訪問(wèn)實(shí)例變量

為什么呢? 使用點(diǎn)語(yǔ)法不好嗎? 這里說(shuō)說(shuō)區(qū)別

  • 直接用_xxx訪問(wèn)實(shí)例變量而不用點(diǎn)語(yǔ)法可以繞過(guò)OC的"方法派發(fā)", 效率比用點(diǎn)語(yǔ)法來(lái)訪問(wèn)快
  • 直接用_xxx訪問(wèn)實(shí)例變量而不用點(diǎn)語(yǔ)法不會(huì)調(diào)用setter方法, 所以不會(huì)觸發(fā)KVO(Key Value Observing), 同時(shí)如果你訪問(wèn)的該屬性是聲明為copy的屬性, 則不會(huì)進(jìn)行拷貝, 而是直接保留新值, 釋放舊值.
  • 使用點(diǎn)語(yǔ)法訪問(wèn)有助于debug, 因?yàn)榭梢栽趕etter或getter中增加斷點(diǎn)來(lái)監(jiān)控方法的調(diào)用
  • 屬性使用懶加載時(shí), 必須使用點(diǎn)語(yǔ)法, 否則實(shí)例變量永遠(yuǎn)不會(huì)初始化(因?yàn)閼屑虞d實(shí)際就是調(diào)用getter方法, 直接訪問(wèn)實(shí)例變量繞過(guò)了該方法, 所以該變量則永遠(yuǎn)為nil)

綜上, 比較折中的方法就是

  • 寫(xiě)入實(shí)例變量時(shí), 用setter
  • 讀取實(shí)例變量時(shí), 直接訪問(wèn)
對(duì)象等同性

比較兩個(gè)對(duì)象是否相同.
我們可以重寫(xiě)isEqual方法自定義對(duì)象等同的條件

類(lèi)族模式

Objective-C的系統(tǒng)框架中普遍使用此模式, 用子類(lèi)來(lái)隱藏"抽象基類(lèi)"的內(nèi)部實(shí)現(xiàn)細(xì)節(jié).
我們肯定使用過(guò)UIButton的這個(gè)類(lèi)方法
+ (UIButton *)buttonWithType:(UIButtonType)type;

這就是UIButton類(lèi)實(shí)現(xiàn)的"工廠方法", 根據(jù)傳入的枚舉創(chuàng)建并返回合乎條件的子類(lèi).

Foundation框架中大部分容器類(lèi)都是類(lèi)族, 如NSArray與NSMutableArray, NSSet與NSMutableSet, NSDictionary與NSMutableDictionary.

用isKindOfClass方法可以判斷對(duì)象所屬的類(lèi)是否位于類(lèi)族之中.

在類(lèi)族中實(shí)現(xiàn)子類(lèi)時(shí)所需遵循的規(guī)范一般都會(huì)定義于基類(lèi)的文檔之中, 使用前應(yīng)先看看.

具體類(lèi)族的使用方法大家請(qǐng)看書(shū)~~

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

在類(lèi)的內(nèi)部利用哈希表映射技術(shù), 關(guān)聯(lián)一個(gè)與該類(lèi)毫無(wú)耦合的對(duì)象.
使用場(chǎng)景

  • 為現(xiàn)有的類(lèi)添加私有變量以幫助實(shí)現(xiàn)細(xì)節(jié)
  • 為現(xiàn)有的類(lèi)添加公有屬性
  • 為KVO創(chuàng)建一個(gè)關(guān)聯(lián)的觀察者

鑒于書(shū)中所說(shuō), 容易出現(xiàn)循環(huán)引用, 以及關(guān)聯(lián)對(duì)象釋放和移除不同步等缺陷,
使用關(guān)聯(lián)對(duì)象這一解決方案總是不到萬(wàn)不得已都不用的, 所以這里只提供兩篇文章, 感興趣的話大家可以去了解了解.
Associated Objects
Objective-C Associated Objects 的實(shí)現(xiàn)原理

消息發(fā)送和轉(zhuǎn)發(fā)機(jī)制

OC的消息發(fā)送和轉(zhuǎn)發(fā)機(jī)制是深入了解OC這門(mén)語(yǔ)言的必經(jīng)之路. 下面我們就來(lái)學(xué)習(xí)學(xué)習(xí)這個(gè)消息發(fā)送和轉(zhuǎn)發(fā)機(jī)制的神奇之處.

objc_msgSend

在解釋OC消息發(fā)送之前, 最好先理解C語(yǔ)言的函數(shù)調(diào)用方式. C語(yǔ)言使用"靜態(tài)綁定", 也就是說(shuō)在編譯器就能決定運(yùn)行時(shí)所應(yīng)調(diào)用的函數(shù). 如下代碼所示

void run() {
    // run
}

void study() {
    // study
}

void doSomething(int type) {
    if (type == 0) {
        run();
    } else {
        study();
    }
}

如果不考慮內(nèi)聯(lián), 那么編譯器在編譯代碼的時(shí)候就已經(jīng)知道程序中有run和study這兩個(gè)函數(shù)了, 于是會(huì)直接生成調(diào)用這些函數(shù)的指令. 如果將上述代碼改寫(xiě)成這樣呢?

void run() {
    // run
}

void study() {
    // study
}

void doSomething(int type) {
    void (*func)();
    if (type == 0) {
        func = run;
    } else {
        func = study;
    }
    func();
}

這就是"動(dòng)態(tài)綁定".

在OC中, 如果向某對(duì)象發(fā)送消息, 那就會(huì)使用動(dòng)態(tài)綁定機(jī)制來(lái)決定需要調(diào)用的方法. OC的方法在底層都是普通的C語(yǔ)言函數(shù), 所以對(duì)象收到消息后究竟要調(diào)用什么函數(shù)完全由運(yùn)行時(shí)決定, 甚至可以在運(yùn)行時(shí)改變執(zhí)行的方法.

現(xiàn)在開(kāi)始來(lái)探索OC的消息機(jī)制

// person : receiver(消息接收者)
// read : selector(選擇子)
// 選擇子 + 參數(shù) = 消息
[person read:book];

編譯器會(huì)將以上代碼編譯成以下代碼

// objc_msgSend方法原型為 void objc_msgSend(id self, SEL cmd, ...)
// self : 接收者
// cmd : 選擇子
// ... : 參數(shù), 參數(shù)的個(gè)數(shù)可變
objc_msgSend(person, @selector(read:), book);

objc_msgSend會(huì)根據(jù)接收者和選擇子的類(lèi)型來(lái)調(diào)用適當(dāng)?shù)姆椒? 流程如下

  1. 查找接收者的所屬類(lèi)的cache列表, 如果沒(méi)有則下一步
  2. 查找接收者所屬類(lèi)的"方法列表"
  3. 如果能找到與選擇子名稱相符的方法, 就跳至其實(shí)現(xiàn)代碼
  4. 找不到, 就沿著繼承體系繼續(xù)向上查找
  5. 如果能找到與選擇子名稱相符的方法, 就跳至其實(shí)現(xiàn)代碼
  6. 找不到, 執(zhí)行"消息轉(zhuǎn)發(fā)".

那么找到與選擇子名稱相符的方法, 就跳至其實(shí)現(xiàn)代碼這一步是怎么實(shí)現(xiàn)的呢? 這里又要引出一個(gè)函數(shù)原型了

<return_type> Class_selector(id self, SEL _cmd, ...);

真實(shí)的函數(shù)名可能有些出入, 不過(guò)這里志在用該原型解釋其過(guò)程, 所以也就無(wú)所謂了.
每個(gè)類(lèi)里都有一張表格, 其中的指針都會(huì)指向這種函數(shù), 而選擇子的名稱則是查表時(shí)所用的key. objc_msgSend函數(shù)正是通過(guò)這張表格來(lái)尋找應(yīng)該執(zhí)行的方法并跳至其實(shí)現(xiàn)的.

方法底層實(shí)現(xiàn)

乍一看覺(jué)得調(diào)用一個(gè)方法原來(lái)要這么多步驟, 豈不是很費(fèi)時(shí)間? 不著急~ objc_msgSend會(huì)將匹配結(jié)果緩存在"快速映射表"里, 每個(gè)類(lèi)都有這樣一塊緩存, 下次調(diào)用相同方法時(shí), 就能很快查找到實(shí)現(xiàn)代碼了.

消息發(fā)送的其他方法

  • objc_msgSend_stret : 消息要返回結(jié)構(gòu)體, 則由此函數(shù)處理.
  • objc_msgSend_fpret : 消息要返回浮點(diǎn)數(shù), 則由此函數(shù)處理.
  • objc_msgSendSuper : 給超類(lèi)發(fā)消息.

消息轉(zhuǎn)發(fā)

上面我們?cè)f(shuō)過(guò), 如果到最后都找不到, 則進(jìn)入消息轉(zhuǎn)發(fā)

  • 動(dòng)態(tài)方法解析 : 先問(wèn)接收者所屬的類(lèi), 你看能不能動(dòng)態(tài)添加個(gè)方法來(lái)處理這個(gè)"未知的選擇子"? 如果能, 則消息轉(zhuǎn)發(fā)結(jié)束.
  • 備胎(后備接收者) : 請(qǐng)接收者看看有沒(méi)有其他對(duì)象能處理這條消息? 如果有, 則把消息轉(zhuǎn)給那個(gè)對(duì)象, 消息轉(zhuǎn)發(fā)結(jié)束.
  • 完整的消息轉(zhuǎn)發(fā) : 備胎都搞不定了, 那就只能把該消息相關(guān)的所有細(xì)節(jié)都封裝到一個(gè)NSInvocation對(duì)象, 再問(wèn)接收者一次, 快想辦法把這個(gè)搞定了. 到了這個(gè)地步如果還無(wú)法處理, 消息轉(zhuǎn)發(fā)機(jī)制也無(wú)能為力了.
動(dòng)態(tài)方法解析 :

對(duì)象在收到無(wú)法解讀的消息后, 首先調(diào)用其所屬類(lèi)的這個(gè)類(lèi)方法 :

+ (BOOL)resolveInstanceMethod:(SEL)selector 
// selector : 那個(gè)未知的選擇子
// 返回YES則結(jié)束消息轉(zhuǎn)發(fā)
// 返回NO則進(jìn)入備胎

假如尚未實(shí)現(xiàn)的方法不是實(shí)例方法而是類(lèi)方法, 則會(huì)調(diào)用另一個(gè)方法resolveClassMethod:

備胎 :

動(dòng)態(tài)方法解析失敗, 則調(diào)用這個(gè)方法

- (id)forwardingTargetForSelector:(SEL)selector
// selector : 那個(gè)未知的選擇子
// 返回一個(gè)能響應(yīng)該未知選擇子的備胎對(duì)象

通過(guò)備胎這個(gè)方法, 可以用"組合"來(lái)模擬出"多重繼承".

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

備胎也無(wú)能為力了, 只能把消息包裝成一個(gè)對(duì)象, 給接收者最后一次機(jī)會(huì), 搞不定就不搞了!

- (void)forwardInvocation:(NSInvovation *)invocation
// invocation : 封裝了與那條尚未處理的消息相關(guān)的所有細(xì)節(jié)的對(duì)象

在這里能做的比較現(xiàn)實(shí)的事就是 : 在觸發(fā)消息前, 先以某種方式改變消息內(nèi)容, 比如追加另外一個(gè)參數(shù), 或是改變選擇子等等. 實(shí)現(xiàn)此方法時(shí), 如果發(fā)現(xiàn)某調(diào)用操作不應(yīng)該由本類(lèi)處理, 可以調(diào)用超類(lèi)的同名方法. 則繼承體系中的每個(gè)類(lèi)都有機(jī)會(huì)處理該請(qǐng)求, 直到NSObject. 如果NSObject搞不定, 則還會(huì)調(diào)用doesNotRecognizeSelector:來(lái)拋出異常, 此時(shí)你就會(huì)在控制臺(tái)看到那熟悉的unrecognized selector sent to instance..

消息轉(zhuǎn)發(fā)

盡量在第一步就把消息處理了, 因?yàn)樵降胶竺嫠ù鷥r(jià)越大.

Method Swizzling

被稱為黑魔法的一個(gè)方法, 可以把兩個(gè)方法的實(shí)現(xiàn)互換.
如上文所述, 類(lèi)的方法列表會(huì)把選擇子的名稱映射到相關(guān)的方法實(shí)現(xiàn)上, 使得"動(dòng)態(tài)消息派發(fā)系統(tǒng)"能夠據(jù)此找到應(yīng)該調(diào)用的方法. 這些方法均以函數(shù)指針的形式來(lái)表示, 這種指針叫做IMP,
id (*IMP)(id, SEL, ...)

NSString類(lèi)的選擇子映射表

OC運(yùn)行時(shí)系統(tǒng)提供了幾個(gè)方法能夠用來(lái)操作這張表, 動(dòng)態(tài)增加, 刪除, 改變選擇子對(duì)應(yīng)的方法實(shí)現(xiàn), 甚至交換兩個(gè)選擇子所映射到的指針. 如,

經(jīng)過(guò)一些操作后的NSString選擇子映射表

如何交換兩個(gè)已經(jīng)寫(xiě)好的方法實(shí)現(xiàn)?

// 取得方法
Method class_getInstanceMethod(Class aClass, SEL aSelector)
// 交換實(shí)現(xiàn)
void method_exchangeImplementations(Method m1, Method m2)

通過(guò)Method Swizzling可以為一些完全不知道其具體實(shí)現(xiàn)的黑盒方法增加日志記錄功能, 利于我們調(diào)試程序. 并且我們可以將某些系統(tǒng)類(lèi)的具體實(shí)現(xiàn)換成我們自己寫(xiě)的方法, 以達(dá)到某些目的. (例如, 修改主題, 修改字體等等)

類(lèi)對(duì)象

OC中的類(lèi)也是對(duì)象的一種, 你同意嗎?

// 對(duì)象的結(jié)構(gòu)體
struct objc_object {
    Class isa;
};
// 類(lèi)的結(jié)構(gòu)體
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_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
}

根據(jù)以上源碼我們可以知道, 其實(shí)類(lèi)本身也是一個(gè)對(duì)象, 稱之為類(lèi)對(duì)象, 并且類(lèi)對(duì)象是單例, 即在程序運(yùn)行時(shí), 每個(gè)類(lèi)的Class僅有一個(gè)實(shí)例.

實(shí)例對(duì)象的isa指針指向所屬類(lèi), 那么類(lèi)對(duì)象的isa指向什么呢? 是元類(lèi)(metaclass)

類(lèi)與對(duì)象的繼承層級(jí)關(guān)系圖
isa指針
  • 根據(jù)上文消息派發(fā)機(jī)制我們可以得知, 實(shí)例方法是存在類(lèi)的方法列表里的, 那么實(shí)例對(duì)象是怎么找到這些方法呢? 沒(méi)錯(cuò), 答案就是isa指針.
  • 還有我們剛剛得知, 類(lèi)也是個(gè)對(duì)象, 那么我們猜測(cè), 類(lèi)方法是不是存在元類(lèi)的方法列表里呢? 是的, 因?yàn)轭?lèi)相當(dāng)于"元類(lèi)"的實(shí)例, 所以實(shí)例方法當(dāng)然是存在類(lèi)的方法列表中. 而類(lèi)對(duì)象中的isa指針當(dāng)然是用來(lái)查找其所屬類(lèi)(元類(lèi))的了.
用類(lèi)型信息查詢方法來(lái)檢視類(lèi)繼承體系
// 判斷對(duì)象是否為某個(gè) 特定類(lèi) 的實(shí)例
- (BOOL)isMemberOfClass:(Class)aClass
// 判斷對(duì)象是否為**某類(lèi)或其派生類(lèi)**的實(shí)例
- (BOOL)isKindOfClass:(Class)aClass

例如, GoodPerson是Person的子類(lèi)

Person *p = [[Person alloc] init];
GoodPerson *gp = [[GoodPerson alloc] init];
// 判斷p的類(lèi)型    
[p isMemberOfClass:[Person class]]; // YES
[p isMemberOfClass:[GoodPerson class]]; // NO
[p isKindOfClass:[Person class]]; // YES
[p isKindOfClass:[GoodPerson class]]; // NO
// 判斷gp的類(lèi)型
[gp isMemberOfClass:[Person class]]; // NO
[gp isMemberOfClass:[GoodPerson class]]; // YES
[gp isKindOfClass:[Person class]]; // YES
[gp isKindOfClass:[GoodPerson class]]; // YES

第3章 : 接口與API設(shè)計(jì)

這一章講的是一些命名規(guī)范, 設(shè)計(jì)API時(shí)的一些注意點(diǎn).

用前綴避免命名空間沖突

OC沒(méi)有其他語(yǔ)言那種內(nèi)置的命名空間, 所以只能通過(guò)前綴來(lái)營(yíng)造一個(gè)假的"命名空間". 這里推薦

  • 給類(lèi)中定義的C語(yǔ)言函數(shù)的名字加上類(lèi)前綴
  • 在類(lèi)中定義的全局變量要加上類(lèi)前綴
  • 為自己所開(kāi)發(fā)的程序庫(kù)中用到的第三方庫(kù)加上前綴

提供"全能初始化方法"

何為全能初始化方法?

  • 所有初始化方法都要調(diào)用的方法稱為全能初始化方法.

為什么要提供全能初始化方法?

  • 該方法用來(lái)存儲(chǔ)一些內(nèi)部數(shù)據(jù), 初始化一些操作, 這樣當(dāng)數(shù)據(jù)存儲(chǔ)機(jī)制改變, 只需修改一處的代碼, 無(wú)須改動(dòng)其他初始化方法.

子類(lèi)的全能初始化應(yīng)該調(diào)用超類(lèi)的全能初始化方法. 若超類(lèi)的初始化方法不適用于子類(lèi), 那么應(yīng)該重寫(xiě)這個(gè)超類(lèi)方法, 并在該方法拋出異常.

實(shí)現(xiàn)description方法

我們知道, 利用%@來(lái)打印一個(gè)對(duì)象得到的永遠(yuǎn)是<類(lèi)名 : 內(nèi)存地址>(NSString, NSDictionary, NSArray等對(duì)象除外). 如果我們需要輸出一些我們想要的內(nèi)容, 那么重寫(xiě)該方法即可. 應(yīng)該注意的是不要在description方法輸出self, 會(huì)引發(fā)死循環(huán).

除了description方法, 還有一個(gè)dubug專用的debugDescription方法. 該方法只有在開(kāi)發(fā)者在調(diào)試器中用LLDC的"po"指令打印對(duì)象時(shí)才會(huì)調(diào)用.

盡量使用不可變對(duì)象

我們知道, 如果我們暴露一個(gè)可變屬性出去, 然而別人就可以繞過(guò)你的API, 隨意地修改該屬性, 進(jìn)行添加, 刪除操作. 為了加強(qiáng)程序的魯棒性, 我們應(yīng)該對(duì)外公布一個(gè)不可變屬性, 然后提供相應(yīng)的方法給調(diào)用者操作該屬性, 而內(nèi)部修改對(duì)象時(shí)我們可以使用可變對(duì)象進(jìn)行修改.

// Person.h
@interface Person : NSObject
@property (nonatomic, strong, readonly) NSSet *friends;
- (void)addFriend:(Person *)person;
- (void)removeFriend:(Person *)person;
@end

// Person.m
@interface Person()
{
    NSMutableSet *_internalFriends;
}

@end

@implementation Person
// 返回所有朋友
- (NSSet *)friends
{
    return [_internalFriends copy];
}
// 添加朋友
- (void)addFriend:(Person *)person
{
    [_internalFriends addObject:person];
}
// 移除朋友
- (void)removeFriend:(Person *)person
{
    [_internalFriends removeObject:person];
}
@end

這樣別人拿到的永遠(yuǎn)是不可變的NSSet, 而且只能用你給的接口來(lái)操作這個(gè)set, 你內(nèi)部依然是使用一個(gè)可變的NSMutableSet來(lái)做事情, 一舉兩得!

為了使我們的程序變得更加健壯, 我們應(yīng)該盡量多聲明readonly屬性!

使用清晰而協(xié)調(diào)的命名方式

我們知道, OC的方法名總是很長(zhǎng), 長(zhǎng)得跟句子一樣, 好處很明顯, 那就是一讀就知道該方法是干嘛用的, 劣處嘛, 那就是麻煩了. 這里給幾個(gè)方法命名規(guī)范

  • 盡量少使用簡(jiǎn)稱, 而使用全稱
  • 返回BOOL類(lèi)型的方法應(yīng)該用has或is當(dāng)前綴
  • 在當(dāng)前對(duì)象上操作則方法名應(yīng)該包含動(dòng)詞, 如果需要參數(shù)著動(dòng)詞后面跟上
  • 返回對(duì)象的方法名, 第一個(gè)單詞應(yīng)該是該返回值的類(lèi)型

為私有方法加前綴

因?yàn)樗接蟹椒ㄖ辉陬?lèi)內(nèi)部調(diào)用, 不像外部方法, 修改會(huì)影響到面向外界的那些API, 對(duì)于私有方法來(lái)說(shuō)可以隨意修改. 所以為私有方法加前綴可以提醒自己哪些方法可以隨意修改, 哪些不應(yīng)輕易改動(dòng).

  • 給私有方法加上一個(gè)p_前綴, 如- (void)p_method;

正確處理錯(cuò)誤信息

不是有錯(cuò)就要拋出異常!!!只有在發(fā)生了可能致使應(yīng)用程序崩潰的嚴(yán)重錯(cuò)誤, 才使用異常. OC多使用代理和NSError對(duì)象來(lái)處理錯(cuò)誤信息.
NSError對(duì)象封裝了三條信息 :

  • Error domain(錯(cuò)誤范圍, 其類(lèi)型為字符串, 產(chǎn)生錯(cuò)誤的根源)
  • Error code(錯(cuò)誤碼, 其類(lèi)型為整數(shù), 多為枚舉)
  • User info(用戶信息, 其類(lèi)型為字典, 有關(guān)錯(cuò)誤的額外信息)

理解NSCopying協(xié)議

巧了, 之前我寫(xiě)過(guò)一篇關(guān)于copy的文章, 這里就直接引用, 不在贅述了.
小結(jié)iOS中的copy


第4章 : 協(xié)議和分類(lèi)

這一章講的協(xié)議和分類(lèi)都是兩個(gè)需要重點(diǎn)掌握的語(yǔ)言特性.

通過(guò)委托和數(shù)據(jù)源協(xié)議進(jìn)行對(duì)象間通信

委托(delegate), 我還是比較習(xí)慣叫代理, 下文就直接說(shuō)代理了..

代理和數(shù)據(jù)源, 我們?cè)谀睦锟吹竭^(guò)? 沒(méi)錯(cuò), UITableView, UICollectionView.
無(wú)論是什么對(duì)象, 只要遵循了代理協(xié)議和數(shù)據(jù)源協(xié)議就都能當(dāng)一個(gè)對(duì)象的代理和數(shù)據(jù)源. 蘋(píng)果這么做完全是為了解耦和復(fù)用.

而使用代理的時(shí)候, 我們是不是總是寫(xiě)以下這些代碼

if ( [self.delegate respondsToSelector:@selector(someClassDidSomething:)] ) {
    [self.delegate someClassDidSomething:self];
}

那大家有沒(méi)有想過(guò), 如果這個(gè)方法調(diào)用得很頻繁很頻繁, 那么每次調(diào)用之前都要問(wèn)問(wèn)代理能不能響應(yīng)這個(gè)方法, 不是很影響效率嗎?

我們可以這樣來(lái)優(yōu)化程序效率 -> 把代理能否響應(yīng)某個(gè)方法這一信息緩存起來(lái)

這里我們需要用到一個(gè)C語(yǔ)言的"位端數(shù)據(jù)類(lèi)型". 我們可以把結(jié)構(gòu)體中某個(gè)字段所占用的二進(jìn)制個(gè)數(shù)設(shè)為特定的值

struct data {
    unsigned int fieldA : 8;
    unsigned int fieldB : 4;
    unsigned int fieldC : 2;
    unsigned int fieldD : 1;
}

以上代碼表示fieldA只占用8個(gè)二進(jìn)制位, dieldB占用4個(gè), 如此類(lèi)推. 那么我們可以根據(jù)此特性設(shè)計(jì)一個(gè)代理對(duì)象是否響應(yīng)某代理方法的結(jié)構(gòu)體

@interface Person() {
    struct {
        unsigned int didEat : 1;
        unsigned int didSleep : 1;
    } _delegateFlags;
}
@end

這時(shí)我們可以攔截setDelegate方法, 在該方法里面一次過(guò)把代理是否響應(yīng)代理方法全部問(wèn)個(gè)遍, 然后對(duì)號(hào)入座把各自的BOOL值賦值給_delegateFalgs結(jié)構(gòu)體的對(duì)應(yīng)變量中. 那么我們下次調(diào)用代理的相關(guān)方法之前就變得優(yōu)雅多了, 如下:

if ( _delegateFlags.didEat ) {
    [self.delegate didEat:self];
}  

將類(lèi)的實(shí)現(xiàn)代碼分散到便于管理的數(shù)個(gè)分類(lèi)之中

如果某個(gè)類(lèi)方法太多, 整個(gè)類(lèi)太臃腫了, 可以根據(jù)方法的功能用分類(lèi)的思想跟方法集分個(gè)類(lèi), 劃分成易于管理的小塊.

總是為第三方類(lèi)的分類(lèi)名稱加前綴

這種情況說(shuō)起來(lái)比較抽象, 直接上代碼, 例如你想要給NSString添加分類(lèi),

@interface NSString (HTTP)
- (NSString *)urlEncodedString;
- (NSString *)urlDecodedString;
@end

我們不應(yīng)該像以上代碼那么做, 因?yàn)樘O(píng)果說(shuō)不定哪一天會(huì)給NSString加上一個(gè)HTTP分類(lèi)呢? 那么你就相當(dāng)于復(fù)寫(xiě)了系統(tǒng)的分類(lèi)了, 這是不允許的. 對(duì)應(yīng)的方法也是, 我們應(yīng)該為自己為第三方類(lèi)的分類(lèi)和方法名加上自己的專用前綴, 如下 :

@interface NSString (JR_HTTP)
- (NSString *)jr_urlEncodedString;
- (NSString *)jr_urlDecodedString;
@end

不要在分類(lèi)中聲明屬性

除了"class-continuation分類(lèi)"之外, 其他分類(lèi)都無(wú)法向類(lèi)中新增實(shí)例變量, 它們無(wú)法將實(shí)現(xiàn)屬性所需的實(shí)例變量合成出來(lái). 所以, 請(qǐng)不要在分類(lèi)中聲明屬性.

分類(lèi)的目的在于擴(kuò)展類(lèi)的功能, 而不是封裝數(shù)據(jù).

使用"class-continuation分類(lèi)"隱藏實(shí)現(xiàn)細(xì)節(jié)

"class-continuation分類(lèi)"與其他分類(lèi)不同

  • 它是定義在類(lèi)的.m文件中的分類(lèi)
  • 他沒(méi)有名稱

"class-continuation分類(lèi)"的作用 :

  • 定義內(nèi)部使用的私密類(lèi), 不暴露在主接口(.h文件)中
  • 將使用到的C++類(lèi)放在這里, 可以屏蔽實(shí)現(xiàn)細(xì)節(jié), 外部甚至不知道你內(nèi)部寫(xiě)了C++代碼
  • 把主接口中聲明readonly的屬性擴(kuò)展為readwrite供類(lèi)內(nèi)部使用
  • 把私有方法聲明在這里, 不給外人知道
  • 向類(lèi)新增實(shí)例變量
  • 遵循一些私密協(xié)議

通過(guò)協(xié)議提供匿名對(duì)象

有時(shí)候?qū)ο蟮念?lèi)型并不那么重要, 我們只需要保證他能滿足我的需求即可, 不管他是什么類(lèi), 這時(shí)候可以使用協(xié)議來(lái)隱藏類(lèi)的類(lèi)型, 如下 :
@property (nonatomic, weak) id<JRDelegate> delegate;

我們使用代理時(shí)總是這樣, 為什么呢? 只要他遵循了這個(gè)協(xié)議, 我們甚至不用關(guān)心代理是什么, 阿貓阿狗都可以成為我的代理.

而字典中也是說(shuō)明這一概念. 在字典中, 鍵的標(biāo)準(zhǔn)內(nèi)存管理語(yǔ)義是"設(shè)置時(shí)拷貝", 而值的語(yǔ)義則是"設(shè)置時(shí)保留".
- (void)setObject:(id)object forKey:(id<NSCopying>)key;

我們可以使用這一方法來(lái)屏蔽代理對(duì)象的實(shí)現(xiàn)細(xì)節(jié), 使用者只需要這種對(duì)象實(shí)現(xiàn)了代理方法即可, 其他的你不需要管.


第5章 : 內(nèi)存管理 與 第6章 : Block與GCD

不知不覺(jué)也寫(xiě)了差不多8千字了, 終于可以歇會(huì)了... 哇你千萬(wàn)不要以為下面的內(nèi)容不重要. 相反, 他們太重要了, 我花了好多時(shí)間去研究?jī)?nèi)存管理和block, GCD. 還好, 這部分內(nèi)容我之前已經(jīng)總結(jié)過(guò)了, 剛好一一對(duì)應(yīng).


所以第5章和第6章我會(huì)用比較少的筆墨來(lái)寫(xiě), 因?yàn)榇蟛糠值膬?nèi)容都已經(jīng)在文章一開(kāi)頭所分享的3篇文章里涵蓋了, 這里只把一些漏網(wǎng)之魚(yú)補(bǔ)上.

在dealloc方法中只釋放引用并解除監(jiān)聽(tīng)

在這個(gè)方法里只釋放指針, 解除KVO監(jiān)聽(tīng)和NSNotificationCenter通知, 不要做其他耗時(shí)操作, 尤其是不要執(zhí)行異步任務(wù)!

用"僵尸對(duì)象"調(diào)試內(nèi)存管理問(wèn)題

在Xcode - Scheme - Run - Diagnostics - 勾選 "Enable Zombie Objects"選項(xiàng)來(lái)開(kāi)啟僵尸對(duì)象.

開(kāi)啟之后, 系統(tǒng)在即將回收對(duì)象時(shí), 會(huì)執(zhí)行一個(gè)附加步驟, 把該對(duì)象轉(zhuǎn)化為僵尸對(duì)象, 而不徹底回收. 這樣你對(duì)僵尸對(duì)象發(fā)送消息后, 控制臺(tái)會(huì)打印錯(cuò)誤.

僵尸類(lèi) : 如果NSZombieEnabled變量已設(shè)置, 那么運(yùn)行時(shí)系統(tǒng)會(huì)swizzle原來(lái)的dealloc方法, 轉(zhuǎn)而執(zhí)行另一方法, 將該類(lèi)轉(zhuǎn)換成_NSZombie_OriginalClass, 這里的OriginalClass是原類(lèi)名.

用handler塊降低代碼分散程度

以前我們總是用代理來(lái)監(jiān)聽(tīng)一個(gè)類(lèi)內(nèi)部發(fā)生的時(shí). 例如一個(gè)下載器類(lèi), 下載完畢后通知代理, 下載出錯(cuò)時(shí)通知代理, 這個(gè)時(shí)候我們的代碼是這樣寫(xiě)的,

- (void)download {
    NSURL *url = [[NSURL alloc] initWithString:@"www.baidu.com"];
    JRDownloader *downloader = [[JRDownloader alloc] initWithURL:url];
    downloader.delegate = self;
    [downloader startDownload];
}
#pragma mark - JRDownloaderDelegate
- (void)downloader:(JRDownloader *)downloader didFinishWithData:(NSData *)data
{
    self.data = data;
}

這種辦法沒(méi)毛病, 也沒(méi)錯(cuò), 很好, 但是如果該類(lèi)中的代理多了起來(lái), 這個(gè)類(lèi)就會(huì)變得十分臃腫, 我們可以使用block來(lái)寫(xiě), 代碼會(huì)更加緊致, 開(kāi)發(fā)者調(diào)用起來(lái)也為方便.如下所示 :

- (void)download {
    NSURL *url = [[NSURL alloc] initWithString:@"www.baidu.com"];
    JRDownloader *downloader = [[JRDownloader alloc] initWithURL:url];
    [downloader startDownloadWithCompletionHandler:^(NSData *data){
        self.data = data;
    }];
}

把completion handler塊傳遞給start方法, 當(dāng)方法調(diào)用完畢方法內(nèi)部就會(huì)調(diào)用該block把data傳進(jìn)來(lái). 這種辦法是不是更加聰明呢~

然而我們?cè)傧胍幌? 終于給我們發(fā)現(xiàn)代理模式的一個(gè)缺點(diǎn)了! 假設(shè)我們要同時(shí)開(kāi)啟若干個(gè)下載器, 那么在代理方法里面是不是就要對(duì)各個(gè)下載器進(jìn)行判斷然后執(zhí)行對(duì)應(yīng)的操作呢? 很麻煩對(duì)吧, 一大堆判斷, if, else, if, else. 然而handler的優(yōu)勢(shì)馬上展現(xiàn)出來(lái)了.

- (void)downloadHeaderData {
    NSURL *url = [[NSURL alloc] initWithString:@"www.baidu.com"];
    JRDownloader *downloader = [[JRDownloader alloc] initWithURL:url];
    [downloader startDownloadWithCompletionHandler:^(NSData *data){
        // do something
        self.headerData = data;
    }];
}
- (void)downloadFooterData {
    NSURL *url = [[NSURL alloc] initWithString:@"www.baidu.com"];
    JRDownloader *downloader = [[JRDownloader alloc] initWithURL:url];
    [downloader startDownloadWithCompletionHandler:^(NSData *data){
        // do something
        self.FooterData = data;
    }];
}

一目了然, 我們根本不需要對(duì)哪個(gè)下載器進(jìn)行判斷, 再處理響應(yīng)的數(shù)據(jù), 因?yàn)樵趧?chuàng)建下載器的時(shí)候已經(jīng)設(shè)定好了.

而且我們還能用handler很easy地處理下載成功和失敗的情況! 例如,

- (void)download {
    NSURL *url = [[NSURL alloc] initWithString:@"www.baidu.com"];
    JRDownloader *downloader = [[JRDownloader alloc] initWithURL:url];
    [downloader startDownloadWithCompletionHandler:^(NSData *data){
        // handler success
    } failureHandler: ^(NSError *error){
        // handler failure
    }];
}

除了這種設(shè)計(jì)模式以外, 還有一個(gè)就是把成功和失敗都放在一個(gè)handler中來(lái)處理, 例如,

- (void)download {
    NSURL *url = [[NSURL alloc] initWithString:@"www.baidu.com"];
    JRDownloader *downloader = [[JRDownloader alloc] initWithURL:url];
    [downloader startDownloadWithCompletionHandler:^(NSData *data, NSError *error){
        if (error) {
            // handler failure
        } else {
            // handler success
        }
    }];
}

這里說(shuō)說(shuō)各自的優(yōu)缺點(diǎn) :

  • 一個(gè)block
    1.代碼長(zhǎng), 比較復(fù)雜
    2.失敗的時(shí)候還能拿到數(shù)據(jù), 做點(diǎn)事情
    3.數(shù)據(jù)長(zhǎng)度不符合時(shí), 需要按下載失敗處理
  • 兩個(gè)block
    1.代碼清晰

兩種方法都可以, 蘿卜青菜各有所愛(ài). 不過(guò)綜上和結(jié)合蘋(píng)果的API, 我建議用一個(gè)block來(lái)同時(shí)處理成功和失敗.

補(bǔ)充 : 使用handler的好處還有, 可以通過(guò)傳遞多一個(gè)隊(duì)列的參數(shù), 指定該block在哪個(gè)隊(duì)列上執(zhí)行.

用塊引用其所屬對(duì)象時(shí), 注意避免循環(huán)引用

__weak typeof(self) wself = self;
self.completionHandler = ^(NSInteger result) { 
    [wself removeObserver: wself forKeyPath:@"dog"];
};

這里我就不介紹__weak來(lái)避免循環(huán)引用了, 要說(shuō)的是蘋(píng)果稱為"Strong-Weak Dance"的一個(gè)技術(shù).

我們知道, 使用__weak確實(shí)可以避免循環(huán)引用. 但是還有點(diǎn)小瑕疵, 假如block是在子線程中執(zhí)行, 而對(duì)象本身在主線程中被銷(xiāo)毀了, 那么block內(nèi)部的弱引用就會(huì)置空(nil). 而這在KVO中會(huì)導(dǎo)致崩潰.

Strong-Weak Dance就是針對(duì)以上問(wèn)題的. 使用方法很簡(jiǎn)單, 只需要加一行代碼

__weak typeof(self) wself = self;
self.completionHandler = ^(NSInteger result) { 
    __strong typeof(wself) sself = wself;
    [sself removeObserver: sself forKeyPath:@"dog"];
};

這樣一來(lái), block中sself所指向的對(duì)象在block執(zhí)行完畢之前都不會(huì)被釋放掉, 因?yàn)樵贏RC下, 只要對(duì)象被強(qiáng)引用著, 就不會(huì)被釋放.

這里推薦一篇文章, 對(duì)Strong-Weak Dance分析得很周到
對(duì) Strong-Weak Dance 的思考


第7章 : 系統(tǒng)框架

我們之所以能夠編寫(xiě)OS X和iOS的程序, 全是因?yàn)橛邢到y(tǒng)框架在背后默默地支持著我們. 系統(tǒng)的框架非常強(qiáng)大, 以置于我們想要實(shí)現(xiàn)一些功能的時(shí)候, 可以不妨先找找系統(tǒng)有沒(méi)有已經(jīng)幫我們實(shí)現(xiàn)好的方法, 往往可以事半功倍.

多用塊枚舉, 少用for循環(huán)

for循環(huán)

用C語(yǔ)言寫(xiě)到OC, 我們?cè)偈煜げ贿^(guò)了

NSArray *array = /* ... */
for (int i = 0; i < array.count; i++) {
    id obj = array[i];
    // do something with 'obj'
}
快速遍歷

OC 2.0的新特性, 語(yǔ)法簡(jiǎn)潔, 好用, 唯一的缺點(diǎn)就是沒(méi)有索引

NSArray *array = /* ... */
**for** (id obj **in** array) {
    // do something with 'obj'
}
用OC 1.0 的NSEnumerator來(lái)遍歷

這種方法已經(jīng)過(guò)時(shí)了, 這里不介紹.

基于塊的遍歷方式

NSArray, NSDictionary, NSSet都有基于block的遍歷方式, 例如數(shù)組的 :

NSArray *array = /* ... */
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // do something with 'obj'
    if (shouldStop) {
        *stop = YES;
    }         
}];

不僅能簡(jiǎn)單遍歷, 還能控制什么時(shí)候退出遍歷. 還有更高級(jí)的塊遍歷方法能夠指定選項(xiàng), 例如反向遍歷, 并行快速遍歷等等.

// 數(shù)組的方法 
- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(ObjectType obj, NSUInteger idx, BOOL *stop))block;
// 字典的方法
- (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(KeyType key, ObjectType obj, BOOL *stop))block;

構(gòu)建緩存時(shí)選用NSCache而非NSDictionary

NSCache是Foundation框架中為緩存而生的類(lèi). NSCache對(duì)比NSDictionary的優(yōu)點(diǎn)

  • 當(dāng)系統(tǒng)的資源將要耗盡時(shí), 它可以自動(dòng)刪減緩存.
  • 先行刪減"最久未使用的對(duì)象"
  • NSCache不會(huì)"拷貝"鍵, 而是"保留"鍵. 所有在鍵不支持拷貝的情況下比字典方便.
  • 線程安全, 開(kāi)發(fā)者無(wú)需自己編寫(xiě)加鎖代碼
  • 開(kāi)發(fā)者可以操縱緩存刪減其內(nèi)容的時(shí)機(jī), 也就是清理緩存的策略
    1.緩存中的對(duì)象總數(shù)(countLimit屬性)
    2.緩存中所有對(duì)象的總開(kāi)銷(xiāo)(totalCostLimit屬性)

NSCache還經(jīng)常會(huì)跟NSPurgeableData(NSMutableData的子類(lèi))搭配使用, 當(dāng)NSPurgeableData對(duì)象所占內(nèi)存被系統(tǒng)所丟棄時(shí), 該對(duì)象自身也會(huì)從緩存中移除.

該類(lèi)有2個(gè)特別的方法

  • beginContentAccess : 告訴它, 現(xiàn)在還不應(yīng)該丟棄自己所占據(jù)的內(nèi)存
  • endContentAccess : 告訴它, 必要時(shí)可以丟棄自己所占據(jù)的內(nèi)存了

緩存使用得當(dāng), 將會(huì)大大提高應(yīng)用程序的響應(yīng)速度. 但并不是什么東西都需要緩存, 只有那種"重新計(jì)算起來(lái)費(fèi)勁"的數(shù)據(jù), 才值得放入緩存中. 例如從網(wǎng)絡(luò)獲取或從磁盤(pán)讀取的數(shù)據(jù).

精簡(jiǎn) initialize 與 load 的實(shí)現(xiàn)代碼

+ (void)load;

該方法特點(diǎn)如下 :

  • 程序一啟動(dòng), 就會(huì)調(diào)用每個(gè)類(lèi)及分類(lèi)的load方法.
  • 執(zhí)行子類(lèi)的load方法之前必定會(huì)先執(zhí)行超類(lèi)的load方法.
  • 執(zhí)行分類(lèi)的load方法之前必定會(huì)先執(zhí)行本類(lèi)的load方法
  • 該方法不遵從繼承規(guī)則, 也就是說(shuō)如果子類(lèi)沒(méi)有實(shí)現(xiàn)load方法, 那么不管其超類(lèi)有沒(méi)有實(shí)現(xiàn)該方法, 系統(tǒng)都不會(huì)自動(dòng)調(diào)用.
  • 該方法會(huì)阻塞主線程!!!!!!!!!!!!!

`+ (void)initialize;

該方法特點(diǎn)如下 :

  • 該方法會(huì)在程序首次使用該類(lèi)之前調(diào)用, 且只調(diào)用一次. 也就是說(shuō)該方法是懶加載的, 如果某個(gè)類(lèi)一直沒(méi)使用, 就永遠(yuǎn)不會(huì)調(diào)用.
  • 該方法一定會(huì)在"線程安全的環(huán)境"下執(zhí)行. 意思就是只有執(zhí)行該方法的那個(gè)線程可以操作類(lèi)和類(lèi)實(shí)例. 其他線程都要先阻塞, 等著該方法執(zhí)行完.
  • 該方法遵從繼承規(guī)則, 子類(lèi)如果沒(méi)實(shí)現(xiàn)它, 則會(huì)調(diào)用超類(lèi)的實(shí)現(xiàn)代碼.

回到主題, 為什么initialize 與 load的代碼要盡量精簡(jiǎn)呢?

  • load方法會(huì)阻塞主線程;
  • initialize方法會(huì)阻塞當(dāng)前線程, 如果該線程恰好是主線程, 你懂的...
  • 開(kāi)發(fā)者無(wú)法控制類(lèi)的初始化時(shí)機(jī), 也許將來(lái)蘋(píng)果會(huì)修改類(lèi)的初始化方式呢?
  • 如果某類(lèi)的該兩方法引入了其他類(lèi), 而那些類(lèi)又沒(méi)初始化, 系統(tǒng)就會(huì)迫使其初始化, 而那些類(lèi)初始化一不小心又剛剛好引入了某類(lèi), 則會(huì)出現(xiàn)"依賴環(huán)"

綜上, 盡量不要重寫(xiě)load方法, 而initialize方法只應(yīng)該用來(lái)

  1. 設(shè)置內(nèi)部數(shù)據(jù), 不應(yīng)該調(diào)用其他方法, 哪怕是本類(lèi)自己的方法.
  2. 如果單例類(lèi)在首次使用之前需要做一些操作, ok, 在這里執(zhí)行吧.

NSTimer會(huì)保留其目標(biāo)對(duì)象

這種情況跟block引用self那種情況差不多. 目標(biāo)對(duì)象保留計(jì)時(shí)器, 計(jì)時(shí)器反過(guò)來(lái)又保留對(duì)象, 則會(huì)導(dǎo)致循環(huán)引用.

我們可以利用block或者"Strong-Weak Dance"來(lái)解決此問(wèn)題.


啰哩啰嗦地講了好多好多, 也算是分享了自己的一點(diǎn)看法, 字有點(diǎn)多, 如果有人能耐心看完的話絕壁是真愛(ài)哈哈..其實(shí)寫(xiě)文章并不是要寫(xiě)給誰(shuí)看, 是對(duì)自己語(yǔ)言總結(jié)能力, 書(shū)寫(xiě)能力的一種鍛煉, 有句話說(shuō)得好, 就算沒(méi)有人看你的博客, 你也要堅(jiān)持寫(xiě)下去. 共勉!


歡迎大家關(guān)注@Jerry4me, 我會(huì)不定時(shí)更新一些學(xué)習(xí)心得與文章.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末闷营,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子膛锭,更是在濱河造成了極大的恐慌粮坞,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件初狰,死亡現(xiàn)場(chǎng)離奇詭異莫杈,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)奢入,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)筝闹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人腥光,你說(shuō)我怎么就攤上這事关顷。” “怎么了武福?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵议双,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我捉片,道長(zhǎng)平痰,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任伍纫,我火速辦了婚禮宗雇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘莹规。我一直安慰自己赔蒲,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著舞虱,像睡著了一般欢际。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上砾嫉,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天幼苛,我揣著相機(jī)與錄音,去河邊找鬼焕刮。 笑死舶沿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的配并。 我是一名探鬼主播括荡,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼溉旋!你這毒婦竟也來(lái)了畸冲?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤观腊,失蹤者是張志新(化名)和其女友劉穎邑闲,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體梧油,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡苫耸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了儡陨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片褪子。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖骗村,靈堂內(nèi)的尸體忽然破棺而出嫌褪,到底是詐尸還是另有隱情,我是刑警寧澤胚股,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布笼痛,位于F島的核電站,受9級(jí)特大地震影響琅拌,放射性物質(zhì)發(fā)生泄漏缨伊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一财忽、第九天 我趴在偏房一處隱蔽的房頂上張望倘核。 院中可真熱鬧泣侮,春花似錦即彪、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)漏益。三九已至,卻和暖如春深胳,著一層夾襖步出監(jiān)牢的瞬間绰疤,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工舞终, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留轻庆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓敛劝,卻偏偏與公主長(zhǎng)得像余爆,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子夸盟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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