這里僅記錄對(duì)我有幫助的內(nèi)容,不是對(duì)這本書的全面概括。有些我目前還沒怎么用過(guò)的東西,比如GCD,現(xiàn)在讀來(lái)還沒什么感覺,所以沒有詳細(xì)記錄;其他的盡量寫得簡(jiǎn)單易懂一些,方便自己查閱。有疑問(wèn)之處以原書為準(zhǔn)。
Forward Declaration
[Item 2]
把聲明(#import、協(xié)議、屬性等)盡量移到.m文件而不是放在頭文件里。如果A類的屬性或方法中用到了B類,則在A.h中用@Class B告知編譯器B類的存在即可,不要#import B.h,除非A是B的子類。這樣做可以:
- 當(dāng)這個(gè)頭文件被引用時(shí),避免引用者知道不必要的細(xì)節(jié),節(jié)省其編譯時(shí)間
- 如果有兩個(gè)類需要相互引用,頭文件中互相
#import xxx.h會(huì)造成交叉引用,移到.m文件即可避免 - 一些私有的方法、屬性和協(xié)議等可以避免在頭文件中暴露
Literal Syntax
[Item 3]
NSNumber不只可以包裝純數(shù)字:
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
Literal Syntax只能創(chuàng)建不可變對(duì)象,對(duì)于可變對(duì)象,應(yīng)該這樣寫:
NSMutableArray *mutableArray = [@[@1, @2, @3] mutableCopy];
Define Constants
[Item 4]
不要使用#define,因?yàn)樗话愋托畔?,而且可能不知不覺地在別處被重定義。
- 如果一個(gè)常量僅在一個(gè)
.m文件中使用,應(yīng)該這樣寫:
static const NSTimeInterval kAnimationDuration = 0.3;
- 如果一個(gè)常量將被多個(gè)文件共享(比如
NSNotification的名稱),應(yīng)該這樣寫:
// EOCView.h
extern NSString *const EOCViewDidAnimateNotification;
// EOCView.m
NSString *const EOCViewDidAnimateNotification = @"EOCViewDidAnimateNotification";
注意
- 常量名的命名規(guī)范:只用于一個(gè)文件時(shí)應(yīng)該以"k"為前綴,用于多個(gè)文件時(shí)應(yīng)該以所在類的類名等為前綴。
Enum
[Item 5]
- 通用枚舉:
typedef NS_ENUM(NSUInteger, EOCConnectionState) {...};
- 二進(jìn)制位枚舉:
typedef NS_OPTIONS(NSUInteger, EOCDirection) {
EOCDirectionUp = 1 << 0,
EOCDirectionLeft = 1 << 1,
EOCDirectionRight = 1 << 2
};
這種枚舉用于描述或的關(guān)系,比如方向?yàn)樽蠡驗(yàn)橛視r(shí)執(zhí)行某一方法,應(yīng)該這樣判斷:
EOCDirection permittedDirection = EOCDirectionLeft | EOCDirectionRight;
if (direction & permittedDirection) {
[self doSomething];
}
注意
- 在
switch語(yǔ)句中判斷這樣的枚舉類型時(shí)不要寫default:。否則如果漏寫了其中一種枚舉的case xxx:,編譯器不會(huì)提示。
Property and Instance Variable
[Item 7]
Property
@property相當(dāng)于創(chuàng)建了加下劃線的成員變量,以及setter和getter方法。@property后面括號(hào)里的內(nèi)容是為了給編譯器自動(dòng)創(chuàng)建setter和getter提供依據(jù),比如若不指定為nonatomic,則默認(rèn)為atomic,系統(tǒng)會(huì)保證setter和getter的完整執(zhí)行,不會(huì)在set到一半時(shí)get;若有readonly,則編譯器不生成setter。
這個(gè)setter僅是對(duì)外部而言的,在類的內(nèi)部仍然可以通過(guò)下劃線加屬性名直接訪問(wèn)對(duì)應(yīng)的成員變量,從而修改其值。用點(diǎn)語(yǔ)法訪問(wèn)屬性,一定會(huì)調(diào)用setter和getter。若有readonly,則不能用點(diǎn)語(yǔ)法來(lái)賦值。
建議用點(diǎn)語(yǔ)法來(lái)set,方便在setter方法里設(shè)斷點(diǎn)觀察狀態(tài);用下劃線來(lái)get,這樣速度快。
注意
-
init方法一定用下劃線來(lái)set,因?yàn)?code>setter方法可能被子類重寫,比如要求一個(gè)字符串不能為空;而父類初始化時(shí)就是將字符串置空,造成init失敗。 - 如果存在懶加載,即
getter中判斷實(shí)例不存在,才初始化一個(gè)實(shí)例,這時(shí)應(yīng)該用點(diǎn)語(yǔ)法,保證調(diào)用getter,而不是用下劃線,否則可能取到nil。
Instance Variable
頭文件里@interface后大括號(hào)里聲明的是成員變量,可以被以下三種符號(hào)修飾:
-
@public對(duì)所有類都可見 -
@protected對(duì)該類及其子類可見 -
@private僅對(duì)該類可見
這些變量是沒有setter和getter方法的,不能用點(diǎn)語(yǔ)法訪問(wèn);在該類及其子類中可以直接用變量名來(lái)訪問(wèn),在其他類中可以用實(shí)例名加->來(lái)訪問(wèn)。
isEqual:
[Item 8] [Item 14]
對(duì)于對(duì)象而言,==所比的是兩個(gè)對(duì)象的地址是否相同(identical),而不是兩個(gè)地址不同的對(duì)象的內(nèi)容是否相同(equal)。后一種比較應(yīng)該用isEqual:方法。系統(tǒng)類如UIColor、NSArray等都可以直接調(diào)用該方法,自定義類則不然,isEqual:返回的是用==判斷的結(jié)果,因此需要重寫該方法。
注意到isEqual方法接受的參數(shù)是id類型,所以重寫時(shí)第一步就是判斷類型是否相同:
if (![object isMemberOfClass:self.class]) return NO;
然后一個(gè)一個(gè)地判斷其他成員變量是否相同。
另外需要重寫的是hash方法。NSSet和NSDictionary判斷兩個(gè)元素是否相等時(shí),首先比較hash值,若相等,再調(diào)用isEqual:。hash值是由實(shí)例的內(nèi)容決定的,equal的實(shí)例hash值一定相同,但hash值相同時(shí)不一定equal。
系統(tǒng)類都有hash方法,但自定義類的hash方法如果不重寫,返回的是地址值,即只有identical時(shí)hash值才相同。重寫時(shí)應(yīng)該對(duì)所有成員變量hash值做異或(^):
- (NSUInteger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}
注意
- 如果將可變對(duì)象加入
NSSet,然后修改它,使之與集合中另一元素內(nèi)容相同,那么對(duì)這個(gè)NSSet做copy的結(jié)果是元素會(huì)變少,因?yàn)椴荒芟蚣霞尤肱c已有元素equal的對(duì)象。必須注意count減少的問(wèn)題,可能帶來(lái)bug。(也可以利用這一特性,通過(guò)NSArray->NSSet->NSArray來(lái)去掉數(shù)組中重復(fù)的對(duì)象) - 判斷類型應(yīng)該用
isMemberOfClass:和isKindOfClass:,而不是[object class] == [self.class]。如果目標(biāo)類利用了消息傳遞機(jī)制,比如NSProxy,==比的是proxy類本身,而isMemberOfClass:和isKindOfClass:比的是被代理的類,即真正接收到message的類。
Class Cluster
[Item 9]
初始化一個(gè)UIButton時(shí),選擇不同的type,實(shí)際上系統(tǒng)會(huì)switch(type),然后創(chuàng)建不同的UIButton的子類。向外暴露的只有UIButton這一個(gè)父類,而其每一個(gè)子類都重寫了父類的所有方法。這就是類簇的設(shè)計(jì)模式,其他很多系統(tǒng)類如NSArray、NSString等都采用這一模式。
注意
- 此時(shí)
[newButton isMemberOfClass:UIButton.class]的結(jié)果一定是NO,應(yīng)該用isKindOf:來(lái)判斷。
Message Forwarding
[Item 12] [Item 13]
[Item 12]介紹了消息傳遞機(jī)制,以及為@dynamic屬性創(chuàng)建setter和getter的方法,與數(shù)據(jù)庫(kù)交互的時(shí)候用得著。
[Item 13]介紹了在不創(chuàng)建子類的情況下為任何已有方法(如NSString的lowercaseString方法)添加附加操作(比如打印出lowercaseString執(zhí)行前后的字符串),從而監(jiān)控方法的執(zhí)行。此法在debug時(shí)有用,但應(yīng)該盡量少用。
Prefixing
[Item 15]
所有的自定義類名、C函數(shù)名和全局變量名都應(yīng)該加前綴,可以是APP名、公司名縮寫等。前綴應(yīng)由不少于3個(gè)字母組成(蘋果保留所有2個(gè)字母的前綴),比如本書常用的EOC。
如果在自己創(chuàng)建的庫(kù)中引入了第三方庫(kù),則應(yīng)該給后者也加上自己的前綴(XYZLibrary -> EOCXYZLibrary),因?yàn)楫?dāng)自己創(chuàng)建的庫(kù)再被其他人引用的時(shí)候,其他人可能又引用了同樣的第三方庫(kù)(可能是不同版本的),編譯時(shí)可能報(bào)duplicated symbols錯(cuò)誤。
Designated Initializer
[Item 16]
init方法可以有很多個(gè),但應(yīng)該有一個(gè)designated initializer,即只有這一個(gè)方法是會(huì)真正創(chuàng)建實(shí)例的,而其他init方法要么都調(diào)用這一方法,要么拋異常(盡量避免拋異常;可以賦默認(rèn)值,然后正常實(shí)例化)。
注意
- 如果在子類中換用另一方法作為designated initializer,記得重寫父類的designated initializer,使之也調(diào)用這一方法來(lái)創(chuàng)建實(shí)例。
Description Information
[Item 17]
為了NSLog(@"%@", customObject);能打印出自定義類的實(shí)例的具體信息,必須重寫其description方法:
- (NSString *)description {
return [NSString stringWithFormat:@"%@ %@",
_firstName,
_lastName];
}
另外為了斷點(diǎn)時(shí)po customObject也能打印出具體信息,還應(yīng)重寫debugDescription方法:
- (NSString *)debugDescription {
return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">",
self,
[self class],
_firstName,
_lastName];
}
具體信息的部分用字典來(lái)寫就更漂亮一些:
- (NSString *)debugDescription {
return [NSString stringWithFormat:@"<%@: %p, %@>",
self,
[self class],
@{@"firstName":_firstName,
@"lastName":_lastName}
];
}
Immutable
[Item 18]
為了避免數(shù)據(jù)遭到不必要的修改:
- 不應(yīng)該被外部直接修改的屬性,應(yīng)該聲明
(readonly)(可以在Extension中重新聲明為(readwrite),使它對(duì)外只讀,對(duì)內(nèi)可讀寫)。 - 不要把
NSMutable(Array/Dictionary/Set...)暴露出來(lái),應(yīng)該只留一個(gè)setter給外部使用,以免它們被其他類修改時(shí),類自身難以察覺。 - 如果數(shù)據(jù)不是特別多,
copy的代價(jià)不是特別大,留給其他類的getter應(yīng)盡量用copy方法,返回一個(gè)不可變的對(duì)象。
Naming
[Item 19] [Item 20]
- 如果方法返回一個(gè)新生成的值,應(yīng)該以該值的類型開頭,例如
stringWithFormat:。直接返回某個(gè)屬性的方法除外,直接用屬性名即可。 - 方法名里應(yīng)該用整個(gè)單詞(string),不要用縮寫(str)。
-
BOOL屬性加前綴is。返回BOOL值的方法(但不是直接返回某個(gè)BOOL型的屬性)應(yīng)該以is或has為前綴。 - 用前綴
p_來(lái)標(biāo)示私有方法。不要只用下劃線,因?yàn)樘O果的保留這種方式,應(yīng)避免無(wú)意中覆蓋系統(tǒng)方法的情況。
Error Handling
[Item 21] [Item 32]
與JAVA、C++等語(yǔ)言不同,OC不會(huì)頻繁地用到異常(NSException)。雖然也有@try、@catch和@finally這些處理異常的語(yǔ)句,但在純OC文件中,異常一般只用于致命的、必須中斷APP生命周期的情況,其他非致命情況下都用NSError來(lái)告知錯(cuò)誤信息;而前述那些語(yǔ)句一般用在Objective-C++文件里,或者是調(diào)用第三方庫(kù)、無(wú)法修改源代碼時(shí)使用。
可以給被調(diào)用的方法傳入一個(gè)NSError參數(shù):
- (BOOL)doSomething:(NSError **)error {
......
if (/* 出錯(cuò) */) {
*error = [NSError errorWithDomain:domain
code:code
userInfo:userInfo];
return NO;
}
}
然后做如下調(diào)用:
NSError *error = nil;
BOOL ret = [self doSomething:&error];
if (ret) {
/* 查看error */
......
}
用額外的BOOL量來(lái)判斷是否出錯(cuò),而不是if(error),是因?yàn)檎{(diào)用者可能不關(guān)心具體錯(cuò)誤是什么,所以這樣調(diào)用:[self doSomething:nil]。
注意
- 傳參時(shí)傳的是*error的地址,所有有兩個(gè)*。如果傳參時(shí)只有一個(gè)*,那么傳的就是指向error的指針的拷貝,在方法里面對(duì)它賦值,只是讓這個(gè)拷貝指向了新的對(duì)象,而外面的指向error的指針以及error的內(nèi)容均未改變。
- ARC不會(huì)給
NSException加上釋放資源的代碼,因?yàn)橐话闱闆r下出現(xiàn)異常都會(huì)引發(fā)APP終止,所有資源都被銷毀。如果要像JAVA、C++等那樣在@finally里面release資源,需要加上-fobjc-arc-exceptions的flag。
Copying
[Item 22]
要使自定義類具有copy方法,應(yīng)該重寫的是copyWithZone方法:
- (id)copyWithZone:(NSZone *)zone {
return [[[self class] allocWithZone:zone] init...];
}
然后調(diào)用copy方法即可實(shí)現(xiàn)復(fù)制。如果該類有可變的屬性,需要使用mutableCopy,那么還需要重寫mutableCopyWithZone方法。
注意
-
copyWithZone方法里應(yīng)該用designated initializer來(lái)初始化實(shí)例。 -
所有類的
copy或mutableCopy都不保證是深拷貝還是淺拷貝,只能根據(jù)文檔判斷。自定義時(shí)應(yīng)該盡量用淺拷貝,減少系統(tǒng)開銷。
Protocol and Delegate
[Item 23]
- 一個(gè)對(duì)象向多個(gè)對(duì)象傳消息 ->
NSNotification - 多個(gè)對(duì)象向一個(gè)對(duì)象傳消息 ->
Protocol and Delegate
第二種的典型情況包括一個(gè)頁(yè)面持有多個(gè)下載器,頁(yè)面遵循一個(gè)protocol,作為下載器的delegate;下載器的下載進(jìn)度有更新時(shí),把下載進(jìn)度傳給頁(yè)面,讓頁(yè)面以進(jìn)度為參數(shù)執(zhí)行某些協(xié)議中規(guī)定了的方法。
協(xié)議規(guī)定了一些方法,一些是@require的,即聲明了遵循此協(xié)議的代理類必須實(shí)現(xiàn)的方法;另一些是@optional的,代理類可以選擇性實(shí)現(xiàn)。對(duì)于后者,調(diào)用協(xié)議方法前必須用respondToSelector:來(lái)檢查代理類是否實(shí)現(xiàn)了這個(gè)方法。比如前面的例子中,頁(yè)面可能不關(guān)心下載的中間進(jìn)度,只關(guān)心下載完成的信息,那么下載器在進(jìn)度更新但下載未完成時(shí),就不必也不能調(diào)用協(xié)議方法。
對(duì)于頻繁調(diào)用的可選的協(xié)議方法(如下載器更新進(jìn)度時(shí)調(diào)用的方法),為了避免多次檢查respondToSelector:,可以只在初始化時(shí)檢查一次,把結(jié)果存在全局的結(jié)構(gòu)體里面,此后直接向結(jié)構(gòu)體查詢。
注意
-
delegate作為屬性,必須用weak修飾。否則如前面下載器的例子,頁(yè)面持有下載器,下載器又通過(guò)delegate持有頁(yè)面,就造成循環(huán)引用,應(yīng)該讓后者為weak:
@property (nonatomic, weak) id <EOCNetworkFetcherDelegate> delegate;
Category and Extension (Class-Continuation Category)
[Item 24] [Item 25] [Item 27]
Category
Category可以給已有的類(包括系統(tǒng)類)添加方法,但不能添加屬性。比如給CustomClass添加一個(gè)名為SayHello的category,Xcode會(huì)創(chuàng)建名為CustomClass+SayHello的.h和.m文件,然后可以添加方法,比如sayHello。在其他類中#import "CustomClass+SayHello.h",則初始化的CustomClass的實(shí)例就可以執(zhí)行sayHello方法。如果CustomClass的子類也#import "CustomClass+SayHello.h",則sayHello方法也被子類繼承。
每個(gè)類本身實(shí)質(zhì)上都是單例,在某一個(gè)category里面定義了一個(gè)方法,就是把方法加到類自身的單例里面,并沒有創(chuàng)建新的子類。所以如果既有#import "CustomClass+SayHello.h",又有#import "CustomClass+SayBye.h",則初始化的實(shí)例仍屬于CustomClass,但既能執(zhí)行sayHello,又能執(zhí)行sayBye。實(shí)際上在任何地方,即使不引用category的頭文件,也能利用消息傳遞機(jī)制直接調(diào)用category新加的方法,引用頭文件只是讓編譯器知道有這個(gè)方法。
Category新加入的方法在APP整個(gè)生命周期中都有效。如果多個(gè)category都有同名的方法,它們會(huì)相互覆蓋,最終執(zhí)行的是系統(tǒng)最后load進(jìn)來(lái)的方法。如果第三方庫(kù)被他人引用(尤其是第三方庫(kù)給系統(tǒng)類新加了category時(shí)),用戶新創(chuàng)建category,且方法名與第三方庫(kù)重復(fù),就說(shuō)不清最后執(zhí)行的是哪個(gè)方法。所以應(yīng)該給category及其方法都加上前綴(前者加EOC_,后者加eoc_)。
Extension (Class-Continuation Category)
Extension可以聲明一些私有的成員變量和方法:
@interface CustomClass () <PrivateProtocol> {
id m_variable;
}
@property (nonatomic, readwrite) id object;
- (void)p_method;
@end
在類名后加一對(duì)小括號(hào)就是extension,這段代碼一般直接寫在.m文件里。
- 大括號(hào)里可以聲明私有變量,對(duì)其他類(包括子類)都是不可見的(也可以把私有變量寫在
@implemention后的大括號(hào)里,效果是一樣的,只是習(xí)慣問(wèn)題)。 - 可以用
@property把已經(jīng)在頭文件里聲明為readonly的屬性聲明為readwrite,這樣這個(gè)屬性對(duì)外只讀,對(duì)本類可讀可寫。 - 可以聲明私有方法,應(yīng)該以
p_為前綴。 - 如果不想把遵循的協(xié)議暴露在頭文件里,可以寫在
extension里面。
Anonymous Object Using Protocol
[Item 28]
隱藏類名可以通過(guò)id <Protocol> object;來(lái)實(shí)現(xiàn)。
有些情況下類實(shí)現(xiàn)的方法(遵循的協(xié)議)比類名更重要,比如某個(gè)方法需要返回某個(gè)操作數(shù)據(jù)庫(kù)的類,但操作不同數(shù)據(jù)庫(kù)(MySQL,PostgreSQL,...)的類來(lái)自不同的庫(kù),繼承自不同父類,因此方法返回類型無(wú)法設(shè)為它們共同的父類,設(shè)為id然后再判斷類型也很麻煩。
這種情況下可以令這些操作類都遵循統(tǒng)一的協(xié)議,都實(shí)現(xiàn)操作數(shù)據(jù)庫(kù)需要的幾種方法(connect,disconnect,performQuery:,...),然后方法返回的類型就設(shè)為id <EOCDatabaseConnection>,這樣就不必判斷具體類型,直接使用協(xié)議方法即可。
Dealloc Method
[Item 31]
dealloc方法應(yīng)該處理:
- 由
malloc分配的空間應(yīng)該free -
CoreFoundation對(duì)象應(yīng)該CFRelease -
NSNotificationCenter移除observer -
KVO移除observer
應(yīng)該避免在dealloc方法里調(diào)用其他方法,因?yàn)楹笳哂玫降膶?duì)象可能已經(jīng)被銷毀。不是所有的清理工作都應(yīng)該由dealloc承擔(dān),比如以下情況:
- 一些比較expensive的資源,如
fileDescriptor、sockets和malloc分配的內(nèi)存,不應(yīng)該多次銷毀和重新創(chuàng)建,應(yīng)該寫open和close方法,多次使用。 - APP被突然中止時(shí)
dealloc不會(huì)被調(diào)用,系統(tǒng)會(huì)直接銷毀所有資源。需要在這時(shí)完成的工作,比如保存信息,應(yīng)該寫在AppDelegate的applicationWillTerminate:里。
Weak
[Item 33]
unsafe_unretained與weak的區(qū)別是,當(dāng)被引用的對(duì)象被釋放時(shí),前者仍然指向那塊內(nèi)存,而weak指針在ARC的作用下變成nil。
只有持有一個(gè)對(duì)象才用強(qiáng)指針。例如,在頭文件中聲明UI控件都用weak,是因?yàn)?code>StoryBoard才是控件的真正持有者,其他類只是弱引用。
Autoreleasepool
[Item 34]
對(duì)于一段反復(fù)執(zhí)行的代碼,必須一個(gè)for循環(huán),如果每次它都會(huì)創(chuàng)建臨時(shí)對(duì)象,那么臨時(shí)對(duì)象就會(huì)一直堆積;應(yīng)該在代碼段的首尾加上一個(gè)@autoreleasepool,使得每次執(zhí)行后都釋放臨時(shí)對(duì)象:
for (/* 循環(huán)條件 */) {
@autoreleasepool {
......
}
}
Zombie Objects
[Item 35]
已經(jīng)被釋放的對(duì)象,只要它所在的內(nèi)存區(qū)域還沒有被重寫,它就仍然可能接受消息并響應(yīng);即使內(nèi)存被重寫,新的對(duì)象也可能接收到發(fā)給舊對(duì)象的消息,作出不可預(yù)料的響應(yīng)。為了記錄運(yùn)行過(guò)程中是否向已經(jīng)被釋放的對(duì)象發(fā)送了消息,可以在debug時(shí)Enable Zombie Objects。此時(shí)舊對(duì)象被釋放后,這塊內(nèi)存不會(huì)被重寫,如果嘗試向它發(fā)送消息,會(huì)引起異常message sent to deallocated instance...。
Block
[Item 37] [Item 38] [Item 39] [Item 40] [Item 52]
存儲(chǔ)位置
如果一個(gè)block沒有用到任何外部變量,它是global的,和其他靜態(tài)變量存在一起。如果用到了外部變量,它將被創(chuàng)建在棧上;如果對(duì)block用了copy方法,或者在ARC環(huán)境下它被賦值給一個(gè)強(qiáng)指針,它就會(huì)被復(fù)制到堆上。
typedef
int (^EOCSomeBlock)(BOOL flag, int value) = ^(BOOL flag, int value) {...};
這樣的寫法會(huì)降低可讀性,名稱EOCSomeBlock被放到了括號(hào)里面。應(yīng)該這樣寫:
typedef int (^EOCSomeBlock)(BOOL flag, int value);
EOCSomeBlock block = ^(BOOL flag, int value) {...};
如果有兩個(gè)block的參數(shù)和返回值都一樣,但完成的功能不同,也應(yīng)該寫成兩個(gè)typefdef,分別用不同的名字,提高可讀性。記得命名時(shí)要加上必要的前綴。
Handler
有時(shí)可以用block來(lái)代替delegate的設(shè)計(jì)模式,這樣不需要把處理回調(diào)的代碼寫在單獨(dú)的代理方法里面,而是就近寫在調(diào)用處,增強(qiáng)可讀性。也可以讓block回傳NSError來(lái)判斷執(zhí)行情況:
typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data, NSError *error);
另一個(gè)好處是block很容易通過(guò)GCD等安排在指定線程上執(zhí)行。
Break Retain Cycle
用block很容易造成循環(huán)引用。如果block里引用了當(dāng)前類的成員變量,它就會(huì)保持對(duì)self的強(qiáng)引用;把它作為參數(shù)傳給另一個(gè)實(shí)例(比如NetworkFetcher)的方法,則這個(gè)實(shí)例持有block(_completionHandler),同時(shí)它又被self持有(_networkFetcher),造成循環(huán)引用。
一種解決方法是解除self對(duì)另一個(gè)實(shí)例的持有,即在block里面最后令方法所在實(shí)例為nil(_networkFetcher = nil;),這就要求每次傳進(jìn)來(lái)的block都加這一句。如果是我們自己編寫了NetworkFetcher類,作為第三方庫(kù)被別人引用,很難保證用戶一定寫了這一句。
另一個(gè)辦法是解除另一個(gè)實(shí)例對(duì)block的持有,即在另一個(gè)實(shí)例中用完block以后,把它釋放掉:
_completionHandler(_downloadedData);
self.completionBlock = nil;
這樣就在NetworkFetcher內(nèi)部解決了循環(huán)引用的問(wèn)題,比較安全。
還可以用__weak指針來(lái)解除block對(duì)self的持有,即:
__weak EOCClass *weakSelf = self;
EOCNetworkFetcherCompletionHandler completionHandler = ^(NSData *data, NSError *error) {
EOCClass *strongSelf = weakSelf;
[strongSelf doSomething];
......
}
block里面先轉(zhuǎn)化為強(qiáng)指針,避免執(zhí)行的過(guò)程中self被釋放掉。執(zhí)行完后遇到大括號(hào),strongSelf就被銷毀,再也不存在對(duì)self的強(qiáng)引用。
GCD
[Item 41] 安排同步/異步操作
[Item 42] 代替performSelector系列
[Item 43] 有些情況下更適合用NSOperation和NSOperationQueue,因?yàn)橄噍^于GCD,它們可以取消執(zhí)行、設(shè)置依賴、KVO觀察完成進(jìn)度、設(shè)置單個(gè)操作(而不是整個(gè)隊(duì)列)的優(yōu)先級(jí)、可重用。不過(guò)它們是OC類,沒有GCD(C API)那么輕便。
[Item 44] 安排分組任務(wù)
[Item 45] 對(duì)于只執(zhí)行一次,而且要保證線程安全的代碼,應(yīng)該用dispatch_once,比如單例的sharedInstance方法,原來(lái)的寫法是:
static EOCClass *sharedInstance = nil;
@synchronized(self) {
if (!sharedInstance) {
sharedInstance = [[self alloc] init...];
}
}
可以改成:
static EOCClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init...];
});
后一種實(shí)現(xiàn)比使用@synchronized快得多。
Enumeration
[Item 48]
快速枚舉即for...in,可以這樣實(shí)現(xiàn)反向枚舉:
for (id object in [anArray reverseObjectEnumerator]) {...}
block枚舉即enumerate...UsingBlock:方法,它的block有一個(gè)參數(shù)BOOL *stop,用*stop = YES即可停止枚舉。這種枚舉的好處在于:
- 枚舉時(shí)可有選項(xiàng),用
enumerate...WithOptions:usingBlock:方法,第一個(gè)參數(shù)是NS_OPTIONS類型,可選枚舉時(shí)是否并發(fā)、是否反向 - 枚舉
NSArray的時(shí)候能知道下標(biāo) - 枚舉
NSDictionary可以同時(shí)得到key和value,而快速枚舉只能枚舉key或value,再用key去取value或相反
block里面key和value默認(rèn)的類型是id,這是可以直接改的,比如改成:
[aDictionary enumerateKeysAndObjectsUsingBlock:
^(NSString *key, CustomClass *obj, BOOL *stop) {...}];
CF Objects
[Item 49]
CoreFoundation是C的API,其類以CF為前綴;Foundation是OC的庫(kù),其類以NS為前綴。
NS -> CF:
-
__bridge是不移交所有權(quán)的轉(zhuǎn)換,ARC仍然負(fù)責(zé)其引用計(jì)數(shù) -
__bridge_retained則是移交控制權(quán),ARC不再負(fù)責(zé),需要手動(dòng)調(diào)用CFRelease()
CF -> NS:
-
__bridge仍是不移交所有權(quán)的轉(zhuǎn)換,仍需要手動(dòng)調(diào)用CFRelease() -
__bridge_transfer則是把引用計(jì)數(shù)交給ARC
很多NS類都有對(duì)應(yīng)的CF類,但兩者有不同的特點(diǎn)。例如NSDictionary的內(nèi)存管理策略是key為copy,value為retain,策略不能更改;而CFDictionary的內(nèi)存管理都是可以自定義的。
NSCache and NSPurgeableData
[Item 50]
NSCache
對(duì)于比較expensive的資源,比如從網(wǎng)絡(luò)下載的或者從硬盤讀取的圖片,應(yīng)該在內(nèi)存允許的情況下存進(jìn)字典,下一次使用時(shí)直接從內(nèi)存讀取,而不是重新下載或從硬盤讀取。但內(nèi)存緊張時(shí),如NSDictionary之類不會(huì)自動(dòng)決定要釋放哪些資源,為避免手動(dòng)管理的麻煩,應(yīng)該用NSCache。
NSCache和NSDictionary很像,都有一對(duì)一對(duì)的key和value。但NSCache可以用不可copy的對(duì)象作為key,而且是線程安全的,可以在不加鎖的狀態(tài)下給它發(fā)數(shù)條讀取命令。
NSCache用兩個(gè)參數(shù)來(lái)輔助決定是否釋放資源、釋放哪些資源:一是key-value對(duì)的總數(shù),而是所有value的cost的總和的上限。這兩個(gè)參數(shù)都是手動(dòng)設(shè)置的,前一個(gè)比較簡(jiǎn)單,也就是設(shè)定字典的count值的上限;第二個(gè)所謂的cost是需要自己定義的,建議直接用NSData的數(shù)據(jù)大小,不要用復(fù)雜的方法來(lái)計(jì)算cost,增加系統(tǒng)開銷。代碼可能是這樣的:
_cache.countLimit = 100;
_cache.totalCostLimit = 5 * 1024 * 1024;
即最多裝100個(gè)對(duì)象,cost總量的上限,也就是數(shù)據(jù)大小的上限值是5MB。
注意
- 這些上限值僅作參考,并不是超過(guò)了總量就一定會(huì)釋放、沒超總量就一定不釋放,全憑系統(tǒng)決定,不要手動(dòng)干預(yù)這個(gè)過(guò)程。
NSPurgeableData
NSPurgeableData是可以配合NSCache使用的一個(gè)類,可以用NSData來(lái)創(chuàng)建。NSCache有一個(gè)BOOL型的屬性evictsObjectsWithDiscardedContent,設(shè)為YES時(shí),NSPurgeableData可以被系統(tǒng)直接移出NSCache;設(shè)為NO時(shí)不會(huì)被自動(dòng)移除,但其數(shù)據(jù)可能被系統(tǒng)釋放掉,使用之前須調(diào)用isContentDiscarded檢查數(shù)據(jù)還在不在。
使用NSPurgeableData之前(除了剛剛初始化的時(shí)候),應(yīng)該調(diào)用beginContenAcess方法,告知系統(tǒng)即將開始使用資源,不要在使用期間釋放資源;用完以后要執(zhí)行endContentAcess方法,告訴系統(tǒng)現(xiàn)在開始可以自行決定是否釋放。
Load and Intialize Methods
[Item 51]
NSObject有一個(gè)叫load的類方法,當(dāng)一個(gè)類或是category被加入runtime時(shí)被調(diào)用一次。此時(shí)處于同一個(gè)library里面的其他類都不保證已被load,所以不要調(diào)用他們。這個(gè)方法里只應(yīng)該做簡(jiǎn)單的打印,不要做太多操作,尤其是會(huì)阻塞主線程的。
另外有一個(gè)叫initialize的類方法,當(dāng)一個(gè)類第一次被使用時(shí)調(diào)用一次(未被使用的類的initialize方法永遠(yuǎn)不被調(diào)用)。此時(shí)執(zhí)行該類的其他方法或者調(diào)用其他類都是安全的,但仍應(yīng)該避免做太多操作。除了打印以外,可以初始化一些編譯時(shí)不能確定的量,如:
static NSMutableArray *anArray;
@implemention EOCClass
+ (void)load {
NSLog("EOCClass loaded");
}
+ (void)initialize {
if (self == [EOCClass class]) {
anArray = [NSMutableArray new];
}
}
注意
-
load方法不會(huì)被繼承或重寫,所以不需要判斷class;initialize則參與繼承和重寫。因?yàn)殪o態(tài)變量只有一個(gè),被父類和子類共享,所以要判斷class,子類無(wú)需再初始化一遍。
NSTimer
[Item 52]
用NSTimer也很容易造成循環(huán)引用,因?yàn)樗鼊?chuàng)建時(shí)有一句target:self。一種解決方法是給NSTimer加一個(gè)category,于其內(nèi)做初始化,target:self就相當(dāng)于target:[NSTimer class],雖然循環(huán)引用仍然存在,但NSTimer類自身是個(gè)單例,自己循環(huán)引用自己是沒問(wèn)題的。
此時(shí)可以把原來(lái)要執(zhí)行的方法裝進(jìn)block傳到category,放在NSTimer的userInfo里面;新建一個(gè)方法,接收NSTimer作為參數(shù),方法的內(nèi)容就是從其userInfo里取出block并執(zhí)行。將這一方法作為初始化NSTimer時(shí)用的@selector。category的代碼如下:
+ (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats {
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(eoc_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
+ (void) eoc_blockInvoke:(NSTimer *)timer {
void (^block)() = timer.userInfo;
if (block) {
block();
}
}
因?yàn)橛玫搅?code>block,也要注意它的循環(huán)引用問(wèn)題。