《Effective Objective-C 2.0》第一份讀書筆記

聲明:這個筆記的系列是我每天早上打開電腦第一件做的事情,當(dāng)然使用的時間也不是很多因為還有其他的事情去做,雖然吧自己買了紙質(zhì)的書但是做筆記和看的時候基本都是看的電子版本,一共52個Tip每一個Tip的要點我是完全謄寫下來的,害怕自己說的不明白所以就謄寫也算是加強記憶,我會持續(xù)修改把自己未來遇到的所有相關(guān)的點都加進去,最后希望讀者尊重原著,購買正版書籍。PS:不要打賞要喜歡~

Demo:GitHub代碼網(wǎng)址,大大們給個鼓勵Star啊。

整個系列筆記目錄

《Effective Objective-C 2.0》第一份讀書筆記
《Effective Objective-C 2.0》第二份讀書筆記
《Effective Objective-C 2.0》第三份讀書筆記

第一章 熟悉Objective-C

1.了解Objective-C語言的起源

Objective-C 起源自Smalltalk的,也就是消息性語言。而不是函數(shù)調(diào)用

[obj perform:parameter1 and:parameter2];  消息語言
obj->perform(parameter1,paramter2);  函數(shù)調(diào)用方式

關(guān)鍵區(qū)別在于:消息語言最終執(zhí)行的代碼由運行環(huán)境決定,而函數(shù)調(diào)用則是由編譯器決定。這也叫做“動態(tài)綁定(dynamic binding)”
我們創(chuàng)建一個OC對象的時候可以這樣:

NSString * someString = @“new string ”;

中間的星號代表的就是指針,所以我們創(chuàng)建的是一個someString指針指向一個NSString的對象,我們把someString指針對象放在棧(stack)中,而真正的指針對象"new string"我們放在堆(heap space)中。如果我們新建一個新的指針anyString的數(shù)值仍然是"new string",那么就不會重新在堆中新建內(nèi)存而是直接吧新的anyString指向已經(jīng)創(chuàng)建的"new string"對象。

兩個指針指向相同堆數(shù)據(jù).png

分配在堆中的內(nèi)存需要程序員直接管理,而分配在棧上的會在棧彈出時自動清理。

Objective-C將堆內(nèi)存管理抽象出來了,不需要用malloc及free來分配或釋放對象所占內(nèi)存。Objective-C運行期環(huán)境把這部分工作抽象為一套內(nèi)存管理框架,名為“引用計數(shù)”,
在OC代碼中也會有使用棧(stack)空間的變量,比如不帶星號的Sturck對象,比如CGRect,如果只是簡單的高度,寬度等的數(shù)據(jù)的時候,就不用建立OC對象了會耗費更多額外的開銷。

要點:
  • Objective-C為C語言添加了面向?qū)ο筇匦裕瞧涑bjective-C使用動態(tài)綁定的消息結(jié)構(gòu),也就是說,在運行時才會檢查對象類型。接受一條消息之后,究竟應(yīng)執(zhí)行何種代碼,由運行期環(huán)境而非編譯器來決定。
  • 理解C語言的核心概念有助于寫好Objective-C程序。尤其要掌握內(nèi)存模型和指針。

2.在類的頭文件中盡量少引入其他頭文件

這里面講了為什么使用@class “EOCEmployer.h”這樣的寫法。
這叫做“向前聲明(forward declaring )” 這是因為不需要知道EOCEmployer的內(nèi)部接口細節(jié)。

要點:
  • 除非確有必要,否則不要引入頭文件。一般來說,應(yīng)在某個類的頭文件中使用向前聲明來體積別的類,并在實現(xiàn)文件中引入那些類的頭文件。這樣做可以盡量降低類之間的耦合(coupling)。
  • 有時無法使用向前聲明,比如要聲明某個類遵守一項協(xié)議。這種情況下,盡量把“該類遵守某協(xié)議”的這條聲明移至"class-continuation分類"中。如果不行的話,就把協(xié)議單獨放在一個頭文件中,然后將其引入。

3.多用字面量語法,少用與之等價的方法

我們可以通過“字符串自變量(string literal)”語法來創(chuàng)建對象:

NSString * someString = @“Effective Objective-C 2.0”; 

使用字面量語法可以縮減源代碼長度,使其更為易讀。
有的時候需要把整數(shù)、浮點數(shù)、布爾值封入Objective-C對象中。這種情況下可以用NSNumber類,該類可處理多種類型的數(shù)值。若是不用字面量,那么就需要按下述方式創(chuàng)建實例:

NSNumber * someNumber = [NSNumber numberWithInt:1];
NSNumber * someNumber = @1;
NSNumber * boolNumber = @YES;
NSNumber * charNumber = @‘a(chǎn)’;
NSNumber * expressionNumber = @(x * y);

關(guān)于NSArray的一個字面量問題:

id object1 = /********/;
id object2 = /********/;
id object3 = /********/;
NSArray * arrayA =[NSArray  arrayWithObjects:object1,object2,object3,nil];
NSArray * arrayB =@[object1,object2,object3];

如果object1和object3 都是指向正常的對象,而object2 是nil。那么按照字面量語法創(chuàng)建數(shù)組arrayB時會拋出異常,而arrayA雖然能創(chuàng)建出來,但是只有一個object1一個對象。”arrayWithObjects”方法會依次處理各個參數(shù),知道發(fā)現(xiàn)nil為止,由于object2是nil,所以該方法會提前結(jié)束。所以arrayB更加安全,直接彈出要比你完美運行后來少一個數(shù)據(jù)來的更安全。

小缺點:

使用字面量語法創(chuàng)建的字符串,數(shù)組,字典對象都是不可變的(immutable)。若想要可變版本的,就需要復(fù)制一份:

NSMutableArray * mutable = [[@1,@2,@3,@4,@5] mutableCopy];

這么做會多調(diào)用一個辦法,但是好處是要多余上面的那個缺少數(shù)據(jù)的缺點的。

要點:
  • 應(yīng)該使用字面量語法來創(chuàng)建字符串,數(shù)值,數(shù)組,字典。與創(chuàng)建此類對象的常規(guī)方法相比,這么做更加簡單扼要。
  • 應(yīng)該通過取下標操作來訪問數(shù)組下標或字典中的鍵所對應(yīng)的元素。
  • 用字面量語法創(chuàng)建數(shù)組和字典時,若值中又nil,則會拋出異常。因此,務(wù)必確保值里不含 nil。

4.多用類型常量,少用#define預(yù)處理指令

#define ANIMATION_DURATION 0.3

這樣的預(yù)處理容易導(dǎo)致整個工程里的ANIMATION_DURATION都是0.3,很有可能會改變系統(tǒng)定義的一些宏。
我們選擇這種方式:

 static const  NSTimerInterval  kAnimationDuration = 0.3;

我們的習(xí)慣是如果常量只是出現(xiàn)在一些類的“編譯單元(translation unit)”之內(nèi),則前面加"k",如果是類之外也可見就用類名做前綴。

定義常量的位置很重要,我們總喜歡在頭文件里聲明預(yù)處理指令,這樣做真的很糟糕,當(dāng)常量名稱有可能互相沖突時更是如此。

如果想要定義一個定義域在某個文件中的定義的話,我們可以這樣:
static const NSTimerInterval kAnimationDuration = 0.3;
變量一定要用static和const一同修飾,用const修飾的變量如果遭到更改就會報錯,而static修飾符則意味著變量僅在定義此變量的編譯單元中可見,也就是這個.m中。假如聲明此變量時不加static,則編譯器就會為它創(chuàng)建一個“外部符號(external symbol)”。此時如果是另一個編譯單元里面也聲明了一樣的變量就會拋出錯誤。

而想要一個全局變量的話:

//.h:
extern  NSString * const  EOCStringConstant;
//.m:
NSString * const EOCStringConstant  = @“VALUE”;

const修飾符放在EOCStringConstant指針外面就符合語義為防止指針的方向。
編譯器看到頭文件中的extern關(guān)鍵字,就會明白如何在引入磁頭文件的代碼中處理該常量了。在全局符號表將會有一個名教EOCStringConstant的符號。也就說,編譯器無需查看器定義,即允許代碼使用此常量。因為它知道,當(dāng)連接成二進制文件之后,肯定能好到這個常量。
此類常量必須要定義。而且只能定義一次。編譯器會在"數(shù)據(jù)段(data section)"為字符串分配存儲空間
。鏈接器會把目標文件和其他目標文件相鏈接,生成最終的二進制文件。鏈接器可以隨時解析這個常量。

要點:
  • 不要用預(yù)處理指令定義常量。這樣定義出來的常量不含類型信息,編譯器只是會在編譯之前根據(jù)此執(zhí)行查找與替代操作。即使有人重新定義了常量值,編譯器也不會產(chǎn)生警告信息。這將導(dǎo)致應(yīng)用程序中的常量值不一致。
  • 在實現(xiàn)文件中使用static const來定義“只在編譯單元內(nèi)可見的常量”。由于次常量不在全局符號表中,所以無需在其前面加前綴。
  • 在頭文件中使用extern來聲明全局常量,并在相關(guān)實現(xiàn)文件中定義其值。這種常量要出現(xiàn)在全局符號表中,所以其名稱應(yīng)加上區(qū)隔,通常用與之相關(guān)的類名做前綴.

5.用枚舉表示狀態(tài),選項,狀態(tài)碼

枚舉可以用來列舉某個對象的一些形態(tài)

enum EOCConnectionState{
       EOCConnectionStateDisconnected,
       EOCConnectionStateConnecting,
       EOCConnectionStateConnected,
};
如果想要簡單編寫的話
typedef enum EOCConnectionState = EOCConnectionState;
EOCConnectionState state = /*****/;    //這樣

還有一種情況適用枚舉類型,定義選項的時候,如果其中可以彼此組合那就更加適合了,各個選項之間可通過"按位或操作符(bitwise)"。

enum UIViewAutoresizing{
      UIViewAutoresizingNone   = 0,
      UIViewAutoresizingFlexibleLeftMargin = 1<<0,
      UIViewAutoresizingFlexibleWidth = 1<<1,
      UIViewAutoresizingFlexibleRightMargin = 1<<2,
}
這樣除了none,其他的都可以多選。

下面是”<<“符號帶來的內(nèi)存存儲方式:


多選項枚舉.png
要點:
  • 應(yīng)該用枚舉來表示狀態(tài)機的狀態(tài),傳遞給方法的選項以及狀態(tài)碼等值,給這些值起一個易懂的名字。
  • 如果把傳遞給某個方法的選項表示為枚舉類型,而多個選項又同時實現(xiàn),那么就將個選項值定義為2的冪,以便通過按位或操作將其組合起來。
  • 用NS_ENUM和NS_OPTIONS宏來定義枚舉類型,并指明其底層數(shù)據(jù)類型。這樣做可以確保枚舉是用開發(fā)者所選的底層數(shù)據(jù)類型實現(xiàn)出來的,而不會采用編譯器所選的類型。
  • 在處理枚舉類型的switch語句中不要定義default分支,這樣的話,加入新枚舉之后,編譯器就會提示開發(fā)者:switch語句并未處理所有枚舉。

第二章 對象,消息,運行期

6.理解“屬性”這一概念

在OC中,對象(object)就是基本構(gòu)造單位(building block),開發(fā)者可以通過對象來存儲并傳遞數(shù)據(jù)。在對象之間傳遞數(shù)據(jù)并執(zhí)行任務(wù)的過程就叫做“消息傳遞”(Messaging)。
當(dāng)應(yīng)用程序運行起來以后,為其提供相關(guān)支持的代碼叫做”O(jiān)bjective-C運行期環(huán)境(Objective-C runtime)”,它提供了一些使得對象之間能夠傳遞消息的重要函數(shù),并且包含創(chuàng)建類實例所用的全部邏輯。

關(guān)于NSString的Copy和Strong修飾符

這邊重申一下為什么NSString使用copy而不用strong:
因為如果NSString對象是取一個NSMutableString對象的數(shù)值的話,當(dāng)NSMutableString的對象數(shù)值發(fā)生改變的時候,NSString會相應(yīng)的發(fā)生改變,而不是保持遠數(shù)值.
為什么NSMutableString使用strong而不用copy:
因為如果是copy修飾的話,NSMutableArray的數(shù)值就不能發(fā)生改變。

先隨便寫一個聲明

@interface EOCPerson:NSObject{
 @public
        NSString *  _firstName;
        NSString * _lastName;
@private
        NSString * _ someInternalData
}

如果我們在_firstName上面在加一個_iSuName而firstName和lastName都向下移動,這樣EOCPerson就會擁有三個屬性,其實一搭眼好像這樣完全沒有問題,其實對象布局在編譯器布局的時候已經(jīng)固定了,所以當(dāng)你在上面加入一個iSuName的時候其實,想要獲取firstName的類還是會根據(jù)編譯器時候的偏移量來獲取firstName,但是實際上獲取的卻是iSuName的值,原來的元素全部都后移的時候,那么以偏移量為參考量的做法會導(dǎo)致請求的元素不兼容(incompatibility)- - ,OC解決這種問題的方法就是把實例變量當(dāng)做一種存儲偏移量所用的“特殊變量”,然后交由“類對象”保存(14條詳解)。這個時候偏移量在運行的時候就會動態(tài)改變,找到正常的數(shù)值。這就是穩(wěn)固的ABI(Application Binary Interface)。

get 和 set方法:

EOCPerson * aPerson =[Person new];
aPerson.firstName = @“Bob”;  // same as  [aPerson  setFirstName: Bob];
NSString * lastName = aPerson.lastName;   // same as  NSString * lastName = [aPerson lastName];

然而屬性還有更多的優(yōu)勢。如果使用了屬性的話,編譯器就會自動編寫訪問這些屬性所需的方法。
如果想要改變名字的長相的話:

@implementation EOCPerson 
@synthesize firstName = _myFirstName ;
@synthesize lastName = _mySecondName;

這樣你在聲明里面定義的firstName,lastName就變成了_myFirstName,_mySecondName。

當(dāng)然你也可以選擇手動生成get,set方法(@dynamic)

@implementation EOCPerson 
@dynamic firstName,lastName 

編譯器不會為上面這兩個屬性自動合成存儲方法和實例變量。如果用代碼訪問其中的屬性,編譯器也不會發(fā)出警告消息。

屬性特質(zhì):

  1. 原子性:嚴格意義上說原子性(automicity)因為使用同步鎖的原因是要比非原子性(nonatomic)要更加安全,但是在屬性上添加同步鎖要消耗過多的資源。所以我們基本上會使用nonatomic做修飾符。
  2. 讀寫權(quán)限:也就是readwrite(讀寫),readonly(只讀)。
  3. 內(nèi)存管理語義:
    assign : 只會執(zhí)行對“純量類型”,例如(CGFloat或者是NSInteger等)的簡單賦值操作。
    strong : 定義了一種“擁有關(guān)系”,為這種屬性設(shè)置新值時,設(shè)置方法先保留新值,并釋放舊值,然后將新值替換上去。
    weak: 定義了一種”非擁有關(guān)系”,為這種屬性設(shè)置新值的時候,設(shè)置方法即不保留新值也不釋放舊值。此特質(zhì)同assign類似,然而在屬性所指的對象遭到摧毀的時候,屬性值也會被清空。
    unsafe_unretained: 和assign相同,但是它適用于“對象類型”,該特質(zhì)表達一種“非擁有關(guān)系”,但是當(dāng)目標對象遭到摧毀的時候,屬性值不會自動清空,這一點和weak有區(qū)別。
    copy: 此特質(zhì)所表示的所屬關(guān)系和strong類似。然而設(shè)置方法并不保留新數(shù)值,而是將其“拷貝”。當(dāng)屬性類型為NSString *時候,經(jīng)常用此特質(zhì)來保護其封裝性。

需要注意:如果自己來實現(xiàn)存取方法,那么應(yīng)該保證其具備相關(guān)屬性所聲明的特質(zhì),比方說,如果將某個屬性聲明為copy,那么就應(yīng)該在”設(shè)置方法”中拷貝相關(guān)對象。否則會誤導(dǎo)該屬性的使用者。
說道這個在初始化的時候為什么不是用set方法來保證每次都調(diào)用NSString都能帶上Copy的內(nèi)存語義。而是使用屬性賦值給copy的:_firstName = [firstName copy];(第七條詳解)

//.h
@interface EOCPerson : NSManagerObject
@property (copy) NSString * firstName ;
@property (copy) NSString * lastName ;

- (id) initWithFirstName: (NSString *)firstName lastName:(NSString *)secondName;
@end
//.m
- (id) initWithFirstName: (NSString *)firstName lastName:(NSString *)secondName{

   if( self = [super init]){
            _firstName = [firstName  copy];
            _lastName = [lastName copy];
    }
}
要點:
  • 可以用@property語法來定義對象中所封存的數(shù)據(jù)。
  • 通過“特質(zhì)”來制定存儲數(shù)據(jù)所需的正確定義。
  • 在設(shè)置屬性所對象的實力變量時,一定要遵從屬性所聲明的寓意。
  • 開發(fā)iOS程序時候應(yīng)該使用nonatomic屬性,因為atomic屬性會嚴重影響性能。

7.在對象內(nèi)部盡量直接訪問實例變量

首先確認一下 _name:直接訪問對象實例變量 self.name 間接訪問實例變量

self.name 和_name 的區(qū)別是:

  1. 由于不經(jīng)過Objective-C的“方法派發(fā)”步驟,所以直接訪問實例變量的速度當(dāng)然比較快。在這種情況下所生成的代碼會直接訪問保存對象變量的那塊內(nèi)存。
  2. 直接訪問實例變量時,不會調(diào)用其“設(shè)置方法”,這就繞過了相關(guān)屬性定義的“內(nèi)存管理語義”。比方說,如果在ARC下直接訪問一個聲明為copy的屬性,那么并不會拷貝該屬性,只會保留新值并釋放舊值。
  3. 如果直接訪問實例變量,那么不會觸發(fā)“鍵值觀察(KVO)”,通知。這樣做是否會產(chǎn)生問題,還取決于具體的對象行為。

回想一下上一個Tip上的問題為什么在初始化的時候不使用self.firstName = firstName,而選擇_firstName = [firstName copy];
嗨呀,好氣啊,我思想跑的太遠了,我以為是在子類重寫父類屬性會影響父類的運行呢,并不是啊只是子類運行的時候如果發(fā)現(xiàn)父類的init方法中使用點語法而子類中正好重寫了這個方法,那么就會走子類的方法,子類如果有限制條件語句的時候會導(dǎo)致條件在不清楚的情況下意外不能通過。而如果直接是屬性的話就不會走set方法直接就跳過子類的判斷環(huán)節(jié)了。例子請看EOCPerson.demo

還有一種情況下使用獲取方法
那就是惰性初始化(lazy initialization)

- (EOCBrain *)brain{
    if(!_brain){
         _brain = [Brain  new];
   }
        return _brain;
}

這種情況下你不self.brain的話代碼就沒法走了。

要點:
  • 在對象內(nèi)部讀取數(shù)據(jù)時,應(yīng)該直接通過實例變量來讀,而寫入數(shù)據(jù)時,則通過屬性來寫。
  • 在初始化方法及dealloc方法中,總是應(yīng)該直接通過實例變量來讀取數(shù)據(jù)。
  • 有時會使用惰性初始化計數(shù)配置某分數(shù)據(jù),在這種情況下,需要通過屬性來讀取數(shù)據(jù)。

8.理解“對象等同性”這一概念

按照 “==” 操作符比較出來的結(jié)果未必是我們想要的,因為該操作比較的是兩個指針本身,而不是期所指的對象。

 NSString * foo = @“Badger  123”;
 NSString * bar = [NSStringWithFormat:@“Badger  %i”,123];
 BOOL  equalA = ( foo == bar);
 BOOL  equalB = [foo isEqual:bar] ;
// set out -> equalA == NO
  1. 第一種判定的方法就是: isEqual
    所謂的所有屬性判斷:
    a.判斷兩個指針是否相等,當(dāng)切僅當(dāng)指針值相等的時候才會想等。
    b.比較兩個對象所屬的類。
    c.逐條屬性判斷。
  2. 第二種判定的方法就是:hash
    若兩個對象相等,則其哈希嗎也相等。但是兩個哈希嗎相等的對象未必相等。
    假如某個collection是用set實現(xiàn)的,那么set可能會根據(jù)哈希嗎把對象分裝到不同的數(shù)組中。向set中添加新對象時,要根據(jù)其哈希嗎找到與之相關(guān)的那個數(shù)組。依次檢查其中各個元素。看是哦否有相同的,如果有相同的,那就說明要添加的對象已經(jīng)在set里面了。由此可知,如果令每個對象返回相同的哈希嗎,那么在set中已經(jīng)有10000000個對象的情況下,如要繼續(xù)向里面添加對象,就需要全部便利一邊。

那么有沒有可能讓set 中含有兩個相同的對象呢。
下面代碼:

    NSMutableSet * set = [NSMutableSet new];
    NSMutableArray * arrayA =[@[@1,@2] mutableCopy];
    [set addObject:arrayA];
    NSLog(@"%@",set);
    
    NSMutableArray * arrayB =[@[@1,@2] mutableCopy];
    [set addObject:arrayB];
    NSLog(@"%@",set);
    
    NSMutableArray * arrayC =[@[@1] mutableCopy];
    [set addObject:arrayC];
    NSLog(@"%@",set);
    
    [arrayC addObject:@2];
    NSLog(@“%@",set);

最后在NSSet * setB =[set Copy];
就成了含有兩個 (1,2)….
要點:
  • 若想檢測對象的等同性,請?zhí)峁眎sEqual”和hash方法。
  • 相同的對象必須具備相同的哈希嗎,但是兩個哈希碼相同的對象確未必相同
  • 不要盲目的逐個檢測每條對象,而是應(yīng)該依照具體需求制定檢測方案。
  • 編寫hash方法時,應(yīng)該使用計算速度快而且哈希碼碰撞幾率低的算嗎

9.以“類族模式”隱藏實現(xiàn)細節(jié)

“類族”是一種很有用的模式(pattern),可以隱藏“抽象基類”背后的實現(xiàn)細節(jié)。Objective-C系統(tǒng)框架中普遍使用此模式。比如:

  + (UIButton *)buttonWithType:(UIButtonType)type;

該方法的返回對象,其類型取決于按鈕的類型。然而,不管返回是什么類型的對象,它們都集成來自同一個基類:UIButton。這么做的意義在于:UIButton類的使用者無需關(guān)心創(chuàng)建出來的按鈕屬于哪一個子類,也不用考慮繪制細節(jié)。
在測試代碼里面有創(chuàng)建類族的代碼

在這些代碼里面,基類實現(xiàn)了一個“類方法”,該方法根據(jù)待創(chuàng)建的雇員類別分配好對應(yīng)的雇員類實力。這種“工廠模式(Factory pattern)”是創(chuàng)建類族的辦法之一。

如果對象所屬的類位于某個類族中,那么在查詢其類型消息(introspection)時就要小心了,你可能覺得創(chuàng)建了某個類的實例,然而實際上創(chuàng)建的卻是其子類的實例。在這個Employee這個例子中,[employee isMemberOfClass:[EOCEmployee class]]似乎會返回YES,但實際上返回的卻是NO,因為employee并非Empoyee類的實例,而是其某個子類的實例。

Cocoa里的類族

系統(tǒng)框架中又許多類族。大部分collection類都是類族,例如NSArray與其可變的版本NSMutableArray。這樣看來,實際上有兩個抽象基類,一個用于不可變數(shù)組,另一個用于可變數(shù)組。盡管具備公共接口的類有兩個,但仍然可以合起來算作一個類族。
像NSArray這樣的類的背后其實是個類族(對于大部分collection類而言都是這樣),明白這一點很重要,否則可能會寫出下面這種代碼

id maybeArray = /****/
if  ([maybeAnArray  class] == [NSArray  class]) {
                 // Will  never  be hit  永遠不會進入
}

你要是知道NSArray是個類族,那就會明白上述代碼錯在哪里:[maybeAnArray class]所返回的類絕不可能是NSArray類本身,因為由NSArray的初始化方法所返回的那個實例其類型是隱藏在類族公共接口后面的某個內(nèi)部類型。我們可以通過
判斷對象是否在類族iskindof來判斷(14條詳細講解):

id maybeAnArray = /*****/
if ([maybeAnArray  isKindOfClass:[NSArray class]]){
    
         // Will be hit     // 會走這邊
}
要點:
  • 類族模式可以把實現(xiàn)細節(jié)隱藏在一套簡單的公共接口后面。
  • 系統(tǒng)框架中經(jīng)常使用類族
  • 從類族的公共抽象基類中繼承子類時要當(dāng)心,若有開發(fā)文檔請先閱讀。

10.在既有類中使用關(guān)聯(lián)對象存放自定義數(shù)據(jù)

有時需要在對象中存放相關(guān)信息。我們可以使用關(guān)聯(lián)對象Associated Object
可以給某對象關(guān)聯(lián)許多其他對象,這些對象通過“鍵”來區(qū)分。存儲對象值的時候,可以指明”存儲策略(storage policy)“,用以維護響應(yīng)的“內(nèi)存管理語義”。

OBJC_ASSOCIATION_ASSIGN   assign 
OBJC_ASSOCIATION_COPY      copy

下列方法可以管理關(guān)聯(lián)對象:

void objc_setAssociatedObject (id object ,void * key ,id value,objc_AssociationPolicy policy)

此方法以給定的鍵和策略為某對象設(shè)置關(guān)聯(lián)對象值。

void objc_getAssociatedObject(id object, void object)

此方法根據(jù)給定的鍵從某對象中獲取相應(yīng)的關(guān)聯(lián)對象值。

void objc_removeAssocatedObjects(id object)

此方法移除指定對象的全部關(guān)聯(lián)對象。
做的方法:

- (void)askUserAQuestion{

    UIAlertView * alert =[[UIAlertView alloc]initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];
    
    void(^block)(NSInteger) = ^(NSInteger buttonIndex){
    if (buttonIndex == 0){
                 [self doCancle];
          }else{
                 [self doContinue];
          }
    };
    objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
    [alert show];
}

//UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{

    void (^block)(NSInteger)= objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
    block(buttonIndex);
}
要點:
  • 可以通過“關(guān)聯(lián)對象”機制來把兩個對象連起來
  • 定義關(guān)聯(lián)對象時可指定內(nèi)存管理語義,用以訪問定義屬性時采用的“擁有關(guān)系”和“非擁有關(guān)系”
  • 只有在其他做法不可行的時候才會應(yīng)用關(guān)聯(lián)對象,因為這種做法通常會引入難于查找的bug。

11.理解 objc_msgSend的作用

objective-C的術(shù)語來說,這叫“傳遞消息”

id returnValue = [someObject messageName: parameter];

在本例中,someObject叫做接受者(receiver),messageName叫做“選擇子(selector)”。

void objc_msgSend(id self,SEL cmd,…)
id returnValue =objc_msgSend (someObject,@selector(messageName:),parameter);

為了完成調(diào)用的做法,該方法需要在接受者所屬的類中搜索器“方法列表”(list of methods),如果能找到與選擇子名稱相符的方法,就跳至其實現(xiàn)代碼,如果找不到就沿著繼承體系向上查找,等找到合適的方法之后在跳轉(zhuǎn)。如果最終還是找不到相符的方法,那就執(zhí)行“消息轉(zhuǎn)發(fā)(message forwording)操作”。

這么看來,調(diào)用一個方法好像需要很多步驟。索性objc_megSend會將匹配結(jié)果緩存在“快速映射表(fash map)里面”。每一個類都有這樣的一塊緩存,當(dāng)然“快速請求路徑”還是不如靜態(tài)來的快,但是也不會慢很多。

objc_msgSend_struct;
objc_msgSend_fpret;
objc_msgSendSuper;

方法存儲的方式大概是:

<return_type> Class_selector(id self ,SEL_cmd,…)

真正的函數(shù)其實和這個差不多,因為“尾調(diào)用優(yōu)化 (tail- call optimization)技術(shù)”
如果某個函數(shù)的最后一項操作是調(diào)用某個函數(shù)的話,就會調(diào)用“尾調(diào)用優(yōu)化”技術(shù)。編譯器會生成調(diào)轉(zhuǎn)至另一個函數(shù)所需的指令碼,而且不會調(diào)用堆棧中推入新的“棧幀(frame stack)”。只有當(dāng)某函數(shù)的最后一個操作僅僅是調(diào)用其他函數(shù)而不會將其返回值另作他用的時候次啊會執(zhí)行”尾調(diào)用優(yōu)化”,這樣做可以防止過做的發(fā)生“棧溢出”現(xiàn)象。

要點:
  • 消息由接受者,選擇子及參數(shù)構(gòu)成。給某對象“發(fā)送消息(invoke a message)”也就相當(dāng)于在該對象上“調(diào)用方法(call a method)”。
  • 發(fā)給某對象的全部消息都要由“動態(tài)消息派發(fā)系統(tǒng)(dynamic message dispatch system)”來處理,該系統(tǒng)會查出對應(yīng)的方法,執(zhí)行其代碼。

12.理解消息轉(zhuǎn)發(fā)機制

若想令類能理解某條消息,我們必須以程序碼實現(xiàn)出對應(yīng)的方法才行。但是,在編譯期向類發(fā)送了無法解讀的消息并不會報錯,因為在運行期可以繼續(xù)給類中添加方法也就是“消息轉(zhuǎn)發(fā)(message forwarding)”機制,程序員可經(jīng)由此過程告訴對象應(yīng)該如何處理位置消息。

消息轉(zhuǎn)發(fā)機制報錯提示.png

關(guān)于錯誤初始化的報錯:
此異常表明:消息接受者的類型是__NSCFNumber,而該接受者無法理解名為lowercaseString的選擇子。

消息轉(zhuǎn)發(fā)分為兩大階段:

  1. 先征詢接受者,所屬的類,看其是否能動態(tài)添加方法,以處理這個“位置的選擇子(unknown selector)”,這叫做“動態(tài)方法解析(dynamic method resolution)”。
  2. 就是完整的消息轉(zhuǎn)發(fā)機制(full forwarding mechanism)。 如果運行期喜用已經(jīng)吧第一階段執(zhí)行完了,那么接受者自己就無法再以動態(tài)新增方法的手段來響應(yīng)包含該選擇自的消息,此時,運行期系統(tǒng)會請求接受者以其他手段來處理與消息相關(guān)的方法調(diào)用。這里面要分成兩個部分,首先,請接受者看看有沒有其他對象能處理這條消息。若有,則運行期系統(tǒng)會把消息轉(zhuǎn)給那個對象,于是消息轉(zhuǎn)發(fā)結(jié)束,若沒有“備用的接受者”,則啟動完整的消息轉(zhuǎn)發(fā)機制,運行期喜用會把與系統(tǒng)有關(guān)的全部細節(jié)都封裝到NSInvocation對象中,再給接受者最后一次機會,令其設(shè)法解決當(dāng)前還未處理的這條消息。

主要的轉(zhuǎn)發(fā)路徑為:
resolveInstanceMethod ----> forwardingTargetForSelector ----> forwardInvocation

要點:
  • 若對象無法響應(yīng)某個選擇子,則進入消息轉(zhuǎn)發(fā)流程。
  • 通過運行時的動態(tài)方法解析功能,我們可以在需要用到某個方法時再將其加入類中。
  • 對象可以把無法解讀的某些選擇子轉(zhuǎn)交給其他對象來處理。
  • 經(jīng)過上述兩步之后,如果還是沒有辦法處理選擇子,那就啟動完整的消息轉(zhuǎn)發(fā)機制。

13.用“方法調(diào)配技術(shù)”調(diào)試“黑盒方法”

類的方法列表會把選擇子的名字映射到相關(guān)的方法實現(xiàn)上,使得“動態(tài)消息派發(fā)系統(tǒng)”能夠依據(jù)此找到應(yīng)該調(diào)用的方法。這些方法均以函數(shù)指針的形式來表示,這種指針叫做IMP,其原型為:

id (*IMP) (id, SEL ,…)

獲得想要交換的兩個函數(shù)的方法:

Method class_getInstanceMethod(Class aClass,SEL aSelector)

交換的方法:

void method_exchangeImplementations(Method m1, Method m2)

Method originalMethod = class_getInstanceMethod(NSStringClass,@selector(lowercaseString));

Method swappedMethod =
class_getInstanceMethod(NSStringClass,@selector(uppercaseString));

method_exchangeImpLementations(originalMethod , swappedMethod);

實際應(yīng)用其實很少交換,我們都是要添加一個方法。

要點:
  • 在運行期,可以向類中新增或替代選擇子所對應(yīng)的方法實現(xiàn)。
  • 使用另一份實現(xiàn)來替代原有的方法實現(xiàn),這道工序叫做“方法調(diào)配”,開發(fā)者常用此技術(shù)向原有實現(xiàn)添加新功能。
  • 一般來說,只要調(diào)試程序的時候次啊會需要在運行期修改方法實現(xiàn),這種做法不適合濫用。

14.理解“類對象”的用意

每一個Objective-C 對象實例都是指向某塊內(nèi)存地址的指針。所以在聲明變量時,類型后要跟上一個 * 字符:

NSString * pointerVariable = @“some String”;

描述OC對象所用的數(shù)據(jù)結(jié)構(gòu)定義在運行期程序庫的頭文件中,id類型本身也在定義在這里:

typedef struct objc_object{
      Class isa;
} * id ;

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;
}

在類集成體系中查詢類型信息,注意下這這個書里面呢mutableDic調(diào)用isMemberofClass對比NSMutableDic是返回YES但是我做測試寫代碼的時候發(fā)現(xiàn)并不是這樣的,返回的還是NO。

NSMutableDictionary  * dict =[NSMutableDictionary  new];
[dict  isMemberofClass: [NSDictionary  class]];          NO
[dict  isMemberofClass:[NSMutbaleDictionary  class]];           NO
[dict  isKindofClass:[NSDictionary class]];               YES
[dict  isKindofClass:[NSArray  class]];           NO
要點:
  • 每個實例都有一個指向Class對象的指針,用以表明類型,而這些Class對象則構(gòu)成類的集成體系。
  • 如果對象類型無法在編譯器確定,那么就應(yīng)該使用類型消息查詢方法來探知。
  • 盡量使用類型消息查詢方法來確定對象類型,而不要直接比較類對象,因為某些對象可能實現(xiàn)了消息轉(zhuǎn)發(fā)功能。

第三章 接口和api設(shè)計

15.用前綴避免命名空間沖突

要點:
  • 選擇與你的公司,應(yīng)用程序或二者都關(guān)聯(lián)的名字做類名的前綴,并在所有代碼中軍使用這個前綴
  • 若自己所開發(fā)的程序中使用了第三方庫,則應(yīng)為其中的名稱加上前綴。

16.提供“全能初始化方法”

要點:
  • 在類中提供一個全能初始化方法,并在文檔中指明。其他初始化方法均應(yīng)調(diào)用此方法。
  • 若全能初始化方法和超類不同,則需覆寫超類中對應(yīng)的方法。
  • 如果超類的初始化方法不適合子類,那么應(yīng)該覆寫這個超類方法,并拋出異常。

17.實現(xiàn)description方法

平時的時候想要看看打印的效果的時候我們通常使用NSLog,但是使用MVVM的時候傳遞的對象基本上都是以自定義model類型來傳遞的,那么直接log可能就會出現(xiàn)這樣的情況:

object = <EOCPerson:0x7fd9a1600600>

顯然model內(nèi)部的成員變量就別想著看了。解決的辦法很簡單,在類中加入description方法:

- (NSString *)description{
     return [NSString StringWithFormat:@<%@:%p,\%@ %@\>,[self class],self,_firstName,_lastName];
}
//這樣打印出來的數(shù)據(jù)
<EOCPerson:0x7f249c030f0,"Bob Smith">
要點:
  • 實現(xiàn)description方法返回一個有意義的字符串,用以描述該實例。
  • 若想在調(diào)試的時候打印出詳盡的對象描述信息,則應(yīng)實現(xiàn)debugDescription。

18.盡量使用不可變對象

如果那可變對象放入容器(collection)之后再修改其內(nèi)容,那么很容易就會破壞set的內(nèi)部數(shù)據(jù)結(jié)構(gòu),使其失去固有的語義。因此,筆者建議大家盡量減少對象中的可變內(nèi)容。

有時候可能想修改封裝在對象內(nèi)部的數(shù)據(jù),但是卻不想令這些數(shù)據(jù)為外人所改動。這種情況下,通常做法是在對象內(nèi)部將readonly屬性重新聲明為readwrite。
也就是在.h使用readOnly 在.m readwrite。

要點:
  • 盡量創(chuàng)建不可變的對象
  • 若某屬性僅可用于對象內(nèi)部修改,則在“class-continuation分類”中將其由readonly屬性擴展為readwrite屬性。
  • 不要把可變的collection(容器)作為屬性公開,而應(yīng)提供相應(yīng)方法,以此修改對象中的可變?nèi)萜鳌?/li>

19.使用清晰而協(xié)調(diào)的命名方式

要點:
  • 起名時應(yīng)遵守標準的 Objective-C命名規(guī)范,這樣創(chuàng)建出來的接口更容易為開發(fā)者所理解。
  • 方法名要言簡意賅,從左到右讀起來要像個日常用語中的句子才好。
  • 方法明理不要使用縮略后的類型名稱
  • 給方法起名時的第一要務(wù)就是確保其風(fēng)格與你自己代碼所要集成的框架相符。

20.為私有方法名加前綴

要點:
  • 給私有方法的名稱加上前綴,這樣可以很容易地將其同公共方法區(qū)別開。
  • 不要單用一個下劃線做私有方法的前綴,因為這種做法是預(yù)留給蘋果公司用的。

21.理解Objective-C 錯誤模型

在不是致命錯誤(fatal error)的情況下,我們是不會讓程序直接拋出異常的,比如創(chuàng)建某個類的時候,Coder初始化沒有給出一個必須要的參數(shù)的時候,我們選擇給這個創(chuàng)建對象返回nil來使得Coder意識到創(chuàng)作對象的時候出現(xiàn)了錯誤。

要點:
  • 只要發(fā)生了可使整個應(yīng)用程序崩潰的嚴重錯誤時,才應(yīng)使用異常。
  • 在錯誤不那么嚴重的情況下,可以指派“委托方法(delegate method)”來處理錯誤,也可以把錯誤消息放在NSError對象里,經(jīng)由“輸出參數(shù)”返回給調(diào)用者。

22.理解NSCopying協(xié)議

使用對象時經(jīng)常需要拷貝它,在Objetive-C中,此操作通常通過copy完成。如果想讓自己的類支持拷貝操作,那就實現(xiàn)NSCopying協(xié)議的- copyWithZone:方法

- (id)copyWithZone:(NSZone *)zone{

    Twentytwo * copy =[[self class] allocWithZone:zone];
    return copy;
}

在官方的例子里面,提到了我們class-continuation里面包含一個實例變量的時候,我們怎么樣防止內(nèi)存管理語義導(dǎo)致的原對象copy之后生成的新對象copy2繼承這個內(nèi)部實例變量的內(nèi)容。所以在做其他屬性copy的時候?qū)τ谶@個不想要繼承的內(nèi)部成員變量我們需要mutableCopy。如同例子Twentytwo的例子一樣。

[NSMutableArray  copy]  =>  NSArray
[NSArray mutableCopy]  =>  NSMutableArray

這邊順便說一下為什么不可變的NSArray,NSArray,NSDictionary使用Copy。而可變的NSMutableString,NSMutableArray,NSDictionary使用MutableCopy。
因為有可能NSString獲取的方式是通過mutableStr賦值的。為了防止當(dāng)mutableStr更改的時候str在不知情的情況下更改。而NSMutableString如果是Copy來修飾的,那么這個容器就將失去可變的特性,而被Copy成一個不可變的字符串,數(shù)組或者是字典。

這里面帶一下 ->這個東西是干嘛的,我的理解是當(dāng)一個內(nèi)部的實例變量需要被方法實現(xiàn)內(nèi)部調(diào)用的時候就可以使用 copy->_friends這樣。具體可以去看我的例子TwntytwoTest。

關(guān)于深拷貝(deep copy)和淺拷貝(shallow copy)
淺拷貝:只拷貝容器對象本身,而不復(fù)制其中數(shù)據(jù)。
深拷貝: 在拷貝對象自身時,將其底層數(shù)據(jù)也一并復(fù)制過去。
比如對NSSet對象的深拷貝:- initWithSet: copyItems:, 如果items 設(shè)置為YES,就是深拷貝。

要點:
  • 若想令自己所寫的對象具備拷貝對象,則需實現(xiàn)NSCopying協(xié)議。
  • 如果自定義的對象分為可變版本與不可變版本,那么就要同時實現(xiàn)NSCopying和NSMutableCopying協(xié)議。
  • 復(fù)制對象時需要決定采用深拷貝還是淺拷貝,一般情況下應(yīng)該盡量執(zhí)行淺拷貝。
  • 如果你所寫的對象需要深拷貝,那么可考慮新增一個專門執(zhí)行深拷貝的方法。

結(jié)尾

自己寫的筆記首先是用Pages寫的,寫完之后放到簡書里面以為也就剩下個排版了,結(jié)果發(fā)現(xiàn)基本上每一個點的總結(jié)都不讓自己滿意,但是又想早點放上去,總感覺自己被什么追趕著,哈哈,本來寫完筆記的時候是2W字的,結(jié)果到第二次發(fā)表的時候發(fā)現(xiàn)就成了2.5W了,需要改進的東西還是太多,希望朋友們有什么改進的提議都可以告訴我,我會一直補充這個筆記,然后抓緊改GitHub上的代碼~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,205評論 2 375

推薦閱讀更多精彩內(nèi)容