《Effective Objective-C》概念篇

1.運行時
  • OC 語言由 Smalltalk(20世紀70年代出現的一種面向對象的語言) 演化而來,後者是消息型語言的鼻祖。
  • OC 使用動態綁定的消息結構,在運行時檢查對象類型。
  • 使用消息結構的語言,其運行時執行的代碼由運行環境來決定。而使用函數調用的語言,則由編譯器決定。
  • OC 對象所佔內存老是分配在「堆空間」,毫不會分配在「棧」上。分配在堆中的內存必須直接管理,而分配在棧上用於保存變量的內存會在棧幀彈出時自動清理。
  • OC 運行期內存管理架構,名叫「引用計數」。
  • OC 中會遇到定義中不含 * 的變量,也是會使用「棧空間」。好比 CoreGraphics 框架的 CGRect,整個系統框架都在使用這種結構體,若是使用 OC 對象,性能會受影響。
2.屬性
  • 用來封裝對象中的數據。
  • 若是使用了屬性的話,那麼編譯器就會主動編寫訪問這些屬性所需的方法,此過程叫作「自動合成」。這個過程在編譯期執行,點語法是編譯時特性。
  • @synthesize 語法來指定實例變量的名字。
@synthesize testString = _testString;
  • @dynamic 關鍵字會告訴編譯器不要建立實現屬性所用的實例變量,也不要爲其建立存取方法。並且,在編譯訪問屬性的代碼時,即便編譯器發現沒有定義存取方法也不會報錯,它相信這些方法能在運行時找到。
@dynamic testString;
_testString //Use of undeclared identifier '_testString'
  • 屬性特質的設定也會影響編譯器所生成的存取方法。屬性特質包括:原子性、讀寫屬性、內存管理語義(assign、strong、weak、unsafe_unretained、copy)、方法名(getter== …)。
  • 若是想在其餘方法裏設置屬性值,一樣要遵照屬性定義中所宣稱的語義,由於「屬性定義」就至關於「類」和「待設置的屬性值」之間所達成的契約
    好比在指定構造器中對成員變量的賦值
@interface TestObject ()
//雖然這個屬性已是隻讀性質,也要寫上具體的語義,以此代表初始化方法在設置這些值時所用方法
@property(copy,readonly) NSString *testString;
@end

@implementation TestObject
- initWithString:(NSString *)string {
    
    self = [super init];
    
    if (self) {
        //用初始化方法設置好屬性值以後,就不要再改變了,此時屬性應設爲「只讀」
        _testString = [string copy];
    }
    return  self;
}
@end
3.對象等同性
  • 「==」操做符比較的是兩個指針自己,而不是所指的對象。應該使用 NSObject 協議中聲明的「isEqual」方法來判斷兩個對象的等同性。
NSString *textA = @"textA";
    NSString *textAnother = [NSString stringWithFormat:@"textA"];
    NSLog(@"%d",textA == textAnother);// 0
    NSLog(@"%d",[textA isEqual:textAnother]);// 1
    NSLog(@"%d",[textA isEqualToString:textAnother]);// 1
  • 在自定義的對象中正確複寫「isEqual」"hash"方法,來斷定兩個方法是否相等。
  • 若是 「isEqual」方法斷定兩個對象相等,那麼其 hash 方法也必須返回同一個值。
    好比下面這個類
@interface TestObject : NSObject
@property NSString *testString;
@end

@implementation TestObject
- (BOOL)isEqual:(id)object {
    if (self == object)  return YES;
    if ([self class] != [object class]) return NO;
    
    TestObject *otherObject = (TestObject *)object;
    if (![self.testString isEqualToString:otherObject.testString]) {
        return  NO;
    }
    return YES;
}
-(NSUInteger)hash {
    //在沒有性能問題下,hash 方法能夠直接返回一個數
    return 1227;
}

@end

在繼承體系中判斷等同性,還需判斷是不是其子類
相同的對象必須具備相同的哈希碼,可是相同哈希碼的對象卻未必相同程序員

特定類型等同性判斷
  • 本身建立等同性判斷方法,無需檢測參數類型,大大提高檢測速度。就像「isEqualToString」同樣。
- (BOOL)isEqualToTestObject:(TestObject *)testobject {
    
    if (self == testobject) {
        return YES;
    }
    if (![self.testString isEqualToString:testobject.testString]) {
        return  NO;
    }
    return YES;
}
- (BOOL)isEqual:(id)object {
    
    if ([self class] == [object class]) {
        return [self isEqualToTestObject:(TestObject *)object];
    }else {
        return [super isEqual:object];
    }
}
  • 有時候無需將全部數據逐個比較,只根據其中部分數據便可判明兩者是否相等。objective-c

    比方說一個模型類的實例是根據數據庫的數據建立而來,那麼其中可能會含有一個惟一標識符(unique identifier),在數據庫中用做主鍵。這時候,咱們就能夠根據標識符來斷定等同性,尤爲是此屬性聲明爲 readonly 時更應該如此。只要標識符相等,就能夠說明這兩個對象是由相同數據源建立,據此判定,其餘數據也相等。
    固然,只有類的編寫者才知道那個關鍵屬性是什麼。數據庫

要點:不要盲目的逐個檢測每條屬性,而是應該按照具體需求制定檢測方案編程

4.理解 objc_msgSend
  • 在對象上調用方法是 OC 中常用的方法。專業術語叫作「傳遞消息」,消息有名稱(或叫選擇子),能夠接受參數,或許還有返回值。
  • 在 OC 中,對象收到消息以後,究竟該調用哪一個方法徹底於運行期決定,甚至能夠在運行時改變,這些特性使 OC 成爲一門真正的動態語言。
    給對象發送消息能夠這樣寫:緩存

    id value = [obj messageName:parameter]安全

obj 叫作接收者,messageName 叫作 selector,selector 和參數合起來稱爲消息
編譯器看到此消息後,將其轉換爲一條標準的 C 語言函數調用網絡

void objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)session

第一個參數表明接收者,第二個表明 selector(SEL是selector類型)
這是個「參數個數可變的函數」,」…「 表明後續參數,就是消息中的參數數據結構

  • objc_msgSend 方法在接收者所屬的類中搜尋其」方法列表「,若是能找到與 selector 名稱相符的方法,就跳至其實現代碼。如果找不到,就沿着繼承體系繼續向上查找,等找到合適的方法以後再跳轉。若是最終仍是找不到相符的方法,就執行」消息轉發「(在以後解釋)。
  • 看起來,想調用一個方法彷佛須要不少步驟。所幸 objc_msgSend 會將匹配結果緩存在」快速映射表「中,每一個類都有這樣一個緩存,如果後來還向該類發送相同的消息,那麼執行起來就會很快了。
  • 其餘消息發送函數
//Sends a message with a simple return value to the superclass of an instance of a class.
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
//Sends a message with a data-structure return value to an instance of a class.
objc_msgSend_stret(id _Nullable self, SEL _Nonnull op, ...)
//Sends a message with a data-structure return value to the superclass of an instance of a class.
objc_msgSendSuper_stret(struct objc_super * _Nonnull super,  SEL _Nonnull op, ...)
  • 剛纔提到 objc_msgSend 找到合適的方法以後,就會」跳轉過去「。之因此能夠這樣作是由於 OC 對象的每一個方法均可以視爲簡單的 C 函數,原型以下:
<return_type> Class_selector(id self, SEL _cmd, ...)

每一個類都有一張表格,selector 的名稱就是查表時所用的 key多線程

  • 原型的樣子和 objc_msgSend 很像,並且函數的最後一項操做是調用另外一個函數並且不會將其返回值另做他用,就能夠利用」尾調用優化「技術,令」跳至方法實現「變得簡單。

    尾調用技術:編譯器會生成跳轉至另外一函數所需的指令碼,並且不會向調用堆棧中推入新的」棧幀「

  • 要點:發給某對象的所有消息都要由「動態消息派發系統」來處理,該系統會查出對應的方法,並執行其代碼

5.消息轉發機制
  • 對象在收到沒法解讀的消息以後會發生什麼狀況?若是在控制檯中看到上面這種提示信息,那就說明你給對象發送了一條其沒法解讀的消息,啓動了消息轉發機制
- 由於在運行期能夠繼續向類中添加方法,因此編譯器在編譯期還沒法確知類中是否有某個方法的具體實現。
- 當對象接收到沒法解讀的消息,就會觸發「消息轉發機制」,程序員能夠經由此過程告訴對象如何處理未知消息

消息轉發分爲兩大階段

  • 第一階段:徵詢接收者,可否動態添加方法,處理當前這個「未知的 selector」
//當未知的 selector 是實例方法時的調用
+ (BOOL)resolveInstanceMethod:(SEL)sel ;
//當未知的 selector 是類方法時的調用
+ (BOOL)resolveClassMethod:(SEL)sel;

使用這種辦法的前提是:相關方法的實現代碼已經寫好,只等着運行時加入類裏面

  • 第二階段:運行時系統會請求接收者以其餘手段處理與消息相關的方法調用,可細分爲兩小步。
//第一步:詢問能不能把未知的消息轉給其餘接收者處理
- (id)forwardingTargetForSelector:(SEL)aSelector ;

若當前接收者能找到備援接收者,則將其返回,若找不到,則返回 nil
-若是返回一個對象,則運行期系統把消息轉給那個對象,因而消息轉發結束
若是返回 nil,執行第二步👇

//第二步:把消息相關的細節封裝在 NSInvocation對象中,再給接收者最後一次機會
- (void)forwardInvocation:(NSInvocation *)anInvocation

能夠在觸發消息前,先以某種方式改變消息內容,好比追加一個參數,或改換 selector,等等
若是實現此方法時,發現某調用操做不該由本類處理,則調用超類的調用方法。若是繼承體系的類都不處理此調用請求,那就最後調用 NSObject 類的方法,那麼該方法會執行👇方法

//拋出異常,代表 selector 未能處理
- (void)doesNotRecognizeSelector:(SEL)aSelector;

圖片來自:《Effective Objective-C 》

  • 接收者在每一步均有機會處理消息。步驟越日後,處理消息的代價就越大。最好能在第一步處理完,這樣的話運行期系統就能夠把消息緩存起來了。若是這個類的實例稍後還收到同名 selector,那麼根本無需啓動消息轉發流程。
  • 接下來給一個添加方法的具體實現
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    NSString *selString = NSStringFromSelector(sel);
    if ([selString hasPrefix:@"set"]) {
        //最後一個參數表示待添加方法的類型編碼(type encoding)
        class_addMethod([self class], sel, (IMP)autoDictionarySetter, "v@:@");
    }else {
        class_addMethod([self class], sel, (IMP)autoDictionaryGetter, "@@:");
    }
    return [super resolveInstanceMethod:sel];
}


SEL 是方法編號,SEL類型經過 @selector() 提取
IMP 是一個函數指針,保存了方法的地址
type encoding:
v 表明 void
: 表明 (method selector)SEL
@ 表明 object(whether statically typed or typed id);

在 setter 實現的時候給一個字典添加鍵值對,getter 從字典獲取值,這樣的實現就在 iOS 的 CoreAnimation 框架中的 CALayer 類裏面。CALayer 是一種「兼容於鍵值編碼的」容器類,能向其中隨意添加屬性,而後以鍵值對的方式訪問。

6.類對象的用意
  • 首先開看看 OC 對象的聲明
NSString *pointerVariable = @"Some string";

pointerVariable 是存放內存地址的變量,而 NSString 自身的數據就存於那個地址中

//對於通用的對象類型 id ,其自己已是指針了,能夠這樣寫
    id genericTypeString = "id String"

上面兩種不一樣的定義方式的區別在於,若是聲明時指定了類型,在實例上調用沒有的方法時,編譯器會發出警告。而 id 類型,編譯器默認它能響應全部本項目中存在的方法。

  • 描述 OC 對象所用的數據結構定義在運行期程序庫的頭文件裏 可見對象是一個含有 isa 指針的結構體,isa 指針表示這個對象是一個(is a)什麼類。
  • 那麼 isa 指向的 Class 是什麼?原來 Class 是指向 objc_class 的指針。
  • objc/runtime.h 中 對 objc_class 的定義以下結構體中包含的具體內容:
    1.能夠看到,結構體內也有一個 isa 指針,也就是說「類也是有類的」。由此說明,類也是一個對象。
    2.下面的內容就是這個類方法列表(method_list)(實際是對象方法)、成員變量列表(ivar_list)、cache 列表、協議列表,還有 super_class 變量,等等

自此,咱們知道的了對象就是一個含有指向 Class 類的 isa 指針的結構體,Class 也是一個對象,它其中也有一個 isa 指針。那麼類對象的 isa 指針指向哪裏,類對象的父類又是什麼呢?看下圖

這樣 OC 中類和對象的關係就清楚了。對象的 isa 指針指向類對象,類對象的 isa 指針指向 metaClass,譯爲元類。元類的 isa 指針指向根元類
由此能夠看出來 Apple 設計類對象的用意就是爲了存儲對象的信息,好比對象方法,對象屬性,遵照的協議,對象的類的父類,等等,而類對象的相關信息被存儲在元類中。

7.理解 Objective-C 錯誤類型
  • 當前不少編程語言都有「異常」機制,OC 也不例外
    首先要注意的是,「自動引用計數」(Automatic Reference Counting, ARC)在默認狀況下不是「異常安全的「。這意味着:若是拋出異常,那麼本應在做用域末尾釋放的對象如今卻不會自動釋放了。
    OC 如今採用的辦法是:只在極其罕見的狀況下拋出異常,異常拋出以後無需考慮恢復問題,並且應用程序此時也應該退出。
  • 異常只應該用於極其嚴重的錯誤。好比說,你編寫了一個抽象基類,它的正確用法是先從中繼承一個子類,而後使用這個子類。在這種狀況下,能夠在那些子類必須複寫的超類方法裏拋出異常。這樣的話,只要有人建立抽象基類並使用它,就會拋出異常。
    下面圖片是在 Masonry 中的異常的使用
  • 異常只用於出現嚴重錯誤,當出現「不那麼嚴重的錯誤」,OC 使用的編程範式是,令方法返回 nil/0,或是使用 NSError,代表其中有錯誤產生。
    好比初始化方法沒法根據傳入的參數來初始化當前實例,就返回nil/0;
- initWithString:(NSString *)string {
    
    if (self = [super init]) {

        if (string == nil) {
            self = nil;
        }else {
            //Initialize instance
        }
    }
    return  self;
}

NSError 的用法更加靈活,由於經過此對象,咱們能夠獲知錯誤的具體信息。NSError 對象裏封裝了三條消息:

  • Error domain(錯誤範圍,類型爲字符串)
    錯誤發生的範圍。也就是錯誤發生的根源。好比在網絡請求時獲取數據失敗或解析數據發生錯誤,就使用 NSURLEroorDomain 來表示錯誤範圍fmAFNetworkingClient.m
  • Error code(錯誤碼,其類型爲整數)
    獨有的錯誤代碼,用來指明在某個範圍內具體發生了何種錯誤。某個特定範圍內可能會發生一系列相關錯誤,這些錯誤狀況一般採用 enum 來定義。例如:當 http 請求出錯時,把 HTTP 狀態碼設爲錯誤碼.NSURLErrorDomain

  • User info(用戶信息,類型爲字典)
    有關此錯誤的額外信息,其中或許包含一段本地化的描述(localized description),或許還含有致使該錯誤發生的另外一個錯誤。好比 SDWebImage 中
@{NSLocalizedDescriptionKey : @"Image data is nil"}
  • 在設計 API 時,NSError 第一種使用方式是經過協議傳遞此錯誤。例如 NSURLSessionTaskDelegate中定義的方法:
/* Sent as the last message related to a specific task.  Error may be
 * nil, which implies that no error occurred and this task is complete. 
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                           didCompleteWithError:(nullable NSError *)error;

這個委託方法未必非得實現不可,是否是必須處理此錯誤,可交由用戶來處理。
NSError 的另外一種常見用法:經由方法的「輸出參數」返回給調用者。好比在 MJExtension 中:

/**
 *  經過字典來建立一個模型
 *  @param keyValues 字典
 *  @return 新建的對象
 */
+ (instancetype)objectWithKeyValues:(id)keyValues;
+ (instancetype)objectWithKeyValues:(id)keyValues error:(NSError **)error;

入參是一個指針,指向另外一個指針,那個指針指向 NSError 對象。使用以下方式獲取錯誤信息。

NSError *error = nil;
 MyModel *model = [MyModel objectWithKeyValues:responseObject error:&error];
        
if (error) {
  //獲取 error 信息
}

實際上,在使用 ARC 時,編譯器會把方法簽名中的 NSError ** 轉換成 NSError *__autoreleasing *,也就是說,指針所指的對象會在方法執行完畢後自動釋放。
MJ 經過如下代碼把 NSError 對象傳遞到「輸出參數」中:

// 構建錯誤
#define MJBuildError(error, msg) \
if (error) *error = [NSError errorWithDomain:msg code:250 userInfo:nil];

-這段代碼以 *error 語法爲 error 參數」解引用「,也就是說,error 所指的那個指針如今要指向一個新的 NSError 對象了。在解引用前,必須確保 error 不是 nil,由於空指針解引用會致使「段錯誤」並使程序奔潰。

  • 爲本身的程序庫中所發生的錯誤指定一個專用的」錯誤範圍「 字符串,使用此字符串建立 NSError 對象,返回給庫的使用者,這樣的話,使用者能夠肯定:該錯誤是由你的程序庫所回報的。
8.理解引用計數
  • Objective-C 語言使用引用計數來管理內存,也就是說,每一個對象都有一個能夠遞增遞減的計數器,若是想要保留對象,就遞增,用完了以後就遞減,當計數爲 0 時,對象就銷燬。要寫出優秀的 OC 代碼,除了知道這些,還必須知道其中的原理。

    引用計數工做原理
  • 在引用計數架構下,每一個對象都有一個計數器,用來表示當前一共有多少調用者想讓此對象存活。NSObject 協議中有如下三個方法操做計數器
- (void)retain;//使遞增
- (void)release;//使遞減
- (void)autorelease;//稍後清理「自動釋放池」時再進行遞減

查看引用計數的方法是 retainCount,不過這個方法不是很準確,蘋果官方也不推薦
應用程序在生命期內會建立不少對象,這些對象相互聯繫着。例如表示我的信息的對象會引用表示名字的字符串對象。對象若是持有其餘對象的強引用,那麼前者就擁有後者。也就是說,對象想讓它所引用的對象繼續存活,可將其「保留」。等用完了以後再釋放。
下圖表示了一個對象從保留到釋放的過程圖片來自Effective Objective-C

按圖能夠想象,有一些其餘對象想要保持 B 或 C 對象存活,而引用程序中又會有另一些對象想讓這些對象存活。若是按「引用樹」回溯,那麼最終會發現有一個「根對象」。在 iOS 中,就是 UIApplication 對象。是在應用程序啓動時建立的對象。

當對象的引用計數爲 0 ,對象所佔內存」解除分配「以後,就被放回」可用內存池「。此時再去調用該對象,可能會有不一樣狀況發生:若是此時內存對象已經作了他用,就會引發程序奔潰;若是此時對象內存未被複寫,就可能正常運行。因而可知,由過早釋放對象而致使的 bug 很難調試。

  • 爲避免無心間使用無效對象,通常調用完 release 以後都會清空指針。
屬性存取方法中的內存管理
  • 當對象要保留其餘對象的時候,通常經過訪問屬性來實現。好比一個屬性名爲 foo,其屬性內存管理語義爲」strog「,那麼他的設置方法爲:
- (void)setFoo:(id)foo {
    [foo retain];
    [_foo release];
    _foo = foo;
}

此方法保留新值,釋放舊值,而後更新變量指向新值。順序很重要。若是先釋放舊值,那此對象就被回收。後續操做就都沒有意義了。

自動釋放池
  • 調用 release 會當即遞減對象的引用計數,若是想要延遲釋放,那麼能夠調用 autorelease,加入自動釋放池,它會在以後清空的時候給其中的全部對象發送一條 release 消息。
保留環
  • 使用引用計數,常常要注意的問題就是「保留環」,也就是呈環狀相互引用的多個對象。
  • 可使用弱引用,或者經過外界命令循環中的一個對象再也不保留另外一個對象,打破保留環,避免內存泄漏。
9.以 ARC 簡化引用計數
此處沒有完全看懂,暫時先放一放
10.理解「塊」這一律念

塊是一種可在 C 、C++、OC 代碼中使用的「詞法閉包」,它很是有用,能夠把一段代碼像對象同樣傳遞。在定義「塊」的範圍內,它能夠訪問到其中全部的變量。

  • 說「塊」必須離不開說多線程。蘋果公司設計的多線程編程的核心就是「塊」(block)和「大中樞派發」(GCD),這雖然是兩種不一樣的技術,但他們是一併引入的。

  • GCD 提供了對線程的抽象,而這種抽象基於「派發隊列」。開發者可將塊排入隊列中,由 GCD 處理全部調度事宜。

塊的基礎知識
  • 塊能夠實現閉包。這種語言特性是作爲擴展加入 GCC 編譯器中的,在近期版本中的 Clang 均可以使用。
    塊其實就是個值,並且自有其相關類型。能夠把塊賦給變量,語法與函數指針相似。塊用符號 「^" 來表示
int adder = 8;
//定義一個變量名爲 myBlock 的塊
//語法結構:return_type (^blockName) (parameters)
int (^addBlock)(int num) = ^(int num){
        //在塊內部可使用外部變量
        return num+ adder;
 };

int addEight = addBlock(5);
  • 默認狀況下,在塊內部不能夠更改變量的值。想要改變值,須要在聲明變量時使用 __block 修飾。
    這裏有個疑問,塊裏面的實例變量在沒有 __block 修飾的狀況下卻也是能夠改變值的,爲何?
  • 若是塊所捕獲的變量是對象,那麼就會自動保留它。塊也是對象,當塊的引用計數爲 0 ,系統釋放塊的時候,也會釋放它所捕獲的對象,以便平衡捕獲時的保留操做。
塊的內部結構

圖片來自Effective Objective-C塊自己是對象,因此他也有內存空間。

  • 首個變量是指向 Class 對象的指針,該指針叫作 isa。
  • 最重要的是 invoke 變量,是個函數指針,指向塊的實現代碼。
  • descriptor 變量是指向結構體的指針,其中聲明瞭塊對象的整體大小,還聲明瞭 copy 和 dispose 這兩個輔助函數對應的函數指針。
  • 塊還會把它捕獲到的變量都拷貝一份,放在 descriptor 後面。拷貝的是指向變量的指針。捕獲了多少個變量,就要佔據多少個內存空間。
全局塊 棧塊 堆塊
  • 定義塊的時候,其所佔的內存區域是在棧中的。也就是說,塊只在定義它的那個範圍內有效。好比下面代碼,就是有危險的:
void (^myBlock)();
    if (/* some condition*/) {
        myBlock = ^{};
    }else {
        myBlock = ^{};
    }
    myBlock();

定義在 if 和 else 裏面的兩個塊都分配在棧內存中。編譯器會給每一個塊分配好棧內存,可是等離開了相應的範圍以後,編譯器就可能把分配給塊的內存覆寫掉。這樣寫出來的代碼時而正確時而錯誤。

爲解決此問題,可給塊對象發送 copy 消息以拷貝之。這樣就把塊對象從棧內存移到了堆內存中,拷貝後的塊,就能夠在定義它的範圍以外使用了。並且,一旦複製到堆上,塊對象就成了具備引用計數的對象了。後續的複製操做就只是遞增引用計數了。

明白了這一點,咱們只要給代碼加上兩個 copy 調用就安全了。

void (^myBlock)();
    if (/* some condition*/) {
        myBlock = [^{} copy];
    }else {
        myBlock = [^{} copy];
    }
    myBlock();
  • 還有一種塊叫作」全局塊」,像這樣的
void (^myBlock)() = ^{
        NSLog(@"This is a block");
    };

因爲運行該塊的全部信息都能在編譯的時候肯定,因此可把他作成全局塊。
這種塊不會捕捉任何狀態(好比外圍的變量等),能夠聲明在全局內存中,不須要在每次用到的時候於棧中建立。這種塊實際上至關於單例。

10.熟悉系統框架
相關文章
相關標籤/搜索