Effective Objective-C 2.0 讀書筆記二(下)

10. 在既有類中使用關聯對象存放自定義數據

注意關鍵詞“關聯對象”,就是把兩個對象關聯起來,例如把對象B關聯到對象A上面,這樣只要我們知道對象A,就能通過關聯方法拿到對象B,這是一個很有用的特性,可以幫助我們攜帶一些數據,以及一些信息。如果通俗一點理解的話可以把對象A理解成一個字典,對象B是存放在對象A中的一個對象,通過對應的key值就能拿到對應的對象B。
下面是關聯對象對應的三個方法(只有三個方法):
1.通過給定的鍵值和關聯策略對某對象設置關聯對象

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

第一個參數,被關聯對象,對應上面的對象A。
第二個參數,鍵值,通過參數形式我們知道,這是一個指針,一般我們在定義這個指針的時候使用靜態全局變量,因為這是一個“不透明指針”(自行查找什么是“不透明指針”)。
第三個參數,關聯的對象,對應上面的對象B。
第四個參數,關聯策略,是一個枚舉值,對應定義屬性時候添加的屬性特性,用于維護內存管理,下表列出對應關系:

關聯類型 等效的屬性特性
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, retain
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy
OBJC_ASSOCIATION_RETAIN retain
OBJC_ASSOCIATION_COPY copy

2.通過給定的鍵值取出相應的關聯對象

id objc_getAssociatedObject(id object, const void *key)

第一個參數,被關聯的對象,對應對象A。
第二個參數,鍵值。
返回值,關聯對象,對應對象B。
3.移除被關聯對象的所有關聯對象

void objc_removeAssociatedObjects(id object)

參數,被關聯對象,對應對象B。
上面就是關聯對象的所有方法,但是在用的時候需要注意,關聯對象應該被我們列在最后的選擇方案,因為關聯對象之間的關系沒有正式的定義,其內存管理是在設置關聯的時候才定義的,而不是在接口中預先設定好的,有時會出現一些不易查找的錯誤。
PS:偶爾在代碼中寫點這樣的代碼,會增加代碼的“氣質”,你懂的。

11. 理解objc_msgSend作用

這一小節的內容和我們寫代碼沒有什么關系,但是我們可以了解一下OC中方法的調用過程,對我們的程序調試很是很有用的。
首先說一下C語言的函數調用方式,用以和OC做比較,C語言使用“靜態綁定”,也就是說,在編譯期就能決定運行時應該調用的函數,而大家都知道,OC是一門動態語言,與之差別的就是OC中有時候是使用“動態綁定”,就是在運行期調用對應的函數,甚至可以在程序運行時改變。
寫一個簡單的方法調用的例子,解釋一下方法的構成:

id returnValue = [someObject messageName:parameter];

在這句調用語句中,someObject就是類或類的實例,messageName就是方法名,parameter就是參數,編譯器會把這條語句編譯成一條標準的C語句,編譯后的語句如下:

id returnValue = objc_msgSend(someObject, @selector(messageName), parameter)

objc_msgSend是一個可變參數的函數,對應OC中方法參數的增加,參數也會增加,相信大家都知道這個方法中參數的意思。
objc_msgSend函數會根據參數,找到對應類的對應“方法列表”,然后找到對應實現代碼,若找不到會沿著繼承關系向上查找,如果還沒找到,觸發“消息轉發”機制(后面會介紹這個機制)。
這樣下來調用一個方法大家可能感覺步驟太多,其實不會,objc_msgSend會將匹配結果放到一張“快速映射表”里,每個類都有一個這樣的表,加快調用速度。另外還有一些特殊情況,OC運行環境中還有另外一些相關的處理函數,例如objc_msgSend_stretobjc_msgSend_fpretobjc_msgSendSuper就不在一一介紹。
另外提一個點,OC對象的每一個方法當編譯成C語言的時候可以看成是下面這種的形式的

<returnType> Class_selector(id self, SEL _cmd, ...)

其中的方法名是隨意起的,大家發現這個函數和objc_msgSend的形式很想,這是為了利用“尾調用優化”,是調用函數更簡單、高效。

12. 理解消息轉發機制

這小節介紹一下上面提到的消息轉發機制,大家都知道,觸發了消息轉發機制,是因為我們沒有找到對應的方法,下面看消息轉發機制怎么處理這個問題。
介紹一下消息轉發機制,大致分為三個階段:
1.第一階段,動態方法解析
對象在無法解讀方法的時候,首先會調用所屬類下面這個方法

+ (BOOL)resolveInstanceMethod:(SEL)sel

sel就是方法名,返回值為Boolean類型,表示這個類是否能新增實例方法處理這個方法(如果是類方法會調用+ (BOOL)resolveClassMethod:(SEL)sel方法),我們需要自定義一些處理方法,用于動態添加到類中,用以解決問題(可以看后面的例子),如果這一步不能解決問題,轉到第二階段。
2.第二階段,備援接收者
來到這一步,我們就要改變解決問題的思路,既然這個類不能處理這個方法,我們可不可以找別的類處理,這時候對應的處理方法:

- (id)forwardingTargetForSelector:(SEL)aSelector

aSelector是方法名,如果當前類能夠找到一個類幫忙處理這個方法,就返回這個類,若找不到就放回nil(通過這個方法我們可以實現類似“多繼承”)。
3.第三階段,完整的消息轉發
如果已經來到了這一步,我們就要做一個完整的消息轉發。首先創建一個NSInvocation對象,把未處理方法的所有信息封裝在里面,此對象包含方法名、目標、參數,這一步要調用下面的方法:

- (void)forwardInvocation:(NSInvocation *)anInvocation

這一步處理的方法很簡單,就是在新的類上調用方法,如果這樣做的話就和第二階段沒有什么差別了。通常在這一步的時候會做一些改進,會選擇某種方式改變消息內容,例如追加參數,改變方法名等。
對于消息的處理,越早越好。
下面粘貼一個利用動態解析方法實現@dynamic屬性的例子:
這個例子實現一個類,類似字典的功能,只不過寫入和讀取信息的時候用屬性,而不是像字典一樣用關鍵字。
.h文件中:

#import <Foundation/Foundation.h>
@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSData *date;
@property (nonatomic, strong) id opaqueObject;
@end

.m文件中:

#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)sel{
    NSString *selectorString = NSStringFromSelector(sel);
    // 通過是否以“set”開頭判斷方法名
    if ([selectorString hasPrefix:@"set"]) {
        /**
         * 向類中添加一個方法
         * 參數一 指定類名.
         * 參數二 新添加的方法的方法名.
         * 參數三 函數指針,指向待添加方法.
         * 參數四 待添加方法的類型編碼.
         */
        class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
    } else {
        class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
    }
    return YES;
}

id autoDictionaryGetter(id self, SEL _cmd){
    // 拿到存儲數據的字典
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    // 拿到方法名
    NSString *key = NSStringFromSelector(_cmd);
    // 返回對應的值
    return [backingStore objectForKey:key];
}
void autoDictionarySetter(id self, SEL _cmd, id value){
    // 拿到存儲數據的字典
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    // 拿到方法名并對其進行處理
    NSString *selectorString = NSStringFromSelector(_cmd);
    NSMutableString *key = [selectorString mutableCopy];
    // 移除方法名中的“:”
    [key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
    // 移除方法名中的“set”
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    // 將方法名第一個字符轉為小寫
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
    // 如果有值,寫入字典中
    if (value) {
        [backingStore setObject:value forKey:key];
    } else {
        [backingStore removeObjectForKey:key];
    }   
}
@end

EOCAutoDictionary的用法也很簡單,只要直接通過對應的屬性名,就可以進行數據的存儲。

13. 用“方法調配技術”調試“黑盒方法”

方法調配技術,簡言之就是,將方法名和方法實現分割開來,任意組合。這樣一來我們可以任意改變一個方法的實現,另外還可以通過這種辦法給原有方法添加功能,對不知道內部實現的方法添加提示語句(黑盒調試)等等。
之所以能這么做,主要是因為方法均以指針的形式來表示,這種指針叫IMP,我們在調用方法的時候,只要將指針指向改變,就能實現我們想要的效果,運用起來也很簡單,通過下面的例子大家就會運用(注意運行時頭文件的引用):

Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString))
method_exchangeImplementations(originalMethod, swappedMethod);

通過上面的例子,我們就把NSString的lowercaseString方法和uppercaseString方法調換了,是不是很簡單。
其實這樣做并沒有什么意義,因為具體的方法實現已經都存在了,我們沒必要改變一個方法實現,但是我們通過這種方法給已知的方法添加功能,例如下面的例子:
.h文件:

@interface NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString; // 在分類中給NSString添加功能
@end

.m文件:

@implementation NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString{
    NSString *lowercase = [self eoc_myLowercaseString];
    NSLog(@"%@ => %@", self, lowercase);
    return lowercase;
}
@end

然后我們使用方法調配技術,將上面的方法和lowercaseString方法進行調換:

Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);

這樣執行完后,當我們再調用lowercaseString方法的時候會有下面的結果:

NSString *string = @"This is tHe StRing";
NSString *lowercaseString = [string lowercaseString];
// Output:This is tHe StRing => this is the string

通過這個方法我們發現,我們可以為那些不知道內部實現的黑盒方法添加日志記錄功能。
一般來說,我們很少用“方法調配”,只有在調試程序的時候才需要在運行期修改方法實現。

14. 理解“類對象”的用意

首先我們要知道,OC的實例對象是指向某塊內存數據的指針,所以在聲明變量時,要用*號。同時我們知道OC中有一種通用對象類型“id”(id本身已是一個指針),所以我們在用“id”聲明變量的時候可能和平常有點不同:

NSString *aString = @"some string";
id aString = @"some string";

上面兩種定義方式相比,語法意義相同,區別在于,指定具體類型后,當實例調用方法的時候,編輯器會給我們提示。
下面看一下“id”類型的定義:

typedef struct objc_object *id;

id其實是objc_object類型的結構體,而objc_object定義如下:

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

結構體中是一個Class類型的變量,該變量定義對象所屬的類。下面我們看一下Class類型是個什么東西:

typedef struct objc_class *Class;
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

我們看到,這個結構體存放類的各種信息(元數據),例如類有多少個實力變量,類名等等信息。
通過上面的關系,我們知道在objc的runtime中,類是用objc_class結構體表示的,對象是用objc_object結構體表示的, 對象的isa用來標示這個對象是哪個類的實例。
這些源碼是屬于objc runtime的,objc runtime的源代碼蘋果已經開源了,你可以在這里下載到objc的runtime源代碼。
其實到這里大家可能會有一個疑問,為什么objc_class結構體里面也有一個isa,那么這個isa指向誰呢?我們往下看,[NSObject class],這里我們調用了+ (Class)class這個類方法,我們再開發中經常用到這個方法,它返回的是這個類所屬的Class類型。+ (Class)class類方法的實現源碼是這樣的:

+ (Class)class { 
    return self; 
} 

為什么會返回self,self總是指的自身,而在這里沒有實例啊!這時候看開發文檔我們會發現,實際上函數的返回值是一個類對象class object,所以其本質上還是一個對象而已。既然是一個對象,它擁有一個self指針也就不奇怪了,所以對于像NSObject這樣的類來說,它其實代表的是一個類對象,本質上還是一個普通的實例對象,那么又會問了,這個類對象是誰的實例呢?很遺憾,要找到這個問題的答案,我們在 objc runtime 這一層上已經沒辦法辦到了,我們需要到更低層,也就是 objc 語言層去尋找答案了,但是 objc 語言層是不開源的,如果想繼續學習,大家可以在網上找模仿OC低層的代碼。
以上了解一下就好,我們只要知道類的繼承體系就行了,下面用一個例子:有一個類(暫且叫SomeClass)繼承于NSObject,那么這些類和元類的繼承關系是,SomeClass實例有一個isa指針指向SomeClass類,SomeClass類有一個isa指針指向SomeClass元類,NSObject類也有一個isa指針指向NSObject元類,SomeClass的父類是NSObject,SomeClass元類的父類是NSObject元類,通過這種關系,我們在類繼承體系中查詢類型信息,用isMenberOfClass:判斷對象是否是某個特定類的實例,用isKindOfClass:判斷對象是否為某類或其派生類的實例。因為OC是動態型語言的特性,上面兩個方法非常有用。
有時我們可以用比較類對象是否等同的辦法來進行比較,這時要用==操作符,而不是用isEqual方法,因為類對象是單利,在應用程序中,每個類的類對象只有一個實例,也就是說另外一種判斷對象是否為某類實例的辦法是:

id object = /*...*/
if ([object class] == [SomeClass class]){
}

這一部分基本都是關于OC運行時的知識,可能我們平時寫代碼的時候涉及很少,但是了解這些,對于我們的開發是很有幫助的,OC運行時是一個很強大的東西,有興趣的同學可以好好研究一下。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容