<<Effective Objective-C 2.0編寫高質量iOS與OS X代碼的52個有效方法>>這本書相信很多人都讀過,最近又重溫了一遍,這本書很薄,只有209頁,幾個小時就可以看完。雖然很薄,但是寫的很好,這本書的內容結構是這樣的,先是擺出結論,然后再花幾倍的筆墨來解釋為什么要這樣。如果作者去除解釋部分,估計本書可以壓縮到80頁。這本書我看了好幾遍了,但是還是感覺意猶未盡,于是自己就根據它的每個章節提了一些問題。(有的章節其實沒必要提問題的,就是了解一下就夠了,但是由于慣例,所以這些不太適合提問題的章節還是提問了。) 其中的少數問題其實”超綱”了,也就是僅憑本書是答不出或者答不完整的,還需要查很多的資料才能完全答出來。有興趣的同學可以看看自己能答出來多少。由于本人水平有限,有的問題出的不是很好。
可以點擊這里下載PDF版
第一章:熟悉Objective-C
第一條:了解Objective-C語言的起源
- 如何理解OC的動態綁定?
- 說說[xxObj doSomething]從開始執行到結束的過程。
- 從運行程序到main方法的調用,這之間發生了什么?
建議大家好好看看runtime component的實現過程,runtime源碼鏈接,重點看看objc-runtime-new.mm類。
第二條: 在類的文件中盡量少引用其他頭文件
- 你有用過@class嗎?請說一下它的常用情景。
- #import和#include的區別。
第三條:多用字面量語法,少用與之等價的方法
- 字面量語法相比傳統創建方法的優勢?
第四條:多用類型常量,少用#define預處理指令
- 類型常量相對于#define預處理指令的優點?
- 請手寫一個通知名稱的聲明和實現。
第五條:多用枚舉表示狀態、選項、狀態碼
- NS_OPTIONS為什么能實現組合多個選項的功能?
- c語言的enum不是夠用了嗎?Foundation框架為啥要再弄一個NS_ENUM的宏?
第二章:對象、消息、運行期
第六條:理解“屬性”這一概念
- 為什么說atomic不能保證屬性在多線程中是安全的?請舉個例子說明。你如何保證iOS中多線程中訪問可變數組,可以得到正確的數據?
- 請分別說明iOS開發中@property的屬性特征中的原子性,讀寫權限和內存管理語義的默認情況?(如什么都不寫的話,是nonatomic還是atomic?)
- weak是如何實現變量銷毀時指向nil的?
- @synthesize和@dynamic有什么區別?你有使用過它們嗎?什么情況下會需要使用它們?請舉例
- 你有在.h文件中使用readonly嗎?為啥要這么設計?一般還需要在.m中做些什么來配合readonly使用?
第七條:在對象內部盡量直接訪問實例變量
- 什么時候使用屬性,什么時候使用實例變量?分別說說使用它們的優缺點。
- 如果在初始化方法和dealloc方法中使用了屬性,可能會出現什么問題?說一說出現這種問題的原因。
第八條:理解“對象等同性”這一概念
- 自定義對象,如何實現isEqual?
- hash方法存在的意義?hash方法如何實現?為什么?為什么說相同的對象一定具有相同的哈希碼,而兩個哈希碼相同的對象卻未必相同?
第九條:以“類族模式”隱藏實現細節
- Foundation框架和UIKit框架中都有類應用了“類族模式”設計,請至少說出2個采用了這種模式的類。
- “類族模式”有什么好處?請你設計一個采用了“類族模式”的類。
第十條:在既有類中使用關聯對象存放自定義數據
- 分類中的@property屬性跟普通類中使用@property聲明屬性有什么不同?如何在分類中實現跟普通類中使用@property一樣的效果?
- 你在項目中或者看到別人使用過關聯對象技術嗎?
第十一條:理解“objc_msgSend”的作用
- SEL的本質是什么?
- 給對象發送消息是如何”動態消息派發系統”派發的?說一說這個過程(消息傳遞機制)。
- 蘋果為動態消息派發系統做了哪些優化來提高查找方法列表的速度?
第十二條:理解消息轉發機制
- 請說說消息轉發機制?你在項目中有使用過消息轉發嗎(在開發中的具體應用場景)?
第十三條:用“方法調配技術”調試“黑盒方法”
- 如何動態添加方法和替換方法實現?
第十四條:理解“類對象”的用意
- 說說你對類對象的理解?清楚它的內存布局嗎?
第三章:接口與API設計
第十五條:用前綴避免命名空間沖突
- 你在項目中創建類和設計分類方法時,如何避免類的重復定義及分類方法的相互覆蓋?
第十六條:提供“全能初始化方法”
- 如果讓你設計一個類,提供多個初始化方法,你的.h文件怎么設計接口?
- 為什么在init方法中要調用super init,不調用行不行?請說明理由。
第十七條:實現description方法
- 有使用過LLDB嗎?
- 在Xcode的打印含有中文字符串的字典和數組時,會出現編碼和不是正常的文字,你是怎么解決的?
第十八條:盡量使用不可變對象
- 在某個類的頭文件中,有個屬性
@property (nonatomic, strong) NSMutableArray *friends;
,這么寫有什么問題嗎?如果有問題,請說明問題,如何修改?
第十九條:使用清晰而協調的命名方式
- 你平時是怎么樣為方法和類與協議命名的?
第二十條:為私有方法名加前綴
- 為什么要給私有方法加前綴?
第二十一條:理解Objective-C錯誤模型
- 第三方崩潰日志收集平臺的設計原理是什么?如果讓你自己設計一個,你怎么設計出類似的功能。
第二十二條:理解NSCopying協議
- 什么是深拷貝,什么是淺拷貝,有什么區別?
- 如何實現自定義對象支持拷貝功能?
第四章:協議與分類
第二十三條:通過委托與數據源協議進行對象間通信
- 什么是委托(代理)模式?這個模式的作用是什么?
- UITableView為什么有了delegate還要增加dataSource屬性,這2個屬性為什么都是weak?
- UITableView的某些代理方法調用的非常頻繁,頻繁判斷某個委托對象是否響應代理方法比較耗費性能,請問如何優化?
第二十四條:將類的實現代碼分散到便于管理的數個分類之中
- 某個類的.m過于臃腫,雜糅了太多的方法,如何重構它?
- 分類里實現了與本類相同的方法,為什么會調用分類里面的方法而不調用本類的原來方法?
第二十五條:總是為第三方類的分類名稱加前綴
- 增加分類和分類方法時要注意什么?(主要是命名上)
第二十六條:勿在分類中聲明屬性
- 為什么分類中的@property不能生成實例變量?
第二十七條:使用“class-continuation”分類隱藏實現細節
- 匿名分類跟分類有什么區別?
第二十八條:通過協議提供匿名對象
- 協議除了用于代理模式,還有什么作用?
第五章:內存管理
第二十九條:理解引用計數
- 說說你對引用計數的理解?蘋果設計引用計數的用意是什么?
第三十條:以ARC簡化引用計數
- ARC和MRC有什么區別?ARC是如何實現的?
- 使用了ARC還會出現內存泄露嗎?請舉例說明。
- weak是如何做到自動置為nil的?
第三十一條:在delloc方法中只釋放引用并解除監聽
- ARC下我們在dealloc方法中應該做什么事情?
第三十二條:編寫“異常安全代碼”時留意內存管理問題
- Java等語言中經常大量用到異常,為什么OC中比較少出現異常機制的代碼?
第三十三條:以弱引用避免保留環
- weak和unsafe_unretained有什么區別?哪些情況下會用到weak,請舉例。
第三十四條:以 “自動釋放池塊” 降低內存峰值
- 項目中有用過自動釋放池嗎?有什么作用?
第三十五條:用“僵尸對象”調試內存管理問題
- 如何監測項目中出現的“僵尸對象”?
第三十六條:不要使用retainCount
- 為什么不要用retainCount? retainCount不準的原因在哪里?
第六章:塊與大中樞派發
第三十七條:理解”塊”這一概念
- block有哪幾種?它的內部結構是什么樣的?
第三十八條:為常用的塊類型創建typedef
- 請手寫一個block屬性,并分別使用typedef重新定義它。
第三十九條:用handler塊降低代碼分散程度
- 使用block代替代理有啥有點和缺點?分別在何種情況使用比較合適?
- NSNotificationCenter在發送通知的時候,是同步還是異步?
第四十條:用塊引用其所屬對象時不要出現保留環
- 什么時候容易出現block的循環引用?出現這種情況除了使用weak,還有其他的解決方法嗎?
第四十一條:多用派發隊列,少用同步鎖
- atomic的實現原理是咋樣的?
- 如何用GCD解決訪問可變數組過程中的多線程安全問題?
第四十二條:多用GCD,少用performSelector系列方法
- performSelector方法有什么局限性和缺點? 如何解決。
第四十三條:掌握GCD以及操作隊列的使用時機
- GCD和NSOperation各有哪些特點?你平時喜歡用哪個?
第四十四條:通過Dispatch Group機制,根據系統資源狀況來執行任務
- 使用過dispatch group嗎?它的作用是什么?
- GCD大概有哪些比較常用的函數?
第四十五條:使用dispatch_once來執行只需要運行一次的線程安全代碼
- 請分別用2種方式來實現單例。
- dispatch_once為什么說是線程安全的?
第四十六條:不要使用dispatch_get_current_queue
- 什么情況會出現死鎖?請用GCD模擬一種情況。
第七章:系統框架
第四十七條:熟悉系統框架
- 除了Foundation和UIKit框架,你還知道哪些系統框架?
第四十八條:多用塊枚舉,少用for循環
- 你知道哪些遍歷的方法?各有什么特點?
第四十九條:對自定義其內存管理語義的collecion使用無縫橋接
- 你了解橋接技術嗎?為什么需要橋接?
第五十條:構建緩存時選用NSCache而非NSDictionary
- 你了解NSCache嗎?它相比于NSDictionary有哪些優點?
第五十一條:精簡initialize與load的實現代碼
- 有聽過load和initialize方法嗎?說說它們的特點。
第五十二條:別忘了NSTimer會保留其目標對象
-
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(repeatDoSomething) userInfo:nil repeats:YES];
這句代碼有什么問題?如何解決?
本書精簡版
1. 了解Object-C 語言的起源
消息結構和函數調用的區別:
使用消息結構的語言,其運行時所應執行的代碼由運行環境來決定;而使用函數調用的語言,則由編譯器決定。另外,CGRect結構體,儲存在棧區
2. 在類的頭文件中盡量少引用其他頭文件
如果不是非用不可,盡量用@class(向前聲明)代替#import,加快編譯速度。
3. 多用字面量語法,少用與之等價的方法
id object1 = @"1";
id object2 = nil;
id object3 = @"3";
NSArray *arrayA = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSArray *arrayB = @[object1, object2, object3];
上面的代碼我們發現,arrayA雖然能創建出來,但是其中卻只有object1一個對象,原因在于“arrayWithObjects:”方法會依次處理各個參數,直到發現nil為止,由于object2是nil,所以該方法會提前結束,按字面量語法創建arrayB時會拋出異常。
所以:
使用字面量語法創建數組、字典...更安全,語法更簡潔。
4. 多用類型常量,少用#define 預處理指令
僅在“編譯單元(.m文件內)”內使用
define ANIMATION_DURATION 0.3 // 不建議
static NSTimeInterval const kANIMATION_DURATION = 0.3; // 建議
需要外露使用
// EOCAnimatedView.h
extern const NSTimeInterval EOCAnimateViewAnimationDuration;
// EOCAnimatedView.m
const NSTimeInterval EOCAnimateViewAnimationDuration = 0.3;
上述宏定義,預處理指令會把源代碼中的ANIMATION_DURATION字符串替換成0.3。假設此指令聲明在某個頭文件中,那么所有引入了這個頭文件的代碼,其ANIMATION_DURATION都會被替換。
涉及知識點:
- 常量命名:若常量局限于某“編譯單元”(也就是實現文件.m)之內,則在前面加上字母k;若常量在類之外可見,則通常以類名為前綴。
- 宏:
1.作用:在預編譯階段替換;
2.其他:在預編譯階段執行,不做編譯檢查,不會報編譯錯誤; - const:
1.作用:僅僅用來修飾右邊的變量,被const修飾的變量是只讀的;
2.其他:在編譯階段執行,會做編譯檢查,會報編譯錯誤; - static的作用:
1.修飾局部變量:(1). 延長局部變量的聲明周期,程序結束才會銷毀;(2).局部變量只會生成一份內存,只會初始化一次;(3). 改變局部變量的作用域;
2.修飾全局變量:(1). 只能在本文件中訪問,修改全局變量的作用域,生命周期不會改變;(2). 避免重復定義全局變量; - extern:
1.作用:只是用來獲取全局變量(包括全局靜態變量)的值,不能用于定義變量;
2.原理:先在擋圈文件中查找有沒有全局變量,沒有找到,才會去其他文件查找; - static與const聯合使用:
1.作用:聲明一個只讀的靜態變量;(在每個文件都需要定義一份靜態全局變量)
2.使用場景:在一個文件中經常使用的字符串常量; - extern與const聯合使用:
1.作用:只需要定義一份全局變量,多個文件共享;
2.使用場景:在多個文件中經常使用同一個字符串常量;
5. 用枚舉表示狀態、選項、狀態碼
在處理枚舉類型的switch語句中不要實現default分支。這樣的話,加入新枚舉之后,編譯器就會提示開發者:switch語句并未處理所有枚舉。
6. 理解“屬性”這一概念
@interface EOCPersion: NSObject {
@public
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someInternalData;
}
@end
編寫時使用實例變量有個問題:就是對象布局在編譯期就已經固定了,只要碰到訪問_firstName變量的代碼,編譯器就把其替換為“偏移量”(offset),這個偏移量是“硬編碼”(hardcode),表示該變量距離存放對象的內存區域的起始地址有多遠,這樣做目前沒問題,但是如果又加了一個實例變量,如:
@interface EOCPersion: NSObject {
@public
NSString *_dateOfBirth;
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someInternalData;
}
@end
那么原來_firstName的偏移量現在卻指向_dateOfBirth了,把偏移量硬編碼于其中的那些代碼都會讀取到錯誤的值。所以,修改之后必須重新編譯,否則就會出錯。Obejcet-C的做法是,把實例變量當做一種存儲偏移變量所用的“特殊變量”,交由“類對象”保管,偏移量會在運行期查找,如果類的定義變了,那么存儲的偏移量也就變了,這樣的話,無論何時訪問實例變量,總能使用正確的偏移量。另外還有一種解決辦法就是,盡量不要直接訪問實例變量,而應該通過存取方法來做。
atomic一定安全嗎?
具備atomic特質的獲取方法會通過鎖定機制來確保其操作的原子性,但這無法保證絕對的“線程安全”,例如:一個線程在連續多次讀取某個屬性值的過程中有別的線程在同時改寫該值,那么即便將屬性聲明為atomic,也還是會讀到不同的屬性值。
7. 在對象內部盡量直接訪問實例變量
在對象之外訪問實例變量時,總是應該通過屬性來做,然后在對象內部訪問實例變量是該如何?作者建議
在讀取實例變量的時候采用直接訪問的形式,而在設置實例變量的時候通過屬性來做。
8. 理解“對象等同性”這一概念
重寫hash的一種思路,相對高效
- (NSUInteger)hash
{
NSInteger firstNameHash = [_firstName hash];
NSInteger lastNameHash = [_lastName hash];
NSInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}
一般重寫“isEqual:”方法
- (BOOL)isEqualToPerson:(EOCPerson *)otherPerson
{
if (self == otherPerson) return YES;
if (![_firstName isEqualToString:otherPerson.firstName]) return NO;
if (![_lastName isEqualToString:otherPerson.lastName]) return NO;
if (_age != otherPerson.age) return NO;
return YES;
}
-(BOOL)isEqual:(id)object
{
if ([self class] == [object class]) {
return [self isEqualToPerson:(EOCPerson *)object];
} else {
return [super isEqual:object];
}
}
9. 以“類簇模式”隱藏實現細節
10. 在既有類中使用關聯對象存放自定義數據
可以把關聯對象想象成NSDictionary,只不過設置關聯對象時用到的鍵(key)是個“不透明的指針”。
11. 理解objc_msgSend的作用
如果某函數的最后一項操作是調用另外一個函數,那么久可以運用“尾調用優化”技術。編譯器會生成調轉至另一函數所需要的指令碼,而且不會向調用堆棧中推入新的“棧幀”。只有當某函數的最后一個操作僅僅是調用其他函數而不會將其返回值另做他用時,才能執行“尾調用優化”。這項優化對objc_msgSend非常關鍵,如果不這么做的話,那么每次調用Object-C方法之前,都需要為調用objc_msgSend函數準備“棧幀”,大家在“棧蹤跡”中可以看到這種“棧幀”。此外,若是不優化,還會過早的發生“棧溢出”現象。
12. 理解消息轉發機制
基本就是消息轉發的大體流程,需要注意的一點就是步驟越往后,處理消息的代價就越大,最好能在第一步就處理完,這樣的話,運行期系統就看可以將此方法緩存起來,如果這個類的實例稍后還受到同樣的選擇子(方法選擇器),那么根本無須啟動消息轉發流程。
13. 用“方法調配技術”調試“黑盒方法”
利用動態特性,運行時動態添加、交換方法
14. 理解“類對象”的用意
盡量使用類型信息查詢方法來確定對象類型,而不要直接比較類對象,因為某些對象可能實現了消息轉發功能。11~14 全是老成長談的runtime機制,這個一時半會且三言兩語是無法講解清楚的(單拿出一整篇文章來講都不見得能面面俱到
15. 用前綴避免命名空間沖突
16. 提供“全能初始化方法”
17. 實現description方法
- 實現description方法返回一個有意義的字符串,用以描述該實例;
- 若想在調試時打印出更詳盡的對象描述信息,則應實現debugDescription方法;
18. 盡量使用不可變對象
這個意思是說:
盡量把對外公布出來的屬性設為只讀,而且只有在確有必要的時候才將屬性對外公布。
盡管把屬性設置成readOnly之后,可以防止外部亂改屬性值,但并不是完全不能改,外界仍然可以通過“鍵值編碼”(KVC)技術設置這個屬性值。
19. 使用清晰而協調的命名方式
別怕麻煩,要見名知意,方法命名可參考系統方法名。
20. 為私有方法名加前綴
作者建議:
編寫類的實現代碼時,經常要寫一些只在內部使用的方法。應該為私有方法的名稱前加上某些前綴,這有助于調試,因為據此很容易就能把公共方法和私有方法區別開。可以用“p_”打頭。(不要單單只用一個下劃線做私有方法的前綴,因為這種做法是預留給蘋果大大用的)。
21. 理解Object-C錯誤模型
當出現“不那么嚴重的錯誤”時,Object-C語言所用的編程范式為:令方法返回nil/0,或者使用NSError。
NSError對象里封裝了三條信息:
- Error domain(錯誤范圍,其類型為字符串);
- Error code(錯誤碼,其類型為整數);
- User info(用戶信息,其類型為字典)
22. 理解NSCoding協議
想要實現復制功能,需要遵從NSCoding協議,實現copyWithZone:方法;
深拷貝:
深拷貝:在拷貝對象自身時,將其底層數據也一并復制過去。Foundation框架中所有collection類在默認情況下都執行淺拷貝。
23. 通過委托與數據源協議進行對象間通信
這里主要是講了使用delegate的場景以及相關的注意事項,比如使用weak防止循環引用等。這里有一個優化點:(不過筆者很少這么干,不是很少,是壓根沒有??)
我們通常把委托對象能否響應某個協議方法這一信息緩存起來,以優化程序效率。 將方法響應能力緩存起來的最佳途徑是實用“位段”數據類型。這是一項乏人問津的C語言特性,但在此處用起來卻正合適。
// 協議聲明部分
@class EOCNetworkFetcher;
@protocol EOCNetworkFetcherDelegate
@optional
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data;
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error;
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didUpdateProgressTo:(float )progress;
@end
在.m文件中 定義一個結構體_delegateFlags用來緩存方法響應能力
@interface EOCNetworkFetcher ()
{
struct {
unsigned int didReceiveData : 1;
unsigned int didFailWithError : 1;
unsigned int didUpdateProgressTo : 1;
} _delegateFlags;
}
@end
@implementation EOCNetworkFetcher
- (void)setDelegate:(id<EOCNetworkFetcherDelegate>)delegate
{
_delegate = delegate;
_delegateFlags.didReceiveData = [_delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
_delegateFlags.didFailWithError = [_delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)];
_delegateFlags.didUpdateProgressTo = [_delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
}
// 在使用的時候 直接判斷
if (_delegateFlags.didUpdateProgressTo) {
[_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
}
@end
24. 將類的實現代碼分散到便于管理的數個分類之中
使用分類機制可以把類的實現代碼劃分成易于管理的小塊;
25. 總是為第三方類的分類名稱增加前綴
- 向第三方類中添加分類時,總應該給其名稱加上你專用的前綴;
- 向第三方類中添加分類時,總應該給其中的方法名加上你專用的前綴;
26. 勿在分類中聲明屬性
把封裝數據所用的全部屬性都定義在主接口里。不建議在分類中添加屬性,雖然通過關聯對象能夠解決在分類中不能合成實例變量的問題。但不是最理想的,且在內管理上容易出錯,分類只是一種手段,目的是在于擴展類的功能,而非封裝數據。
27. 使用“class-continuation分類” 隱藏實現細節
其實就是類擴展
- 通過“class-continuation分類”向類中新增實例變量;
- 如果某屬性在主接口中聲明為“只讀”,而類的內部又要用設置方法修改此屬性,那么就在“class-continuation分類”中將其擴展為“可讀寫”。
- 把私有方法的原型聲明在“class-continuation分類”里面;
- 若想使類所遵循的協議不為人所知,則可于“class-continuation分類”中聲明。
28. 通過協議提供匿名對象
29. 理解引用計數
- (void)setFoo:(id)foo
{
[foo retain];
[_foo release];
_foo = foo;
}
此方法將保留新值釋放舊值,然后更新實例變量,令其指向新值。順序很重要。假如還未保留新值就先把舊值釋放了,而且這兩個值又指向同一個對象,那么,限執行的release操作就有可能導致系統將此對象永久回收。而后續的retain操作則無法令這個已經徹底回收的對象復生,于是實例變量就成了懸掛指針。
30. 以ARC簡化引用計數
- ARC在調用這些方法時,并不通過普通的Object-C消息派發機制,而是直接調用其底層的C語言版本,這樣性能更好,因為保留及釋放操作需要頻繁執行,所以直接調用底層函數能節省很多CPU周期,比如ARC會調用與retain等價的底層函數objc_retain。
- 在編譯期,ARC會把能夠相互抵消的retain、release、autorelease操作簡約。如果發現在同一個對象上執行多次的“保留”與“釋放”操作,那么ARC有時可以成對的移除這兩個操作;
- ARC也包含運行期組件。用于檢測當前方法返回之后即將執行的那段代碼。如果發現那段代碼要在返回的對象上執行retain操作,則設置全局數據結構中的一個標志位,而不執行autorelease操作。
- ARC最在dealloc方法中自動生成.cxx_destrucet方法,釋放對象
31. 在dealloc方法中只釋放引用并解除監聽
簡單說就是在dealloc方法里,只應該釋放引用,或者移除監聽者(KVO或NSNotificationCenter),不要做其他事。
32. 編寫“異常安全代碼”時留意內存管理問題
- 捕獲異常時,一定要注意將try塊內所創建的對象清理干凈;
- 在默認情況下,ARC不生成安全處理異常所需的清理代碼,開啟編譯器標志后(-fobjc-arc-exceptions),可生成這種代碼,不過會導致應用程序變大,而且會降低運行效率。
33. 以弱引用避免保留環
使用weak。weak所指向的實例回收后,weak屬性指向nil,而unsafe_unreatined屬性仍然指向那個已經回收的實例。
34. 以“自動釋放池塊”降低內存峰值
for (int i = 0; i < 100000; i++) {
[self doSomethingWithInt:i];
}
如果“doSomethingWithInt:”方法要創建臨時對象,那么這些對象很可能放在自動釋放池里。但是,這些對象在調用完方法之后就不再使用了,他們也依然處于存活狀態,因為目前還在自動釋放池里,等待線程執行下一次事件循環時才會清空。這就意味著在執行for循環時,會持續有新對象創建出來,加入到自動釋放池中。所有這些對象要等到for循環執行完之后才會釋放,所以會造成內存高峰,即:內存用量持續上漲,然后突然下降。
NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray array];
for (NSDictionary *record in databaseRecords) {
@autoreleasepool {
EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
[people addObject:person];
}
}
35. 用“僵尸對象”調試內存管理問題
- 僵尸對象:已經被銷毀的對象(不能再使用的對象);
- 野指針:指向僵尸對象(不可用內存)的指針
- 給野指針發消息會報EXC_BAD_ACCESS錯誤
- 系統在回收對象時,可以不將其真的回收,而是把它轉成僵尸對象;
- 系統會修改對象的isa指針,令其指向特殊的僵尸類,從而使該對象變為僵尸對象。僵尸對象能夠響應所有的選擇器,響應方式:打印一條包含消息內容及其接受者的消息,然后終止應用程序。
開啟“僵尸對象”的檢測
在Xcode中設置Edit Scheme -> Diagnostics -> Zombie Objects
從匯編的調用順序可以阿蓋總結僵尸對象生成過程:
//1、獲取到即將deallocted對象所屬類(Class)
Class cls = object_getClass(self);
//2、獲取類名
const char *clsName = class_getName(cls)
//3、生成僵尸對象類名
const char *zombieClsName = "_NSZombie_" + clsName;
//4、查看是否存在相同的僵尸對象類名,不存在則創建
Class zombieCls = objc_lookUpClass(zombieClsName);
if (!zombieCls) {
//5、獲取僵尸對象類 _NSZombie_
Class baseZombieCls = objc_lookUpClass(“_NSZombie_");
//6、創建 zombieClsName 類
zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}
//7、在對象內存未被釋放的情況下銷毀對象的成員變量及關聯引用。
objc_destructInstance(self);
//8、修改對象的 isa 指針,令其指向特殊的僵尸類
objc_setClass(self, zombieCls);
那么Zombie Object是如何被觸發的?
//1、獲取對象class
Class cls = object_getClass(self);
//2、獲取對象類名
const char *clsName = class_getName(cls);
//3、檢測是否帶有前綴_NSZombie_
if (string_has_prefix(clsName, "_NSZombie_")) {
//4、獲取被野指針對象類名
const char *originalClsName = substring_from(clsName, 10);
//5、獲取當前調用方法名
const char *selectorName = sel_getName(_cmd);
  
//6、輸出日志
NSLog(''*** - [%s %s]: message sent to deallocated instance %p", originalClsName, selectorName, self);
 //7、結束進程
 abort();
36. 不要使用retainCount
- 對象的保留計數看似有用,實則不然,因為任何給定時間點上的“絕對保留計數”都無法反映對象生命期的全貌;
- ARC下調用該方法會編譯報錯。
37. 理解“塊”這一概念
塊和函數類似,只不過是直接定義在另一個函數里,和定義它的那個函數共享同一范圍內的東西。
block的內存管理:
- 默認情況下block的內存是在棧中(不需要手動去管理block內存),它不會對所引用的對象進行任何操作;
- 如果對block進行了copy操作, block的內存會搬到堆里面,它會對所引用的對象做一次retain操作。
為什么加上__block就可以修改外部的變量了?
真正的原因是這樣的:我們都知道:Block不允許修改外部變量的值,這里所說的外部變量的值,指的是棧中指針的內存地址。__block所起到的作用就是只要觀察到該變量被 block 所持有,就將“外部變量”在棧中的內存地址放到了堆中。進而在block內部也可以修改外部變量的值。
38. 為常用的塊型創建typedef
以typedef重新定義塊類型,可令變量用起來更加簡單
39. 用handler塊降低代碼的分散程度
40. 用塊引用其所屬對象時不要出現保留環
41. 多用派發隊列,少用同步鎖
為啥@synchronized(self)效率低?
同步行為針對的對象是self。然而共用同一個鎖的那些同步塊,都必須按順序執行,若是在self對象頻繁加鎖,那么程序可能要等另一段與此無關的代碼執行完畢,才能繼續執行當前代碼。
手動實現atomic特性,通常會這么寫:
- (NSString *)someString
{
@synchronized(self) {
return _someString;
}
}
- (void)setSomeString:(NSString *)someString
{
@synchronized(self) {
_someString = someString;
}
}
這么寫雖然能提供某種程度的“線程安全”,但卻無法保證訪問該對象時絕對的線程安全,在同一個線程上多次調用獲取方法(getter),每次獲取到的結果卻未必相同。在兩次訪問操作之間,其他線程可能會寫入新的屬性值。
優化后:
- (NSString *)someString
{
__block NSString *localSomeString;
dispatch_sync(__syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString
{
dispatch_barrier_async(__syncQueue, ^{
_someString = someString;
});
}
42. 多用GCD,少用performSelector
performSelector系列方法有局限性。如:具備延后執行功能的那些方法(performSelector: withObject: afterDelay:)都無法處理帶有兩個參數的選擇器。而能夠指定執行線程的那些方法(performSelector: onThread: withObject: waitUntilDone:)也不是特別通用,如果要用這些方法,就得把許多參數都打包到字典里,然后在受調用的方法里將其提取出來,這樣會增加開銷且可能出bug。
43. 掌握GCD及操作隊列的使用時機
使用NSOperation及NSOperationQueue的好處如下:
- 取消某個操作。在運行任務之前,可以在NSOperation對象上調用cancel方法,該方法會設置對象內的標志位,用以表名此任務不需要執行。
- 指定操作間的依賴關系;
- 通過鍵值觀察機制(KVO)監控NSOperation對象的屬性。比如isCancelled(是否已經取消)、isFinished(是否已完成);
- 指定操作的優先級。GCD的隊列確實有優先級,不過那是針對整個隊列來說的,而不是針對每個塊來說的;
- 重用NSOperation對象。
44. 通過Dispatch Group機制,根據系統資源狀況來執行任務
- 使用場景:這個比較適合比如下載多張圖片然后再合成一張圖片,或者請求C需要依賴請求A和請求B的結果的情況;
- 注意事項:調用dispatch_group_enter與dispatch_group_leave須成對出現;
- dispathc_apply所用的隊列可以是并發隊列,然而dispathc_apply會持續阻塞,知道所有任務都執行完畢。
45. 使用dispathc_once來執行只需要運行一次的線程安全代碼
單例
46. 不要使用dispathc_get_current_queue
- dispathc_get_current_queue函數行為常常與開發者所預期的不同。
- dispathc_get_current_queue函數用于解決由不可重入的代碼所引發的死鎖,然而能用此函數解決的問題,通常也能改用“隊列特定數據”來解決。
47. 熟悉系統框架
48. 多用塊枚舉,少用for循環
“塊枚舉法”本身就能通過GCD來并發執行遍歷操作。
49. 對自定義其內存管理語義的collection使用無縫橋接
主要講了下CF的一些底層函數,比如CFDictionary
50. 構建緩存時選用NSCache而非NSDictionary
相比NSDictionary,優勢在于:自動刪減功能,而且是“線程安全的”,它與字典不同,并不會拷貝鍵。
51. 精簡initialize與load方法
load是在編譯階段執行,是方法地址執行,不走消息發送機制。首次使用某個類之前,系統會向其發送initialize消息; load方法:
執行順序:父類 -> 子類 -> 分類。都執行
initialize方法:(執行順序)
- 分類會覆蓋本類。
- 父類 -> 子類
- 如果子類沒有執行initialize 那么父類的initialize可能會執行多次。
52. 別忘了NSTimer會保留其目標對象
書中是用分類擴展,用“塊”來實現的。也可以通過關聯對象來實現。