《Effective Objective C》重讀校驗本身的知識體系

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 來定義"只在編譯單元內可見的常量",因爲此類常量不在全局符號表中, 因此無須爲其名稱加前綴安全

舉例說明 不合適的寫法bash

//動畫時間
#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 未來在公共接口裏提供此功能, 不然必須建立分類,將相關實現代碼加入其中
相關文章
相關標籤/搜索