Effective Objective-C 學(xué)習(xí)總結(jié)

0. 前言

  • 這是一本傳說中不看完不讓轉(zhuǎn)正的書。瑟瑟發(fā)抖系列。
  • 并不適合作為入門書籍,更像是實(shí)戰(zhàn)經(jīng)驗(yàn)總結(jié)。

1. 熟悉OC

1.1 盡量少在頭文件中import

  • 減少編譯時間
  • 避免循環(huán)引用,如果A B互相import,雖然不會造成死循環(huán)(不是include),但是兩個文件里面有一個會編譯失敗
  • 協(xié)議盡量單獨(dú)拆分,方便別的文件進(jìn)行引用
  • 可以使用@class進(jìn)行向前聲明

1.2 多用語法糖(字面量語法)

use:     @(1);
instead: [NSNumber numberWithInt:1];

類似的語法有:
NSString *str = @"1";
Person *person = people[0];
NSDictionary *dict = @{@"key":@"value"};

1.3 使用常量類型,少用Define

  • define有被重復(fù)覆蓋的可能
  • define沒有類型檢查,即使被更改了類型也不會有警告

how to use:

  • 在頭文件中extern const
  • 在.m中定義
  • 使用k開頭,與一般變量/常量進(jìn)行區(qū)分
xxxxx.h
extern NSString *const kGlobalA;

xxxxx.m
NSString *const kGlobalA = @"test string";

2. 對象 消息 運(yùn)行時

2.1 消息轉(zhuǎn)發(fā)機(jī)制

在OC中,如果向某個對象傳遞消息,使用的是動態(tài)綁定技術(shù)

  1. msg_send
  • 實(shí)例接受消息
  • 當(dāng)前不能處理,isa尋找父類
  • 遵循繼承鏈都不能處理,做消息轉(zhuǎn)發(fā)(forwarding)【- +方法都可以進(jìn)行轉(zhuǎn)發(fā)】

2.2 Method Swizzling

通過交換IMP,將selector映射到不同的方法實(shí)現(xiàn)上。

//取出方法:
class_getInstanceMethod(Class class, SEL selector)

//交換方法:
class_exchangeImplementations(method1, method2)

2.3 理解類對象

  • 對象是分配在堆空間上的,一個“*”代表一個內(nèi)存地址
  • 對象不能分配在??臻g上,例如以下就是非法的:
  // 嘗試分配在棧空間
  NSString str = [NSString xxxxxx];
  • id可以指代任意對象,本身就是一個指針

3. 接口與API設(shè)計(jì)

3.1 使用前綴來避免命名沖突

  • 重名時,app的鏈接過程會報(bào)錯。
  • 重名發(fā)生在動態(tài)鏈接時,會引發(fā)crash
  • 可以使用適當(dāng)?shù)那熬Y來避免庫之間、app之間的重名問題。

需要注意的是,Apple保留其使用雙字幕前綴的權(quán)利,所以我們最好選用三字母作為前綴
eg: 用項(xiàng)目名稱的簡寫來命名,QQXXXViewController QRXXXXXView等

使用前綴的好處:

  • 避免重名引發(fā)的編譯問題/運(yùn)行時問題
  • 在回溯問題時,可以比較方便地定位代碼塊

3.2 重寫對象的description方法

  • 直接NSLog打印對象/在斷點(diǎn)時po對象,看不到具體的屬性信息
  • 重寫description方法,格式化輸出對象信息
  • 當(dāng)然,最好保持原有的打印體驗(yàn)
// for NSLog
- (NSString *)description {
    return [NSString stringWithFormat:@"<%@: @p, %@>
        [self class], // 類名
        self,         // 地址
        @{@"title" : self.title,
          @"name"  : self.name,
          ...}
    "];
}
// for po in lldb
- (NSString *)debugDescription {
    // detail description
}

3.3 盡量使用不可變對象

聲明property時:

  • 在.h中時指定 readonly
  • 在.m中使用 raedwrite

3.4 使用合理的方法命名

  • 清晰
  • 可以稍長,但是不要啰嗦
// better
- (CGFloat)area;

// worse
- (CGFloat)calculateTheArea;
  • 范圍值為BOOL時,方法名一般以is或者h(yuǎn)as開頭
- (BOOL)isEqualToString:
- (BOOL)hasPrefix:
  • 方法中盡量不要使用縮略詞,除非是默認(rèn)熟知的詞匯(eg,MD5)

3.5 為私用方法名加上前綴

Apple喜歡用單下劃線作為私用方法的前綴。因此,我們應(yīng)該盡量避免以“_”作為方法的開頭

  • 便于區(qū)分方法是否被外部調(diào)用(如果方法實(shí)現(xiàn)有調(diào)整,被外部使用的方法需要全量過一遍,對減包來說同理)

3.6 理解OC的錯誤模型

ARC下不是“異常安全的(Exception Safe)”
如果拋出異常,那么本應(yīng)在作用域末尾釋放的對象就不會自動釋放了
如果想要異常安全,需要加上編譯選項(xiàng) -fobjc-arc-exceptions

  • 異常應(yīng)該只作用于極其嚴(yán)重的錯誤(比如放在某個重要基類的init方法中)
  • OC中處理一般錯誤,可以返回nil/0,業(yè)務(wù)層做校驗(yàn)。一些比較重要的路徑,可以asset一下。
  • 對于一些自定義類,在處理錯誤的時候,可以使用delegate將錯誤信息返回給調(diào)用者

3.7 理解NSCopying

  • 如果想讓自己實(shí)現(xiàn)的類有copy操作,需要實(shí)現(xiàn)NSCopying協(xié)議。該協(xié)議只有一個實(shí)現(xiàn)方法:
- (id)copyWithZone:(NSZone *)zone;
  • 當(dāng)然,也有可變版本的mutableCopy方法:
- (id)mutableCopyWithZone:(NSZone *)zone;
  • 可以根據(jù)自己的需要實(shí)現(xiàn)不同的協(xié)議

需要注意的是,Zone這個概念是個歷史遺留問題。在以前的開發(fā)時,會將內(nèi)存分為不同的區(qū)域(Zone),而對象會創(chuàng)建在特定的區(qū)域中。現(xiàn)在只有一個區(qū)(默認(rèn)區(qū),default zone)的概念?,F(xiàn)在仍使用這個協(xié)議進(jìn)行copy的重寫,但是無需關(guān)心zone的概念了。

4. 分類與協(xié)議

4.1 使用委托與數(shù)據(jù)源/協(xié)議進(jìn)行對象間通信

  • delegate對象需要使用weak修飾(使用strong會造成retain cycle)
  • 類內(nèi)部調(diào)用委托方法進(jìn)行信息傳遞的時候,最好使用一下respondsToDelegate:,判斷一下delegate是否真的實(shí)現(xiàn)了這個協(xié)議方法,防止crash

4.2 使用分類來分離臃腫的代碼

  • 將類的方法劃分成易于管理的小模塊
  • 可以覆蓋主類的方法實(shí)現(xiàn)
  • 不同分類間的方法可以重名,后編譯的會被調(diào)用

4.3 使用分類來擴(kuò)展第三方類時加上前綴

4.2 中所述,通過加上自己的前綴,可以避免方法/分類名重復(fù)

@interface NSString(ZK_Debug)
// some code
@end

4.4 勿在分類中聲明變量

分類中無法聲明新的屬性,這與分類的實(shí)現(xiàn)是息息相關(guān)的。

// 分類結(jié)構(gòu)體 源碼實(shí)現(xiàn)
struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods; // 對象方法
    struct method_list_t *classMethods; // 類方法
    struct protocol_list_t *protocols; // 協(xié)議
    struct property_list_t *instanceProperties; // 屬性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

從源碼基本可以看出我們平時使用categroy的方式,對象方法,類方法,協(xié)議,和屬性都可以找到對應(yīng)的存儲方式。并且我們發(fā)現(xiàn)分類結(jié)構(gòu)體中是不存在成員變量的,因此分類中是不允許添加成員變量的。分類中添加的屬性并不會幫助我們自動生成成員變量,只會生成get set方法的聲明,需要我們自己去實(shí)現(xiàn)。

一些iOS底層的知識,例如對runtime、category、block、runloop等的詳細(xì)解析,推薦@xx_cc在簡書上的這一系列文章
http://www.itdecent.cn/notebooks/24110540

  • 不要在分類中添加屬性
  • 可以在主類中定義,分類中使用

5. 內(nèi)存管理

OC的內(nèi)存管理是基于引用計(jì)數(shù)機(jī)制來實(shí)現(xiàn)的。有了ARC之后,幾乎所有的內(nèi)存管理工作都交給了編譯器,開發(fā)者可以更關(guān)注于業(yè)務(wù)邏輯。
從MacOS 10.8開始,垃圾收集器(garbage collector)被正式廢棄;對iOS來說,這個概念是從未被引入的。

5.1 理解引用計(jì)數(shù)

  • retain 遞增引用計(jì)數(shù)
  • release 遞減
  • autorelease 等autorelease pool來進(jìn)行遞減操作

當(dāng)引用計(jì)數(shù)為0時,會將對應(yīng)內(nèi)存標(biāo)記為reuse,所有指向?qū)ο蟮囊枚紵o效

  • 計(jì)數(shù)為0,內(nèi)存不一定立即被回收。
  • 如果在執(zhí)行后續(xù)代碼時,未覆寫對象內(nèi)存,不會crash
  • 一般在release后執(zhí)行一下置空操作
[object release];
object = nil;

5.2 在dealloc中釋放引用&解除監(jiān)聽

[[NSNotificationCenter defaultCenter] removeObserver:self];

5.3 善用Autorelease Pool降低內(nèi)存峰值

  • App生命周期中會自動持有一個autorelease pool
  • 在遇到for里大量創(chuàng)建臨時對象時,可以使用autorelease pool,來降低內(nèi)存峰值
for (int i = 0; i < 100000; i++) {
    @autoreleasepool {
        // code here
    }
}

5.4 使用Zombie Object來調(diào)試內(nèi)存管理問題

啟用Zombie Object后,在運(yùn)行期,系統(tǒng)會將所有已經(jīng)收回的實(shí)例轉(zhuǎn)化為僵尸對象,而不會重新回收它們;這種對象的內(nèi)存地址不會遭到覆寫。在向僵尸對象發(fā)送消息時,會拋出異常,App crash。

實(shí)現(xiàn)方式:

  • 通過對dealloc方法進(jìn)行method swizzle。
  • 目的是為了保留原類名(LOG的時候能區(qū)分出來具體是哪個類)

原理:

  • 類似KVO,系統(tǒng)會自動創(chuàng)建以_NSZombie_開頭的類,通過修改isa來指向新類
  • 新類會將整個類結(jié)構(gòu)copy一份
  • 新類會響應(yīng)原有類的所有selector,但是會打印出消息內(nèi)容,app crash

6. Block & GCD

Block是可在C、C++、OC中使用的語法閉包,開發(fā)者可以將代碼像對象一樣傳遞。

GCD是一種與Block有關(guān)的技術(shù),基于dispatch queue,提供對線程的抽象

6.1 理解block

// block的定義
return_type(^block_name)(paramters) {
    //code here
}

以下是一個實(shí)例:

// define
int (^addBlock)(int a, int b) = ^(int a, int b) {
    return a+b;
}

// imp
int sum = addBlock(a, b);
  • 在block聲明范圍內(nèi)的所有變量,均可以被block捕獲;但是如果需要在block內(nèi)改變它的值,則需要加上__block前綴。
  • 要注意避免retain cycle,對于self來說,可以定義__weak
  • block可以分配在?;蚨焉?, 也可以是全局的
  • 棧上的block可以通過copy,移至堆中。

6.2 為常用block進(jìn)行typedef

如果我們有網(wǎng)絡(luò)框架,回包數(shù)據(jù)通過block傳遞給上層,我們可能會這么寫:

- (void)requestWithCompletion:(void(^)(NSData *data, NSError *error));

如果封裝了若干個方法,回包結(jié)構(gòu)又是一致的,我們可以typedef一下block,使方法看起來可讀性更強(qiáng)。

typedef void(^completeBlock)(NSData *data, NSError *error);

- (void)requestWithCompletion:(completeBlock)completeBlock;
  • 增強(qiáng)代碼可讀性
  • 后續(xù)如果需要擴(kuò)展block,直接修改定義的地方,可以通過觸發(fā)編譯錯誤的方式,快速定位所有需要修改的方法實(shí)現(xiàn)。

6.3 使用block來降低代碼分散程度

對比的對象是delegate。
如我們熟悉的tableView和早期的UIAlertView組件,都是使用delegate來完成一些相關(guān)邏輯的。
缺點(diǎn)在于,組件創(chuàng)建、展示和業(yè)務(wù)處理的代碼,是分離開的。且同一種類型對象的業(yè)務(wù)邏輯,需要在delegate中使用if-else進(jìn)行區(qū)分,久而久之代碼比較臃腫。

  • 開發(fā)者可以主動選擇在什么線程上執(zhí)行代碼
  • 在創(chuàng)建的時候聲明后續(xù)業(yè)務(wù)邏輯,更完整也更簡潔

6.4 多用GCD,少用同步鎖

在多線程問題的時候,往往需要對一些數(shù)據(jù)進(jìn)行“加鎖”。

  • @synchronize()
  • NSLock
  • NSRecursiveLock
  • ...

可以使用GCD來進(jìn)行優(yōu)化。(對synchronize來說,不濫用即可)

  • 使用串行同步隊(duì)列
  • 混合使用同步和異步GCD
  • 加入隊(duì)列和barrier,可以掌控同步時序,提升效率

6.5 掌握GCD及NSOperationQueue的使用時機(jī)

GCD:

  • GCD并不是永遠(yuǎn)都是最佳解決方案
  • GCD是基于C的API

NSOperationQueue:

  • NSOperationQueue是比較重量級的封裝,是OC對象。
  • 面向?qū)ο蠓庋b,使用起來更直觀簡單
  • 底層使用GCD實(shí)現(xiàn)
  • 在各種設(shè)置項(xiàng)上更簡單(線程池,并發(fā)數(shù),線程優(yōu)先級)

需要注意的是,NSNotificationCenter內(nèi)部是使用NSOperationQueue的。

6.6 使用dispatch group

// 創(chuàng)建
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// mission1
dispatch_group_enter(group); //group引用計(jì)數(shù)+1
dispatch_async(queue, ^{
    //code here
    dispatch_group_leave(group); //group引用計(jì)數(shù)-1
});

// mission2
dispatch_group_enter(group);
dispatch_async(queue, ^{
    //code here
    dispatch_group_leave(group);
});

使用dispatch_group后,以下是兩種繼續(xù)執(zhí)行的方式:

  • 當(dāng)前線程等待,直到group全部執(zhí)行完再往下執(zhí)行
//...
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
//code here...
  • 當(dāng)前線程繼續(xù)往下執(zhí)行;當(dāng)group全部執(zhí)行完成后,執(zhí)行block
dispatch_group_notify(group, queue, ^{
    // code here...
});

6.7 dispatch_once 只執(zhí)行一次的線程安全代碼

最典型的代表:【單例】

+ (instancetype)sharedInstance {
    static QRAccountManager *__QRAccountManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        __QRAccountManager = [QRAccountManager new];
    });
    return __QRAccountManager;
}
  • dispatch_once_t類似token,可以唯一標(biāo)識block
  • 使用dispatch_once的效率是@synchronize的兩倍

6.8 不要使用dispatch_get_current_queue

  • iOS6.0起已經(jīng)廢棄
  • 該函數(shù)的行為往往與預(yù)期不一致
  • 可以做調(diào)試,但是盡量避免使用。

7. 系統(tǒng)框架

7.1 熟悉系統(tǒng)框架

  • Foundation
    • 為什么是NS開頭?將OC用作是NeXTSTEP操作系統(tǒng)(喬老板被趕出去建立的公司的產(chǎn)物,OSX的前身)的編程語言時確定的
  • CoreFoundation:在里面,很多Foundation的功能可以找到對應(yīng)的C語言API
  • CFNetwork:對BSD socket的封裝,提供網(wǎng)絡(luò)接口
  • CoreAudio
  • AVFoundation
  • CoreText:文字渲染和排版

在使用OC時,會經(jīng)常用到C語言級別的API。好處是可以繞過OC的運(yùn)行時,提升執(zhí)行速度;缺點(diǎn)是,使用C語言級別API,需要自己管理內(nèi)存。

一些其他的框架:

  • Appkit
  • UIKit
  • CoreAnimation
  • CoreGraphics
  • QuartzCore
  • MapKit
  • SceneKit
  • CoreML
  • ...

7.2 多用Enumberator,少用for

  • 幾乎所有的collection都可以用這個套API
  • 有一些快速遍歷的接口(FastEnumberator)
  • 本身就可以通過GCD來進(jìn)行并發(fā)操作

7.3 toll-free bridging

toll-free bridging,無縫橋接,是Foundation和CoreFoundation之間的粘合劑。使用這個技術(shù),可以將兩個框架之間的對象進(jìn)行轉(zhuǎn)換

NSArray *anNSArray = @[@"1", @"2", @"3"];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray; // OC -> C
  • __bridge表示ARC仍具有OC對象的使用權(quán)
  • __bridge_retain則表示ARC需要交出OC對象的控制權(quán),開發(fā)者自己使用CFRelease()來手動釋放
  • __bridge_transfer,CFArrayRef --> NSArray

7.4 構(gòu)建緩存的時候選用NSCache而非NSDictionary

  • NSCache在系統(tǒng)資源緊張的時候,會自動刪除緩存(Dict則需要在內(nèi)存警告中進(jìn)行重寫)
  • NSCache會優(yōu)先刪除最久未使用的對象
  • NSCache本身是線程安全的

開發(fā)者是可以手動控制緩存刪除內(nèi)容的時機(jī):

  • 緩存中的對象總數(shù)
  • 所有對象的總開銷

想通過調(diào)整“開銷”來迫使緩存優(yōu)先刪除某對象,不是個好主意。
不想要這種不確定性,還是用dict,手動管理好內(nèi)存峰值比較合適

_cache = [NSCache new];
_cache.countLimit = 100; // 對象上限
_cache.totalCostLimit = 5 * 1024 * 1024; // 開銷上限,5M

7.5 精簡initialize與load

+(void)load;

  • App啟動時,對于運(yùn)行時的每個class和category,必定會調(diào)用此方法
  • 僅調(diào)用一次
  • 執(zhí)行子類的 +load 前,必定會執(zhí)行父類的 +load
  • 原類的優(yōu)先于分類執(zhí)行
  • 并不完全遵從繼承規(guī)則(子類沒有實(shí)現(xiàn) +load ,各級超類的 +load 不會執(zhí)行)

實(shí)現(xiàn)的時候,應(yīng)該精簡一些:

  • 會在App啟動的時候執(zhí)行
  • 盡量不要使用鎖
  • 不要在里面使用別的類,因?yàn)橛锌赡苓€沒有被加載

+(void)initialize;

  • 程序首次使用某個類前調(diào)用(惰性調(diào)用,使用類時才調(diào)用)
  • 僅調(diào)用一次
  • 該方法是線程安全的
  • 如果子類沒有實(shí)現(xiàn),會自動調(diào)用父類的方法(如果父類實(shí)現(xiàn)了)
  • 一般在initialize里面加上if (self == [XXXClass class]),來特定

實(shí)現(xiàn)的時候,也應(yīng)該精簡一些:

  • 不好掌控觸發(fā)時機(jī)
  • 不宜使用鎖,避免阻塞線程
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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