一、前言
如果你沒有Objective-C基礎,請學習了基礎的iOS開發再來,這個1小時是給有一定iOS基礎的童鞋的。如果你是大牛或者你感覺Objective-C Runtime太簡單不用1小時學習的,也請您繞道,這或許只是我的私人筆記了。
請跟著教程“一步步來”,請不要大概地掃兩眼就說看不懂——以這種態度寫成什麼樣你也看不懂。這是1小時入門教程,請不要試圖在1分鐘內入門!
二、本文目標
1小時讓你知道什麼是Objective-C Runtime,並對它有一定的基本瞭解,可以在開發過程中運用自如。
三、Objective-C Runtime到底是什麼東西?
簡而言之,Objective-C Runtime是一個將C語言轉化為面嚮物件語言的擴充套件。
我們將C++和Objective進行對比,雖然C++和Objective-C都是在C的基礎上加入物件導向的特性擴充而成的程式設計語言,但二者實現的機制差異很大。C++是基於靜態型別,而Objective-C是基於動態執行時型別。也就是說用C++編寫的程式編譯時就直接編譯成了可令機器讀懂的機器語言;用Objective-C編寫的程式不能直接編譯成可令機器讀懂的機器語言,而是在程式執行的時候,通過Runtime把程式轉為可令機器讀懂的機器語言。Runtime是Objective不可缺少的重要一部分。
傳送門->runtime原始碼
四、Objective-C的元素認知
4.1 id和Class
開啟/Public Headers/objc.h檔案可以看到如下定義:
1 2 3 4 5 6 7 8 9 10 11 12 |
#if !OBJC_TYPES_DEFINED /// An opaque type that represents an Objective-C class. typedef struct objc_class *Class; /// Represents an instance of a class. struct objc_object { Class isa OBJC_ISA_AVAILABILITY; }; /// A pointer to an instance of a class. typedef struct objc_object *id; #endif |
Class是一個指向objc_class結構體的指標,而id是一個指向objc_object結構體的指標,其中的isa是一個指向objc_class結構體的指標。其中的id就是我們所說的物件,Class就是我們所說的類。
開啟/Public Headers/runtime.h檔案
objc_class的定義如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
typedef struct objc_class *Class; struct objc_class { Class isa OBJC_ISA_AVAILABILITY; // metaclass #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; // 父類 const char *name OBJC2_UNAVAILABLE; // 類名 long version OBJC2_UNAVAILABLE; // 類的版本資訊,預設為0,可以通過runtime函式class_setVersion或者class_getVersion進行修改、讀取 long info OBJC2_UNAVAILABLE; // 類資訊,供執行時期使用的一些位標識,如CLS_CLASS (0x1L) 表示該類為普通 class,其中包含例項方法和變數;CLS_META (0x2L) 表示該類為 metaclass,其中包含類方法; long instance_size OBJC2_UNAVAILABLE; // 該類的例項變數大小(包括從父類繼承下來的例項變數) struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 該類的成員變數地址列表 struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法地址列表,與 info 的一些標誌位有關,如CLS_CLASS (0x1L),則儲存例項方法,如CLS_META (0x2L),則儲存類方法; struct objc_cache *cache OBJC2_UNAVAILABLE; // 快取最近使用的方法地址,用於提升效率; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 儲存該類宣告遵守的協議的列表 #endif } /* Use `Class` instead of `struct objc_class *` */ |
由以上程式碼可見,類與物件的區別就是類比物件多了很多特徵成員,類也可以當做一個objc_object來對待,也就是說類和物件都是物件,分別稱作類物件(class object)和例項物件(instance object),這樣我們就可以區別物件和類了。
isa:objc_object(例項物件)中isa指標指向的類結構稱為class(也就是該物件所屬的類)其中存放著普通成員變數與動態方法(“-”開頭的方法);此處isa指標指向的類結構稱為metaclass,其中存放著static型別的成員變數與static型別的方法(“+”開頭的方法)。
super_class: 指向該類的父類的指標,如果該類是根類(如NSObject或NSProxy),那麼super_class就為nil。
類與物件的繼承層次關係如圖(圖片源自網路):
所有的metaclass中isa指標都是指向根metaclass,而根metaclass則指向自身。根metaclass是通過繼承根類產生的,與根class結構體成員一致,不同的是根metaclass的isa指標指向自身。
4.2 SEL
SEL是selector在Objective-C中的表示型別。selector可以理解為區別方法的ID。
1 |
typedef struct objc_selector *SEL; |
objc_selector的定義如下:
1 2 3 4 |
struct objc_selector { char *name; OBJC2_UNAVAILABLE;// 名稱 char *types; OBJC2_UNAVAILABLE;// 型別 }; |
name和types都是char型別。
4.3 IMP
終於到IMP了,它在objc.h中得定義如下:
1 |
typedef id (*IMP)(id, SEL, ...); |
IMP是“implementation”的縮寫,它是由編譯器生成的一個函式指標。當你發起一個訊息後(下文介紹),這個函式指標決定了最終執行哪段程式碼。
4.4 Method
Method代表類中的某個方法的型別。
1 |
typedef struct objc_method *Method; |
objc_method的定義如下:
1 2 3 4 5 |
struct objc_method { SEL method_name OBJC2_UNAVAILABLE; // 方法名 char *method_types OBJC2_UNAVAILABLE; // 方法型別 IMP method_imp OBJC2_UNAVAILABLE; // 方法實現 } |
方法名method_name型別為SEL,上文提到過。
方法型別method_types是一個char指標,儲存著方法的引數型別和返回值型別。
方法實現method_imp的型別為IMP,上文提到過。
4.5 Ivar
Ivar代表類中例項變數的型別
1 |
typedef struct objc_ivar *Ivar; |
objc_ivar的定義如下:
1 2 3 4 5 6 7 8 |
struct objc_ivar { char *ivar_name OBJC2_UNAVAILABLE; // 變數名 char *ivar_type OBJC2_UNAVAILABLE; // 變數型別 int ivar_offset OBJC2_UNAVAILABLE; // 基地址偏移位元組 #ifdef __LP64__ int space OBJC2_UNAVAILABLE; // 佔用空間 #endif } |
4.6 objc_property_t
objc_property_t是屬性,它的定義如下:
1 |
typedef struct objc_property *objc_property_t; |
objc_property是內建的型別,與之關聯的還有一個objc_property_attribute_t,它是屬性的attribute,也就是其實是對屬性的詳細描述,包括屬性名稱、屬性編碼型別、原子型別/非原子型別等。它的定義如下:
1 2 3 4 |
typedef struct { const char *name; // 名稱 const char *value; // 值(通常是空的) } objc_property_attribute_t; |
4.7 Cache
Catch的定義如下:
1 |
typedef struct objc_cache *Cache |
objc_cache的定義如下:
1 2 3 4 5 |
struct objc_cache { unsigned int mask OBJC2_UNAVAILABLE; unsigned int occupied OBJC2_UNAVAILABLE; Method buckets[1] OBJC2_UNAVAILABLE; }; |
mask: 指定分配cache buckets的總數。在方法查詢中,Runtime使用這個欄位確定陣列的索引位置。
occupied: 實際佔用cache buckets的總數。
buckets: 指定Method資料結構指標的陣列。這個陣列可能包含不超過mask+1個元素。需要注意的是,指標可能是NULL,表示這個快取bucket沒有被佔用,另外被佔用的bucket可能是不連續的。這個陣列可能會隨著時間而增長。
objc_msgSend(下文講解)每呼叫一次方法後,就會把該方法快取到cache列表中,下次的時候,就直接優先從cache列表中尋找,如果cache沒有,才從methodLists中查詢方法。
4.8 Catagory
這個就是我們平時所說的類別了,很熟悉吧。它可以動態的為已存在的類新增新的方法。
它的定義如下:
1 |
typedef struct objc_category *Category; |
objc_category的定義如下:
1 2 3 4 5 6 7 |
struct objc_category { char *category_name OBJC2_UNAVAILABLE; // 類別名稱 char *class_name OBJC2_UNAVAILABLE; // 類名 struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 例項方法列表 struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 類方法列表 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 協議列表 } |
因為是入門,以上就列舉這些吧!
五、Objective-C的訊息傳遞
5.1 基本訊息傳遞
在物件導向程式設計中,物件呼叫方法叫做傳送訊息。在編譯時,程式的原始碼就會從物件傳送訊息轉換成Runtime的objc_msgSend函式呼叫。
例如某例項變數receiver實現某一個方法oneMethod
1 |
[receiver oneMethod]; |
Runtime會將其轉成類似這樣的程式碼
1 |
objc_msgSend(receiver, selector); |
具體會轉換成什麼程式碼呢?
Runtime會根據型別自動轉換成下列某一個函式:
objc_msgSend:普通的訊息都會通過該函式傳送
objc_msgSend_stret:訊息中有資料結構作為返回值(不是簡單值)時,通過此函式傳送和接收返回值
objc_msgSendSuper:和objc_msgSend類似,這裡把訊息傳送給父類的例項
objc_msgSendSuper_stret:和objc_msgSend_stret類似,這裡把訊息傳送給父類的例項並接收返回值
當訊息被髮送到例項物件時,是如圖所示處理的(圖片源自網路):
objc_msgSend函式的呼叫過程:
- 第一步:檢測這個selector是不是要忽略的。
- 第二步:檢測這個target是不是nil物件。nil物件傳送任何一個訊息都會被忽略掉。
- 第三步:
1.呼叫例項方法時,它會首先在自身isa指標指向的類(class)methodLists中查詢該方法,如果找不到則會通過class的super_class指標找到父類的類物件結構體,然後從methodLists中查詢該方法,如果仍然找不到,則繼續通過super_class向上一級父類結構體中查詢,直至根class;
2.當我們呼叫某個某個類方法時,它會首先通過自己的isa指標找到metaclass,並從其中methodLists中查詢該類方法,如果找不到則會通過metaclass的super_class指標找到父類的metaclass物件結構體,然後從methodLists中查詢該方法,如果仍然找不到,則繼續通過super_class向上一級父類結構體中查詢,直至根metaclass; - 第四部:前三部都找不到就會進入動態方法解析(看下文)。
5.2 訊息動態解析
動態解析流程圖(圖片來自網路):
請參照圖片品味以下步驟(例項請看下文《6.6 蒼老師的唱歌篇》):
- 第一步:通過resolveInstanceMethod:方法決定是否動態新增方法。如果返回Yes則通過class_addMethod動態新增方法,訊息得到處理,結束;如果返回No,則進入下一步;
- 第二步:這步會進入forwardingTargetForSelector:方法,用於指定備選物件響應這個selector,不能指定為self。如果返回某個物件則會呼叫物件的方法,結束。如果返回nil,則進入第三部;
- 第三部:這步我們要通過methodSignatureForSelector:方法簽名,如果返回nil,則訊息無法處理。如果返回methodSignature,則進入下一步;
- 第四部:這步呼叫forwardInvocation:方法,我們可以通過anInvocation物件做很多處理,比如修改實現方法,修改響應物件等,如果方法呼叫成功,則結束。如果失敗,則進入doesNotRecognizeSelector方法,若我們沒有實現這個方法,那麼就會crash。
到這裡大家可能暈乎乎的,下面看實戰篇吧!蒼老師必須讓你懂!
六、Runtime實戰
請大家放心,以下所有實戰篇,在最後都會分享Demo給大家!
6.1 蒼老師問好篇
蒼老師見到我們廣大的粉絲們,第一反應當然是:大家好!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
#if TARGET_IPHONE_SIMULATOR #import <objc/objc-runtime.h> #else #import <objc/runtime.h> #import <objc/message.h> #endif // 自定義一個方法 void sayFunction(id self, SEL _cmd, id some) { NSLog(@"%@歲的%@說:%@", object_getIvar(self, class_getInstanceVariable([self class], "_age")),[self valueForKey:@"name"],some); } int main(int argc, const char * argv[]) { @autoreleasepool { // 動態建立物件 建立一個Person 繼承自 NSObject類 Class People = objc_allocateClassPair([NSObject class], "Person", 0); // 為該類新增NSString *_name成員變數 class_addIvar(People, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*)); // 為該類新增int _age成員變數 class_addIvar(People, "_age", sizeof(int), sizeof(int), @encode(int)); // 註冊方法名為say的方法 SEL s = sel_registerName("say:"); // 為該類增加名為say的方法 class_addMethod(People, s, (IMP)sayFunction, "v@:@"); // 註冊該類 objc_registerClassPair(People); // 建立一個類的例項 id peopleInstance = [[People alloc] init]; // KVC 動態改變 物件peopleInstance 中的例項變數 [peopleInstance setValue:@"蒼老師" forKey:@"name"]; // 從類中獲取成員變數Ivar Ivar ageIvar = class_getInstanceVariable(People, "_age"); // 為peopleInstance的成員變數賦值 object_setIvar(peopleInstance, ageIvar, @18); // 呼叫 peopleInstance 物件中的 s 方法選擇器對於的方法 // objc_msgSend(peopleInstance, s, @"大家好!"); // 這樣寫也可以,請看我部落格說明 ((void (*)(id, SEL, id))objc_msgSend)(peopleInstance, s, @"大家好"); peopleInstance = nil; //當People類或者它的子類的例項還存在,則不能呼叫objc_disposeClassPair這個方法;因此這裡要先銷燬例項物件後才能銷燬類; // 銷燬類 objc_disposeClassPair(People); } return 0; } |
最後的結果是:18歲的蒼老師說:大家好!
在使用
1 |
objc_msgSend(peopleInstance, s, @"大家好!"); |
預設會出現以下錯誤:
objc_msgSend()報錯Too many arguments to function call ,expected 0,have3
直接通過objc_msgSend(self, setter, value)是報錯,說引數過多。
請這樣解決:
Build Setting–> Apple LLVM 7.0 – Preprocessing–> Enable Strict Checking of objc_msgSend Calls 改為 NO
當然你也可以這樣寫(推薦):
1 |
((void (*)(id, SEL, id))objc_msgSend)(peopleInstance, s, @"大家好"); |
強制轉換objc_msgSend函式型別為帶三個引數且返回值為void函式,然後才能傳三個引數。
此實戰內容是,動態建立一個類,並建立成員變數和方法,最後賦值成員變數併傳送訊息。其中成員變數的賦值使用了KVC和object_setIvar函式兩種方式,這些東西大家舉一反三就可以了。
Demo傳送門->6.1蒼老師問好篇Demo
6.2 蒼老師的特徵篇
蒼老師在大家心目中應該有很多特徵吧,下面我們通過程式碼來獲取蒼老師的特徵。
People.h檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@interface People : NSObject { NSString *_occupation; NSString *_nationality; } @property (nonatomic, copy) NSString *name; @property (nonatomic) NSUInteger age; - (NSDictionary *)allProperties; - (NSDictionary *)allIvars; - (NSDictionary *)allMethods; @end |
People.m檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
#if TARGET_IPHONE_SIMULATOR #import <objc/objc-runtime.h> #else #import <objc/runtime.h> #import <objc/message.h> #endif @implementation People - (NSDictionary *)allProperties { unsigned int count = 0; // 獲取類的所有屬性,如果沒有屬性count就為0 objc_property_t *properties = class_copyPropertyList([self class], &count); NSMutableDictionary *resultDict = [<a href="http://www.jobbole.com/members/www821839432">@{}</a> mutableCopy]; for (NSUInteger i = 0; i < count; i ++) { // 獲取屬性的名稱和值 const char *propertyName = property_getName(properties[i]); NSString *name = [NSString stringWithUTF8String:propertyName]; id propertyValue = [self valueForKey:name]; if (propertyValue) { resultDict[name] = propertyValue; } else { resultDict[name] = @"字典的key對應的value不能為nil哦!"; } } // 這裡properties是一個陣列指標,我們需要使用free函式來釋放記憶體。 free(properties); return resultDict; } - (NSDictionary *)allIvars { unsigned int count = 0; NSMutableDictionary *resultDict = [<a href="http://www.jobbole.com/members/www821839432">@{}</a> mutableCopy]; Ivar *ivars = class_copyIvarList([self class], &count); for (NSUInteger i = 0; i < count; i ++) { const char *varName = ivar_getName(ivars[i]); NSString *name = [NSString stringWithUTF8String:varName]; id varValue = [self valueForKey:name]; if (varValue) { resultDict[name] = varValue; } else { resultDict[name] = @"字典的key對應的value不能為nil哦!"; } } free(ivars); return resultDict; } - (NSDictionary *)allMethods { unsigned int count = 0; NSMutableDictionary *resultDict = [<a href="http://www.jobbole.com/members/www821839432">@{}</a> mutableCopy]; // 獲取類的所有方法,如果沒有方法count就為0 Method *methods = class_copyMethodList([self class], &count); for (NSUInteger i = 0; i < count; i ++) { // 獲取方法名稱 SEL methodSEL = method_getName(methods[i]); const char *methodName = sel_getName(methodSEL); NSString *name = [NSString stringWithUTF8String:methodName]; // 獲取方法的引數列表 int arguments = method_getNumberOfArguments(methods[i]); resultDict[name] = @(arguments-2); } free(methods); return resultDict; } @end |
在main.m中執行以下程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
int main(int argc, const char * argv[]) { @autoreleasepool { People *cangTeacher = [[People alloc] init]; cangTeacher.name = @"蒼井空"; cangTeacher.age = 18; [cangTeacher setValue:@"老師" forKey:@"occupation"]; NSDictionary *propertyResultDic = [cangTeacher allProperties]; for (NSString *propertyName in propertyResultDic.allKeys) { NSLog(@"propertyName:%@, propertyValue:%@",propertyName, propertyResultDic[propertyName]); } NSDictionary *ivarResultDic = [cangTeacher allIvars]; for (NSString *ivarName in ivarResultDic.allKeys) { NSLog(@"ivarName:%@, ivarValue:%@",ivarName, ivarResultDic[ivarName]); } NSDictionary *methodResultDic = [cangTeacher allMethods]; for (NSString *methodName in methodResultDic.allKeys) { NSLog(@"methodName:%@, argumentsCount:%@", methodName, methodResultDic[methodName]); } } return 0; } |
最後的輸出結果如下:
是不是有點失望,我沒有加一些特殊的技能,留給下文了。此實戰內容是通過蒼老師的一些特徵學習:如何獲取物件所有的屬性名稱和屬性值、獲取物件所有成員變數名稱和變數值、獲取物件所有的方法名和方法引數數量。
Demo傳送門->6.2蒼老師的特徵篇Demo
6.3 蒼老師增加新技能篇
蒼老師要通過Category和Associated Objects增加技能了,快看!
建立People+Associated.h檔案如下:
1 2 3 4 5 6 7 8 9 10 |
#import "People.h" typedef void (^CodingCallBack)(); @interface People (Associated) @property (nonatomic, strong) NSNumber *associatedBust; // 胸圍 @property (nonatomic, copy) CodingCallBack associatedCallBack; // 寫程式碼 @end |
People+Associated.m如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#import "People+Associated.h" #if TARGET_IPHONE_SIMULATOR #import <objc/objc-runtime.h> #else #import <objc/runtime.h> #import <objc/message.h> #endif @implementation People (Associated) - (void)setAssociatedBust:(NSNumber *)bust { // 設定關聯物件 objc_setAssociatedObject(self, @selector(associatedBust), bust, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSNumber *)associatedBust { // 得到關聯物件 return objc_getAssociatedObject(self, @selector(associatedBust)); } - (void)setAssociatedCallBack:(CodingCallBack)callback { objc_setAssociatedObject(self, @selector(associatedCallBack), callback, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (CodingCallBack)associatedCallBack { return objc_getAssociatedObject(self, @selector(associatedCallBack)); } @end |
在main.m中執行以下程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#import "People.h" #import "People+Associated.h" int main(int argc, const char * argv[]) { @autoreleasepool { People *cangTeacher = [[People alloc] init]; cangTeacher.name = @"蒼井空"; cangTeacher.age = 18; [cangTeacher setValue:@"老師" forKey:@"occupation"]; cangTeacher.associatedBust = @(90); cangTeacher.associatedCallBack = ^(){ NSLog(@"蒼老師要寫程式碼了!"); }; cangTeacher.associatedCallBack(); NSDictionary *propertyResultDic = [cangTeacher allProperties]; for (NSString *propertyName in propertyResultDic.allKeys) { NSLog(@"propertyName:%@, propertyValue:%@",propertyName, propertyResultDic[propertyName]); } NSDictionary *methodResultDic = [cangTeacher allMethods]; for (NSString *methodName in methodResultDic.allKeys) { NSLog(@"methodName:%@, argumentsCount:%@", methodName, methodResultDic[methodName]); } } return 0; } |
這次執行結果多出了一個associatedBust(胸圍)和一個associatedCallBack(寫程式碼)屬性。
如圖:
我們成功的給蒼老師新增個一個胸圍的屬性和一個寫程式碼的回撥,但是新增屬性沒有什麼意義,我們平時在開發過成功中用的比較多的就是新增回撥了。
Demo傳送門->6.3蒼老師增加新技能篇Demo
6.4 蒼老師的資料歸檔篇
蒼老師的資料總要整理一下吧!
建立People.h
1 2 3 4 5 6 7 8 9 10 11 |
#import <Foundation/Foundation.h> @interface People : NSObject <NSCoding> @property (nonatomic, copy) NSString *name; // 姓名 @property (nonatomic, strong) NSNumber *age; // 年齡 @property (nonatomic, copy) NSString *occupation; // 職業 @property (nonatomic, copy) NSString *nationality; // 國籍 @end P |
People.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#import "People.h" #if TARGET_IPHONE_SIMULATOR #import <objc/objc-runtime.h> #else #import <objc/runtime.h> #import <objc/message.h> #endif @implementation People - (void)encodeWithCoder:(NSCoder *)aCoder { unsigned int count = 0; Ivar *ivars = class_copyIvarList([People class], &count); for (NSUInteger i = 0; i < count; i ++) { Ivar ivar = ivars[i]; const char *name = ivar_getName(ivar); NSString *key = [NSString stringWithUTF8String:name]; id value = [self valueForKey:key]; [aCoder encodeObject:value forKey:key]; } free(ivars); } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super init]; if (self) { unsigned int count = 0; Ivar *ivars = class_copyIvarList([People class], &count); for (NSUInteger i = 0; i < count; i ++) { Ivar ivar = ivars[i]; const char *name = ivar_getName(ivar); NSString *key = [NSString stringWithUTF8String:name]; id value = [aDecoder decodeObjectForKey:key]; [self setValue:value forKey:key]; } free(ivars); } return self; } @end |
Demo傳送門->6.4蒼老師的資料歸檔篇Demo
6.5 蒼老師的資料轉換篇
伺服器返回了大量蒼老師的資料,手機端這邊接收後如何去轉換呢?當然是要將JSON轉換為Model啦!
相信平時你們的專案中也用到過這些三方庫,下面我們去了解下runtime實現JSON和Model互轉。
建立People.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#import <Foundation/Foundation.h> @interface People : NSObject @property (nonatomic, copy) NSString *name; // 姓名 @property (nonatomic, strong) NSNumber *age; // 年齡 @property (nonatomic, copy) NSString *occupation; // 職業 @property (nonatomic, copy) NSString *nationality; // 國籍 // 生成model - (instancetype)initWithDictionary:(NSDictionary *)dictionary; // 轉換成字典 - (NSDictionary *)covertToDictionary; @end |
People.m的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
#import "People.h" #if TARGET_IPHONE_SIMULATOR #import <objc/objc-runtime.h> #else #import <objc/runtime.h> #import <objc/message.h> #endif @implementation People - (instancetype)initWithDictionary:(NSDictionary *)dictionary { self = [super init]; if (self) { for (NSString *key in dictionary.allKeys) { id value = dictionary[key]; SEL setter = [self propertySetterByKey:key]; if (setter) { // 這裡還可以使用NSInvocation或者method_invoke,不再繼續深究了,有興趣google。 ((void (*)(id, SEL, id))objc_msgSend)(self, setter, value); } } } return self; } - (NSDictionary *)covertToDictionary { unsigned int count = 0; objc_property_t *properties = class_copyPropertyList([self class], &count); if (count != 0) { NSMutableDictionary *resultDict = [<a href="http://www.jobbole.com/members/www821839432">@{}</a> mutableCopy]; for (NSUInteger i = 0; i < count; i ++) { const void *propertyName = property_getName(properties[i]); NSString *name = [NSString stringWithUTF8String:propertyName]; SEL getter = [self propertyGetterByKey:name]; if (getter) { id value = ((id (*)(id, SEL))objc_msgSend)(self, getter); if (value) { resultDict[name] = value; } else { resultDict[name] = @"字典的key對應的value不能為nil哦!"; } } } free(properties); return resultDict; } free(properties); return nil; } #pragma mark - private methods // 生成setter方法 - (SEL)propertySetterByKey:(NSString *)key { // 首字母大寫,你懂得 NSString *propertySetterName = [NSString stringWithFormat:@"set%@:", key.capitalizedString]; SEL setter = NSSelectorFromString(propertySetterName); if ([self respondsToSelector:setter]) { return setter; } return nil; } // 生成getter方法 - (SEL)propertyGetterByKey:(NSString *)key { SEL getter = NSSelectorFromString(key); if ([self respondsToSelector:getter]) { return getter; } return nil; } @end |
main.m中執行以下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#import <Foundation/Foundation.h> #if TARGET_IPHONE_SIMULATOR #import <objc/objc-runtime.h> #else #import <objc/runtime.h> #import <objc/message.h> #endif #import "People.h" int main(int argc, const char * argv[]) { @autoreleasepool { NSDictionary *dict = @{ @"name" : @"蒼井空", @"age" : @18, @"occupation" : @"老師", @"nationality" : @"日本" }; // 字典轉模型 People *cangTeacher = [[People alloc] initWithDictionary:dict]; NSLog(@"熱烈歡迎,從%@遠道而來的%@歲的%@%@",cangTeacher.nationality,cangTeacher.age,cangTeacher.name,cangTeacher.occupation); // 模型轉字典 NSDictionary *covertedDict = [cangTeacher covertToDictionary]; NSLog(@"%@",covertedDict); } return 0; } |
最後輸出內容如下:
相信通過前面的學習,這些程式碼不用寫過多的註釋你也可以看懂了,我把假設是網路返回的蒼老師的資料轉化為了model,然後又將model轉回字典。這是一個JSON轉Model相互轉換的一個思路,大家稍後執行Demo細細品味。
Demo傳送門->6.5蒼老師的資料轉換篇Demo
6.6 蒼老師的唱歌篇
這個例項主要是驗證一下上文《5.2 訊息動態解析》
第一首:
新增sing例項方法,但是不提供方法的實現。驗證當找不到方法的實現時,動態新增方法。
建立People.h
1 2 3 4 5 6 7 8 9 |
#import <Foundation/Foundation.h> @interface People : NSObject @property (nonatomic, copy) NSString *name; - (void)sing; @end |
建立People.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#import "People.h" #if TARGET_IPHONE_SIMULATOR #import <objc/objc-runtime.h> #else #import <objc/runtime.h> #import <objc/message.h> #endif @implementation People + (BOOL)resolveInstanceMethod:(SEL)sel { // 我們沒有給People類宣告sing方法,我們這裡動態新增方法 if ([NSStringFromSelector(sel) isEqualToString:@"sing"]) { class_addMethod(self, sel, (IMP)otherSing, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; } void otherSing(id self, SEL cmd) { NSLog(@"%@ 唱歌啦!",((People *)self).name); } |
在main.m中執行以下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#import <Foundation/Foundation.h> #if TARGET_IPHONE_SIMULATOR #import <objc/objc-runtime.h> #else #import <objc/runtime.h> #import <objc/message.h> #endif #import "People.h" int main(int argc, const char * argv[]) { @autoreleasepool { People *cangTeacher = [[People alloc] init]; cangTeacher.name = @"蒼老師"; [cangTeacher sing]; } return 0; } |
結果如下:
我們沒有提供蒼老師唱歌的方法實現,因此在呼叫此方法的時候,會呼叫resolveInstanceMethod方法,我們動態新增了方法。我們也可以返回No,繼續向下傳遞。(此處請返回《5.2 訊息動態解析》第一步品味下)
Demo傳送門->6.6蒼老師唱歌篇(第一首)Demo
第二首
外面的小鳥在唱歌,但是蒼老師的歌聲蓋過了小鳥,我們只能聽到蒼老師唱歌了。
這裡我們不宣告sing方法,將呼叫途中動態更換呼叫物件。
在第一首程式碼的基礎上,建立Bird的model
Bird.h
1 2 3 4 5 6 7 |
#import <Foundation/Foundation.h> @interface Bird : NSObject @property (nonatomic, copy) NSString *name; @end |
Bird.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
#import "Bird.h" #import "People.h" @implementation Bird // 第一步:我們不動態新增方法,返回NO,進入第二步; + (BOOL)resolveInstanceMethod:(SEL)sel { return NO; } // 第二部:我們不指定備選物件響應aSelector,進入第三步; - (id)forwardingTargetForSelector:(SEL)aSelector { return nil; } // 第三步:返回方法選擇器,然後進入第四部; - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return [super methodSignatureForSelector:aSelector]; } // 第四部:這步我們修改呼叫物件 - (void)forwardInvocation:(NSInvocation *)anInvocation { // 我們改變呼叫物件為People People *cangTeacher = [[People alloc] init]; cangTeacher.name = @"蒼老師"; [anInvocation invokeWithTarget:cangTeacher]; } @end |
main.m執行程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#import <Foundation/Foundation.h> #if TARGET_IPHONE_SIMULATOR #import <objc/objc-runtime.h> #else #import <objc/runtime.h> #import <objc/message.h> #endif #import "People.h" #import "Bird.h" int main(int argc, const char * argv[]) { @autoreleasepool { Bird *bird = [[Bird alloc] init]; bird.name = @"小小鳥"; ((void (*)(id, SEL))objc_msgSend)((id)bird, @selector(sing)); } return 0; } |
執行結果如下:
成功更換了物件,把物件更換為蒼老師了。(此處請返回《5.2 訊息動態解析》品味)
Demo傳送門->6.6蒼老師唱歌篇(第二首)Demo
第三首
蒼老師不想唱歌想跳舞了。
這裡我是實現不提供宣告,不修改呼叫物件,但是將sing方法修改為dance方法。
建立People.h
1 2 3 4 5 |
#import <Foundation/Foundation.h> @interface People : NSObject @end |
People.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
#import "People.h" #if TARGET_IPHONE_SIMULATOR #import <objc/objc-runtime.h> #else #import <objc/runtime.h> #import <objc/message.h> #endif @implementation People // 第一步:我們不動態新增方法,返回NO,進入第二步; + (BOOL)resolveInstanceMethod:(SEL)sel { return NO; } // 第二部:我們不指定備選物件響應aSelector,進入第三步; - (id)forwardingTargetForSelector:(SEL)aSelector { return nil; } // 第三步:返回方法選擇器,然後進入第四部; - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return [super methodSignatureForSelector:aSelector]; } // 第四部:這步我們修改呼叫方法 - (void)forwardInvocation:(NSInvocation *)anInvocation { [anInvocation setSelector:@selector(dance)]; // 這還要指定是哪個物件的方法 [anInvocation invokeWithTarget:self]; } // 若forwardInvocation沒有實現,則會呼叫此方法 - (void)doesNotRecognizeSelector:(SEL)aSelector { NSLog(@"訊息無法處理:%@", NSStringFromSelector(aSelector)); } - (void)dance { NSLog(@"跳舞!!!come on!"); } @end |
在main.m中執行如下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#import <Foundation/Foundation.h> #if TARGET_IPHONE_SIMULATOR #import <objc/objc-runtime.h> #else #import <objc/runtime.h> #import <objc/message.h> #endif #import "People.h" int main(int argc, const char * argv[]) { @autoreleasepool { People *cangTeacher = [[People alloc] init]; ((void(*)(id, SEL)) objc_msgSend)((id)cangTeacher, @selector(sing)); } return 0; } |
結果如圖:
成功更換了方法,蒼老師由唱歌改為跳舞了(此處請返回《5.2 訊息動態解析》品味)
Demo傳送門->6.6蒼老師唱歌篇(第三首)Demo
總結
好吧,我承認我騙了你,當你讀到這裡你肯定花了不止1小時。都是我的錯,不是因為你笨,之所以說1小時是為了讓你有信心,有耐心繼續下去。讀到這裡恭喜你已經在iOS開發的道路上又向前了一步!同時我也要感謝以下參考文獻以及文章,是他們讓我更好的理解了runtime,再次表示感謝!這篇文章斷斷續續寫了將近一週的時間,您可以讀到這裡就是對我最大的鼓勵,謝謝!
Demo傳送門->所有的Demo打包下載
本文參考文獻以及文章:
Objective-C Runtime Reference
Object Model
初識Objective-C Runtime
Objective-C Runtime
Objective-C Runtime 執行時之一:類與物件
Runtime Message Forwarding
runtime模型與字典互轉
iOS開發之深入探討runtime機制
Objective-C runtime之訊息(二)
Objective-C runtime之訊息轉發機制(三)
《編寫高質量程式碼:改善Objective-C程式的61個建議》
《正規表示式30分鐘入門教程》(參考寫作方式)