《Effective Objective-C 2.0》筆記

這里僅記錄對(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的子類。這樣做可以:

  1. 當(dāng)這個(gè)頭文件被引用時(shí),避免引用者知道不必要的細(xì)節(jié),節(jié)省其編譯時(shí)間
  2. 如果有兩個(gè)類需要相互引用,頭文件中互相#import xxx.h會(huì)造成交叉引用,移到.m文件即可避免
  3. 一些私有的方法、屬性和協(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)建了加下劃線的成員變量,以及settergetter方法。@property后面括號(hào)里的內(nèi)容是為了給編譯器自動(dòng)創(chuàng)建settergetter提供依據(jù),比如若不指定為nonatomic,則默認(rèn)為atomic,系統(tǒng)會(huì)保證settergetter的完整執(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)用settergetter。若有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ì)該類可見

這些變量是沒有settergetter方法的,不能用點(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方法。NSSetNSDictionary判斷兩個(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è)NSSetcopy的結(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)建settergetter的方法,與數(shù)據(jù)庫(kù)交互的時(shí)候用得著。

[Item 13]介紹了在不創(chuàng)建子類的情況下為任何已有方法(如NSStringlowercaseString方法)添加附加操作(比如打印出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ù)遭到不必要的修改:

  1. 不應(yīng)該被外部直接修改的屬性,應(yīng)該聲明(readonly)(可以在Extension中重新聲明為(readwrite),使它對(duì)外只讀,對(duì)內(nèi)可讀寫)。
  2. 不要把NSMutable(Array/Dictionary/Set...)暴露出來(lái),應(yīng)該只留一個(gè)setter給外部使用,以免它們被其他類修改時(shí),類自身難以察覺。
  3. 如果數(shù)據(jù)不是特別多,copy的代價(jià)不是特別大,留給其他類的getter應(yīng)盡量用copy方法,返回一個(gè)不可變的對(duì)象。

Naming

[Item 19] [Item 20]

  1. 如果方法返回一個(gè)新生成的值,應(yīng)該以該值的類型開頭,例如stringWithFormat:。直接返回某個(gè)屬性的方法除外,直接用屬性名即可。
  2. 方法名里應(yīng)該用整個(gè)單詞(string),不要用縮寫(str)。
  3. BOOL屬性加前綴is。返回BOOL值的方法(但不是直接返回某個(gè)BOOL型的屬性)應(yīng)該以ishas為前綴。
  4. 用前綴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í)例。
  • 所有類copymutableCopy都不保證是深拷貝還是淺拷貝,只能根據(jù)文檔判斷。自定義時(shí)應(yīng)該盡量用淺拷貝,減少系統(tǒng)開銷。

Protocol and Delegate

[Item 23]

  1. 一個(gè)對(duì)象向多個(gè)對(duì)象傳消息 -> NSNotification
  2. 多個(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è)名為SayHellocategory,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文件里。

  1. 大括號(hào)里可以聲明私有變量,對(duì)其他類(包括子類)都是不可見的(也可以把私有變量寫在@implemention后的大括號(hào)里,效果是一樣的,只是習(xí)慣問(wèn)題)。
  2. 可以用@property把已經(jīng)在頭文件里聲明為readonly的屬性聲明為readwrite,這樣這個(gè)屬性對(duì)外只讀,對(duì)本類可讀可寫。
  3. 可以聲明私有方法,應(yīng)該以p_為前綴。
  4. 如果不想把遵循的協(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)該處理:

  1. malloc分配的空間應(yīng)該free
  2. CoreFoundation對(duì)象應(yīng)該CFRelease
  3. NSNotificationCenter移除observer
  4. KVO移除observer

應(yīng)該避免在dealloc方法里調(diào)用其他方法,因?yàn)楹笳哂玫降膶?duì)象可能已經(jīng)被銷毀。不是所有的清理工作都應(yīng)該由dealloc承擔(dān),比如以下情況:

  1. 一些比較expensive的資源,如fileDescriptor、socketsmalloc分配的內(nèi)存,不應(yīng)該多次銷毀和重新創(chuàng)建,應(yīng)該寫openclose方法,多次使用。
  2. APP被突然中止時(shí)dealloc不會(huì)被調(diào)用,系統(tǒng)會(huì)直接銷毀所有資源。需要在這時(shí)完成的工作,比如保存信息,應(yīng)該寫在AppDelegateapplicationWillTerminate:里。

Weak

[Item 33]

unsafe_unretainedweak的區(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] 有些情況下更適合用NSOperationNSOperationQueue,因?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í)得到keyvalue,而快速枚舉只能枚舉keyvalue,再用key去取value或相反

block里面keyvalue默認(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)存管理策略是keycopy,valueretain,策略不能更改;而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。

NSCacheNSDictionary很像,都有一對(duì)一對(duì)的keyvalue。但NSCache可以用不可copy的對(duì)象作為key,而且是線程安全的,可以在不加鎖的狀態(tài)下給它發(fā)數(shù)條讀取命令。

NSCache用兩個(gè)參數(shù)來(lái)輔助決定是否釋放資源、釋放哪些資源:一是key-value對(duì)的總數(shù),而是所有valuecost的總和的上限。這兩個(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ì)被繼承或重寫,所以不需要判斷classinitialize則參與繼承和重寫。因?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,放在NSTimeruserInfo里面;新建一個(gè)方法,接收NSTimer作為參數(shù),方法的內(nèi)容就是從其userInfo里取出block并執(zhí)行。將這一方法作為初始化NSTimer時(shí)用的@selectorcategory的代碼如下:

+ (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)題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Effective Objective-C讀書筆記,記錄書中的總結(jié)點(diǎn),加入了一些例子,方便理解和后期回顧。 一、熟...
    peaktan閱讀 414評(píng)論 0 2
  • 第一章 1. Objective-C 使用的是消息結(jié)構(gòu)而非函數(shù)調(diào)用,其區(qū)別在于: 消息結(jié)構(gòu)的語(yǔ)言,其運(yùn)行時(shí)所應(yīng)執(zhí)行...
    鄭嘉成_閱讀 799評(píng)論 3 11
  • 一 熟悉Objective-C 了解Objective-C語(yǔ)言的起源 在類的頭文件中盡量少引入其他頭文件 除非確有...
    gamper閱讀 297評(píng)論 0 3
  • Objective-C 入門 Object-C 是 C 的超集,它使用運(yùn)行時(shí)來(lái)進(jìn)行動(dòng)態(tài)綁定,所有對(duì)象的類型都是在運(yùn)...
    王小明if閱讀 1,101評(píng)論 0 7
  • 第一條:了解Objective-C語(yǔ)言的起源 1.Objective-C語(yǔ)言的對(duì)象所占內(nèi)存總是分配在“堆空間”中。...
    阿凡提說(shuō)AI閱讀 199評(píng)論 0 0

友情鏈接更多精彩內(nèi)容