<p align='center'>
<img src='http://p95ytk0ix.bkt.clouddn.com/2018-07-31-8726ab1532ca52746711381b07cc9971.jpg'>
</p>
《Effective Objective-C 2.0》讀書/實戰(zhàn)筆記 二
第3章:接口與API設(shè)計
???? 第15條:用前綴避免命名空間沖突
- 選擇與你的公司,應用程序或兩者皆有關(guān)聯(lián)之名稱作為類名的前綴,并在所有代碼中均使用這一前綴
- 若自己所開發(fā)的程序庫中用到了第三方庫,則應為其中的名稱加上前綴
顧名思義就是說在自己開發(fā)的類需要加前綴,iOS程序員開發(fā)工程師普遍使用雙字母的前綴,就像我在開發(fā)時習慣加前綴 XW,其實,這是不科學的,因為蘋果爸爸公司保留使用所有“兩字母前綴”的權(quán)利,所以自己的前綴應該是三個字母的,不僅僅是類名,還有分類、全局變量...
???? 第16條:提供“全能初始化方法”
- 在類中提供一個全能初始化方法,并于文檔里指明。其他初始化方法均應調(diào)用此方法。
- 若全能初始化方法與超類不同,則需覆寫超類中對應的方法
- 如果超類的初始化方法不適應于子類,那么應該覆寫這個超類方法,并在其中拋出異常
舉一個生動形象的例子:
Chinese 類
//.h
// 中國人
#import <Foundation/Foundation.h>
@interface Chinese : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, assign, readonly) NSUInteger age;
/// 全能初始化對象方法
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age;
/// 全能初始化類方法
+ (instancetype)chineseWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age;
/// 其他初始化對象方法
+ (instancetype)chineseWithFirstName:(NSString *)firstName lastName:(NSString *)lastName;
@end
//.m
#import "Chinese.h"
@interface Chinese()
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Chinese
/// 全能初始化函數(shù)-只有全能初始化函數(shù)才能進行賦值操作
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age {
if (self = [super init]) {
self.firstName = firstName;
self.lastName = lastName;
self.age = age;
}
return self;
}
+ (instancetype)chineseWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age {
Chinese *people = [[self alloc] initWithFirstName:firstName lastName:lastName age:age];
return people;
}
- (instancetype)init {
return [self initWithFirstName:@"龍的" lastName:@"傳人" age:1]; // 調(diào)用指定初始化函數(shù)賦予其默認值
}
+ (instancetype)chineseWithFirstName:(NSString *)firstName lastName:(NSString *)lastName {
return [self chineseWithFirstName:firstName lastName:lastName age:1];
}
@end
Student 類繼承自 Chinese
//.h
// 中國學生
#import "Chinese.h"
@interface Student : Chinese
@property (nonatomic, strong, readonly) NSArray *homework;
/// 指定初始化函數(shù)-需直接調(diào)用父類初始化函數(shù)
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age homework:(NSArray *)homework;
/// 指定初始化類方法
+ (instancetype)studentWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age homework:(NSArray *)homework;
/// 其他初始化方法
+ (instancetype)studentWithHomework:(NSArray *)homework;
@end
//.m
#import "Chinese.h"
@implementation Student {
NSMutableArray *p_homework;
}
/// 子類重寫父類全能初始化函數(shù)-更改默認值!
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age {
return [self initWithFirstName:firstName lastName:lastName age:age homework:@[]];
}
/// 指定初始化函數(shù)-需直接調(diào)用父類初始化函數(shù)
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age homework:(NSArray *)homework {
if (self = [super initWithFirstName:firstName lastName:lastName age:age]) {
p_homework = homework.mutableCopy;
}
return self;
}
/// 指定初始化類方法
+ (instancetype)studentWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age homework:(NSArray *)homework {
return [[self alloc] initWithFirstName:firstName lastName:lastName age:age homework:homework];
}
/// 重寫系統(tǒng)初始化方法
- (instancetype)init {
return [self initWithFirstName:@"祖國的" lastName:@"花朵" age:6 homework:@[]];
}
/// 其他初始化方法
+ (instancetype)studentWithHomework:(NSArray *)homework {
return [self studentWithHomework:homework];
}
@end
???? 第17條:實現(xiàn) description 方法
- 實現(xiàn)
description方法返回一個有意義的字符串,用以描述該實例 - 若想在調(diào)試時打印出更詳盡的對象描述信息。則應實現(xiàn)
debugDescription方法
若直接打印自定義對象,控制臺僅僅是顯示該對象的地址,不會顯示對象的具體細節(jié),在程序開發(fā)中對象指針的地址或許有用,但大多數(shù)情況下,我們需要得知對象內(nèi)部的具體細節(jié),所以O(shè)C提供了 description 方法可以實現(xiàn)。
@interface Chinese()
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Chinese
- (NSString *)description {
return [NSString stringWithFormat:@"<%@ : %p, %@>",[self class],self,
@{
@"firstName":_firstName,
@"lastName" :_lastName,
@"age": @(_age)
}];
}
@end
這種使用字典輸出各屬性或成員變量內(nèi)存的方式比較好,若之后需要增刪屬性直接修改字典的鍵值對就可以了。
另外 debugDescription 方法是在控制臺使用 po 命令打印對象信息所調(diào)用的方式,若已經(jīng)實現(xiàn) description 方法, 可不覆寫 debugDescription 方法,因為系統(tǒng)會默認調(diào)用 description 方法。
???? 第18條:盡量使用不可變對象
- 盡量創(chuàng)建不可變對象
- 若某屬性盡可用于對象內(nèi)部修改,則在 “class-continuation分類” 中將其由readonly屬性擴展為readwrite屬性
- 不要把可變對象的collection作為屬性公開,而應提供相關(guān)方法,以此修改對象中的可變 collection
在開發(fā)自定義類時,在 .h 里聲明的屬性盡量設(shè)置為不可變,只讀的屬性,外界只能通過特定的方法更改其內(nèi)容,這對于一個功能的封裝性是至關(guān)重要的。例如我們之前所聲明的 Student 類:
// .h
@interface Student : Chinese
@property (nonatomic, copy, readonly) NSString *school;
@property (nonatomic, strong, readonly) NSArray *homework;
- (void)addHomeworkMethod:(NSString *)homework;
- (void)removeHomeworkMethod:(NSString *)homework;
@end
// .m
@interface Student()
@property (nonatomic, copy) NSString *school;
@end
@implementation Student {
NSMutableArray *p_homework;
}
- (void)addHomeworkMethod:(NSString *)homework {
[p_homework addObject:homework];
}
- (void)removeHomeworkMethod:(NSString *)homework {
[p_homework removeObject:homework];
}
- (instancetype)initWithSchool:(NSString *)school homework:(NSArray *)homework {
if (self = [self init]) {
self.school = school;
p_homework = homework.mutableCopy;
}
return self;
}
@end
如此定義外界只能通過固定的方法對對象內(nèi)的屬性進行更新,便于功能的封裝,減少 bug 出現(xiàn)的概率。
另外使用不可變對象也增強程序的執(zhí)行效率。
???? 第19條:使用清晰而協(xié)調(diào)的命名方式
- 起名時應遵從標準的 Objective-C命名規(guī)范,這樣創(chuàng)建出來的接口更容易為開發(fā)者所理解
- 方法名要言簡意賅,從左至右讀起來要像個日常用語的句子才好
- 方法名里不要使用縮略后的類型名稱
- 給方法起名時的第一要務就是確保其風格與你自己的代碼或所要集成的框架相符
就是說在為自己創(chuàng)建的屬性、成員變量、方法、協(xié)議等起名要見名知意。
???? 第20條:為私有方法名加前綴
- 給私有方法的名稱加上前綴,這樣可以很容易地將其同公共方法區(qū)分開
- 不要單用一個下劃線做私有方法的前綴,因為這種做法是預留給蘋果公司用的
對于一個寫好的類而言,若為公開方法更改名稱,則需要在外部調(diào)用此類的方法的地方同樣做修改,這樣比較麻煩,在類內(nèi)部實現(xiàn)的私有方法不會有這個問題,所以為私有方法加前綴可更好的區(qū)分兩者。便于后期開發(fā)。用何種前綴取決于開發(fā)者的開發(fā)習慣,不建議使用下劃線開頭的前綴,因為這是Apple Dad 專屬的方式。作者的習慣是私有方法的前綴是 p_ ,例如:
/// 這是一個私有方法
- (id)p_playAirplaneMethod {
id xx = @"**";
return xx;
}
???? 第21條:理解 Objective-C 錯誤類型
- 只有發(fā)生了可使整個應用程序崩潰的嚴重錯誤時,才應使用異常
- 在錯誤不那么嚴重的情況下,可以指派 “委托方法” 來處理錯誤,也可以把錯誤信息放在
NSError對象里,經(jīng)由“輸出參數(shù)”返回給調(diào)用者
在項目中可以自定義一個錯誤類型模型:
// .h
// 自定義錯誤類型
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, XWErrorCode) {
XWErrorCodeUnknow = -1, //未知錯誤
XWErrorCodeTypeError = 100,//類型錯誤
XWErrorCodeNullString = 101,//空字符串
XWErrorCodeBadInput = 500,//錯誤的輸入
};
extern NSString * const XWErrorDomain;
@interface XWError : NSError
+ (instancetype)errorCode:(XWErrorCode)errorCode userInfo:(NSDictionary *)userInfo;
@end
// .m
#import "XWError.h"
@implementation XWError
NSString * const XWErrorDomain = @"XWErrorDomain";
+ (instancetype)errorCode:(XWErrorCode)errorCode userInfo:(NSDictionary *)userInfo {
XWError *error = [[XWError alloc] initWithDomain:XWErrorDomain code:errorCode userInfo:userInfo];
return error;
}
@end
在調(diào)試程序合適的回調(diào)中可傳入自定義錯誤信息。
???? 第22條:理解 NSCopying 協(xié)議
- 若想令自己所寫的對象具有拷貝功能,則需實現(xiàn)
NSCopying協(xié)議 - 如果自定義的對象分為可變版本和不可變版本。那么就要同時實現(xiàn)
NSCopying協(xié)議 與NSMutableCopying協(xié)議 - 賦值對象時需決定采用淺拷貝還是深拷貝,一般情況下應該盡量執(zhí)行淺拷貝
- 如果你寫的對象需要深拷貝,那么可考慮新增一個專門執(zhí)行深拷貝的方法
我想讓我創(chuàng)建的 1Student 類具備拷貝屬性,那我需要實現(xiàn) NSCopying 協(xié)議,實現(xiàn)它僅有的一個 - (id)copyWithZone:(nullable NSZone *)zone 方法。 如下:
@interface Student() <NSCopying>
@end
@implementation Student {
NSMutableArray *p_homework;
}
#pragma mark - NSCopying
- (id)copyWithZone:(nullable NSZone *)zone {
Student *stuCopy = [[Student allocWithZone:zone] initWithFirstName:self.firstName lastName:self.lastName age:self.age homework:p_homework.copy];
return stuCopy;
}
如此在調(diào)用 Student 的 copy 方法便會生成一個內(nèi)容相同的不同 Student 對象
Student *stu = [Student studentWithFirstName:@"小極客" lastName:@"學偉" age:6 homework:@[@"小提琴",@"籃球"]];
Student *stu2 = [stu copy];
若希望自定義對象擁有 深拷貝 功能,那需要實現(xiàn) NSMutableCopying 協(xié)議,并實現(xiàn)其唯一的方法
- (id)mutableCopyWithZone:(nullable NSZone *)zone 具體實現(xiàn)如下:
#pragma mark - NSMutableCopying
- (id)mutableCopyWithZone:(nullable NSZone *)zone {
Student *stuMtableCopy = [[Student allocWithZone:zone] initWithFirstName:self.firstName lastName:self.lastName.mutableCopy age:self.age homework:p_homework.copy];
return stuMtableCopy;
}
補充一個 Array 和 Dictionary 分別指向淺復制和深復制之后的類型列表:
Array
首先聲明兩個數(shù)組:
NSArray *array = @[@1,@2];
NSMutableArray *mutableArray = [NSMutableArray arrayWithArray:array];
對其進行淺拷貝和深拷貝,打印結(jié)果如下:
2018-08-01 11:46:32.255187+0800 XWInterviewDemos[80249:5837261] [array copy]:__NSArrayI
2018-08-01 11:46:32.255337+0800 XWInterviewDemos[80249:5837261] [array mutableCopy]:__NSArrayM
2018-08-01 11:46:32.255431+0800 XWInterviewDemos[80249:5837261] [mutableArray copy]:__NSArrayI
2018-08-01 11:46:32.255516+0800 XWInterviewDemos[80249:5837261] [mutableArray mutableCopy]:__NSArrayM
其中 __NSArrayI 為不可變數(shù)組,__NSArrayM 為可變數(shù)組,結(jié)論:
| 原類 | 操作 | 拷貝結(jié)果 |
|---|---|---|
| NSArray | 淺拷貝(copy) | 不可變(__NSArrayI) |
| NSArray | 深拷貝(mutableCopy) | 可變(__NSArrayM) |
| NSMutableArray | 淺拷貝(copy) | 不可變(__NSArrayI) |
| NSMutableArray | 深拷貝(mutableCopy) | 可變(__NSArrayM) |
Dictionary
首先聲明兩個字典:
NSDictionary *dictionary = @{@"key":@"value"};
NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionary];
對其進行淺拷貝和深拷貝,打印結(jié)果如下:
2018-08-01 11:57:20.810019+0800 XWInterviewDemos[80385:5844478] [dictionary copy]:__NSSingleEntryDictionaryI
2018-08-01 11:57:20.810162+0800 XWInterviewDemos[80385:5844478] [dictionary mutableCopy]:__NSDictionaryM
2018-08-01 11:57:20.810277+0800 XWInterviewDemos[80385:5844478] [mutableDictionary copy]:__NSFrozenDictionaryM
2018-08-01 11:57:20.810374+0800 XWInterviewDemos[80385:5844478] [mutableDictionary mutableCopy]:__NSDictionaryM
其中 __NSSingleEntryDictionaryI 和 __NSFrozenDictionaryM 為不可變字典,__NSDictionaryM 為可變字典,結(jié)論:
| 原類 | 操作 | 拷貝結(jié)果 |
|---|---|---|
| NSDictionary | 淺拷貝(copy) | 不可變(__NSSingleEntryDictionaryI) |
| NSDictionary | 深拷貝(mutableCopy) | 可變(__NSDictionaryM) |
| NSMutableDictionary | 淺拷貝(copy) | 不可變(__NSFrozenDictionaryM) |
| NSMutableDictionary | 深拷貝(mutableCopy) | 可變(__NSDictionaryM) |
第4章:協(xié)議與分類
???? 第23條:通過委托與數(shù)據(jù)源協(xié)議進行對象間通信
- 委托模式為對象提供了一套接口,使其可由此將相關(guān)事件告知其他對象
- 將委托對象應該支持的接口定義成協(xié)議,在協(xié)議中把可能需要處理的事件定義成方法
- 當某對象需要從另一個對象獲取數(shù)據(jù)時,可以使用委托模式。這種情景下,該模式亦稱“數(shù)據(jù)源協(xié)議”
- 若有必要,可實現(xiàn)有位移段的結(jié)構(gòu)體,將委托對象是否能響應協(xié)議方法這一信息緩存至其中
委托與數(shù)據(jù)源協(xié)議我們在使用 UITableView 時經(jīng)常用到,我們在開發(fā)時可仿照其設(shè)計模式,將需要的數(shù)據(jù)通過數(shù)據(jù)源獲取;將執(zhí)行操作后的事件通過代理回調(diào);并弱引用其代理對象。
@class Chinese;
@protocol ChineseDelegate <NSObject>
@optional
- (void)chinese:(Chinese *)chinese run:(double)kilometre;
- (void)chinese:(Chinese *)chinese didReceiveData:(NSData *)data;
- (void)chinese:(Chinese *)chinese didReceiveError:(NSError *)error;
@end
@interface Chinese : NSObject
// 委托對象-需弱引用
@property (nonatomic, weak) id<ChineseDelegate> delegate;
@end
在對象跑步時,通過代理方法回調(diào)給委托對象:
- (void)run {
double runDistance = 0.0;
if (self.delegate && [self respondsToSelector:@selector(chinese:run:)]) {
[self.delegate chinese:self run:runDistance];
}
}
倘若此方法每分鐘都會調(diào)用 成百上千次,每次都執(zhí)行 respondsToSelector 方法難免會對性能有一定影響,因為除第一次有效外其余都是重復判斷,所以我們可以將是否能夠響應此方法進行緩存!如例所示:
#import "Chinese.h"
@interface Chinese() {
/// 定義一個結(jié)構(gòu)體擁有三個位段,分別存儲是否實現(xiàn)了三個對應的代理方法
struct {
unsigned int didReceiveData : 1; //是否實現(xiàn) didReceiveData
unsigned int didReceiveError : 1; //是否實現(xiàn) didReceiveError
unsigned int didRun : 1; //是否實現(xiàn) run
}_chineseDelegateFlags;
}
@end
@implementation Chinese
/// 重寫 Delegate 方法,為 位段進行賦值
- (void)setDelegate:(id<ChineseDelegate>)delegate {
_delegate = delegate;
_chineseDelegateFlags.didRun = [delegate respondsToSelector:@selector(chinese:run:)];
_chineseDelegateFlags.didReceiveData = [delegate respondsToSelector:@selector(chinese:didReceiveData:)];
_chineseDelegateFlags.didReceiveError = [delegate respondsToSelector:@selector(chinese:didReceiveError:)];
}
/// 在調(diào)用delegate 的相關(guān)協(xié)議方法不再進行方法查詢,直接取結(jié)構(gòu)體位段存儲的內(nèi)容進行調(diào)用
- (void)run {
double runDistance = 0.0;
if (_chineseDelegateFlags.didRun) {
[self.delegate chinese:self run:runDistance];
}
///if (self.delegate && [self respondsToSelector:@selector(chinese:run:)]) {
/// [self.delegate chinese:self run:runDistance];
///}
}
若代理方法可能回調(diào)多次,那此項優(yōu)化將大大提升程序運行效率!
???? 第24條:將類的實現(xiàn)代碼分散到便于管理的數(shù)個分類之中
- 使用分類機制把類的實現(xiàn)代碼劃分成易于管理的小塊
- 將應該視為“私有”的方法歸入名叫
Private的分類,可隱藏實現(xiàn)細節(jié)
在開發(fā)一個類一般將所有的代碼都放在一起,即便都是高聚合低耦合的代碼,若程序越來越大,難免也會感覺不優(yōu)雅,優(yōu)雅的方式是按照功能將實現(xiàn)抽離到不同的分類中實現(xiàn),在主類中引入其分類,直接調(diào)用分類中實現(xiàn)的方法。這樣也便于管理。
根據(jù)分類的名稱,可快速定位代碼所屬功能區(qū),便于擴展維護。另外可創(chuàng)建一個所開發(fā)類名對應的 Private 分類,存放一些私有方法。這些方法無需暴露給外界,開發(fā)者自己維護。
???? 第25條:總是為第三方類的分類名稱加前綴
- 向第三方類中添加分類時,總應給其名稱加上你專用的前綴
- 向第三方類中添加分類時,總應給其中的方法名加上你專用的前綴
分類中所實現(xiàn)的方法最終會在編譯時加載到本類的方法列表中,若存在相同名稱的方法,后編譯的分類會覆蓋前編譯的,所以為分類中的方法加前綴是很有必要的。
???? 第26條:勿在分類中聲明屬性
- 把封裝數(shù)據(jù)所用的全部屬性都定義在主接口里
- 在 “class-continuation 分類“ 中,可以定義存取方法,但盡量不要定義屬性
原本分類中聲明屬性僅僅是自動生成該屬性 getter 方法和 setter 方法的聲明,不會生成成員變量和對應屬性的getter 方法和 setter 方法
雖然 可以使用 runtime 的關(guān)聯(lián)對象的方式為分類添加屬性 getter 方法和 setter 方法的實現(xiàn),使得分類能夠定義屬性。
但是分類的本質(zhì)在于擴展類的功能,而非封裝數(shù)據(jù)。使用上述方式需要寫大量相似的代碼,并且在內(nèi)存管理上容易出錯,改動屬性的類型需要改變關(guān)聯(lián)對象的相關(guān)類型,不利于維護,代碼不優(yōu)雅!
???? 第27條:使用 “class-continuation 分類” 隱藏實現(xiàn)細節(jié)
- 使用 “class-continuation 分類” 向類中新增實例變量
- 如果某屬性在主接口中聲明為 “只讀”,而類的內(nèi)部又要用設(shè)置方法修改此屬性,那么就在 “class-continuation 分類” 中將其擴展為 “可讀寫”
- 把私有方法的原型聲明在 “class-continuation 分類” 里面
- 若想使類所遵循的協(xié)議不為人所知,則可于 “class-continuation 分類” 中聲明
例如:
// .h 對外聲明為只讀,防止外界隨意修改
@interface Chinese : NSObject
@property (nonatomic, weak) id<ChineseDelegate> delegate;
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@end
// .m 對內(nèi)聲明為可讀寫。使用擴展聲明一些外界不得而知的私有成員變量
@interface Chinese() <NSCopying> {
struct {
unsigned int didReceiveData : 1; //是否實現(xiàn) didReceiveData
unsigned int didReceiveError : 1; //是否實現(xiàn) didReceiveError
unsigned int didRun : 1; //是否實現(xiàn) run
}_chineseDelegateFlags;
NSString *p_girlFriend;
}
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end
或者在 “class-continuation 分類” 里面聲明 C++ 對象,這樣 .m 編譯為 .mm 文件,對外界暴露依然是純正的 OC 接口,在 Foundation 框架中,經(jīng)常使用此策略。
???? 第28條:通過協(xié)議提供匿名對象
- 協(xié)議可在某種程度上提供匿名類型。具體的對象類型可以淡化成遵從某協(xié)議的id類型,協(xié)議里規(guī)定了對象所應實現(xiàn)的方法
- 使用匿名對象來隱藏類型名稱(或類名)
- 如果具體類型不重要,重要的是對象能夠響應(定義在協(xié)議里的)方法,那么可使用匿名對象來表示
第5章:內(nèi)存管理
???? 第29條:理解引用計數(shù)
- 引用計數(shù)機制通過可以遞增遞減的計數(shù)器來管理內(nèi)存。對象創(chuàng)建好之后,其保留技術(shù)至少為1。若保留計數(shù)為正,則對象繼續(xù)存活。當保留計數(shù)降為0時,對象就被銷魂了。
- 在對象生命期中,其余對象通過引用來保留或釋放此對象。保留與釋放操作分別會遞增及遞減保留計數(shù)
何為引用計數(shù),用一張圖表示便是:

圖轉(zhuǎn)自 《Objective-C高級編程+iOS與OS+X多線程和內(nèi)存管理》圖1.3
看完此圖差不多已經(jīng)理解引用計數(shù)了,OK,本條完結(jié)。。。
另外,補充一個概念-自動釋放池
使用自動釋放池可使對象的生命周期跨越 “方法調(diào)用邊界”后存活到 runloop 的下一次事件循環(huán)。
???? 第30條:以 ARC 簡化引用計數(shù)
- 有ARC之后,程序員就無須擔心內(nèi)存管理問題了,使用ARC來編程,可省去類中的許多 “樣板代碼”
- ARC 管理對象生命周期的辦法基本上就是:在合適的地方插入“保留”及“釋放”操作。在ARC環(huán)境下,變量的內(nèi)存管理語義可以通過修飾符指明,則原來則需要手工執(zhí)行“保留”及“釋放”操作
- 由方法返回的對象,其內(nèi)存管理語義總是通過方法名來體現(xiàn)。ARC將此確定為開發(fā)者必須遵守的規(guī)則
- ARC只負責管理 Objective-C 對象的內(nèi)存。尤其要注意:CoreFoundation 對象不歸ARC 管理,開發(fā)者必須適時調(diào)用 CFRetain/CFRelease
ARC會以一種安全的方式設(shè)置:先保留新值,再釋放舊值,最后設(shè)置實例變量,其中可以使用以下修飾符改變局部變量和實例變量的語義:
__strong 默認語義,保留此值
__unsafe_unretained 不保留此值,這么做可能不安全,因為等再次使用變量時,其對象可能已經(jīng)回收了
__weak 不保留此值,但是變量可安全使用,因為如果系統(tǒng)把這個對象回收了,那么變量也會自動清空 - 可避免循環(huán)引用
__autoreleasing 把對象“按引用傳遞”給方法時,使用這個特殊的修飾符。此值在方法返回時自動釋放
???? 第31條:在 dealloc 方法中只釋放引用并解除監(jiān)聽
- 在
dealloc方法里,應該做的事情就是釋放指向其他對象的引用,并取消原來訂閱的“簡直觀測”(KVO)或NSNotificationCenter等通知,不要做其他事情 - 如果對象持有文件描述符等系統(tǒng)資源,那么應該專門編寫一個方法來解釋此種資源。這樣的類要和其使用者約定:用完資源后必須調(diào)用
close方法 - 執(zhí)行異步任務的方法不應在
dealloc里調(diào)用;只能在正常狀態(tài)下執(zhí)行的那些方法也不應在dealloc里調(diào)用,因為此時對象已處于正在回收的狀態(tài)了
dealloc 方法是對象釋放所調(diào)用的方法,此時若使用對象的成員變量可能已經(jīng)被釋放掉了,若使用異步回調(diào)時自身已經(jīng)被釋放,若回調(diào)中包含 self 會導致程序崩潰。
- (void)dealloc {
// 移除通知
[[NSNotificationCenter defaultCenter] removeObserver:self];
// 釋放需手動釋放的資源
//CFRelease(coreFoundationObject);
}
另外 即便對象釋放,在極個別情況下并不會調(diào)用 dealloc 方法,程序終止時一定會調(diào)用的是在 application delegate 的 - (void)applicationWillTerminate:(UIApplication *)application 方法, 若一定要清理某些對象,可在此方法中處理。
???? 第32條:編寫“異常安全代碼”時留意內(nèi)存管理問題
- 捕獲異常時,一定要注意將 try 塊內(nèi)所創(chuàng)立的對象清理干凈
- 在默認情況下,ARC不生成安全處理異常所需的清理代碼,開啟編譯器標志后,可生產(chǎn)這種代碼,不過會導致應用程序變大,而且會降低運行效率
???? 第33條:以弱引用避免保留環(huán)
- 將某些引用設(shè)為
weak,可避免出現(xiàn) “保留環(huán)” -
weak引用可以自動清空,也可以不自動清空。自動清空是隨著ARC而引入的新特性,由運行期系統(tǒng)來實現(xiàn)。在具備自動清空功能的弱引用上,可以隨意讀取其數(shù)據(jù),因為這種引用不會指向已經(jīng)回收過的對象
若兩個對象互相引用,會形成保留環(huán)(循環(huán)引用),如圖:

保留環(huán)會引起內(nèi)存泄露,對象間的互相持有導致保留環(huán)內(nèi)的所有對象均無法正常釋放。
避免保留環(huán)最佳方式是弱引用,通過“非擁有關(guān)系”的聲明將環(huán)打破。這種關(guān)系可用 weak 和 unsafe_unretained 實現(xiàn)。兩者的區(qū)別是 weak 修飾的對象在釋放之后本身會置 nil, 而 unsafe_unretained 不會,在對象釋放之后會依然指向被釋放的那塊內(nèi)存。如圖:

@property (nonatomic, weak) id<ChineseDelegate> delegate;
__weak typeof(self) weakSelf = self;
[NSTimer xw_timerTimeInterval:1.0 block:^{
[weakSelf timerMethod];
} repeats:YES];
???? 第34條:以“自動釋放池塊”降低內(nèi)存峰值
- 自動釋放池排布在棧中,對象收到
autorelease消息后,系統(tǒng)將其放入最頂端的池里。 - 合理利用自動池,可降低應用程序的內(nèi)存峰值
-
@autoreleasepool這種新式寫法能創(chuàng)建出更為輕便的自動釋放池
主線程和GCD機制中的線程默認都會有自動釋放池,無需程序員手動創(chuàng)建,并且系統(tǒng)會自動在 runloop 的執(zhí)行下次時間循環(huán)時將池內(nèi)對象清空。
如果在一個大的循環(huán)體中需要創(chuàng)建n多個對象時,使用 “自動釋放池塊” 可降低內(nèi)存峰值,如例所示:
未做優(yōu)化的方式:
- (void)testFor1 {
NSMutableArray *arrayM = [NSMutableArray array];
for (int i = 0; i < 100000; i++) {
NSString *str = [NSString stringWithFormat:@"%d",i];
[arrayM addObject:str];
NSLog(@"%@",str);
}
}
此時內(nèi)存使用情況:

CPU使用情況:

使用 “自動釋放池塊” 優(yōu)化的方式:
- (void)testFor2 {
NSMutableArray *arrayM = [NSMutableArray array];
for (int i = 0; i < 100000; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"%d",i];
[arrayM addObject:str];
NSLog(@"%@",str);
}
}
}
優(yōu)化后內(nèi)存使用情況:

優(yōu)化后CPU使用情況:

顯而易見根據(jù)Xcode 顯示:并沒有什么卵用,此條原理上是可以降低內(nèi)存占用峰值,但實際情況確實兩者沒有太大區(qū)別,能否起到優(yōu)化的作用還需日后繼續(xù)觀察...
???? 第35條:用“僵尸對象”調(diào)試內(nèi)存管理問題
- 系統(tǒng)在回收對象時,可以不將其真的回收,而是把它轉(zhuǎn)化為僵尸對象。通過環(huán)境變量
NSZombieEnabled可開啟此功能 - 系統(tǒng)會修改對象的
isa指針,令其指向特殊的僵尸類,從而使該對象變?yōu)榻┦瑢ο?。僵尸類能夠響應所有的選擇子,響應方式為:打印一條包含消息內(nèi)容及其接受者的消息,然后終止應用程序
在Xcode 中勾選 Zombie Objects 可啟用僵尸對象檢測,此時給僵尸對象發(fā)送消息將會在控制臺打印相關(guān)信息:

???? 第36條:不要使用 retainCount
- 對象的保留計數(shù)看似有用,實則不然,因為任何給定時間點上的“絕對保留計數(shù)”都無法反映對象生命期的全貌
- 引入 ARC 之后,
retainCount方法就正式廢止了,在ARC下調(diào)用該方法會導致編譯器報錯。
在 ARC 時代下本身就不會使用 retainCount, 書中所講述的幾種情況僅出現(xiàn)在 MRC 編程環(huán)境下, 例如 retainCount 可能不為 0 的時候?qū)ο缶蜁幌到y(tǒng)釋放,所以 retainCount 引用計數(shù)可能永遠不為0,這是系統(tǒng)優(yōu)化對象的釋放行為所導致的。