《Effective Objective-C》乾貨三部曲(一):概念篇

J_Knight_發表於2018-01-08

這本書講授了很多編寫Objective-C語言時所應該遵循的規範。剛好筆者前段時間因為產品剛開發完,有了一點空檔期,於是用了3個星期的時間仔細研讀和總結了這本書。

在學習過程中也看過很多總結這本書的部落格和文章,但是發現多數只是將每節的總結部分摘錄了過來,因此講得並不是很詳細。於是筆者就想按照自己的方式對這本書進行總結,並以部落格的形式展現出來:既能分享,同時又能對知識進行一下梳理和二次複習。

雖然本書的作者按照知識模組來將這本書分成七個章節,共52節,但是筆者在拜讀的過程中發現本書介紹的知識點可以大致分為三類:概念類,規範類,和技巧類。筆者打算按照這三類來對這本書進行總結,形成三部曲:

  • 概念類:講解了一些概念性知識。
  • 規範類:講解了一些為了避免一些問題或者為後續開發提供便利所需要遵循的規範性知識。
  • 技巧類:講解了一些為了解決某些特定問題而需要用到的技巧性知識。

而且,筆者也按照自己的歸類將這本書的結構用思維導圖工具畫了出來:

三部曲分佈圖

從圖中可以看到,筆者並沒有打亂原來作者的標題順序。本篇總結即是三部曲之一:概念篇,後續會呈上規範篇和技巧篇。 備註:本總結所有的程式碼和圖片都來自原書。其中,程式碼會適當加上筆者的註釋,便於各位看官理解。

好了,不囉嗦了, 開始吧!

第1條:瞭解Objective-C的起源

執行期元件

對於訊息結構的語言,執行時所執行的程式碼由執行環境來決定;在執行時才回去查詢索要執行的方法。其實現原理是由執行期元件完成(runtime component),使用Objective-C的物件導向特性所需的全部資料結構以及函式都在執行期元件裡面。

執行期元件本質上是一種與開發者所編寫的程式碼相連結的動態庫(dynamic library),其程式碼能把開發者所編寫的所有程式粘合起來,所以只要更新執行期元件,就可以提升應用程式效能。

記憶體:物件分配到堆空間,指標分配到棧空間。 分配在隊中的記憶體必須直接管理,而分配在棧上用於儲存變數的記憶體則會在其棧幀彈出時自動清理。

不含*的變數,可能會使用棧空間。結構體儲存非物件型別。

第6條:理解“屬性”這一概念

屬性用於封裝物件中的資料。

1. 存取方法

在設定完屬性後,編譯器會自動寫出一套存取方法,用於訪問相應名稱的變數:

@interface EOCPerson : NSObject

@property NSString *firstName;
@property NSString *lastName;
@end


@interface EOCPerson : NSObject

- (NSString*)firstName;
- (void)setFirstName:(NSString*)firstName;
- (NSString*)lastName;
- (void)setLastName:(NSString*)lastName;

@end
複製程式碼

訪問屬性,可以使用點語法。編譯器會把點語法轉換為對存取方法的呼叫:

aPerson.firstName = @"Bob"; // Same as:
[aPerson setFirstName:@"Bob"];


NSString *lastName = aPerson.lastName; // Same as:
NSString *lastName = [aPerson lastName];
複製程式碼

如果我們不希望編譯器自動生成存取方法的話,需要設定@dynamic 欄位:

@interface EOCPerson : NSManagedObject

@property NSString *firstName;
@property NSString *lastName;

@end


@implementation EOCPerson
@dynamic firstName, lastName;
@end
複製程式碼

2. 屬性關鍵字

定義屬性的時候,通常會賦予它一些特性,來滿足一些對類儲存資料所要遵循的需求。

原子性:

  • nonatomic:不使用同步鎖
  • atomic:加同步鎖,確保其原子性

讀寫

  • readwrite:同時存在存取方法
  • readonly:只有獲取方法

記憶體管理

  • assign:純量型別(scalar type)的簡單賦值操作
  • strong:擁有關係保留新值,釋放舊值,再設定新值
  • weak:非擁有關係(nonowning relationship),屬性所指的物件遭到摧毀時,屬性也會清空
  • unsafe_unretained :類似assign,適用於物件型別,非擁有關係,屬性所指的物件遭到摧毀時,屬性不會清空。
  • copy:不保留新值,而是將其拷貝

注意:遵循屬性定義

如果屬性定義為copy,那麼在非設定方法裡設定屬性的時候,也要遵循copy的語義

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName
{
         if ((self = [super init])) {
            _firstName = [firstName copy];
            _lastName = [lastName copy];
        }
       return self;
}

複製程式碼

第8條:理解“物件等同性”這一概念

1. 同等性判斷

==操作符比較的是指標值,也就是記憶體地址。

然而有的時候我們只是想比較指標所指向的內容,在這個時候,就需要通過isEqual:方法來比較。

而且,如果已知兩個物件是字串,最好通過isEqualToString:方法來比較。 對於陣列和字典,也有isEqualToArray:方法和isEqualToDictionary:方法。

另外,如果比較的物件型別和當前物件型別相同,就可以採用自己編寫的判定方法,否則呼叫父類的isEqual:方法:

- (BOOL)isEqualToPerson:(EOCPerson*)otherPerson {

     //先比較物件型別,然後比較每個屬性
     if (self == object) return YES;
     if (![_firstName isEqualToString:otherPerson.firstName])
         return NO;
     if (![_lastName isEqualToString:otherPerson.lastName])
         return NO;
     if (_age != otherPerson.age)
         return NO;
     return YES;
}


- (BOOL)isEqual:(id)object {
    //如果物件所屬型別相同,就呼叫自己編寫的判定方法,如果不同,呼叫父類的isEqual:方法
     if ([self class] == [object class]) {    
         return [self isEqualToPerson:(EOCPerson*)object];
    } else {    
         return [super isEqual:object];
    }
}

複製程式碼

2. 深度等同性判定

比較兩個陣列是否相等的話可以使用深度同等性判斷方法:

1.先比較陣列的個數 2.再比較兩個陣列對應位置上的物件均相等。

第11條:理解objc_msgSend的作用

在OC中,如果向某物件傳遞資訊,那就會使用動態繫結機制來決定需要呼叫的方法。在底層,所有方法都是普通的C語言函式.

然而物件收到 訊息後,究竟該呼叫哪個方法則完全於執行期決定,甚至可以在程式執行時改變,這些特性使得OC成為一門真正的動態語言。

在OC中,給物件傳送訊息的語法是:

id returnValue = [someObject messageName:parameter];
複製程式碼

這裡,someObject叫做“接收者(receiver)”,messageName:叫做"選擇子(selector)",選擇子和引數合起來稱為“訊息”。編譯器看到此訊息後,將其轉換為一條標準的C語言函式呼叫,所呼叫的函式乃是訊息傳遞機制中的核心函式叫做objc_msgSend,它的原型如下:

void objc_msgSend(id self, SEL cmd, ...)
複製程式碼

第一個引數代表接收者,第二個引數代表選擇子,後續引數就是訊息中的那些引數,數量是可變的,所以這個函式就是引數個數可變的函式。

因此,上述以OC形式展現出來的函式就會轉化成如下函式:

id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
複製程式碼

這個函式會在接收者所屬的類中搜尋其“方法列表”,如果能找到與選擇子名稱相符的方法,就去實現程式碼,如果找不到就沿著繼承體系繼續向上查詢。如果找到了就執行,如果最終還是找不到,就執行訊息轉發操作。

注意:如果匹配成功的話,這種匹配的結果會快取在“快速對映表”裡面。每個類都有這樣一塊快取。所以如果將來再次向該類傳送形同的訊息,執行速度就會更快了。

第12條:理解訊息轉發機制

如果物件所屬類和其所有的父類都無法解讀收到的訊息,就會啟動訊息轉發機制(message forwarding)。

尤其我們在編寫自己的類時,可在訊息轉發過程中設定掛鉤,用以執行預定的邏輯,而不應該使應用程式崩潰。

訊息轉發分為兩個階段:

  1. 徵詢接受者,看它能否動態新增方法,以處理這個未知的選擇子,這個過程叫做動態方法解析(dynamic method resolution)。

  2. 請接受者看看有沒有其他物件能處理這條訊息:

    2.1 如果有,則執行期系統會把訊息轉給那個物件。 2.2 如果沒有,則啟動完整的訊息轉發機制(full forwarding mechanism),執行期系統會把與訊息有關的全部細節都封裝到NSInvocation物件中,再給接受者最後一次機會,令其設法解決當前還未處理的這條訊息。

圖片來自:《Effective Objective-C 》

類方法+(BOOL)resolveInstanceMethod:(SEL)selector:檢視這個類是否能新增一個例項方法用以處理此選擇子

例項方法- (id)forwardTargetForSelector:(SEL)selector;:詢問是否能找到未知訊息的備援接受者,如果能找到備援物件,就將其返回,如果不能,就返回nil。

例項方法- (void)forwardInvocation:(NSInvocation*)invocation:建立NSInvocation物件,將尚未處理的那條訊息 有關的全部細節都封於其中,在觸發NSInvocation物件時,“訊息派發系統(message-dispatch system)”就會將訊息派給目標物件。

下面來看一個關於動態方法解析的例子:

#import <Foundation/Foundation.h>

@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;

@end



#import "EOCAutoDictionary.h"
#import <objc/runtime.h>


@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end



@implementation EOCAutoDictionary

@dynamic string, number, date, opaqueObject;



- (id)init {
 if ((self = [super init])) {
    _backingStore = [NSMutableDictionary new];
}

   return self;

}



+ (BOOL)resolveInstanceMethod:(SEL)selector {

     NSString *selectorString = NSStringFromSelector(selector);
     if ([selectorString hasPrefix:@"set"]) {
         class_addMethod(self,selector,(IMP)autoDictionarySetter, "v@:@");
     } else {
         class_addMethod(self,selector,(IMP)autoDictionaryGetter, "@@:");
    }
     return YES;
}

複製程式碼

在本例中,EOCAutoDictionary類將屬性設定為@dynamic,也就是說編譯器無法自動為其屬性生成set和get方法,因此我們需要動態給其新增set和get方法。

我們實現了resolveInstanceMethod:方法:首先將選擇子轉換為String,然後判斷字串是否含有set欄位,如果有,則增加處理選擇子的set方法;如果沒有,則增加處理選擇子的get方法。其中class_addMethod可以給類動態新增方法。

實現增加處理選擇子的get方法:

id autoDictionaryGetter(id self, SEL _cmd) {

     // Get the backing store from the object
     EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
     NSMutableDictionary *backingStore = typedSelf.backingStore;

     // The key is simply the selector name
     NSString *key = NSStringFromSelector(_cmd);

     // Return the value
     return [backingStore objectForKey:key];
}

複製程式碼

在這裡,鍵的名字就等於方法名,所以在取出鍵對應的值之前,要將方法名轉換為字串。

實現增加處理選擇子的set方法:


void autoDictionarySetter(id self, SEL _cmd, id value) {

     // Get the backing store from the object
     EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
     NSMutableDictionary *backingStore = typedSelf.backingStore;

     /** The selector will be for example, "setOpaqueObject:".
     * We need to remove the "set", ":" and lowercase the first
     * letter of the remainder.
     */
     NSString *selectorString = NSStringFromSelector(_cmd);
     NSMutableString *key = [selectorString mutableCopy];

     // Remove the ':' at the end
    [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];

     // Remove the 'set' prefix
    [key deleteCharactersInRange:NSMakeRange(0, 3)];

     // Lowercase the first character
     NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];

     if (value) {
       [backingStore setObject:value forKey:key];
    } else {
        [backingStore removeObjectForKey:key];        
    }
}

複製程式碼

因為key的名字對應了屬性名,也就是沒有set,首字母小寫,尾部沒有:的字串。然而,將set方法轉換為字串後,我們需要將set方法的這些“邊角”都處理掉。最後得到了“純淨”的鍵後,再進行字典的賦值操作。

第14條:理解“類物件”的用意

在執行期程式庫的標頭檔案裡定義了描述OC物件所用的資料結構:

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指標指向了物件所屬的類:元類(metaclass),它是整個結構體的第一個變數。super_class定義了本類的超類。

我們也可以向物件傳送特定的方法來檢視類的繼承體系:自身屬於哪一類;自身繼承與哪一類。

我們使用isMemberOfClass:能夠判斷出物件是否為某個特定類的例項; 而isKindOfClass:方法能夠判斷出物件是否為某類或其派生類的例項。

這兩種方法都是利用了isa指標獲取物件所屬的類,然後通過super_class類在繼承體系中查詢。在OC語言中,必須使用這種查詢型別資訊的方法才能完全瞭解物件的真實型別。因為物件型別無法在編譯期決定。

尤其注意在集合類裡獲取物件時,通常要查詢型別資訊因為這些物件不是強型別的(strongly typed),將它們從集合類中取出來的型別通常是id,也就是能響應任何訊息(編譯期)。

所以如果我們對這些物件的型別把握不好,那麼就會有可能造成物件無法響應訊息的情況。因此,在我們從集合裡取出物件後,通常要進行型別判斷:


- (NSString*)commaSeparatedStringFromObjects:(NSArray*)array {

         NSMutableString *string = [NSMutableString new];

             for (id object in array) {
                    if ([object isKindOfClass:[NSString class]]) {
                            [string appendFormat:@"%@,", object];
                    } else if ([object isKindOfClass:[NSNumber class]]) {
                            [string appendFormat:@"%d,", [object intValue]];
                    } else if ([object isKindOfClass:[NSData class]]) {
                           NSString *base64Encoded = /* base64 encoded data */;
                            [string appendFormat:@"%@,", base64Encoded];
                    } else {
                            // Type not supported
                    }
              }
             return string;
}
複製程式碼

第21條:理解Objective-C錯誤型別

在OC中,我們可以用NSError描述錯誤。 使用NSError可以封裝三種資訊:

  • Error domain:錯誤範圍,型別是字串
  • Error code :錯誤碼,型別是整數
  • User info:使用者資訊,型別是字典

1. NSError的使用

用法:

1.通過委託協議來傳遞NSError,告訴代理錯誤型別。

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
複製程式碼

2.作為方法的“輸出引數”返回給呼叫者

- (BOOL)doSomething:(NSError**)error
複製程式碼

使用範例:


NSError *error = nil;
BOOL ret = [object doSomething:&error];

if (error) {
    // There was an error
}

複製程式碼

2. 自定義NSError

我們可以設定屬於我們自己程式的錯誤範圍和錯誤碼

  • 錯誤範圍可以用全域性常量字串來定義。
  • 錯誤碼可以用列舉來定義。

// EOCErrors.h
extern NSString *const EOCErrorDomain;

//定義錯誤碼
typedef NS_ENUM(NSUInteger, EOCError) {

    EOCErrorUnknown = –1,
    EOCErrorInternalInconsistency = 100,
    EOCErrorGeneralFault = 105,
    EOCErrorBadInput = 500,
};



// EOCErrors.m
NSString *const EOCErrorDomain = @"EOCErrorDomain"; //定義錯誤範圍

複製程式碼

第22條:理解NSCopying協議

如果我們想令自己的類支援拷貝操作,那就要實現NSCopying協議,該協議只有一個方法:

- (id)copyWithZone:(NSZone*)zone
複製程式碼

作者舉了個:


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

     EOCPerson *copy = [[[self class] allocWithZone:zone] initWithFirstName:_firstName  andLastName:_lastName];
    copy->_friends = [_friends mutableCopy];
     return copy;
}

複製程式碼

之所以是copy->_friends,而不是copy.friends是因為friends並不是屬性,而是一個內部使用的例項變數。

1. 複製可變的版本:

遵從協議

而且要執行:

- (id)mutableCopyWithZone:(NSZone*)zone;
複製程式碼

注意:拷貝可變型和不可變型傳送的是copymutableCopy訊息,而我們實現的卻是- (id)copyWithZone:(NSZone*)zone- (id)mutableCopyWithZone:(NSZone*)zone 方法。

而且,如果我們想獲得某物件的不可變型,統一呼叫copy方法;獲得某物件的可變型,統一呼叫mutableCopy方法。

例如陣列的拷貝:

-[NSMutableArray copy] => NSArray
-[NSArray mutableCopy] => NSMutableArray
複製程式碼

2. 淺拷貝和深拷貝

Foundation框架中的集合類預設都執行淺拷貝:只拷貝容器物件本身,而不復制其中的資料。 而深拷貝的意思是連同物件本身和它的底層資料都要拷貝。

作者用一個圖很形象地體現了淺拷貝和深拷貝的區別:

圖片來自:《Effective Objective-C》

淺拷貝後的內容和原始內容指向同一個物件 深拷貝後的內容所指的物件是原始內容對應物件的拷貝

3. 如何深拷貝?

我們需要自己編寫深拷貝的方法:遍歷每個元素並複製,然後將複製後的所有元素重新組成一個新的集合。

- (id)initWithSet:(NSArray*)array copyItems:(BOOL)copyItems;

複製程式碼

在這裡,我們自己提供了一個深拷貝的方法:該方法需要傳入兩個引數:需要拷貝的陣列和是否拷貝元素(是否深拷貝)


- (id)deepCopy {
       EOCPerson *copy = [[[self class] alloc] initWithFirstName:_firstName andLastName:_lastName];
        copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES];
        return copy;
}

複製程式碼

第29條:理解引用計數

儘管在iOS系統已經支援了自動引用計數,但仍然需要開發者瞭解其記憶體管理機制。

1. 計數器的操作:

  1. retain:遞增保留計數。
  2. release:遞減保留計數
  3. autorelease :待稍後清理“自動釋放池時”,再遞減保留計數。

注意:在物件初始化後,引用計數不一定是1,還有可能大於1。因為在初始化方法的實現中,或許還有其他的操作使得引用計數+1,例如其他的物件也保留了此物件。

有時,我們無法確定在某個操作後引用計數的確切值,而只能判斷這個操作是遞增還是遞減了保留計數。

2. 自動釋放池:

將物件放入自動釋放池之後,不會馬上使其引用計數-1,而是在當前執行緒的下一次事件迴圈時遞減。

使用舉例:如果我們想釋放當前需要使用的方法返回值是,可以將其暫時放在自動釋放池中:


- (NSString*)stringValue {
     NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
     return [str autorelease];
}

複製程式碼

3. 保留迴圈(retain cycle)

物件之間相互用強引用指向對方,會使得全部都無法得以釋放。解決方案是講其中一端的引用改為弱引用(weak reference),在引用的同時不遞增引用計數。

第30條:以ARC簡化引用計數


使用ARC,可以省略對於引用計數的操作,讓開發者專注於開發本身:

if ([self shouldLogMessage]) {
     NSString *message = [[NSString alloc] initWithFormat:@"I am object, %p", self];
     NSLog(@"message = %@", message);
      [message release]; ///< Added by ARC
}
複製程式碼

顯然這裡我們不需要message物件了,那麼ARC會自動為我們新增記憶體管理的語句。

因此,在ARC環境下呼叫記憶體管理語句是非法的:

  • retain
  • release
  • autorelease
  • dealloc

注意:ARC只負責管理OC物件的記憶體,CoreFoundation物件不歸ARC管理

第37條:理解block這一概念

對於“塊”的基礎知識就不再贅述了,這裡強調一下塊的種類。

塊(Block)分為三類:

  • 棧塊
  • 堆塊
  • 全域性塊

1. 棧block

定義塊的時候,其所佔記憶體區域是分配在棧中的,而且只在定義它的那個範圍內有效:

void (^block)();

if ( /* some condition */ ) {
    block = ^{
     NSLog(@"Block A");
    };

} else {
    block = ^{
     NSLog(@"Block B");
    };
}

block();
複製程式碼

上面定義的兩個塊只在if else語句範圍內有效,一旦離開了最後一個右括號,如果編譯器覆寫了分配給塊的記憶體,那麼就會造成程式崩潰。

2. 堆block

為了解決這個問題,我們可以給物件傳送copy訊息,複製一份到堆裡,並自帶引用計數:

void (^block)();

if ( /* some condition */ ) {
    block = [^{
         NSLog(@"Block A");
   } copy];
} else {
    block = [^{
         NSLog(@"Block B");
    } copy];
}

block();
複製程式碼

3. 全域性block

全域性塊宣告在全域性記憶體裡,而不需要在每次用到的時候於棧中建立。

void (^block)() = ^{
     NSLog(@"This is a block");
};

複製程式碼

第47條:熟悉系統框架

如果我們使用了系統提供的現成的框架,那麼使用者在升級系統後,就可以直接享受系統升級所帶來的改進。

主要的系統框架:

  • Foundation:NSObject,NSArray,NSDictionary等
  • CFoundation框架:C語言API,Foundation框架中的許多功能,都可以在這裡找到對應的C語言API
  • CFNetwork框架:C語言API,提供了C語言級別的網路通訊能力
  • CoreAudio:C語言API,操作裝置上的音訊硬體
  • AVFoundation框架:提供的OC物件可以回放並錄製音訊和視訊
  • CoreData框架:OC的API,將物件寫入資料庫
  • CoreText框架:C語言API,高效執行文字排版和渲染操作

用C語言來實現API的好處:可以繞過OC的執行期系統,從而提升執行速度。

最後的話

像本文開頭所說,本文是三部曲系列的第一篇:概念篇,筆者主要將本書講解概念的知識點抽取出來合併而成,內容相對後兩篇簡單一些。筆者會在一週的時間裡陸續推出第2篇(規範篇),第3篇(技巧篇)~ 望各路大神和在大神路上的夥伴們多多交流。

本文已同步到個人部落格:傳送門

另外兩篇傳送門:

《Effective Objective-C 》乾貨三部曲(二):規範篇

《Effective Objective-C 》乾貨三部曲(三):技巧篇

---------------------------- 2018年7月17日更新 ----------------------------

注意注意!!!

筆者在近期開通了個人公眾號,主要分享程式設計,讀書筆記,思考類的文章。

  • 程式設計類文章:包括筆者以前釋出的精選技術文章,以及後續釋出的技術文章(以原創為主),並且逐漸脫離 iOS 的內容,將側重點會轉移到提高程式設計能力的方向上。
  • 讀書筆記類文章:分享程式設計類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

因為公眾號每天釋出的訊息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,後續會逐步釋出的。

而且因為各大部落格平臺的各種限制,後面還會在公眾號上釋出一些短小精幹,以小見大的乾貨文章哦~

掃下方的公眾號二維碼並點選關注,期待與您的共同成長~

公眾號:程式設計師維他命

相關文章