《Effective Objective-C 2.0》讀書/實戰筆記 一

極客學偉發表於2018-07-31

《Effective Objective-C 2.0》讀書/實戰筆記 一

第1章:熟悉Objective-C

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

  • Objective-C 為C語言新增了物件導向的特性,是其超級。Objective-C 說那個動態繫結的訊息結構,也就是說,在執行時才檢查物件型別。接收一條訊息之後,究竟應執行何種程式碼,由執行期環境而非編譯器來決定。
  • 理解C語言的核心概念有助於寫好Objective-C程式。尤其要掌握記憶體模型和指標。
NSString *theString = @"Hello World";
NSString *theString2 = @"Hello World";
NSLog(@"theString:%p --- theString:2%p",theString,theString2);
    
複製程式碼

列印結果:

theString:0x11bb0d158 --- theString:20x11bb0d158
複製程式碼

兩個變數為指向同一塊記憶體的相同指標。此時將 theString2 賦值為 “Hello World !!!!”

theString2 = @"Hello World !!!!";
NSLog(@"theString:%p --- theString:2%p",theString,theString2);
複製程式碼

列印結果:

theString:0x12002e158 --- theString:20x12002e198
複製程式碼

此時,兩者變為不同的記憶體地址。所以,物件的本質是指向某一塊記憶體區域的指標,指標的儲存位置取決於物件宣告的區域和有無成員變數指向。若在方法內部宣告的物件,記憶體會分配到棧中,隨著棧幀彈出而被自動清理;若物件為成員變數,記憶體則分配在堆區,宣告週期需要程式設計師管理。 另外在探尋物件本質的過程中發現物件的本質為宣告為isa的指標,一枚指標在32位計算機佔4位元組,64位計算機佔8位元組,真正在iOS系統中,isa指標實際佔用了16位元組的記憶體區域,在此文中通過 clang 將OC程式碼轉化為 C++程式碼探究了一個物件所佔的實際記憶體大小,詳細可參閱 iOS底層原理探究- NSObject 記憶體大小

?? 第2條:在類的標頭檔案中儘量少引入其他標頭檔案

  • 除非確有必要,否則不要引入標頭檔案,一般來說,應該在某個類的標頭檔案中使用向前宣告來提及別的類,並在實現檔案中引入那些類的標頭檔案。這樣做可以儘量降低類之間的耦合。
  • 有時無法使用向前宣告,比如要宣告某個類遵循一項協議,儘量把“該類遵循某協議” 的這條宣告移至“class-continuation 分類中”。如果不行的話,就把協議單獨放在某一個標頭檔案中,然後將其引入。
//Student.h
@class Book; //向前引用,避免在 .h 裡匯入其他檔案
@interface Student : NSObject
@property (nonatomic, strong) BOOK *book;
@end

//student.m
#import "Book.h"
@implementation Student
- (void)readBook {
    NSLog(@"read the book name is %@",self.book);
}
@end

複製程式碼

?? 第3條:多用字面量語法,少用與之等價的方法

  • 應該使用字面量語法來建立字串、數值、陣列、字典。與建立此類物件的常規方法相比,這麼做更加簡明扼要。
  • 應該通過取下標操作來訪問陣列下標或字典中的鍵所對應的元素。
  • 用字面量語法建立陣列或字典,若值中有 nil ,則會丟擲異常。因此,務必確保值裡不含 nil。
0️⃣ 字面數值
NSNumber *number = [NSNumber numberWithInteger:10086];
複製程式碼

改為

NSNumber *number = @10086;
複製程式碼
1️⃣ 字面量陣列
 NSArray *books = [NSArray arrayWithObjects:@"演算法圖解",@"高效能iOS應用開發",@"Effective Objective-C 2.0", nil];
 
 NSString *firstBook = [books objectAtIndex:0];
複製程式碼

改為

NSArray *books = @[@"演算法圖解",@"高效能iOS應用開發",@"Effective Objective-C 2.0"];

NSString *firstBook = books[0];
複製程式碼
2️⃣ 字面量字典
NSDictionary *info1 = [NSDictionary dictionaryWithObjectsAndKeys:@"極客學偉",@"name",[NSNumber numberWithInteger:18],@"age", nil];
NSString *name1 = [info1 objectForKey:@"name"];
複製程式碼

改為

NSDictionary *info2 = @{
                        @"name":@"極客學偉",
                        @"age":@18,
                        };
NSString *name2 = info2[@"name"];
複製程式碼
3️⃣ 可變陣列與字典
[arrayM replaceObjectAtIndex:0 withObject:@"new Object"];
[dictM setObject:@19 forKey:@"age"];
複製程式碼

改為

arrayM[0] = @"new Object";
dictM[@"age"] = @19;
複製程式碼
4️⃣ 侷限性
1、字面量語法所建立的物件必須屬於 Foundation 框架,自定義類無法使用字面量語法建立。
2、使用字面量語法建立的物件只能是不可變的。若希望其變為可變型別,可將其深複製一份
NSMutableArray *arrayM = @[@1,@"123",@"567"].mutableCopy;
複製程式碼

?️‍? 第4條:多用型別常量,少用 #define 預處理指令

  • 不要用預處理指令定義常量。這樣定義的常量不含型別資訊,編譯器只是會在編譯前據此執行查詢與替換操作。即使有人重新定義了常量值,編譯器也不會產生警告資訊⚠️,這將導致應用程式中的常量值不一致。
  • 在實現檔案中使用 static const 來定義“只在編譯單元內可見的常量”。由於此類常量不在全域性符號表中,所以無需為其名稱加字首。
  • 在標頭檔案中使用 extern 來宣告全域性常量,並在相關實現檔案中定義其值。這種常量要出現在全域性符號表中,所以名稱應該加以區隔,通常用與之相關的類名做字首。

預處理指令是程式碼拷貝,在編譯時會將程式碼中所有預處理指令展開填充到程式碼中,減少預處理指令也會加快編譯速度。

私有常量
.m
static const NSTimeInterval kAnimationDuration = 0.3;
複製程式碼
全域性常量
.h
extern NSString * const XWTestViewNoticationName;

.m
NSString * const XWTestViewNoticationName = @"XWTestViewNoticationName";
複製程式碼

?? 第5條:用列舉表示狀態、選項、狀態碼

  • 應該用列舉來表示狀態機的狀態、傳遞給方法的選項以及狀態碼等值,給這些值起個易懂的名字。
  • 如果把傳遞給某個方法的選項表示為列舉型別,而多個選項又可以同時使用,那麼就將各選項定義為2的冪,以便通過按位或操作將其組合起來。
  • NS_ENUUMNS_OPTIONS 巨集來定義列舉型別,並指明其底層資料型別。這樣做可以確保列舉是用開發者所選的底層資料型別實現出來的,而不會採用編譯器所選型別。
  • 在處理列舉型別的switch語句中不要實現 default分支。這樣的話,加入新列舉之後,編譯器就會提示開發者:switch 語句並未處理所以列舉。
/// 位移列舉
typedef NS_OPTIONS(NSUInteger, XWDirection) {
    XWDirectionTop          = 0,
    XWDirectionBottom       = 1 << 0,
    XWDirectionLeft         = 1 << 1,
    XWDirectionRight        = 1 << 2,
};

/// 常量列舉
typedef NS_ENUM(NSUInteger, SexType) {
    SexTypeMale,
    SexTypeFemale,
    SexTypeUnknow,
};
複製程式碼

第2章:物件、訊息、執行時

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

  • 可以用 @property 語法來定義物件中所封裝的資料。
  • 通過“特質”來指定儲存資料所需的正確語義。
  • 在設定屬性所對應的例項變數時,一定要遵從該屬性所宣告的語義。

使用屬性編譯器會自動生成例項變數和改變數的get方法和set方法。 同時可以使用 @synthesize 指定例項變數的名稱,使用 @dynamic 使編譯器不自動生成get方法和set方法。 屬性可分為四類,分別:

1.原子性
  • atomic 原子性,系統預設。並不是執行緒安全,release 方法不受原子性約束.
  • nonatomic 非原子性
2.讀寫許可權
  • readwrite 可讀可寫,同時擁有get方法和set方法。
  • readonly 只讀,僅有 get 方法。
3.記憶體管理語義
  • assign 簡單賦值,用於基本成員型別
  • strong 表示“擁有關係”,設定新值時會保留新值,釋放舊值,再把新值設定給當前屬性。
  • weak 表示“非擁有關係”,設定新值時既不保留新值,也不釋放舊值。同 assign 類似,所指物件銷燬時會置nil
  • unsafe_unretained 表示一種非擁有關係,語義同 assign,僅適用於物件型別。當目標物件被銷魂時不會自動清空。
  • copy 表達的關係和 strong 類似。區別在於設定新值時不會保留新值,而是將其 拷貝 後賦值給當前屬性。
4.方法名
  • getter=<name> 指定獲取方法(getter)的方法名, 如: @property (nonatomic, getter=isOn) BOOL on;
  • setter=<name> 指定設定方法(setter)的方法名。

?? 第7條:在物件內部儘量直接訪問例項變數

  • 在物件內部讀取資料時,應該直接通過例項變數來讀,而寫入資料時,則應通過屬性來寫。
  • 在初始化方法及 dealloc方法中,應該直接通過例項變數來讀寫資料。
  • 有時會使用懶載入配置某資料,這種情況需要通過屬性來讀取資料。

在物件內部直接使用成員變數比使用點語法的優勢在於,前者不需要經過 Objective-C 的方法派發過程,執行速度會更快,這時編譯器會直接訪問儲存物件例項變數的那塊記憶體。不過直接訪問成員變數不會觸發 KVO,所以使用點語法訪問屬性還是直接使用成員變數取決於具體行為。

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

  • 若想監測物件的等同性,請提供 isEqual:hash 方法。
  • 相同物件必須具有相同的雜湊碼,但是兩個雜湊碼相同的物件未必相同。
  • 不要盲目地逐個監測每條屬性,而是應該依照具體需求來制定檢測方案。
  • 編寫 hash 方法時,應該使用計算速度快而且雜湊碼碰撞機率低的演算法。

常規比較相等的方式 == 比較的是兩個物件指標是否相同。 在自定義物件重寫 isEqual 方法可使用此方式:

- (BOOL)isEqualToBook:(Book *)object {
    if (self == object) return YES;
    if (![_name isEqualToString:object.name]) return NO;
    if (![_author isEqualToString:object.author]) return NO;
    return YES;
}
複製程式碼

在自定義物件重寫 hash 方法可使用此方式:

@implementation Book
- (NSUInteger)hash {
    NSUInteger nameHash = [_name hash];
    NSUInteger authorHash = [_author hash];
    return nameHash ^ authorHash;
}
@end
複製程式碼

?? 第9條:以“類族模式”隱藏實現細節

  • 類族模式可以把實現細節隱藏在一套簡單的公共介面後面
  • 系統框架中經常使用類族
  • 從類族的公共抽象基類中繼承子類時要當心,若有開發文件,則應先閱讀

例如宣告一本書作為基類,通過“類族模式“建立相關的類,對應型別的在子類中實現相關方法。如下:

.h
typedef NS_ENUM(NSUInteger, BookType) {
    BookTypeMath,
    BookTypeChinese,
    BookTypeEnglish,
};
@interface Book : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *author;
+ (instancetype)bookWithType:(BookType)type;
- (void)read;
@end
複製程式碼
.m
@interface BookMath : Book
- (void)read;
@end
@implementation BookMath
- (void)read {
    NSLog(@"read The Math");
}
@end

@interface BookChinese : Book
- (void)read;
@end
@implementation BookChinese
- (void)read {
    NSLog(@"read The Chinese");
}
@end

@interface BookEnglish : Book
- (void)read;
@end
@implementation BookEnglish
- (void)read {
    NSLog(@"read The English");
}
@end

@implementation Book
+ (instancetype)bookWithType:(BookType)type {
    switch (type) {
        case BookTypeMath:
            return [BookMath new];
            break;
        case BookTypeChinese:
            return [BookChinese new];
            break;
        case BookTypeEnglish:
            return [BookEnglish new];
            break;
    }
}
@end
複製程式碼

?? 第10條:在既有類中使用關聯物件存放自定義資料

  • 可以通過“關聯物件”機制把兩個物件連起來
  • 定義關聯物件時可指定記憶體管理語義,用以模仿定義屬性時所採用的“擁有關係” 與 “非擁有關係”
  • 只有在其他做法不可行時才應選用關聯物件,因為這種做法通常會引入難於查詢的 bug

關聯物件的語法:

#import <objc/runtime.h>

// Setter 方法
void objc_setAssociatedObject(id  _Nonnull object, const void * _Nonnull key, id  _Nullable value, objc_AssociationPolicy policy)
    
// Getter 方法
id objc_getAssociatedObject(id  _Nonnull object, const void * _Nonnull key)
    
// 移除指定物件的所有關聯物件值
void objc_removeAssociatedObjects(id  _Nonnull object)
複製程式碼

例項一:使用關聯物件將宣告和執行進行 聚合 原寫法

- (void)testAlertAssociate {
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:@"要培養哪種生活習慣?" delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"早起",@"早睡", nil];
    [alertView show];
}
#pragma mark - UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    if (buttonIndex == 1) {
        NSLog(@"你要早起");
    }else if (buttonIndex == 2) {
        NSLog(@"你要晚睡");
    }else{
        NSLog(@"取消");
    }
}
複製程式碼

使用 “關聯物件改寫” 改寫為:

static void *kAlertViewKey = "kAlertViewKey";
- (void)testAlertAssociate {
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:@"要培養哪種生活習慣?" delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"早起",@"早睡", nil];
    [alertView show];
    void(^AlertBlock)(NSUInteger) = ^(NSUInteger buttonIndex){
        if (buttonIndex == 1) {
            NSLog(@"你要早起");
        }else if (buttonIndex == 2) {
            NSLog(@"你要早睡");
        }else{
            NSLog(@"取消");
        }
    };
    objc_setAssociatedObject(alertView, kAlertViewKey, AlertBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
#pragma mark - UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    void(^AlertBlock)(NSUInteger) = objc_getAssociatedObject(alertView, kAlertViewKey);
    AlertBlock(buttonIndex);
}
複製程式碼

如此可將實現和宣告在一起處理,在回撥處取出所關聯的程式碼塊執行。可使得程式碼更易讀。

例項二:為分類新增屬性 眾所周知,在 Objective-C 的分類中宣告屬性只能自動生成該屬性的 getter 方法和 setter 方法 的宣告,沒有具體實現。所以真正給分類新增屬性,使用關聯物件是比較好的一種方式。

//NSTimer+XW.h
@interface NSTimer (XW)
@property (nonatomic, assign) NSUInteger tag;
@end

//NSTimer+XW.m
#import "NSTimer+XW.h"
#import <objc/runtime.h>
@implementation NSTimer (XW)
static void *kXW_NSTimerTagKey = "kXW_NSTimerTagKey";
#pragma mark - tag / getter setter
/// setter
- (void)setTag:(NSUInteger)tag {
    NSNumber *tagValue = [NSNumber numberWithUnsignedInteger:tag];
    objc_setAssociatedObject(self, kXW_NSTimerTagKey, tagValue, OBJC_ASSOCIATION_ASSIGN);
}
/// getter
- (NSUInteger)tag {
    NSNumber *tagValue = objc_getAssociatedObject(self, kXW_NSTimerTagKey);
    return tagValue.unsignedIntegerValue;
}
@end
複製程式碼

?? 第11條:理解 objc_msgSend 的作用

  • 訊息由接受者、選擇子及引數構成。給某物件“傳送訊息”也就是相當於在該物件上“呼叫方法”
  • 發給某物件的全部訊息都要有“動態訊息派發系統”來處理,該系統會查出對應的方法,並執行其程式碼。

objc_msgSend 執行流程

Snip20180731_5
注:上圖出自 SEEMYGO MJ老師

眾所周知, OC 中方法呼叫的本質是傳送訊息 objc_msgSend ,其原型為:

/// self:訊息接受者,cmd:選擇子即執行方法,...:其他引數
void objc_msgSend(id self, SEL cmd, ...);
複製程式碼

舉個例子?:

// xx類
id returnValue = [self doSomething:@"param"];

實質為:
id returnValue = objc_msgSend(xx類, @selector(doSomething:),@"param");
複製程式碼

其中OC在實現此機制的同時設計了快取機制,每次呼叫一個方法會將此方法進行快取,再次執行相同方法會提高執行效率,使其和靜態繫結呼叫方法的速度相差不會那麼懸殊。

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

  • 若物件無法響應某個選擇子(seletor),則進入訊息轉發流程
  • 通過執行期的動態方法解析功能,我們可以在需要用到某個方法時再將其加入類中
  • 物件可以把其無法解讀的某些選擇子轉交給其他物件來處理
  • 經過上述兩步之後,如果還是沒辦法處理選擇子,那就啟動完整的訊息轉發機制

訊息轉發的全流程:

Snip20180731_4

倘若呼叫一個沒有實現的方法,控制檯會丟擲如下經典錯誤資訊: unrecognized selector sent to instance 0xxx

在方法呼叫和丟擲異常中間還經歷了一段鮮為人知的歷程,名曰:訊息轉發機制。上述錯誤提示便是呼叫沒實現的方法之後底層轉發給 NSObjectdoedNotRecognizeSelector:方法所丟擲的。 訊息轉發的具體過程,首先:

動態方法解析
/// 呼叫了未實現的類方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    return [super resolveClassMethod:sel];
}
/// 呼叫了未實現的例項方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return [super resolveInstanceMethod:sel];
}
複製程式碼

表示是否可以新增一個例項方法用以處理此方法,前提此類需要在程式中提前寫好,可用Runtime 的 class_addMethod動態新增。

/// 呼叫了未實現的例項方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test)) {
        /// 呼叫了未實現的 test 方法,動態新增一個 trendsMethod 方法,使其轉發給新加的方法 trendsMethod
        
        // 引數1:新增到的類, 引數2:新增新方法在類中的名稱, 引數3:新方法的具體實現 
        // 引數4:新方法的引數返回值說明,如 v@: - 無引數無返回值  i@: - 無引數返回Int  i@:@ - 一個引數返回Int
        class_addMethod(self, sel, (IMP)class_getMethodImplementation([self class], @selector(trendsMethod)), "v@:");
       
        return YES; //此處返回 YES or NO 都可以
    }
    return [super resolveInstanceMethod:sel];
}
- (void)trendsMethod {
    NSLog(@"這是動態新增的方法");
}
複製程式碼
備援接收者
/// 可將未實現的例項方法轉發給其他類處理
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(testInstanceMethod)) {
        return [Chinese new]; // 訊息轉發給能夠處理該例項方法的類的物件
    }
    return [super forwardingTargetForSelector:aSelector];
}
/// 可將未實現的類方法轉發給其他類處理
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(testClassMethod)) {
        return [Chinese class]; // 訊息轉發給能夠處理該類方法的類
    }
    return [super forwardingTargetForSelector:aSelector];
}
複製程式碼
完整的訊息轉發

若上述過程都沒有處理,程式會有最後一次處理機會,便是:

動態轉發 例項 方法
/// 方法簽名,定義 返回值,引數
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(testInstanceMethod:)) {
        /// "v@:@"
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}
/// NSInvocation 封裝了一個函式呼叫
//anInvocation.target  - 方法呼叫者
//anInvocation.selector - 方法名
//anInvocation getArgument:<#(nonnull void *)#> atIndex:<#(NSInteger)#>  - 獲取第 index 個引數
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if (anInvocation.selector == @selector(testInstanceMethod:)) {
        return [anInvocation invokeWithTarget:[Chinese new]];//將實現轉給另外一個實現了此方法的物件進行處理
    }
    return [super forwardInvocation:anInvocation];
}
複製程式碼
動態轉發 類 方法
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(testClassMethod:)) {
        /// "v@:@"
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    if (anInvocation.selector == @selector(testClassMethod:)) {
        return [anInvocation invokeWithTarget:[Chinese class]];//將實現轉給另外一個實現了此方法的物件進行處理
    }
    return [super forwardInvocation:anInvocation];
}
複製程式碼

如上方法其實在實現 forwardingTargetForSelector 方法進行轉發就可以實現相同的功能,何必到最後這步處理呢。所以,他的功能不止於此。實際可以函式中直接對未處理方法進行實現,如下:

/// 方法簽名,定義 返回值,引數
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(testInstanceMethod:)) {
        /// "v@:@"
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}
// 轉發方法最終實現
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if (anInvocation.selector == @selector(testInstanceMethod:)) {
        /// 可以在此處理, 未實現的方法
        NSLog(@"這個方法 %s Student 沒有實現!!!",sel_getName(anInvocation.selector));
        id param;
        [anInvocation getArgument:&param atIndex:2];
        NSLog(@"傳進來的引數為: %@  - 可以使其搞事情",param);
        return;
    }
    return [super forwardInvocation:anInvocation];
}
複製程式碼
訊息轉發的實際應用

我們可以使用訊息轉發的機制,使程式永遠不會出現 unrecognized selector sent to instance 0xxx 這種崩潰。並在控制檯輸出具體資訊,我們可以實現一個 NSObject的分類 如下:

#import "NSObject+XWTool.h"
@implementation NSObject (XWTool)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([self respondsToSelector:aSelector]) {/// 已實現不做處理
        return [self methodSignatureForSelector:aSelector];
    }
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"在 %@ 類中, 呼叫了沒有實現的例項方法: %@ ",NSStringFromClass([self class]),NSStringFromSelector(anInvocation.selector));
}
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([self respondsToSelector:aSelector]) {/// 已實現不做處理
        return [self methodSignatureForSelector:aSelector];
    }
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"在 %@ 類中, 呼叫了沒有實現的類方法: %@ ",NSStringFromClass([self class]),NSStringFromSelector(anInvocation.selector));
}
複製程式碼

?? 第13條:用“方法調配技術“除錯“黑盒方法“

  • 在執行期,可以向類中新增或替換選擇子所對應的方法實現
  • 使用另一份實現來替換原有的方法實現,這道工序叫做“方法調配”,開發者常用此技術向原有類中增加新功能
  • 一般來說,只有除錯程式的時候才需要在執行時修改方法實現,這種做法不宜濫用

本質是使用 runtime 在執行時實現方法的替換:

/// 動態交換 m1 和 m2 兩個方法的實現
method_exchangeImplementations(Method  _Nonnull m1, Method  _Nonnull m2);
複製程式碼

方法的實現可通過如下方法獲取:

/// 獲取方法的實現 cls: 方法所在的物件, name: 方法名
Method class_getInstanceMethod(Class  _Nullable __unsafe_unretained cls, SEL  _Nonnull name)
複製程式碼
實際應用,在程式執行過程中控制檯列印當前所展示的控制器資訊,這在程式碼熟悉過程中十分有用:
//UIViewController+XWDebug.m
#import "UIViewController+XWDebug.h"
#import <objc/runtime.h>
@implementation UIViewController (XWDebug)
#ifdef DEBUG
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        /// 交換 class 的 viewDidLoad 方法
        Method originViewDidLoad = class_getInstanceMethod(self, @selector(viewDidLoad));
        Method xwViewDidLoad = class_getInstanceMethod(self, @selector(xw_viewDidLoad));
        method_exchangeImplementations(originViewDidLoad, xwViewDidLoad);
        
        /// 交換 class 的 viewDidAppear方法
        Method originViewDidAppear = class_getInstanceMethod(self, @selector(viewDidAppear:));
        Method xwViewDidAppear = class_getInstanceMethod(self, @selector(xw_viewDidAppear:));
        method_exchangeImplementations(originViewDidAppear, xwViewDidAppear);
    });
}
- (void)xw_viewDidLoad {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"*********  %@  **** viewDidload ****",self);
    });
    [self xw_viewDidLoad];
}
- (void)xw_viewDidAppear:(BOOL)animated {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"*********  %@  **** viewDidAppear ****",self);
    });
    [self xw_viewDidAppear:animated];
}
#else
#endif
@end
複製程式碼

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

  • 每個例項都有一個指向Class物件的指標,用以表名其型別,而這些 Class 物件則構成類的繼承體系
  • 如果物件型別無法在編譯期確定,那麼就應該使用型別資訊查詢方法來探知
  • 儘量使用型別資訊查詢方式來確定物件型別,而不要直接比較類物件,因為某些物件可能實現了訊息轉發功能

判斷物件是否為某個類例項:

- (BOOL)isMemberOfClass:(Class)aClass;
複製程式碼

判斷物件是否為某類或其派生類的例項:

- (BOOL)isKindOfClass:(Class)aClass;
複製程式碼

例如判斷 一個 NSDictionary 的例項:

NSMutableDictionary  *dict = @{@"key":@"value"}.mutableCopy;
BOOL example1 = [dict isMemberOfClass:[NSDictionary class]];            // NO
BOOL example2 = [dict isMemberOfClass:[NSMutableDictionary class]];     // NO
BOOL example3 = [dict isKindOfClass:[NSDictionary class]];              // YES
BOOL example4 = [dict isKindOfClass:[NSMutableDictionary class]];       // YES
BOOL example5 = [dict isKindOfClass:[NSArray class]];                   // NO
//    BOOL example6 = [dict isKindOfClass:[__NSDictionaryM class]];     // YES
複製程式碼

注意,在 [dict isMemberOfClass:[NSMutableDictionary class]] 的判斷中,實際上返回的 NO,雖然我們宣告 dictNSMutableDictionary 的例項,但實際上 dict__NSDictionaryM 類的一個例項,在控制檯可驗證:

(lldb) po [dict isMemberOfClass:[__NSDictionaryM class]]
YES
複製程式碼

《Effective Objective-C 2.0》書中所寫的例項是錯誤的!!

Snip20180731_7

故 盡信書不如無書,相信實際所驗證的,這也啟發讀者在讀書過程中需要儘量將例項驗證一下,說不定作者在寫書時也是想當然的落筆。

前兩章完結,後續幾天會陸續發表其餘篇章的讀書/實戰筆記,筆者期待和眾大神一起學習,共同進步。

未完待續...

相關文章