前言
這本書(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è).
- 如果枚舉類(lèi)型的多個(gè)選項(xiàng)不需要組合使用, 則用NS_ENUM
typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
UIViewAnimationTransitionNone,
UIViewAnimationTransitionFlipFromLeft,
UIViewAnimationTransitionFlipFromRight,
UIViewAnimationTransitionCurlUp,
UIViewAnimationTransitionCurlDown,
};
- 如果枚舉類(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了. 如圖
有人可能會(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)在的編譯器下,
- @property會(huì)為屬性生成setter和getter的方法聲明, 同時(shí)調(diào)用@synthesize ivar = _ivar生成_ivar實(shí)例變量和存取方法的實(shí)現(xiàn)
- 手動(dòng)調(diào)用@synthesize可以用來(lái)修改實(shí)例變量的名稱
- 手動(dòng)調(diào)用@dynamic可以告訴編譯器: 不要自動(dòng)創(chuàng)建
實(shí)現(xiàn)屬性所用的實(shí)例變量, 也不要為其創(chuàng)建實(shí)例變量的存取方法聲明與實(shí)現(xiàn).
以上文檔說(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ù)姆椒? 流程如下
- 查找接收者的所屬類(lèi)的cache列表, 如果沒(méi)有則下一步
- 查找接收者所屬類(lèi)的"方法列表"
- 如果能找到與選擇子名稱相符的方法, 就跳至其實(shí)現(xiàn)代碼
- 找不到, 就沿著繼承體系繼續(xù)向上查找
- 如果能找到與選擇子名稱相符的方法, 就跳至其實(shí)現(xiàn)代碼
- 找不到, 執(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)的.
乍一看覺(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..
盡量在第一步就把消息處理了, 因?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, ...)
OC運(yùn)行時(shí)系統(tǒng)提供了幾個(gè)方法能夠用來(lái)操作這張表, 動(dòng)態(tài)增加, 刪除, 改變選擇子對(duì)應(yīng)的方法實(shí)現(xiàn), 甚至交換兩個(gè)選擇子所映射到的指針. 如,
如何交換兩個(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)
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)
- 設(shè)置內(nèi)部數(shù)據(jù), 不應(yīng)該調(diào)用其他方法, 哪怕是本類(lèi)自己的方法.
- 如果單例類(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í)心得與文章.