聲明:這個筆記的系列是我每天早上打開電腦第一件做的事情,當然使用的時間也不是很多因為還有其他的事情去做,雖然吧自己買了紙質的書但是做筆記和看的時候基本都是看的電子版本,一共52個Tip每一個Tip的要點我是完全謄寫下來的,害怕自己說的不明白所以就謄寫也算是加強記憶,我會持續修改把自己未來遇到的所有相關的點都加進去,最后希望讀者尊重原著,購買正版書籍。PS:不要打賞要喜歡~
Demo:GitHub代碼網址,大大們給個鼓勵Star啊。
整個系列筆記目錄
《Effective Objective-C 2.0》第一份讀書筆記
《Effective Objective-C 2.0》第二份讀書筆記
《Effective Objective-C 2.0》第三份讀書筆記
第一章 熟悉Objective-C
1.了解Objective-C語言的起源
Objective-C 起源自Smalltalk的,也就是消息性語言。而不是函數調用
[obj perform:parameter1 and:parameter2]; 消息語言
obj->perform(parameter1,paramter2); 函數調用方式
關鍵區別在于:消息語言最終執行的代碼由運行環境決定,而函數調用則是由編譯器決定。這也叫做“動態綁定(dynamic binding)”
我們創建一個OC對象的時候可以這樣:
NSString * someString = @“new string ”;
中間的星號代表的就是指針,所以我們創建的是一個someString指針指向一個NSString的對象,我們把someString指針對象放在棧(stack)中,而真正的指針對象"new string"我們放在堆(heap space)中。如果我們新建一個新的指針anyString的數值仍然是"new string",那么就不會重新在堆中新建內存而是直接吧新的anyString指向已經創建的"new string"對象。
分配在堆中的內存需要程序員直接管理,而分配在棧上的會在棧彈出時自動清理。
Objective-C將堆內存管理抽象出來了,不需要用malloc及free來分配或釋放對象所占內存。Objective-C運行期環境把這部分工作抽象為一套內存管理框架,名為“引用計數”,
在OC代碼中也會有使用棧(stack)空間的變量,比如不帶星號的Sturck對象,比如CGRect,如果只是簡單的高度,寬度等的數據的時候,就不用建立OC對象了會耗費更多額外的開銷。
要點:
- Objective-C為C語言添加了面向對象特性,是其超集。Objective-C使用動態綁定的消息結構,也就是說,在運行時才會檢查對象類型。接受一條消息之后,究竟應執行何種代碼,由運行期環境而非編譯器來決定。
- 理解C語言的核心概念有助于寫好Objective-C程序。尤其要掌握內存模型和指針。
2.在類的頭文件中盡量少引入其他頭文件
這里面講了為什么使用@class “EOCEmployer.h”這樣的寫法。
這叫做“向前聲明(forward declaring )” 這是因為不需要知道EOCEmployer的內部接口細節。
要點:
- 除非確有必要,否則不要引入頭文件。一般來說,應在某個類的頭文件中使用向前聲明來體積別的類,并在實現文件中引入那些類的頭文件。這樣做可以盡量降低類之間的耦合(coupling)。
- 有時無法使用向前聲明,比如要聲明某個類遵守一項協議。這種情況下,盡量把“該類遵守某協議”的這條聲明移至"class-continuation分類"中。如果不行的話,就把協議單獨放在一個頭文件中,然后將其引入。
3.多用字面量語法,少用與之等價的方法
我們可以通過“字符串自變量(string literal)”語法來創建對象:
NSString * someString = @“Effective Objective-C 2.0”;
使用字面量語法可以縮減源代碼長度,使其更為易讀。
有的時候需要把整數、浮點數、布爾值封入Objective-C對象中。這種情況下可以用NSNumber類,該類可處理多種類型的數值。若是不用字面量,那么就需要按下述方式創建實例:
NSNumber * someNumber = [NSNumber numberWithInt:1];
NSNumber * someNumber = @1;
NSNumber * boolNumber = @YES;
NSNumber * charNumber = @‘a’;
NSNumber * expressionNumber = @(x * y);
關于NSArray的一個字面量問題:
id object1 = /********/;
id object2 = /********/;
id object3 = /********/;
NSArray * arrayA =[NSArray arrayWithObjects:object1,object2,object3,nil];
NSArray * arrayB =@[object1,object2,object3];
如果object1和object3 都是指向正常的對象,而object2 是nil。那么按照字面量語法創建數組arrayB時會拋出異常,而arrayA雖然能創建出來,但是只有一個object1一個對象。”arrayWithObjects”方法會依次處理各個參數,知道發現nil為止,由于object2是nil,所以該方法會提前結束。所以arrayB更加安全,直接彈出要比你完美運行后來少一個數據來的更安全。
小缺點:
使用字面量語法創建的字符串,數組,字典對象都是不可變的(immutable)。若想要可變版本的,就需要復制一份:
NSMutableArray * mutable = [[@1,@2,@3,@4,@5] mutableCopy];
這么做會多調用一個辦法,但是好處是要多余上面的那個缺少數據的缺點的。
要點:
- 應該使用字面量語法來創建字符串,數值,數組,字典。與創建此類對象的常規方法相比,這么做更加簡單扼要。
- 應該通過取下標操作來訪問數組下標或字典中的鍵所對應的元素。
- 用字面量語法創建數組和字典時,若值中又nil,則會拋出異常。因此,務必確保值里不含 nil。
4.多用類型常量,少用#define預處理指令
#define ANIMATION_DURATION 0.3
這樣的預處理容易導致整個工程里的ANIMATION_DURATION都是0.3,很有可能會改變系統定義的一些宏。
我們選擇這種方式:
static const NSTimerInterval kAnimationDuration = 0.3;
我們的習慣是如果常量只是出現在一些類的“編譯單元(translation unit)”之內,則前面加"k",如果是類之外也可見就用類名做前綴。
定義常量的位置很重要,我們總喜歡在頭文件里聲明預處理指令,這樣做真的很糟糕,當常量名稱有可能互相沖突時更是如此。
如果想要定義一個定義域在某個文件中的定義的話,我們可以這樣:
static const NSTimerInterval kAnimationDuration = 0.3;
變量一定要用static和const一同修飾,用const修飾的變量如果遭到更改就會報錯,而static修飾符則意味著變量僅在定義此變量的編譯單元中可見,也就是這個.m中。假如聲明此變量時不加static,則編譯器就會為它創建一個“外部符號(external symbol)”。此時如果是另一個編譯單元里面也聲明了一樣的變量就會拋出錯誤。
而想要一個全局變量的話:
//.h:
extern NSString * const EOCStringConstant;
//.m:
NSString * const EOCStringConstant = @“VALUE”;
const修飾符放在EOCStringConstant指針外面就符合語義為防止指針的方向。
編譯器看到頭文件中的extern關鍵字,就會明白如何在引入磁頭文件的代碼中處理該常量了。在全局符號表將會有一個名教EOCStringConstant的符號。也就說,編譯器無需查看器定義,即允許代碼使用此常量。因為它知道,當連接成二進制文件之后,肯定能好到這個常量。
此類常量必須要定義。而且只能定義一次。編譯器會在"數據段(data section)"為字符串分配存儲空間
。鏈接器會把目標文件和其他目標文件相鏈接,生成最終的二進制文件。鏈接器可以隨時解析這個常量。
要點:
- 不要用預處理指令定義常量。這樣定義出來的常量不含類型信息,編譯器只是會在編譯之前根據此執行查找與替代操作。即使有人重新定義了常量值,編譯器也不會產生警告信息。這將導致應用程序中的常量值不一致。
- 在實現文件中使用static const來定義“只在編譯單元內可見的常量”。由于次常量不在全局符號表中,所以無需在其前面加前綴。
- 在頭文件中使用extern來聲明全局常量,并在相關實現文件中定義其值。這種常量要出現在全局符號表中,所以其名稱應加上區隔,通常用與之相關的類名做前綴.
5.用枚舉表示狀態,選項,狀態碼
枚舉可以用來列舉某個對象的一些形態
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,其他的都可以多選。
下面是”<<“符號帶來的內存存儲方式:
要點:
- 應該用枚舉來表示狀態機的狀態,傳遞給方法的選項以及狀態碼等值,給這些值起一個易懂的名字。
- 如果把傳遞給某個方法的選項表示為枚舉類型,而多個選項又同時實現,那么就將個選項值定義為2的冪,以便通過按位或操作將其組合起來。
- 用NS_ENUM和NS_OPTIONS宏來定義枚舉類型,并指明其底層數據類型。這樣做可以確保枚舉是用開發者所選的底層數據類型實現出來的,而不會采用編譯器所選的類型。
- 在處理枚舉類型的switch語句中不要定義default分支,這樣的話,加入新枚舉之后,編譯器就會提示開發者:switch語句并未處理所有枚舉。
第二章 對象,消息,運行期
6.理解“屬性”這一概念
在OC中,對象(object)就是基本構造單位(building block),開發者可以通過對象來存儲并傳遞數據。在對象之間傳遞數據并執行任務的過程就叫做“消息傳遞”(Messaging)。
當應用程序運行起來以后,為其提供相關支持的代碼叫做”Objective-C運行期環境(Objective-C runtime)”,它提供了一些使得對象之間能夠傳遞消息的重要函數,并且包含創建類實例所用的全部邏輯。
關于NSString的Copy和Strong修飾符
這邊重申一下為什么NSString使用copy而不用strong:
因為如果NSString對象是取一個NSMutableString對象的數值的話,當NSMutableString的對象數值發生改變的時候,NSString會相應的發生改變,而不是保持遠數值.
為什么NSMutableString使用strong而不用copy:
因為如果是copy修飾的話,NSMutableArray的數值就不能發生改變。
先隨便寫一個聲明
@interface EOCPerson:NSObject{
@public
NSString * _firstName;
NSString * _lastName;
@private
NSString * _ someInternalData
}
如果我們在_firstName上面在加一個_iSuName而firstName和lastName都向下移動,這樣EOCPerson就會擁有三個屬性,其實一搭眼好像這樣完全沒有問題,其實對象布局在編譯器布局的時候已經固定了,所以當你在上面加入一個iSuName的時候其實,想要獲取firstName的類還是會根據編譯器時候的偏移量來獲取firstName,但是實際上獲取的卻是iSuName的值,原來的元素全部都后移的時候,那么以偏移量為參考量的做法會導致請求的元素不兼容(incompatibility)- - ,OC解決這種問題的方法就是把實例變量當做一種存儲偏移量所用的“特殊變量”,然后交由“類對象”保存(14條詳解)。這個時候偏移量在運行的時候就會動態改變,找到正常的數值。這就是穩固的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];
然而屬性還有更多的優勢。如果使用了屬性的話,編譯器就會自動編寫訪問這些屬性所需的方法。
如果想要改變名字的長相的話:
@implementation EOCPerson
@synthesize firstName = _myFirstName ;
@synthesize lastName = _mySecondName;
這樣你在聲明里面定義的firstName,lastName就變成了_myFirstName,_mySecondName。
當然你也可以選擇手動生成get,set方法(@dynamic)
@implementation EOCPerson
@dynamic firstName,lastName
編譯器不會為上面這兩個屬性自動合成存儲方法和實例變量。如果用代碼訪問其中的屬性,編譯器也不會發出警告消息。
屬性特質:
- 原子性:嚴格意義上說原子性(automicity)因為使用同步鎖的原因是要比非原子性(nonatomic)要更加安全,但是在屬性上添加同步鎖要消耗過多的資源。所以我們基本上會使用nonatomic做修飾符。
- 讀寫權限:也就是readwrite(讀寫),readonly(只讀)。
- 內存管理語義:
assign : 只會執行對“純量類型”,例如(CGFloat或者是NSInteger等)的簡單賦值操作。
strong : 定義了一種“擁有關系”,為這種屬性設置新值時,設置方法先保留新值,并釋放舊值,然后將新值替換上去。
weak: 定義了一種”非擁有關系”,為這種屬性設置新值的時候,設置方法即不保留新值也不釋放舊值。此特質同assign類似,然而在屬性所指的對象遭到摧毀的時候,屬性值也會被清空。
unsafe_unretained: 和assign相同,但是它適用于“對象類型”,該特質表達一種“非擁有關系”,但是當目標對象遭到摧毀的時候,屬性值不會自動清空,這一點和weak有區別。
copy: 此特質所表示的所屬關系和strong類似。然而設置方法并不保留新數值,而是將其“拷貝”。當屬性類型為NSString *時候,經常用此特質來保護其封裝性。
需要注意:如果自己來實現存取方法,那么應該保證其具備相關屬性所聲明的特質,比方說,如果將某個屬性聲明為copy,那么就應該在”設置方法”中拷貝相關對象。否則會誤導該屬性的使用者。
說道這個在初始化的時候為什么不是用set方法來保證每次都調用NSString都能帶上Copy的內存語義。而是使用屬性賦值給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語法來定義對象中所封存的數據。
- 通過“特質”來制定存儲數據所需的正確定義。
- 在設置屬性所對象的實力變量時,一定要遵從屬性所聲明的寓意。
- 開發iOS程序時候應該使用nonatomic屬性,因為atomic屬性會嚴重影響性能。
7.在對象內部盡量直接訪問實例變量
首先確認一下 _name:直接訪問對象實例變量 self.name 間接訪問實例變量
self.name 和_name 的區別是:
- 由于不經過Objective-C的“方法派發”步驟,所以直接訪問實例變量的速度當然比較快。在這種情況下所生成的代碼會直接訪問保存對象變量的那塊內存。
- 直接訪問實例變量時,不會調用其“設置方法”,這就繞過了相關屬性定義的“內存管理語義”。比方說,如果在ARC下直接訪問一個聲明為copy的屬性,那么并不會拷貝該屬性,只會保留新值并釋放舊值。
- 如果直接訪問實例變量,那么不會觸發“鍵值觀察(KVO)”,通知。這樣做是否會產生問題,還取決于具體的對象行為。
回想一下上一個Tip上的問題為什么在初始化的時候不使用self.firstName = firstName,而選擇_firstName = [firstName copy];
嗨呀,好氣啊,我思想跑的太遠了,我以為是在子類重寫父類屬性會影響父類的運行呢,并不是啊只是子類運行的時候如果發現父類的init方法中使用點語法而子類中正好重寫了這個方法,那么就會走子類的方法,子類如果有限制條件語句的時候會導致條件在不清楚的情況下意外不能通過。而如果直接是屬性的話就不會走set方法直接就跳過子類的判斷環節了。例子請看EOCPerson.demo
還有一種情況下使用獲取方法
那就是惰性初始化(lazy initialization)
- (EOCBrain *)brain{
if(!_brain){
_brain = [Brain new];
}
return _brain;
}
這種情況下你不self.brain的話代碼就沒法走了。
要點:
- 在對象內部讀取數據時,應該直接通過實例變量來讀,而寫入數據時,則通過屬性來寫。
- 在初始化方法及dealloc方法中,總是應該直接通過實例變量來讀取數據。
- 有時會使用惰性初始化計數配置某分數據,在這種情況下,需要通過屬性來讀取數據。
8.理解“對象等同性”這一概念
按照 “==” 操作符比較出來的結果未必是我們想要的,因為該操作比較的是兩個指針本身,而不是期所指的對象。
NSString * foo = @“Badger 123”;
NSString * bar = [NSStringWithFormat:@“Badger %i”,123];
BOOL equalA = ( foo == bar);
BOOL equalB = [foo isEqual:bar] ;
// set out -> equalA == NO
- 第一種判定的方法就是:
isEqual
所謂的所有屬性判斷:
a.判斷兩個指針是否相等,當切僅當指針值相等的時候才會想等。
b.比較兩個對象所屬的類。
c.逐條屬性判斷。 - 第二種判定的方法就是:
hash
若兩個對象相等,則其哈希嗎也相等。但是兩個哈希嗎相等的對象未必相等。
假如某個collection是用set實現的,那么set可能會根據哈希嗎把對象分裝到不同的數組中。向set中添加新對象時,要根據其哈希嗎找到與之相關的那個數組。依次檢查其中各個元素。看是哦否有相同的,如果有相同的,那就說明要添加的對象已經在set里面了。由此可知,如果令每個對象返回相同的哈希嗎,那么在set中已經有10000000個對象的情況下,如要繼續向里面添加對象,就需要全部便利一邊。
那么有沒有可能讓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)….
要點:
- 若想檢測對象的等同性,請提供”isEqual”和hash方法。
- 相同的對象必須具備相同的哈希嗎,但是兩個哈希碼相同的對象確未必相同
- 不要盲目的逐個檢測每條對象,而是應該依照具體需求制定檢測方案。
- 編寫hash方法時,應該使用計算速度快而且哈希碼碰撞幾率低的算嗎
9.以“類族模式”隱藏實現細節
“類族”是一種很有用的模式(pattern),可以隱藏“抽象基類”背后的實現細節。Objective-C系統框架中普遍使用此模式。比如:
+ (UIButton *)buttonWithType:(UIButtonType)type;
該方法的返回對象,其類型取決于按鈕的類型。然而,不管返回是什么類型的對象,它們都集成來自同一個基類:UIButton。這么做的意義在于:UIButton類的使用者無需關心創建出來的按鈕屬于哪一個子類,也不用考慮繪制細節。
在測試代碼里面有創建類族的代碼
在這些代碼里面,基類實現了一個“類方法”,該方法根據待創建的雇員類別分配好對應的雇員類實力。這種“工廠模式(Factory pattern)”是創建類族的辦法之一。
如果對象所屬的類位于某個類族中,那么在查詢其類型消息(introspection)時就要小心了,你可能覺得創建了某個類的實例,然而實際上創建的卻是其子類的實例。在這個Employee這個例子中,[employee isMemberOfClass:[EOCEmployee class]]
似乎會返回YES,但實際上返回的卻是NO,因為employee并非Empoyee類的實例,而是其某個子類的實例。
Cocoa里的類族
系統框架中又許多類族。大部分collection類都是類族,例如NSArray與其可變的版本NSMutableArray。這樣看來,實際上有兩個抽象基類,一個用于不可變數組,另一個用于可變數組。盡管具備公共接口的類有兩個,但仍然可以合起來算作一個類族。
像NSArray這樣的類的背后其實是個類族(對于大部分collection類而言都是這樣),明白這一點很重要,否則可能會寫出下面這種代碼
id maybeArray = /****/
if ([maybeAnArray class] == [NSArray class]) {
// Will never be hit 永遠不會進入
}
你要是知道NSArray是個類族,那就會明白上述代碼錯在哪里:[maybeAnArray class]所返回的類絕不可能是NSArray類本身,因為由NSArray的初始化方法所返回的那個實例其類型是隱藏在類族公共接口后面的某個內部類型。我們可以通過
判斷對象是否在類族iskindof
來判斷(14條詳細講解):
id maybeAnArray = /*****/
if ([maybeAnArray isKindOfClass:[NSArray class]]){
// Will be hit // 會走這邊
}
要點:
- 類族模式可以把實現細節隱藏在一套簡單的公共接口后面。
- 系統框架中經常使用類族
- 從類族的公共抽象基類中繼承子類時要當心,若有開發文檔請先閱讀。
10.在既有類中使用關聯對象存放自定義數據
有時需要在對象中存放相關信息。我們可以使用關聯對象Associated Object
。
可以給某對象關聯許多其他對象,這些對象通過“鍵”來區分。存儲對象值的時候,可以指明”存儲策略(storage policy)“,用以維護響應的“內存管理語義”。
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_COPY copy
下列方法可以管理關聯對象:
void objc_setAssociatedObject (id object ,void * key ,id value,objc_AssociationPolicy policy)
此方法以給定的鍵和策略為某對象設置關聯對象值。
void objc_getAssociatedObject(id object, void object)
此方法根據給定的鍵從某對象中獲取相應的關聯對象值。
void objc_removeAssocatedObjects(id object)
此方法移除指定對象的全部關聯對象。
做的方法:
- (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);
}
要點:
- 可以通過“關聯對象”機制來把兩個對象連起來
- 定義關聯對象時可指定內存管理語義,用以訪問定義屬性時采用的“擁有關系”和“非擁有關系”
- 只有在其他做法不可行的時候才會應用關聯對象,因為這種做法通常會引入難于查找的bug。
11.理解 objc_msgSend的作用
objective-C的術語來說,這叫“傳遞消息”
id returnValue = [someObject messageName: parameter];
在本例中,someObject叫做接受者(receiver),messageName叫做“選擇子(selector)”。
void objc_msgSend(id self,SEL cmd,…)
id returnValue =objc_msgSend (someObject,@selector(messageName:),parameter);
為了完成調用的做法,該方法需要在接受者所屬的類中搜索器“方法列表”(list of methods)
,如果能找到與選擇子名稱相符的方法,就跳至其實現代碼,如果找不到就沿著繼承體系向上查找,等找到合適的方法之后在跳轉。如果最終還是找不到相符的方法,那就執行“消息轉發(message forwording)
操作”。
這么看來,調用一個方法好像需要很多步驟。索性objc_megSend會將匹配結果緩存在“快速映射表(fash map)里面”。每一個類都有這樣的一塊緩存,當然“快速請求路徑”還是不如靜態來的快,但是也不會慢很多。
objc_msgSend_struct;
objc_msgSend_fpret;
objc_msgSendSuper;
方法存儲的方式大概是:
<return_type> Class_selector(id self ,SEL_cmd,…)
真正的函數其實和這個差不多,因為“尾調用優化 (tail- call optimization)技術”
:
如果某個函數的最后一項操作是調用某個函數的話,就會調用“尾調用優化”技術。編譯器會生成調轉至另一個函數所需的指令碼,而且不會調用堆棧中推入新的“棧幀(frame stack)”。只有當某函數的最后一個操作僅僅是調用其他函數而不會將其返回值另作他用的時候次啊會執行”尾調用優化”,這樣做可以防止過做的發生“棧溢出”現象。
要點:
- 消息由接受者,選擇子及參數構成。給某對象“發送消息(invoke a message)”也就相當于在該對象上“調用方法(call a method)”。
- 發給某對象的全部消息都要由“動態消息派發系統(dynamic message dispatch system)”來處理,該系統會查出對應的方法,執行其代碼。
12.理解消息轉發機制
若想令類能理解某條消息,我們必須以程序碼實現出對應的方法才行。但是,在編譯期向類發送了無法解讀的消息并不會報錯,因為在運行期可以繼續給類中添加方法也就是“消息轉發(message forwarding)”機制,程序員可經由此過程告訴對象應該如何處理位置消息。
關于錯誤初始化的報錯:
此異常表明:消息接受者的類型是__NSCFNumber,而該接受者無法理解名為lowercaseString的選擇子。
消息轉發分為兩大階段:
- 先征詢接受者,所屬的類,看其是否能動態添加方法,以處理這個“位置的選擇子(unknown selector)”,這叫做“動態方法解析(dynamic method resolution)”。
- 就是完整的消息轉發機制(full forwarding mechanism)。 如果運行期喜用已經吧第一階段執行完了,那么接受者自己就無法再以動態新增方法的手段來響應包含該選擇自的消息,此時,運行期系統會請求接受者以其他手段來處理與消息相關的方法調用。這里面要分成兩個部分,首先,請接受者看看有沒有其他對象能處理這條消息。若有,則運行期系統會把消息轉給那個對象,于是消息轉發結束,若沒有“備用的接受者”,則啟動完整的消息轉發機制,運行期喜用會把與系統有關的全部細節都封裝到NSInvocation對象中,再給接受者最后一次機會,令其設法解決當前還未處理的這條消息。
主要的轉發路徑為:
resolveInstanceMethod ----> forwardingTargetForSelector ----> forwardInvocation
要點:
- 若對象無法響應某個選擇子,則進入消息轉發流程。
- 通過運行時的動態方法解析功能,我們可以在需要用到某個方法時再將其加入類中。
- 對象可以把無法解讀的某些選擇子轉交給其他對象來處理。
- 經過上述兩步之后,如果還是沒有辦法處理選擇子,那就啟動完整的消息轉發機制。
13.用“方法調配技術”調試“黑盒方法”
類的方法列表會把選擇子的名字映射到相關的方法實現上,使得“動態消息派發系統”能夠依據此找到應該調用的方法。這些方法均以函數指針的形式來表示,這種指針叫做IMP,其原型為:
id (*IMP) (id, SEL ,…)
獲得想要交換的兩個函數的方法:
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);
實際應用其實很少交換,我們都是要添加一個方法。
要點:
- 在運行期,可以向類中新增或替代選擇子所對應的方法實現。
- 使用另一份實現來替代原有的方法實現,這道工序叫做“方法調配”,開發者常用此技術向原有實現添加新功能。
- 一般來說,只要調試程序的時候次啊會需要在運行期修改方法實現,這種做法不適合濫用。
14.理解“類對象”的用意
每一個Objective-C 對象實例都是指向某塊內存地址的指針。所以在聲明變量時,類型后要跟上一個 * 字符:
NSString * pointerVariable = @“some String”;
描述OC對象所用的數據結構定義在運行期程序庫的頭文件中,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調用isMemberofClass對比NSMutableDic是返回YES但是我做測試寫代碼的時候發現并不是這樣的,返回的還是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對象則構成類的集成體系。
- 如果對象類型無法在編譯器確定,那么就應該使用類型消息查詢方法來探知。
- 盡量使用類型消息查詢方法來確定對象類型,而不要直接比較類對象,因為某些對象可能實現了消息轉發功能。
第三章 接口和api設計
15.用前綴避免命名空間沖突
要點:
- 選擇與你的公司,應用程序或二者都關聯的名字做類名的前綴,并在所有代碼中軍使用這個前綴
- 若自己所開發的程序中使用了第三方庫,則應為其中的名稱加上前綴。
16.提供“全能初始化方法”
要點:
- 在類中提供一個全能初始化方法,并在文檔中指明。其他初始化方法均應調用此方法。
- 若全能初始化方法和超類不同,則需覆寫超類中對應的方法。
- 如果超類的初始化方法不適合子類,那么應該覆寫這個超類方法,并拋出異常。
17.實現description方法
平時的時候想要看看打印的效果的時候我們通常使用NSLog,但是使用MVVM的時候傳遞的對象基本上都是以自定義model類型來傳遞的,那么直接log可能就會出現這樣的情況:
object = <EOCPerson:0x7fd9a1600600>
顯然model內部的成員變量就別想著看了。解決的辦法很簡單,在類中加入description方法:
- (NSString *)description{
return [NSString StringWithFormat:@<%@:%p,\%@ %@\>,[self class],self,_firstName,_lastName];
}
//這樣打印出來的數據
<EOCPerson:0x7f249c030f0,"Bob Smith">
要點:
- 實現description方法返回一個有意義的字符串,用以描述該實例。
- 若想在調試的時候打印出詳盡的對象描述信息,則應實現debugDescription。
18.盡量使用不可變對象
如果那可變對象放入容器(collection)之后再修改其內容,那么很容易就會破壞set的內部數據結構,使其失去固有的語義。因此,筆者建議大家盡量減少對象中的可變內容。
有時候可能想修改封裝在對象內部的數據,但是卻不想令這些數據為外人所改動。這種情況下,通常做法是在對象內部將readonly屬性重新聲明為readwrite。
也就是在.h使用readOnly 在.m readwrite。
要點:
- 盡量創建不可變的對象
- 若某屬性僅可用于對象內部修改,則在“class-continuation分類”中將其由readonly屬性擴展為readwrite屬性。
- 不要把可變的collection(容器)作為屬性公開,而應提供相應方法,以此修改對象中的可變容器。
19.使用清晰而協調的命名方式
要點:
- 起名時應遵守標準的 Objective-C命名規范,這樣創建出來的接口更容易為開發者所理解。
- 方法名要言簡意賅,從左到右讀起來要像個日常用語中的句子才好。
- 方法明理不要使用縮略后的類型名稱
- 給方法起名時的第一要務就是確保其風格與你自己代碼所要集成的框架相符。
20.為私有方法名加前綴
要點:
- 給私有方法的名稱加上前綴,這樣可以很容易地將其同公共方法區別開。
- 不要單用一個下劃線做私有方法的前綴,因為這種做法是預留給蘋果公司用的。
21.理解Objective-C 錯誤模型
在不是致命錯誤(fatal error)的情況下,我們是不會讓程序直接拋出異常的,比如創建某個類的時候,Coder初始化沒有給出一個必須要的參數的時候,我們選擇給這個創建對象返回nil來使得Coder意識到創作對象的時候出現了錯誤。
要點:
- 只要發生了可使整個應用程序崩潰的嚴重錯誤時,才應使用異常。
- 在錯誤不那么嚴重的情況下,可以指派“委托方法(delegate method)”來處理錯誤,也可以把錯誤消息放在NSError對象里,經由“輸出參數”返回給調用者。
22.理解NSCopying協議
使用對象時經常需要拷貝它,在Objetive-C中,此操作通常通過copy完成。如果想讓自己的類支持拷貝操作,那就實現NSCopying協議的- copyWithZone:
方法
- (id)copyWithZone:(NSZone *)zone{
Twentytwo * copy =[[self class] allocWithZone:zone];
return copy;
}
在官方的例子里面,提到了我們class-continuation里面包含一個實例變量的時候,我們怎么樣防止內存管理語義導致的原對象copy之后生成的新對象copy2繼承這個內部實例變量的內容。所以在做其他屬性copy的時候對于這個不想要繼承的內部成員變量我們需要mutableCopy。如同例子Twentytwo的例子一樣。
[NSMutableArray copy] => NSArray
[NSArray mutableCopy] => NSMutableArray
這邊順便說一下為什么不可變的NSArray,NSArray,NSDictionary使用Copy。而可變的NSMutableString,NSMutableArray,NSDictionary使用MutableCopy。
因為有可能NSString獲取的方式是通過mutableStr賦值的。為了防止當mutableStr更改的時候str在不知情的情況下更改。而NSMutableString如果是Copy來修飾的,那么這個容器就將失去可變的特性,而被Copy成一個不可變的字符串,數組或者是字典。
這里面帶一下 ->這個東西是干嘛的,我的理解是當一個內部的實例變量需要被方法實現內部調用的時候就可以使用 copy->_friends這樣。具體可以去看我的例子TwntytwoTest。
關于深拷貝(deep copy)和淺拷貝(shallow copy)
淺拷貝:只拷貝容器對象本身,而不復制其中數據。
深拷貝: 在拷貝對象自身時,將其底層數據也一并復制過去。
比如對NSSet對象的深拷貝:- initWithSet: copyItems:
, 如果items 設置為YES,就是深拷貝。
要點:
- 若想令自己所寫的對象具備拷貝對象,則需實現NSCopying協議。
- 如果自定義的對象分為可變版本與不可變版本,那么就要同時實現NSCopying和NSMutableCopying協議。
- 復制對象時需要決定采用深拷貝還是淺拷貝,一般情況下應該盡量執行淺拷貝。
- 如果你所寫的對象需要深拷貝,那么可考慮新增一個專門執行深拷貝的方法。
結尾
自己寫的筆記首先是用Pages寫的,寫完之后放到簡書里面以為也就剩下個排版了,結果發現基本上每一個點的總結都不讓自己滿意,但是又想早點放上去,總感覺自己被什么追趕著,哈哈,本來寫完筆記的時候是2W字的,結果到第二次發表的時候發現就成了2.5W了,需要改進的東西還是太多,希望朋友們有什么改進的提議都可以告訴我,我會一直補充這個筆記,然后抓緊改GitHub上的代碼~