1. 程式人生 > IOS開發 >動態方法解析演示-KVC容器

動態方法解析演示-KVC容器

上一篇我們瞭解了訊息轉發機制的全貌,傳送門在這裡:理解訊息轉發機制。但有時候一些抽象的理念得用實際例子才能幫助理解,尤其是runtime,我們很多時候只知道它的一些基礎用法。所以,先從訊息轉發機制的第一階段:動態方法解析,舉一個完整的例子。

先回顧一下動態方法解析的大概定義:先徵詢當前接收者所屬的類,是否能動態新增方法並處理這個未知的selector,如果接收者沒有動態新增方法或者動態新增的方法依然不能處理這個未知的selector,則當前接收者自己就沒有辦法通過動態新增方法的手段來響應這個selector了,之後就進入訊息轉發的第二階段。換句話說,就是沒有具體方法實現的時候,通過runtime的徵詢,動態插入方法的實現,前文提到,CoreData屬性的動態繫結,就是在這個階段完成的。但是CoreData現在並不常用,所以這個例子也沒什麼代入感。

本文的例子KVC容器感覺也是個很生的名詞,但實際上OC中的CAAnimationCALayer就是常用的KVC容器,下面摘錄蘋果官方文件的解釋:

The CAAnimation and CALayer classes are key-value coding compliant container classes,which means that you can set values for arbitrary keys. Even if the key someKey is not a declared property of the CALayer class,you can still set a value for it as follows:

[theLayer setValue:[NSNumber numberWithInteger:50] forKey:@"someKey"];
複製程式碼

You can also retrieve the value for arbitrary keys like you would retrieve the value for other key paths. For example,to retrieve the value of the someKey path set previously,you would use the following code:

someKeyValue=[theLayer valueForKey:@"someKey"
]; 複製程式碼

換成人話就是CAAnimationCALayer都是KVC容器類,他們可以很方便的通過鍵值對的形式設定和獲取值,哪怕key並沒有通過屬性宣告過。這使得自定義子類的時候能夠更方便的宣告屬性和使用屬性,也給了API更多的設計空間。

下面是本次例子的主角:KVCContainerDictionary。他同樣是一個KVC容器,本身及其子類只需要將屬性設定為@dynamic,屬性就可以通過點語法和鍵值對形式訪問和賦值,除了屬性,還可以存入任意自己所需要的資料。下面來看看它的實現。

///KVCContainerDictionary.h

#import <Foundation/Foundation.h>

@interface KVCContainerDictionary : NSObject

@property(nonatomic,strong)NSArray *list;

- (void)setObject:(id)object forKey:(NSString*)key;
- (id)objectForKey:(NSString*)key;
@end
複製程式碼

標頭檔案中聲明瞭一個NSArray型別的屬性list,還有鍵值對訪問和賦值的方法。

將實現檔案的關鍵部分拆分,一步步來說明。

#import "KVCContainerDictionary.h"
#import <objc/runtime.h>

@interface KVCContainerDictionary()
///宣告一個儲存用的字典storeDic,用於儲存屬性值
@property(nonatomic,strong)NSMutableDictionary *storeDic;
@end

@implementation KVCContainerDictionary
///用@dynamic修飾屬性,代表不自動生成setter和getter
@dynamic list;
複製程式碼

宣告一個儲存用的字典storeDic,用於儲存屬性值,並用@dynamic修飾屬性,代表不自動生成setter和getter,這個很重要。

///初始化和動態方法解析
- (instancetype)init {
    self = [super init];
    if (self) {
          ///初始化儲存用的字典
        _storeDic = [NSMutableDictionary dictionary];
    }
    return self;
}

///本例的關鍵,在這裡進行動態方法解析,並插入新的方法實現
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selString = NSStringFromSelector(sel);
    if ([selString hasPrefix:@"set"]) {
        class_addMethod(self,sel,(IMP)dicSetter,"v@:@");
    }else {
        class_addMethod(self,(IMP)dicGetter,"@@:");
    }
    return YES;
}
複製程式碼

由於沒有自動生成setter和getter,所以在屬性存取的時候selector並沒有實現,所以在objc_msgSend失敗後,進入動態方法解析階段,這個時候會呼叫resolveInstanceMethod這一個類方法,嘗試進行方法實現的動態插入,這裡檢查selector是否為set開頭判斷是否是setter。之後通過class_addMethod方法動態插入新的setter和getter的實現。這裡有大家發現,class_addMethod方法最後一個引數很奇怪,完全不知所云,其實很簡單,就是個字元對照,用來表示新的方法實現的入參和返回引數的型別。下面列出一張對照表,就可以很清晰的知道"v@:@""@@:"的含義了。

Code Meaning
c A char
i An int
s A short
l A long``l is treated as a 32-bit quantity on 64-bit programs.
q A long long
C An unsigned char
I An unsigned int
S An unsigned short
L An unsigned long
Q An unsigned long long
f A float
d A double
B A C++ bool or a C99 _Bool
v A void
* A character string (char *)
@ An object (whether statically typed or typed id)
# A class object (Class)
: A method selector (SEL)
[array type] An array
{name=type...} A structure
(name=type...) A union
bnum A bit field of num bits
^type A pointer to type
? An unknown type (among other things,this code is used for function pointers)
///getter
id dicGetter(id self,SEL sel) {
    KVCContainerDictionary *kvcDic = (KVCContainerDictionary*)self;
    NSString *key = NSStringFromSelector(sel);
    return [kvcDic.storeDic objectForKey:key];
}
複製程式碼
///setter
void dicSetter(id self,SEL sel,id value) {
    KVCContainerDictionary *kvcDic = (KVCContainerDictionary*)self;
    NSString *selString = NSStringFromSelector(sel);
    
    NSMutableString *key = [selString mutableCopy];
    
    //去除尾部":"
    [key deleteCharactersInRange:NSMakeRange(key.length-1,1)];
    
    //去除"set"
    [key deleteCharactersInRange:NSMakeRange(0,3)];
    
    //更改駝峰大小寫
    [key replaceCharactersInRange:NSMakeRange(0,1) withString:[[key substringToIndex:1] lowercaseString]];
    
    if (value) {
        [kvcDic.storeDic setObject:value forKey:key];
    }else {
        [kvcDic.storeDic removeObjectForKey:key];
    }
}
複製程式碼

這兩個方法就沒啥好說的了,就是新的setter和getter的實現,set時將值寫入storeDic,get時通過key將值從storeDic中取出來。

之後補全鍵值對所對應的存取方法:

///詳細的異常處理我就省略了。。。
- (void)setObject:(id)object forKey:(NSString*)key {
    if (object && key && key.length >0) {
        [self.storeDic setObject:object forKey:key];
    }else {
        [self.storeDic removeObjectForKey:key];
    }
}

- (id)objectForKey:(NSString*)key {
    if (key && key.length > 0) {
        return [self.storeDic objectForKey:key];
    }else {
        return nil;
    }
}
複製程式碼

做的更細緻一些的話,可以模仿系統KVC的機制,實現類似- (id)valueForUndefinedKey:(NSString *)key- (void)setValue:(id)value forUndefinedKey:(NSString *)key。使得key的異常能夠更合理的被處理。

之後使用一下看看。


KVCContainerDictionary *dic = [KVCContainerDictionary new];
///點語法設定屬性
dic.list = @[@"hello",@"world"];
///鍵值對取出屬性
NSLog(@"%@",[dic objectForKey:@"list"]);///輸出(hello,world)

///鍵值對設定任意值
[dic setObject:@"goodbye,world" forKey:@"bye"];
///鍵值對取出對應的值
NSLog(@"%@",[dic objectForKey:@"bye"]);///輸出goodbye,world
複製程式碼

之後建立一個子類CYCustomDictionary

///.h
@interface CYCustomDictionary : KVCContainerDictionary
///宣告一個新的屬性
@property(nonatomic,strong)NSDate *currentDate;
@end

///.m
@implementation CYCustomDictionary
///不自動生成setter和getter
@dynamic currentDate;
複製程式碼

同樣我們使用一下

CYCustomDictionary *customDic = [CYCustomDictionary new];
///點語法賦值
customDic.currentDate = [NSDate date];
///鍵值對取出
NSLog(@"%@",[customDic objectForKey:@"currentDate"]);
複製程式碼

從以上的例子可以看出,KVC容器類的實現其實就是利用的動態方法解析,將setter和getter的過程重新實現並在obj_msgSend後動態注入,將屬性和值通過鍵值對形式儲存起來。這樣自定義的子類的屬性的存取其實都有基類負責,子類可以很方便的任意新增屬性,也可以更方便的利用屬性讀取和新增鍵值對的形式配合使用,讓介面設計更加統一。