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

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

在學習過程當中也看過不少總結這本書的博客和文章,可是發現多數只是將每節的總結部分摘錄了過來,所以講得並非很詳細。因而筆者就想按照本身的方式對這本書進行總結,並以博客的形式展示出來:既能分享,同時又能對知識進行一下梳理和二次複習。程序員

雖然本書的做者按照知識模塊來將這本書分紅七個章節,共52節,可是筆者在拜讀的過程當中發現本書介紹的知識點能夠大體分爲三類:概念類,規範類,和技巧類。筆者打算按照這三類來對這本書進行總結,造成三部曲:github

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

並且,筆者也按照本身的歸類將這本書的結構用思惟導圖工具畫了出來:數據庫

三部曲分佈圖

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

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

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

運行期組件

對於消息結構的語言,運行時所執行的代碼由運行環境來決定;在運行時纔回去查找索要執行的方法。其實現原理是由運行期組件完成(runtime component),使用Objective-C的面向對象特性所需的所有數據結構以及函數都在運行期組件裏面。緩存

運行期組件本質上是一種與開發者所編寫的代碼相連接的動態庫(dynamic library),其代碼能把開發者所編寫的全部程序粘合起來,因此只要更新運行期組件,就能夠提高應用程序性能。bash

內存:對象分配到堆空間,指針分配到棧空間。 分配在隊中的內存必須直接管理,而分配在棧上用於保存變量的內存則會在其棧幀彈出時自動清理。網絡

不含*的變量,可能會使用棧空間。結構體保存非對象類型。數據結構

第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 的內容,將側重點會轉移到提升編程能力的方向上。
  • 讀書筆記類文章:分享編程類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

由於公衆號天天發佈的消息數有限制,因此到目前爲止尚未將全部過去的精選文章都發布在公衆號上,後續會逐步發佈的。

並且由於各大博客平臺的各類限制,後面還會在公衆號上發佈一些短小精幹,以小見大的乾貨文章哦~

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

公衆號:程序員維他命
相關文章
相關標籤/搜索