《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 !!!!」objective-c

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"];
複製代碼

改成bash

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️⃣ 侷限性
一、字面量語法所建立的對象必須屬於 Foundation 框架,自定義類沒法使用字面量語法建立。
二、使用字面量語法建立的對象只能是不可變的。若但願其變爲可變類型,可將其深複製一份
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

故 盡信書不如無書,相信實際所驗證的,這也啓發讀者在讀書過程當中須要儘可能將實例驗證一下,說不定做者在寫書時也是想固然的落筆。

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

未完待續...

相關文章
相關標籤/搜索