《Effective Objective C》重讀校驗自己的知識體系

奧卡姆剃鬚刀發表於2018-02-07

Effective Objective-C

讀後感先放在前邊 現在詳細來看這本書應該也不晚吧,iOS 開發之類的書籍其實網上的總結還是蠻多的 有很多文章寫得都是挺不錯的, 但是終歸是別人的的讀後感總結,看著別人的總結終歸不能完全吸收為自己的,所以近期抽空把 iOS 相關書籍看一遍 來對自己的知識體系做一個校驗 書中舉得很多例子都是挺好的 此文章也總結了此書的大綱,只有一些本人比較生疏的知識點才會展開詳細描述,書中有很多細節並不是我們日常開發中能注意到的但是很重要的一些知識點, 此篇文章寫得耗費時間還是挺久的

第一章 熟悉 Objective-C

1 瞭解 Objective-C 語言的起源

OC 語言使用了"訊息結構" 而非是"函式呼叫" 訊息結構與函式呼叫區別關鍵在於: 一 使用訊息結構的語言,其執行時所應執行的程式碼有執行環境來決定 二 使用函式呼叫的語言,則有編譯器決定的 OC 語言使用動態繫結的訊息結構,也就是說在在執行時才會檢查物件型別,接受一條訊息之後,究竟應執行何種程式碼, 有執行期環境而非編譯器來決定

下圖來看一下 OC 物件的記憶體分配

WechatIMG86.jpeg
此圖佈局演示了一個分配在對堆中的 NSString 例項, 有兩個分配在棧上的指標指向改例項 OC 系統框架中也有很多使用結構體的, 比如 CGRect, 因為如果改用 OC 物件來做的話, 效能就會受影響

2 在類的標頭檔案中儘量少引用其他標頭檔案
  • 我們如非必要, 就不要引入標頭檔案, 一般來說, 應在某個類的標頭檔案中使用向前宣告(向前宣告的意思就是用@Class Person 來表明 Person 是一個類)來提及別的類, 並在實現檔案中引入那些類的 標頭檔案, 這樣做盡量降低類之間 的耦合
  • 有時無法使用向前宣告,比如要宣告某個類遵循一項協議,這種情況下,儘量吧"該類遵循某協議"的這條宣告一直"Class-Continuation 分類"中,如果不行的話, 就把協議單獨放在一個標頭檔案中.然後將其引入
3 多用字面量語法 少用與之等價的方法

推薦使用字面量語法:

NSString * someString = @"奧卡姆剃鬚刀";
NSNumber *number = @18;
NSArray *arr = @[@"123",@"456];
NSDictionary *dict = @{
                         @"key":@"value"
                              };
複製程式碼

對應的非字面量語法

    NSString *str = [NSString stringWithString:@"奧卡姆"];
    NSNumber *number = [NSNumber numberWithInt:18];
    NSArray *arr = [NSArray arrayWithObject:@"123",@"456"]; 
複製程式碼
4 多用型別常量,少用 #define 預處理指令
  • 不要使用預處理指令定義常量, 這樣定義出來的常量不含型別,編譯器只會在編譯前據此執行查詢與替換操作, 即使有人重新定義了常量值, 編譯器也不會產生警告資訊, 這將導致應用程式中的常量值不一致

  • 在實現檔案中使用 static const 來定義"只在編譯單元內可見的常量",由於此類常量不在全域性符號表中, 所以無須為其名稱加字首

舉例說明 不合適的寫法

//動畫時間
#define ANIMATION_DUATION 0.3
複製程式碼

正確的寫法

檢視修改 const修飾的變數則會報錯 
static const NSTimeInterval KAnimationDuration = 0.3
複製程式碼
  • 在標頭檔案中使用 extern 來宣告全域性變數,並在相關實現檔案中定義其值.這種常量要出現在全域性符號表中, 所以其名稱應該加以區隔,通常用與之相關的類名做字首.
// EOCAnimatedView.h
extern const NSTiemInterval EOCAnimatedViewANmationDuration
//  EOCAnimatedView.m
const NSTiemInterval EOCAnimatedViewANmationDuration = 0.3
複製程式碼

這樣定義的常量要優於# Define 預處理指令, 因為編譯器會確保常量不變, 而且外部也可以使用

5 用列舉表示狀態,選項, 狀態碼
  • 如果把傳遞給某個方法的選項表示為列舉型別,而對個選項又可以同事使用, 那麼就將各選項值定義為2的冪, 以便通過按位或操作器組合起來
  • 在處理列舉型別的 switch 語句中不要實現 default 分支, 這樣的話, 加入新列舉之後,編譯器就會提示開發者, switch 語句並未處理所有列舉

按位或操作符列舉

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
複製程式碼

第二章 物件,訊息,執行期

6 理解"屬性"這一概念
  • 可以用@ property 語法來定義物件中所封裝的資料
  • 通過"特性"來指定儲存資料所需的正確語義
  • 在設定屬性所對應的例項變數時, 一定要遵守該遵守該屬性所宣告的語義
  • 開發 IOS 程式是應該使用 nonatomic 屬性,因為 atomic 屬性會嚴重影響效能
7 在物件內部儘量直接訪問例項變數
  • 在物件內部讀取資料是, 應該直接通過例項變數來讀,而寫入資料是,則應該通過屬性來寫
  • 在初始化方法及 dealloc 中,總是應該直接通過例項變數來讀寫資料
  • 有時會使用惰性初始化技術(高大上的說法,其實就是懶載入)配置某份資料,這種情況下,需要通過屬性來讀取資料
8 理解"物件等同性"這一概念
  • 若想檢測物件的等同性. 請提供isEqualhash 方法
  • 相同的物件必須具有相同的雜湊碼,但是兩個雜湊碼相同的物件卻未必相同
  • 不要盲目的逐個檢測每條屬性,而是應該依照具體需求來制定檢測方案
  • 編寫hash 方法是,應該使用計算速度快而且雜湊碼碰撞機率低的演算法
9 以類族模式隱藏實現細節
  • 類族模式可以吧實現細節隱藏在一套簡單的公共介面後面,
  • 系統框架中經常使用類族
  • 從類族的公共抽象基類中繼承自雷是要當心,若有開發文件,則應實現閱讀

此小節比較抽象,用文中的規則來總結一下 大概如下

  • 1 子類應該繼承自類族中的抽象基類 若想編寫 NSArray 類族的子類,則需令其繼承自不可變陣列的基類或可變陣列的基類
  • 2 子類應該定義自己的資料儲存方式 開發者編寫 NSArray 子類時, 經常在這個問題上受阻, 子類必須用一個例項變數來存放陣列中的物件, 這似乎與大家預想的不同, 我們以為 NSArray 自己肯定會儲存那些物件,所以子類中就無需在存一份了, 但是大家要記住, NSArray 本身只不過是包在其他隱藏物件外面的殼, 他僅僅定義了所有陣列都需具備的一些介面,對於這個自定義的陣列子類來說, 可以用 NSArray 來儲存其例項
  • 3 子類應該複寫超類文件中指明需要複寫的方法 在每個抽象基類中, 都有一些子類必須腹瀉的方法, 比如說,想要編寫 NSArray 的子類, 就需要實現 count 及 objectAtIndex 方法,像 lastObject 這種方法則無需事先,因為基類可以根據前兩個方法實現出這個方法
10 在既有類中使用關聯物件存放自定義資料
  • 可以通過"關聯物件" 機制來吧兩個物件連起來
  • 定義關聯物件時,可指定記憶體管理語義,用以模仿定義屬性時所採用的"擁有關係"和"非擁有關係"
  • 只有在其他做法不可行是,才應選用關聯物件,因為這種做法通常會引入難於查詢的 bug

這種方法我在分類中經常使用,而且屢試不爽 以下是本人在專案中的用法

static void *callBackKey = "callBackKey";

@implementation UIView (category)
- (void)addTapWithBlock:(callBack)callback{    
    objc_setAssociatedObject(self, callBackKey, callback, OBJC_ASSOCIATION_COPY);
    self.userInteractionEnabled = YES;
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapClick)];
    [self addGestureRecognizer:tap];
}
- (void)tapClick{
    callBack block = objc_getAssociatedObject(self, callBackKey);
    if (block) {
        block();
    }
}
複製程式碼
11 理解 objc_msgSend 的作用

objc_msgSend 函式會依據接受者與選擇子的型別來呼叫適當的方法,為了完成此操作, 該方法需要在接受者所屬的類中搜尋其"方法列表" ,如果能找到與選擇名稱相符的方法,就跳至其實現程式碼, 若是找不到 就沿著繼承體系繼續向上查詢, 等找到合適的方法在挑戰, 如果還是找不到相符的方法,那就執行"訊息轉發"操作 這個會在12條來講

  • 訊息有接受者,選擇子及引數構成, 給某物件"傳送訊息"也就相當於在該物件上"呼叫方法"
  • 發給某物件的全部訊息都要有"動態訊息派發系統"來處理, 該系統會查出對應的方法,並執行其程式碼
12 理解 訊息轉發機制 重點再次複習一遍

訊息轉發分為兩大階段,第一階段先徵詢接受者,所屬的類, 看其是否能動態新增方法,以處理當前這個"未知的的選擇子" 這叫做"動態方法解析",第二階段涉及完整的訊息轉發機制. 如果執行期系統已經把第一階段執行完了, 那麼接受者自己就無法再以動態新增方法的手段來響應包含蓋選擇子的訊息了, 此時,執行期系統會請求接受者以其他手段來處理與訊息相關的方法呼叫, 這又細分兩小步. 首先請接受者看看有沒有其他物件能處理這條訊息,若有 則執行期系統會吧訊息轉給那個物件,於是訊息轉發過程結束,一切如常, 若沒有背援的接受者,則啟動完整的訊息轉發機制,執行期系統會吧訊息有關的全部細節都封裝在 NSInvocation 物件中, 在給接受者最後一次機會, 令其設法解決當前還沒處理的這條訊息

動態方法解析 + (Bool) resolveInstanceMethod:(SEL)selector 該方法的引數就是那個未知的選擇子,其返回值為 Boolean 型別,表示這個類是否能新增一個例項方法用以處理此選擇子.在繼續往下執行轉發機制之前, 本類有機會新增一個處理此選擇子的方法,假如尚未實現的方法不是例項方法而是類方法, 那麼執行期系統就會呼叫另外一個方法 和當前方法類似 resolveClassMethod

備援接受者 當前接受者還有第二次機會能處理未知的選擇子,在這一步,執行期系統會問它: 能不能把這條訊息轉給其他接受者來處理. 與該步驟對應的處理方法如下: - (id)forwardingTargetForSelestor:(SEL)selector 方法引數代表未知的選擇子, 若當前接受者能找到備援物件,則將其返回,若找不到,就返回 nil

完整的訊息轉發 如果轉發演算法已經到這一步的話,俺那麼唯一能做的就是啟用完整的訊息轉發機制了.首先建立 NSInvocation 物件, 把與尚未處理的那條訊息有關的全部細節, 都封裝於其中,此物件包含選擇子、目標,及引數, 在觸發 NSInvocation 物件時, "訊息派發系統"將親自出馬,把訊息指派給目標物件 此步驟會呼叫下列方法來轉發訊息 - (void)forwardInvocation:(NSInvocation * )invocation 再觸發訊息前, 先以某種方式改變訊息內容,比如追加另外一個引數,或是改換選擇子等等 實現此方法是,若發現某呼叫不應有本類處理,擇婿呼叫超類的同名方法, 這樣的話,繼承體系中的每個類都有機會處理此呼叫請求,直至 NSObject, 如果最後呼叫了 NSOBject 方法,那麼該方法還會繼而呼叫doesNotRecognizeSelector以丟擲異常,此異常表明選擇子最終未能得到處理

訊息轉發全流程

訊息轉發全流程.jpg

  • 若物件無法響應某個選擇子,則進入訊息轉發流程
  • 通過運城期的動態方法解析功能,我們可以在需要用到某個方法時再將其加入類中
  • 物件可以把其中無法解讀的某些選擇子轉交給其他物件來處理
  • 經過上述兩步之後, 如果還是沒辦法處理選擇子,那就啟動完整的訊息轉發機制
13 用"方法調配技術"除錯"黑盒方法"

通俗講 其實就是利用 runtime 實現方法交換 這個就不再詳細解說了

  • 在執行器,可以向類中新增或替換選擇子所對應的方法實現
  • 使用另一份實現來替換原有的方法實現, 這道工序叫做"方法調配", 開發者常用此技術向原有實現中新增功能
  • 一般來說, 只有除錯程式的時候,才需要在執行期修改方法實現, 這種做法不易濫用
14 理解"類物件"的用意

每個 Objective-C 物件例項都是指向某塊記憶體資料的指標,描述 Objective-C物件所用的資料結構定義在執行期程式庫的標頭檔案裡, id 型別本身也是定義在這裡

typedef struct objc_object {
Class isa;
} * id
複製程式碼

由此可見,每個物件結構體的首個成員是 Class 類的變數. 該變數定義了物件所屬的類,通常稱為 isa 指標 Class 物件也定義在執行期程式庫的標頭檔案中中:

typedef struct objc_class *Class;
struct objc_class{
         Class isa;
         Class super_class;
         const char *name;
         long version;
         long info;
         long instance_size;
         struct objc_ivar_list *ivars;
         struct objc_method_list **methodLists;
         struct objc_cache *cache;
         struct objc_protocol_list *protocols;
}
複製程式碼

此結構體存放類的後設資料,此結構體的首個變數也是 isa 指標, 這說明, Class 本身也是 Objective-C 物件,結構體中的 super_class 它定義了本類的超類, 類物件所屬的型別(也就是 isa 指標所指向的型別)是另外一個類, 叫做元類,用來表述類物件本身所具備的後設資料.每個類僅有一個類物件,而每個類物件僅有一個與之相關的元類 假設有一個 someClass 的子類從 NSObject 中繼承而來,則它的繼承體系可由下圖表示

繼承體系.jpg

在類繼承體系中查詢型別資訊 可以用型別資訊查詢方法來檢視類繼承體系,isMemberOfClass能夠判斷出物件是否是特定類的例項 而isKindOfClass則能夠判斷出物件是否為某類或某派生派類的例項

  • 每個例項都一個指向 Class 物件的指標, 用以表明其型別,而這些 Class 物件則構成了類的繼承體系
  • 如果物件型別無法在編譯期確定,那麼應該使用型別資訊查詢方法來探知
  • 儘量使用型別資訊查詢方法來確定物件型別,而不要直接比較類物件,因為某些物件可能實現了訊息轉發功能

第三章 介面與 API 設計

15 用字首避免名稱空間衝突
  • 選擇與你的公司,應用程式或二者皆有關聯之名稱作為類名的字首,並在所有程式碼中均使用這一字首.
  • 若自己所開發的程式庫中使用到第三方庫, 則應為其中的名稱加上字首
16 提供"全能初始化方法"

UITableViewCell 初始化該類物件的時候,需要指明其樣式及識別符號, 識別符號能夠區分不同型別的單元格, 由於這種物件的建立成本比較高, 所以繪製表格時 可依照識別符號來服用,提升程式執行效率,這種可以為物件提供必要資訊以便其能完成工作的初始化方法叫做"全能初始化方法"

  • 在類中提供一個全能初始化方法,並於文件中指明, 其他初始化方法均應呼叫此方法
  • 若全能初始化方法與超類不同, 則需覆蓋超類中的對應方法
  • 如果超類的初始化方法不適用於子類, 那麼應該複寫這個超類方法,並在其中排除異常

這一點寫開源框架的時候十分的受用

17 實現 description 方法

這個就不多說了 實際開發中經常用

  • 實現 description 方法 返回一個有意義的字串,用以描述該例項
  • 若想在除錯時列印出更詳盡的物件描述資訊,則應實現 debugDescription
18 儘量使用不可變物件
  • 儘量建立不可變物件
  • 若某屬性僅可在物件內部修改,則在class-continuation分類中將其有 readonly 屬相擴充套件為 readwrite 屬性
  • 不要把可變的 collection 作為屬性公開,而應提供相關方法, 以此修改物件中的可變 collection
LLPerson.h
@interface LLPerson : NSObject
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, assign, readonly) NSInteger age;
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age;
@end
LLPerson.m

@interface LLPerson()

@property (nonatomic, copy, readwrite) NSString *name;
@property (nonatomic, assign, readwrite) NSInteger age;

@end

@implementation LLPerson

- (instancetype)initWithName:(NSString *)name age:(NSInteger)age{
    if (self = [super init]) {
        self.name = name;
        self.age = age;
    }
    return self;
}
複製程式碼
19 使用清晰而協調的命名方式

方法命名的幾條規則

  • 如果方法的返回值是新建立的, 那麼方法名的首個詞應是返回值得型別,除非前面還有修飾語,例如 localizedString 屬性的存取方法不遵循這種命名方式,因為一般以為這些方法不會建立物件,即便有時返回內部物件的一份拷貝, 我們也認為那相當於原有的物件,這些存取方法應該按照其所對應的屬性來命名
  • 應該把表示引數型別的名詞放在引數前面
  • 如果方法要在當前物件執行操作,那麼久應該包含動詞;若執行操作時還需要引數,則應該在動詞後面加上一個或多個名詞
  • 不要使用 str 這種簡稱,應該用 string 這樣的全稱
  • Boolean 屬性應加上 is 字首,如果方法返回非屬性的 Boolean 值, 那麼應該根據其功能 選用 has 或 is 當字首
  • 將 get 這個字首留給那些藉由"輸出引數"來儲存返回值的方法, 比如說,把返回值填充到"C語言陣列"裡的那張方法就可以使用這個詞做字首 類與協議的命名
  • 起名時應遵從標準的 objective-C 命名規範,這樣建立出來的介面更容易為開發者所理解
  • 方法名要言簡意賅,從左至右讀起來要像個日常用於中的句子才好
  • 方法名不要使用縮略後的型別名稱
  • 給方法起名時的第一要務 就是確保其風格與你自己的程式碼或所要整合的框架相符
20 為私有方法名加字首
  • 給私有方法的名稱加上字首, 這樣可以很容易的將其同公共方法區分
  • 不要單用一個下劃線做私有方法的字首, 因為這種做法是預留給蘋果公司用的
21 理解 OBjective -C 錯誤型別
  • 只有發生了可使整個應用程式崩潰的嚴重錯誤時, 才應使用異常
  • 在錯誤不那麼嚴重的情況下, 可以指派"委託方法"來處理錯誤,也可以把錯誤資訊放在 NSError 物件裡, 經由"輸出引數"返回給呼叫者
// 比如 有一個抽象基類, 他的正確用法是先從中繼承一個類,然後使用這個子類, 在這種情況下,如果有人直接使用了一個抽象基類,那麼久丟擲異常
- (void)mustOverrideMethod{
    NSString *reason = [NSString stringWithFormat:@"%@m must be overridden",
                        NSStringFromSelector(_cmd)];
    @throw [NSException
            exceptionWithName:NSInternalInconsistencyException
                                   reason:reason
                                 userInfo:nil];
}
複製程式碼
22 理解 NSCopying 協議
  • 若想另自己所寫的物件具有拷貝功能, 則需實現 NSCopying 協議
  • 如果自定義的物件分為可變版本與不可變版本, 那麼就要同時實現 NSCoping與 NSMutableCopying 協議
  • 賦值物件是需決定採用淺拷貝還是深拷貝,一般情況下應該儘量執行淺拷貝
  • 如果你所寫的物件需要深拷貝,那麼可考慮新增一個專門執行深拷貝的方法

第四章 協議與分類

23 通過委託與資料來源協議進行物件間通訊

這個就是常規我們使用的代理了 但是書中講了一個新的知識點 我倒是從前從沒有見過的 可以一起來看一下

  • 如果有必要,可實現含有位段的結構體, 將委託物件是否能相應相關協議方法這一資訊快取至其中 這個知識點比較有價值
// 定義一個結構體
@interface LLNetWorkFetcher(){
  struct {
    unsigned int didReceiveData       : 1;
    unsigned int didDailWIthError     : 1;
    unsigned int didUpdateProgressTo  : 1;
} _delegateFlags;

// 在外界設定代理的時候 重寫 delegate 的 set 方法 對此結構體進行賦值

- (void)setDelegate:(id<LLNetworkFetcherDelegate>)delegate{
    _delegate = delegate;
    _delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
    _delegateFlags.didDailWIthError = [delegate respondsToSelector:@selector(networkFetcher:didDailWIthError:)];
    _delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
}

// 這樣在呼叫的時候只需判斷 結構體裡邊的標誌就可以了 不需要一直呼叫 respondsToSelector 這個方法
     if (_delegateFlags.didUpdateProgressTo) {
            [_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
        }
}
複製程式碼
24 將類的實現程式碼分散到便於管理的數個分類之中
  • 使用分類機制把類的實現程式碼劃分成易於管理的小塊
  • 將應該視為私有的方法歸入名叫 Private 的分類中, 以隱藏實現細節
25 總是為第三方類的分類名稱加字首

分類的方法加入到類中這一操作是在執行期系統載入分類是完成的.執行期系統會把分類中所實現的每個方法都加入到類的方法列表中,如果類中本來就有此方法,而分類又實現了一次, 那麼分類中的方法會覆蓋原來那一份實現程式碼, 實際上可能會發生多次覆蓋, 多次覆蓋的結果一最後一個分類為準

  • 向第三方類中新增分類時, 總應給其名稱加上你專用的字首
  • 向第三方類中新增分類是,總應給其中的方法加上你專用的字首
26 勿在分類中宣告屬性

這個老生常談了

  • 把封裝資料 所用的全部屬性都定義在主介面裡
  • 在"Class-continuation分類"之外的其他分類中,可以定義存取方法,但儘量不要定義屬性
27 使用"class - continuation分類" 隱藏實現細節

class - continuation分類 通俗點來講其實就是我們平時所說的延展

  • 通過"class - continuation分類"向類中新增例項變數
  • 如果某屬性在主介面中宣告為"只讀" 而在類的內部又要用設定方法修改此屬性,那麼就在"class - continuation分類" 將其擴充套件為"可讀寫"
  • 把私有方法的原型宣告在"class - continuation分類裡面
  • 若想是類所遵循的協議不為人所知, 則可於"class - continuation分類中宣告
28 通過協議提供匿名物件
  • 協議可在某種程度上提供匿名型別, 具體的物件型別可以淡化成遵從某協議的 id 型別,協議裡規定了物件所應實現的方法
  • 使用匿名物件來隱藏型別名稱(或類名)
  • 如果具體型別不重要,重要的是物件能夠響應(定義在協議裡)特定方法,那麼可以使用匿名物件來表示

第五章 記憶體管理

29 理解引用計數器

這一點也不多說了 不過有一個概念確實是之前沒想過的 UIApplication 物件是 跟物件

  • 引用計數機制通過可以遞增遞減的計數器來管理記憶體, 物件建立好之後, 其保留計數至少為1 , 若保留計數為正,則物件繼續存活, 當保留計數降為0時,物件就被銷燬了
  • 在物件生命期中, 其餘物件通過引用來保留或釋放此物件, 保留與釋放操作分別會遞增及遞減保留計數
30 以 ARC 簡化引用計數
  • 有了 ARC 之後, 程式設計師就無需擔心記憶體管理問題了, 使用 ARC 來程式設計,可省去類中許多"樣板程式碼"
  • ARC 管理物件生命期的辦法基本就是:在合適的地方插入"保留"及釋放操作, 在 ARC 環境下, 變數的記憶體管理語義可以通過修飾符指明,而原來則需要手工執行"保留"及"釋放" 操作
  • 由方法所返回的物件,其記憶體管理語義總是通過方法名來體現, ARC 將此確定為開發者必須遵守的規則
  • ARC 只負責管理 OBjectice-C 物件的記憶體, 尤其注意: CoreFounfation 物件不歸 ARC 管理,開發者必須適時使用 CFRetain/CFRelease
31 在 dealloc 方法中只釋放引用並解除監聽
  • 在 dealloc 方法裡, 應該做的事情就是釋放指向其他物件的引用, 並取消原來訂閱的"鍵值觀測"(KVO) 或 NSNotificationCenter 等通知, 不要做其他事情
  • 如果物件持有檔案描述符等系統資源, 那麼應該專門編寫一個方法來釋放此種資源. 這樣的類要和其使用者約定,用完資源後必須呼叫 close
  • 執行非同步任務的方法不應該放在 dealloc 裡呼叫;只能在正常狀態下,執行的那些方法也不應在 dealloc 裡呼叫,因此此時物件已處於正在回收的狀態了
32 編寫"異常安全程式碼"時留意記憶體管理問題
  • 在捕獲異常時, 一定要注意將 Try 塊內所創立的物件清理乾淨
  • 在預設情況下, ARC 不生成安全處理代異常所需的清理程式碼,開啟編譯器標誌後, 可以生成這種程式碼,不過會導致應用程式變大, 而且會降低執行效率 如下邊程式碼 若在 ARC 且必須捕獲異常時, 則需要開啟-fobjc-arc-exceptions標誌
    NSObject *object;
    @try {
        object = [NSObject new];
        [object doSomeThingThatMayThrow];
    }
    @catch(...){        
    }
    @finally{
    }    
複製程式碼
33 以弱引用避免保留環
  • 將某些引用設為 weak 可避免出現"保留環"
  • weak 引用可以自動清空,也可以不自動清空.自動清空(autonilling)是隨著 ARC 而引入的新特性,由執行期系統來實現.在具備自動清空功能的弱引用上,可以隨意讀取其資料,因為這種引用不會指向已經回收的物件
34 以"自動釋放池塊"降低記憶體峰值
  • 自動釋放池排布在棧中, 物件收到 autorelease 訊息後, 系統將其放入最頂端的池裡
  • 要合理運用自動釋放池, 可降低應用程式的記憶體封值
  • @autoreleasepool 這種新式寫法能建立出更為輕便的自動釋放池 常見的例子就是 下邊的 加上@autoreleasepool應用程式在執行迴圈的時候記憶體峰值就會降低
    NSArray *dataArr = [NSArray array];
    NSMutableArray *personArrM = [NSMutableArray array];
    for (NSDictionary *recode in dataArr) {
        @autoreleasepool{            
            LLPerson *person = [[LLPerson alloc]initWithRecode:recode];
            [personArrM addObject:person];
        }
    }
複製程式碼
35 用"殭屍物件"除錯記憶體管理問題
  • 系統在回收物件時,可以不將其真的回收, 而是把它轉化為殭屍物件,通過環境變數 NSZombieEnabled 可開啟此功能
  • 系統會修改物件的 isa 指標,令其指向特殊的殭屍類, 從而使改物件變為殭屍物件.殭屍類能夠相應所有的選擇子, 相應方式為:列印一條包含訊息內容及其接受者的訊息,然後終止應用程式
36 不要使用retainCount
  • 物件的保留計數看似有用, 實則不然,因為任何給定時間點上的"絕對保留計數"都無法反應物件生命期的全貌
  • 引入 ARC 之後, retainCount 方式就正式廢止了,在 ARC 下呼叫該方法會導致編譯器報錯

###第六章 塊與大中樞派發

37 塊的內部結構

塊物件內部結構.jpeg

塊本身也是物件,在存放塊物件記憶體區域中, 首個變數是指向 Class 物件的指標,該指標叫做 isa, 其餘記憶體裡含有塊物件正常運轉所需的各種資訊, 在記憶體佈局中,最重要的就是 invoke 變數,這就是函式指標,指向塊的實現程式碼, 函式原型只要要接受一個 void* 型的引數, 此引數代表塊.剛才說過,塊其實就是一種代替函式指標的語法結構, 原來使用函式指標是,需要用不透明的 void 指標來傳遞狀態 而改用塊之後, 則可以把原來用標準 C 語言特性所編寫的程式碼封裝成簡明且易用的介面.

descriptor 變數是指向結構體的指標, 每個塊裡都包含此結構體,其中宣告瞭塊物件的總體大小,還宣告瞭 copy 和 dispose 這兩個輔助函式所物件的函式指標, 輔助函式在拷貝及丟棄塊物件時執行, 其中會執行一些操作, 比方說 前者要保留捕獲的物件, 而後者則將之釋放

塊還會把它所捕獲的所有變數都拷貝一份, 這些拷貝放在 descriptor 變數後邊,捕獲了多少變數,就要佔據多少記憶體空間, 請注意, 拷貝的並不是物件變數,而是指向這些物件的指標變數, invoke 函式為何需要把塊物件作為引數傳進來呢? 原因就在於,執行塊的時候 要從記憶體中把這些捕獲到的變數讀出來

38 為常用的塊型別建立 typedef
  • 以 typedef 重新定義塊型別, 可令塊變數用起來更加簡單
  • 定義新型別時應遵從現有的命名習慣,無使其名稱與別的型別相沖突
  • 不妨為同一個塊簽名定義多個型別別名, 如果要重構的程式碼 使用了塊型別的某個別名, 那麼只需要就該相應的 typedef 中的塊簽名即可, 無序改動氣的 typedef
39 用 Handel 塊降低程式碼分散程度 其實也就是我們所說的 block 回撥
  • 在建立物件時, 可以使用內聯的 handle 塊將相關業務邏輯一併宣告
  • 在有多個例項需要監控時, 如果採用委託模式, 那麼經常需要根據傳入的物件來切換, 而若改用 handle 塊來實現, 則可直接將塊與相關物件放在一起
  • 設計 API 是如果用到了 handle 塊 那麼可以增加一個引數, 使呼叫者可通過引數來決定把塊安排在哪個佇列上執行
40 用塊引用其所屬物件時不要出現保留環
  • 如果塊所捕獲的物件直接或間接的保留了塊本身, 那麼就得當心保留環問題了
  • 一定要找個適當的時機解除保留環, 而不能把責任推給 API 的呼叫者
41 多用派發佇列,少用同步鎖

這一點就詳細說說吧

在 OC 中多執行緒要執行同一份程式碼,那麼有時可能會出問題, 這種情況下,通常要使用鎖來實現某種同步機制.

在 GCD 出現之前, 有兩種方法:

  • 1 第一種採用內建的"同步塊"
-  (void)synchronizedMethod{
      @synchronized(self){
          // safe
      }
}
複製程式碼
  • 2 直接使用 NSLock 物件
_lock = [[NSLock alloc]init];
- (void)synchronizedMethod{
  [_lock lock];
// safe
  [_lock unlock];
}
複製程式碼

這兩種方法都很好不過也都有缺陷 比如說,在極端情況下,同步塊會導致死鎖, 另外 其效率也不見得高, 而如果直接使用鎖物件的話,一旦遇到死鎖, 就會非常麻煩

GCD 的到來它能以更簡單更高效的形式為程式碼加鎖

我們都知道屬性就是開發者經常需要同步的地方,這種屬性需要做成"原子的", 用 atomic 即可實現這一點, 但如果我們自己實現的話就可以用 GCD 來實現

  • 優化1 使用"序列同步佇列, 將讀取操作及寫入操作都安排在同一個佇列裡,即可保證資料同步" 如一下程式碼
    _syncQueue = dispatch_queue_create("aokamu.syncQueue", NULL);
- (NSString *)someString{
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}
- (void)setSomeString:(NSString *)someString{
    dispatch_sync(_syncQueue, ^{
        _someString = someString;
    })
}
複製程式碼

上述程式碼: 把設定操作與獲取操作都安排在序列化的佇列裡執行了, 這樣的話, 所有針對屬性的訪問操作就都同步了, 全部加鎖任務都在 GCD 中處理, 而 GCD 是相當深的底層來實現的,於是能夠做許多優化

  • 優化2 設定方法不一定非得是同步的
- (void)setSomeString:(NSString *)someString{    
    dispatch_async(_syncQueue, ^{
        _someString = someString;
    })
}
複製程式碼

這個吧同步派發改成非同步派發,可以提升設定方法的執行速度, 而讀取操作與寫入操作依然會按順序執行, 不過這樣寫昂寫還是有一個弊端. :如果你測一下程式效能,那麼可能會發現這種寫法比原來慢, 因為執行非同步派發時需要拷貝塊.

  • 優化3 終極優化 不用序列佇列, 而改用併發佇列 並且使用 柵欄(barrier)
    _syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    
- (NSString *)someString{
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}
- (void)setSomeString:(NSString *)someString{    
    dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    })
}
複製程式碼

在佇列中 柵欄塊必須單獨執行, 不能與其他塊並行, 這隻對併發佇列有意義, 因為序列佇列中的塊總是按順序逐個來執行的, 併發佇列如果發現接下來要處理的塊是個柵欄塊,那麼久一直要等當前所有併發塊都執行完畢,才會單獨執行這個柵欄塊 待柵欄塊執行過後 再按正常方式向下處理 如下圖

Snip20171031_1.png

  • 派發佇列可用來表述同步語義,這種做法要比使用@synchronized塊或者NSLock物件更簡單
  • 將同步與非同步派發結合起來,可以實現與普通加鎖機制一樣的同步下行為,而這麼做卻不會阻塞執行非同步派發的執行緒
  • 使用同步佇列及柵欄塊.可以令同步行為更加高效
42 多用 GCD 少用 performSelector 系列方法

這個現在已經沒有人去用performSelector 系列方法了

  • performSelector 系列方法在記憶體管理方面容易有疏失,他無法確定將要執行的選擇子具體是什麼, 因而 ARC 編譯器也就無法插入適當的記憶體管理方法
  • performSelector 系列方法所能處理的選擇子太過於侷限了,選擇子的返回值型別及傳送給方法的引數個數都受到限制
  • 如果想把任務放在另一個執行緒上執行,那麼最好不要用performSelector系列方法,而是應該把任務封裝到塊裡, 然後呼叫大中樞派發機制的相關方法來實現
43 掌握 GCD 及操作佇列的使用時機

在來簡單總結一下操作佇列(NSOPeration)的幾種使用方法 ① 取消某個操作 執行任務前可以呼叫 cancel 方法 ,該方法會設定物件內的標誌位,用以表明此任務不需要執行, 不過已經啟動的任務無法取消了, ②指定操作間的依賴關係 一個操作可以依賴其他多個操作 ③ 通過鍵值觀測機制監控 NSOperation 物件的屬性. NSOPeration 物件有許多屬性都適合通過鍵值觀測機制來監聽 ④指定操作的優先順序 NSOperation 物件也有"執行緒優先順序",這決定了執行此操作的執行緒處在何種優先順序上

  • 在解決多執行緒與任務管理問題時,派發佇列並非唯一方案
  • 操作佇列提供了一套高層的 Objective-CAPI, 能實現純 GCD 所具備的絕大部分功能,而且還能完成一些更為複雜的操作, 那些操作弱改用 GCD 來實現, 則需另外編寫程式碼
44 通過 Dispatch Group 機制, 根據系統資源狀況來執行任務

這個也簡單記錄一下把 Dispatch Group 俗稱 GCD 任務組,我們 用虛擬碼來看一下 Dispatch Group的用法

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t dispatchGroup = dispatch_group_create();    
    for (id object in collectin) {        
        dispatch_group_async(dispatchGroup,
                             queue,
                             ^{
            [object performTask];
        })
    }
    dispatch_group_notify(dispatchGroup,
                          dispatch_get_main_queue(),
                          ^{
        [self updateUI];
    })
複製程式碼

notify回撥的佇列完全可以自己來定 可以用自定義的序列佇列或全域性併發佇列

這裡還有 GCD 的另一個函式平時比較少用的 那就是dispatch_apply

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply(array.count,
                   queue,
                   ^(size_t i) {                       
                       id object = array[i];
                       [object performTask];
    })    
複製程式碼

dispatch_apply所使用的佇列可以使併發佇列, 也可以是序列佇列, 加入把塊派給了當前佇列(或體系中高於當前佇列的某個序列佇列),這將會導致死鎖,

  • 一系列任務可以歸入一個 dispatch group 之中,開發者可以在這組任務執行完畢是獲得通知
  • 通過 dispatch Group, 可以在併發式派發佇列裡同時執行多項任務, 此時 GCD 會根據系統資源狀況來排程這些併發執行的任務, 開發者若自己來實現此功能,則需編寫大量程式碼
45 使用 dispatch_once 來執行只需要執行一次的執行緒安全程式碼

這個就是老生常談的單例了 也就不多說了

46 不要使用 dispatch_get_current_queue

這個函式已經廢棄了 此處就不多說了

第七章 系統框架

47 熟悉系統框架

我們開發者經常碰到的就是 Foundation 框架 像NSobject,NSArray,NSDictionary 等類 都在其中,

還有一個與Foundation相伴的框架是 CoreFoundation,CoreFoundation 不是 OC 框架,但是確定編寫 OC 應用程式時所應熟悉的重要框架,Foundation框架中的許多功能都可以在此框架中找到對應的 C 語言 API 除了 Foundation和CoreFoundation還有以下系統庫:

  • CFNetwork 此框架提供了 C 語言級別的網路通訊, 它將"BSD 套接字"抽象成易於使用的網路介面

  • CoreAudio 該框架所提供的 C語言 API 可用來操作裝置上的音訊硬體, 這個框架屬於比較難用的那種, 因為音訊處理本身就很複雜,所幸由這套 API 可以抽象出另外一個 OC 的 API, 用後者來處理音訊問題會簡單些

  • AVFoundation 此框架所提供的 OC 物件可用來回放並錄製音訊及視訊,比如 能夠在 UI 檢視類播放視訊

  • CoreData 此框架中所提供的 OC 介面可將物件放入到資料庫中,便於持久儲存

  • CoreText 此框架提供的 C語言介面可以高效執行文字排版及渲染操作

  • 請記住 用純 C 語言寫的框架與用 OC 寫成的一樣重要, 若想成為優秀的 OC 開發者, 應該掌握 C 語言的核心概念

48 多用塊列舉 少用 for 迴圈
  • 塊列舉法 本身就能通過 GCD 來併發執行遍歷操作,無須另行編寫程式碼,而採用其他遍歷方式則無法輕易實現這一點
  • 若提前知道待遍歷的 collection 含有何種物件,則應修改塊簽名, 指出物件的具體內容
    NSArray<LLPerson *> *dataArr = [NSArray array];    
    [dataArr enumerateObjectsUsingBlock:^(LLPerson * _Nonnull obj,
                                          NSUInteger idx,
                                          BOOL * _Nonnull stop) {        
    }];
複製程式碼
49 對自定義其記憶體管理語義的 collection 使用無縫橋接
  • 通過無縫橋接技術, 可以在 Foundation 框架中的 OC 物件與 CoreFoundation 框架中的 C語言資料結構之間來回轉換
  • 在CoreFoundation 層面建立collection 時,可以執行許多回撥函式, 這些函式表示此 collection 應如何處理其元素, 然後可運用無縫橋接技術, 將其轉換成具備特殊記憶體管理語義的 OC collection
    NSArray *anNSArray = @[@1,@2,@3,@4,@5];
    CFArrayRef aCFArray = (__bridge CFArrayRef)(anNSArray);
    NSLog(@"count = %li",CFArrayGetCount(aCFArray));
// Output: count = 5 ;   
複製程式碼
50 構建快取時選用 NSCache 而非 NSDIctionary
  • 實現快取時選用 NSCache 而非 NSDictionary 物件,因為 NSCache 可以提供優雅的自動刪減功能,而且是執行緒安全的, 此外 他與字典不同,並不會拷貝鍵
  • 可以給 NSCache 物件設定上限, 用以限制快取中的物件總個數及"總成本".而這些初度則定義了快取刪減其中物件的時機, 但是絕對不要把這些尺度當成可靠地"硬限制"他們僅僅對 NSCache 起指導作用
  • 將 NSPurgeableData 與 NSCache 搭配使用.可實現自動清除資料的功能,也就是說,當NSPurgeableData物件所佔記憶體為系統所丟棄時,該物件自身也會從快取中移除
  • 如果快取使用得當, 那麼應用程式的響應速度就能提高,只有那種"重新計算起來很費事的"資料才值得放入快取,比如那些需要從網路獲取或者從磁碟讀取的資料 來看下邊虛擬碼

typedef void(^LLNetWorkFetcherCompleteHandler)(NSData *data);

@interface LLNetWorkFetcher : NSObject

- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(LLNetWorkFetcherCompleteHandler)handler;
@end


#import "LLClass.h"
#import "LLNetWorkFetcher.h"

@implementation LLClass{    
    NSCache *_cache;    
}
- (instancetype)init{
    if (self = [super init]) {        
        _cache = [NSCache new];        
        _cache.countLimit = 100;        
        _cache.totalCostLimit = 5 * 1024 * 1024;        
    }
    return self;
}
- (void)downLoadDataForURL:(NSURL *)url{
    NSData *cacheData = [_cache objectForKey:url];
    if (cacheData) {
        [self useData:cacheData];
    }else{        
        LLNetWorkFetcher *fetcher = [[LLNetWorkFetcher alloc]initWithURL:url];        
        [fetcher startWithCompletionHandler:^(NSData *data) {            
            [_cache setObject:data forKey:url cost:data.length];            
            [self useData:cacheData];            
        }];
    }
}

複製程式碼
51 精簡 initialize 與 load 的實現程式碼
  • + (void) load 對於加入執行期系統的每個類及分類來說,必定會呼叫此方法而且僅呼叫一次,當包含類或者分類的程式庫載入系統時, 就會執行此方法 如果分類和其所屬的類都定義了 load 方法, 則先呼叫類裡邊的 在呼叫分類裡邊的 load 方法的問題在於執行該方法時,執行期系統是"脆弱狀態",在執行子類的 load 方法之前,必定會先執行所有超類的 load 方法, 如果程式碼還依賴了其他程式庫,那麼程式庫裡相關類的 load 方法也必定會先執行, 根據某個給定的程式庫,卻無法判斷出其中各個類的載入順序, 因此 在 load 方法中使用其他類是不安全的. load 方法不像其他普通方法一樣, 他不遵從那套繼承規則, 如果某個類本身沒有實現 load 方法,那麼不管其各級超類是否實現此方法, 系統都不會呼叫.

  • + (void)initialize 對於每個類來說 該方法會在程式首次使用該類之前呼叫, 且只呼叫一次,他是有執行期系統來呼叫的,絕不應該通過程式碼直接呼叫 他與 load 方法有一定的區別的 首先 他是惰性呼叫的, 也就是說只有當程式用到了相關的類是,才會呼叫 如果某個類一直都沒有使用, 那麼其 initialize 方法就一直不會執行 其次, 執行期系統在執行該方法時,是處於正常狀態的, 因此 從執行期系統完整度來講, 此時可以安全使用並呼叫任意類中的任意方法 而且執行期系統也能確保initialize 方法一定會在"執行緒安全的環境"中執行,也就是說 只有執行initialize的那個執行緒 可以操作類與類例項, 最後, initialize 方法與其他訊息一樣,如果某個類未實現它, 而其超類實現了,俺那麼就會執行超類的實現程式碼

  • 在載入階段 如果類實現了 load 方法,那麼系統就會呼叫它.分類裡也可以定義此方法,類的 load 方法要比分類中先呼叫,其他方法不同, load 方法不參與複寫機制

  • 首次使用某個類之前,系統會向其傳送initialize 訊息,由於此方法遵從普通的複寫規則,所以通常應該在裡邊判斷當前要初始化的是哪一個類

  • load 和initialize 方法都應該實現的精簡一些, 這有助於保持應用程式的相應能力 也能減少引入"依賴環"的機率

  • 無法在編譯期設定的全域性變數,可以放在initialize 方法裡初始化

52 別忘了 NSTimer 會保留其目標物件

計時器是一種很方便也很有用的物件,但是 由於計時器會保留其目標物件, 所以反覆執行任務通常會導致應用程式出問題,也就是很容易引入"保留環" 來看下列程式碼

@interface LLClass : NSObject

- (void)startPolling;
- (void)stopPolling;

@end


@implementation LLClass{
    NSTimer *_pollTimer;
}

- (void)startPolling{
    
    _pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
                                                  target:self
                                                selector:@selector(p_doPoll) 
                                                userInfo:nil
                                                 repeats:YES];
}
- (void)stopPolling{
    [_pollTimer invalidate];
    _pollTimer = nil;
}
- (void)p_doPoll{   
}
- (void)dealloc{
    [_pollTimer invalidate];
}
複製程式碼

計時器的目標物件是 self, 然後計時器使用例項變數來存放的, 所以例項變數也儲存李計時器, 於是就產生了保留環

本書中提供了一個用"塊"來解決的方案 雖然計時器當前並不直接支援塊,但是可以用下面這段程式碼新增功能

@implementation NSTimer (LLBlocksSupport)

+ (NSTimer *)ll_schedeledTimerWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                       repeats:(BOOL)repeats{
    
    return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(ll_blockInvoke:) userInfo:[block copy] repeats:repeats];
   
}
+ (void)ll_blockInvoke:(NSTimer *)timer{
    void (^block)() = timer.userInfo;
    if (block) {
        block();
    }
}
複製程式碼

上邊的程式碼是在 NSTimer 分類中新增的程式碼 來看一下具體的使用

- (void)startPolling{
    __weak LLClass *weakSelf = self;
    _pollTimer = [NSTimer ll_schedeledTimerWithTimeInterval:5.0
                                                      block:^{
                                                         LLClass *strongSelf = weakSelf;                                                          
                                                         [strongSelf p_doPoll];
   
                                                      }
                                                    repeats:YES];
複製程式碼

先定義弱引用,然後用block捕獲這個引用,但是在用之前在立刻生成 strong 引用.保證例項在執行期間持續存活

  • NSTimer 物件會保留其目標, 直到計時器本身失效為止,呼叫 invalidate 方法可令計時器失效, 另外 一次性的計時器, 在觸發任務之後,也會失效,
  • 反覆執行任務的計時器,很容易引入保留環, 如果這種計時器的目標物件有保留了計時器本事,那麼肯定會導致保留環,這種環保留,可能直接發生,也可能是通過物件圖裡的其他物件間接發生
  • 可以擴充 NSTimer 的功能,用"塊"來打破保留環,不過 除非 NSTimer 將來在公共介面裡提供此功能, 否則必須建立分類,將相關實現程式碼加入其中

相關文章