Effective Objective-C 2.0筆記(一)

weixin_34353714發表於2017-04-10

Effective Objective-C讀書筆記,記錄書中的總結點,加入了一些例子,方便理解和後期回顧。

一、熟悉Objective-C

1、瞭解OC語言的起源

  • oc為c語言新增了物件導向特性,是其超集。oc使用動態繫結的訊息結構,也就是說,在執行時才會檢查物件型別。接收一條訊息之後,究竟應執行何種程式碼,由執行期環境而非編譯器來決定。
  • 理解c語言的核心概念有助於寫好oc程式。尤其要掌握記憶體模型與指標。
    C語言記憶體模型

2、在類的標頭檔案中儘量少引入其他標頭檔案

  • 除非卻有必要,否則不要引入標頭檔案。一般來說,應在某各類的標頭檔案中使用向前宣告來提及別的類,並在實現檔案中引入那些類的標頭檔案。這樣做可以儘量降低類之間的耦合(coupling),縮短編譯時間。
.h
#import <Foundation/Foundation.h>
//向前宣告
@class Car;
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, strong) Car *car;
@end
.m
#import "Person.h"
#import "Car.h"
@implementation Person
@end
  • 有時無法使用向前宣告,比如要宣告某個類遵循一項協議。這種情況下,儘量把“該類遵循某協議”的這條宣告移至“class-continuation分類”中。如果不行的話,就把協議單獨放在一個標頭檔案中,然後將其引入。

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

字面量字串
NSString *str = @"This is string";
字面量數值
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5;
NSNumber *doubleNumber = @3.14159;
NSNumber *bollNumber = @YES;
NSNumber *charNumber = @'a';
字面量陣列
NSArray *array = @[@"1`",@"2",@"3"];
字面量字典
NSDictionary *dic = @{@"one":@"1",@"two":@2};
  • 應該使用字面量語法來建立字串、數值、陣列、字典。與建立此類物件的常規方法相比,這麼做更加簡明扼要。
  • 應該通過取下標操作來訪問陣列下標或字典中的鍵所對應的元素。
  • 用字面量語法建立陣列或字典時,若值中有nil,則會丟擲異常。因此,務必確保值裡不含nil。

4、多用型別常量,少用#define預處理指令

  • 不要用預處理指令定義常量。這樣定義出來的常量不含型別資訊,編譯器只是會在編譯前據此執行查詢與替換操作。即使有人重新定義了常量值,編譯器也不會產生警告資訊,這將導致應用程式中的常量值不一致。
  • 在實現檔案中使用 static const 來定義 “只在編譯單元內可見的常量(translation-unit-specific constant)” 。由於此類常量不在全域性符號表中,所以無須為其名稱加字首。
static const NSString *kObserverName = @"name";
//用const修飾之後,如果修改它,那麼編譯器就會報錯。
//而static修飾符則意味著該變數僅在定義次變數的編譯單元可見。
  • 在標頭檔案中使用 extern 來宣告全域性變數,並在相關實現檔案中定義其值。這種常量要出現在全域性符號表中,所以其名稱應加以區分,通常用與之相關的類名做字首。
.h
//宣告一個全域性常量
extern NSString *const PersonStringConstant;
.m
//定義全域性常量
NSString *const PersonStringConstant = @"";

5、用列舉表示狀態、選項、狀態碼

  • 應該用列舉來表示狀態機的狀態、傳遞給方法的選項以及狀態碼等值,給這些值起個一棟的名字。
/**
 網路請求型別列舉
 */
typedef NS_ENUM(NSInteger, NetworkMethod) {
    NetworkMethodGet,
    NetworkMethodPost,
    NetworkMethodPut,
    NetworkMethodDelete,
};
  • 如果把傳遞給某個方法的選項表示為列舉型別,而多個選項又可同時使用,那麼就將各選項定義為2的冪,以便通過按位或操作將其組合起來。
typedef NS_OPTIONS(NSUInteger, UIRemoteNotificationType) {
    UIRemoteNotificationTypeNone    = 0,
    UIRemoteNotificationTypeBadge   = 1 << 0,
    UIRemoteNotificationTypeSound   = 1 << 1,
    UIRemoteNotificationTypeAlert   = 1 << 2,
    UIRemoteNotificationTypeNewsstandContentAvailability = 1 << 3,
}
1248713-95613ef3e47e2b75.png
螢幕快照 2017-03-20 下午6.30.59.png
  • 用NS_ENUM 與 NS_OPTIONS 巨集來定義列舉型別,並指明其底層資料型別。這樣做可以確保列舉是用開發者所選的底層資料型別實現出來的,而不會採用編譯器所選的型別。
  • 在處理列舉型別的 switch 語句中不要實現 default 分支。這樣的話,加入新列舉之後,編譯器就會提示開發者:switch 語句並未處理所有列舉。

二、物件、訊息、執行時

1、理解“屬性”這一概念

  • 可以用 @property 語法來定義物件中封裝的資料
  • 通過 “特質” 來指定儲存資料所需的正確語義
  • 在設定屬性所對應的例項變數時,一定要遵從該屬性所宣告的語義
  • copy特質一般用在NSString、NSArray、NSDictionary等不變的變數型別上,這樣能保護其封裝性。因為傳遞給設定方法的新值可能指向一個可變的例項(如NSMutableString)。
    如一個Person類,有一個name屬性,如果用strong特質來進行修飾:

import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

如果像下面這樣,那麼這個Person的例項中的name屬性值就可能會莫名其妙的被改了:
- ```
Person *p = [[Person alloc] init];
NSMutableString *newName = [[NSMutableString alloc] initWithFormat:@"gaofeng"];
p.name = newName;
NSLog(@"====== %@",p.name);// gaofeng
[newName appendString:@"tan"];
NSLog(@"====== %@",p.name);// gaofengtan 

這並不是我們想要的結果。

  • 開發iOS程式時應該使用 nonatomic 屬性,因為atomic屬性開銷太大,會嚴重影響效能,而且 nonatomic 特質也並不能保證執行緒安全。
    列如,一個執行緒在連續多多次讀取某屬性值得過程中有別的執行緒在同時改寫該值,那麼即便將屬性宣告為 *atomic* ,也還是會讀到不同的屬性值。

2、在物件內部儘量直接訪問例項變數

  • 在物件內部讀取資料時,應該直接通過例項變數來讀,而寫入資料時,則應通過屬性來寫。
  • 在初始化方法及delloc方法中,總是應該直接通過例項變數來 讀寫 資料。
  • 有時會使用懶載入技術配置某份資料,這種情況下,需要通過屬性來讀取資料。
    列如:
 - (Car *)car {
    if (!_car) {
        _car = [Car new];
    }
    return _car;
}

若沒有呼叫 “獲取方法” 就直接訪問例項變數,則會看到尚未設定好的car,所以說,如果使用懶載入技術,那麼必須通過存取方法來訪問其資料。

3、理解 “物件等同性(equality)” 這一概念

  • 若想檢測物件的等同性,請提供 “isEqual:” 和 “hash” 方法。
    這裡提供一篇文章,比較詳細的講解了這兩個方法 isEqual & hash
  • 相同的物件必須具有相同的雜湊碼,但是兩個雜湊碼相同的物件卻未必相同。
  • 不要盲目地逐個檢測每條屬性,而是應該依照具體需求來制定檢測方案。
  • 編寫hash方法時,應該使用計算速度快而且雜湊碼碰撞機率低的演算法。

4、以 “類族模式” 隱藏實現細節

  • 類族模式可以把實現細節隱藏在一套簡單的公共介面後面。
    .h
 #import <Foundation/Foundation.h>
 typedef NS_ENUM(NSUInteger, GFEmployeeType) {
    GFEmployeeTypeDeveloper,
    GFEmployeeTypeDesigner,
    GFEmployeeTypeFinance
 };
 //僱員基類
 @interface GFEmployee : NSObject
 @property (nonatomic, copy) NSString *name;
 @property (nonatomic, assign) NSUInteger salary;
 + (instancetype)employeeWithType:(GFEmployeeType)type;
 - (void)doADaysWork;
 @end

.m

 #import "GFEmployee.h"
 #import "GFEmployeeDeveloper.h"
 #import "GFEmployeeDesign.h"
 #import "GFEmployeeFinance.h"
 @implementation GFEmployee
 + (instancetype)employeeWithType:(GFEmployeeType)type {
    switch (type) {
        case GFEmployeeTypeDeveloper:
            return [GFEmployeeDeveloper new];
            break;
        case GFEmployeeTypeDesigner:
            return [GFEmployeeDesign new];
            break;
        case GFEmployeeTypeFinance:
            return [GFEmployeeFinance new];
            break;
     }
 }
 - (void)doADaysWork {
    // subclass implement this
 }
@end

使用

GFEmployee *employee = [GFEmployee employeeWithType:GFEmployeeTypeDeveloper];
[employee doADaysWork];
  • 系統框架中經常使用類族。
比如UIButton:UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
  • 從類族的公共抽象基類中繼承子類時要當心,若有開發文件,則應首先閱讀。

5、在既有類中使用關聯物件(Associated Object)存放自定義資料

  • 可以通過 “關聯物件” 機制來把兩個物件連起來。
//建立一個自定義UIBarButtonItem,點選方法直接通過一個block來回撥
#import "UIBarButtonItem+Common.h"
#import <objc/runtime.h>
static const void *BarButtonItemBlockKey = &BarButtonItemBlockKey;
@implementation UIBarButtonItem (Common)
 + (UIBarButtonItem *)itemWithBtnTitle:(NSString *)title clickHandle:(void (^)(void))action {
    UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] init];
    buttonItem.title = title;
    buttonItem.target = buttonItem;
    buttonItem.style = UIBarButtonItemStylePlain;
    buttonItem.action = @selector(handleClick:);
    [buttonItem setTitleTextAttributes:@{NSForegroundColorAttributeName: [UIColor lightGrayColor]} forState:UIControlStateDisabled];
    //給buttonItem新增關聯物件
    objc_setAssociatedObject(buttonItem, BarButtonItemBlockKey, action, OBJC_ASSOCIATION_COPY_NONATOMIC);
    return buttonItem;
}
 - (void)handleClick:(UIBarButtonItem *)buttonItem {
    //獲取buttomItem對應key的關聯物件
    void (^block)(void) = objc_getAssociatedObject(buttonItem, BarButtonItemBlockKey);
    block();
}
  • 定義關聯物件時可指定記憶體管理語義,用以模仿定義屬性時所採用 “擁有關係” 與 “非擁有關係”。
    記憶體管理語義:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,  // assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,   // nonatomic, retain
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // nonatomic, copy
    OBJC_ASSOCIATION_RETAIN = 01401,   // retain
    OBJC_ASSOCIATION_COPY = 01403  // copy
};
  • 關聯物件的key是個 “不透明的指標(所指向的資料結構不侷限於某種特定型別的指標)” ,在設定關聯物件值時,通常使用靜態變數做鍵。
  • 只有在其他做法不可行時才應選用關聯物件,因為這種做法通常會引入難於查詢的bug(迴圈引用)。

6、理解 objc_msgSend 的作用(訊息傳遞機制)

  • 訊息由接收者(receiver)、選擇子(selector)及引數(parms)構成。給某物件 “傳送訊息” 也就相當於在該物件上 “呼叫方法”(call a method)。
// 傳送訊息最終會轉變為呼叫函式,叫做 objc_msgSend,它的原型如下:
void objc_msgSend(id self, SEL _cmd, ...)
  • 每個類裡面都有一張表用來存放該類的所有方法,其中選擇子的名稱則是查詢方法時所用的 “鍵”。
  • 發給某物件的全部訊息都要由 “動態訊息派發系統”(dynamic message dispatch system) 來處理,該系統會查出對應的方法,並執行其程式碼。

7、理解訊息轉發機制

  • 若物件無法響應某個選擇子,則進入訊息轉發流程。
  • 通過執行期的動態方法解析功能,我們可以在需要用到某個方法時再講其加入類中。(動態方法解析)
 + (BOOL)resolveClassMethod:(SEL)sel
 + (BOOL)resolveInstanceMethod:(SEL)sel
  • 物件可以把其無法解讀的某些選擇子轉交給其他物件來處理。(備援接收者)
 - (id)forwardingTargetForSelector:(SEL)aSelector
  • 經過上述兩步之後,如果還是沒辦法處理選擇子,那就啟動完整的訊息轉發機制。(完整的訊息轉發)
 - (void)forwardInvocation:(NSInvocation *)anInvocation
  • 經過以上訊息轉發後若訊息還是無法得到處理,那就會呼叫NSObject的方法 “doesNotRecognizeSelector:” 丟擲異常(unrecognized selector sent to instance)


    1248713-aa9a27cb51ffd04c.png
    訊息轉發流程.png

8、用 “方法調配技術(method swizzling)” 除錯 “黑盒方法”

  • 在執行期,可以向類中新增或替換選擇子所對應的方法實現。
  • 使用另一份實現來替換原有的方法實現,這道工序叫做 “方法調配” ,開發者常利用此技術向原有實現中新增新功能。
  • 一般來說,只有除錯程式的時候才需要在執行期修改方法實現,這種方法不宜濫用。
    方法調配中常用的方法:
//獲取方法
Method method = class_getClassMethod(Class cls, SEL name)
Method method = class_getInstanceMethod(Class cls, SEL name)
//交換方法
method_exchangeImplementations(Method m1, Method m2) 

9、理解 “類物件” 的用意

  • 每個例項都有一個指向Class物件的指標(isa),用以表明其型別,而這些Class物件則構成了類的繼承體系。
  • 類物件本質是一個結構體,此結構體存放類的“後設資料”,例如類的例項實現了幾個方法,具備了多少個例項變數等資訊。類物件所屬的型別(也就是isa指標所指向的型別)是另外一個類,叫做“元類”(metaclass),用來表述類物件本身所具備的後設資料。


    1248713-92ea0222da3a582d.png
    類物件的定義.png

    1248713-457ef169d0eb8704.png
    類的繼承體系.png
  • 如果物件型別無法再編譯期確定,那麼就應該使用型別資訊查詢方法來探知。
// 能夠判斷出物件是否為某個特定類的例項
isMemberOfClass:
// 能夠判斷出物件是否為某類或者其派生類的例項
isKindOfClass:
  • 儘量使用型別資訊查詢方法來確定物件型別,而不要直接比較類物件,因為某些物件可能實現了訊息轉發功能。

相關文章