什么是runtime?
runtime 是 OC底層的一套C語言的API(引入 <objc/runtime.h>
或<objc/message.h>
),編譯器最終都會將OC代碼轉化為運行時代碼,通過終端命令編譯.m 文件:clang -rewrite-objcxxx.m
可以看到編譯后的xxx.cpp(C++文件)。 比如我們創建了一個對象 [[NSObject alloc]init]
,最終被轉換為幾萬行代碼,截取最關鍵的一句可以看到底層是通過runtime創建的對象
.cpp 文件
刪除掉一些強制轉換語句,可以看到調用方法本質就是發消息,[[NSObject alloc]init]
語句發了兩次消息,第一次發了alloc消息,第二次發送init 消息。利用這個功能我們可以探究底層,比如block的實現原理。 需要注意的是,使用objc_msgSend()
sel_registerName()
方法需要導入頭文件<objc/message.h>
RunTime中常用的基本概念
SEL
它是selector
在 Objc 中的表示(Swift 中是 Selector 類)。selector是方法選擇器,其實作用就和名字一樣,日常生活中,我們通過人名辨別誰是誰,注意 Objc 在相同的類中不會有命名相同的兩個方法。selector對方法名進行包裝,以便找到對應的方法實現。它的數據結構是:
typedef struct objc_selector *SEL;
我們可以看出它是個映射到方法的 C 字符串,你可以通過 Objc 編譯器器命令@selector()
或者 Runtime 系統的sel_registerName
函數來獲取一個 SEL
類型的方法選擇器。
注意: 不同類中相同名字的方法所對應的 selector 是相同的,由于變量的類型不同,所以不會導致它們調用方法實現混亂。
id
id 是一個參數類型,它是指向某個類的實例的指針。定義如下:
typedef struct objc_object *id;
struct objc_object { Class isa; };
以上定義,看到 objc_object
結構體包含一個isa
指針,根據 isa 指針就可以找到對象所屬的類。
注意: isa 指針在代碼運行時并不總指向實例對象所屬的類型,所以不能依靠它來確定類型,要想確定類型還是需要用對象的 -class 方法。KVO 的實現機理就是將被觀察對象的 isa 指針指向一個中間類而不是真實類型。
Class
typedef struct objc_class *Class;
Class 其實是指向 objc_class
結構體的指針。objc_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_class 可以看到,一個運行時類中關聯了它的父類指針、類名、成員變量、方法、緩存以及附屬的協議。
其中 objc_ivar_list
和 objc_method_list
分別是成員變量列表
和方法列表
:
// 成員變量列表
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
// 方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
由此可見,我們可以動態修改 *methodList
的值來添加成員方法,這也是 Category 實現的原理,同樣解釋了 Category不能添加屬性的原因。
objc_ivar_list
結構體用來存儲成員變量的列表,而 objc_ivar
則是存儲了單個成員變量的信息;同理,objc_method_list
結構體存儲著方法數組的列表,而單個方法的信息則由objc_method
結構體存儲。
值得注意的時,objc_class
中也有一個 isa 指針,這說明 Objc 類本身也是一個對象。為了處理類和對象的關系,Runtime庫創建了一種叫做 Meta Class(元類)
的東西, 類對象所屬的類就叫做元類 。Meta Class 表述了類對象本身所具備的元數據。
我們所熟悉的類方法,就源自于 Meta Class。我們可以理解為類方法就是類對象的實例方法。每個類僅有一個類對象,而每個類對象僅有一個與之相關的元類。
當你發出一個類似 [NSObject alloc](類方法)
的消息時,實際上,這個消息被發送給了一個類對象(ClassObject),這個類對象必須是一個元類的實例,而這個元類同時也是一個根元類(Root Meta Class)的實例。所有元類的 isa指針最終都指向根元類。
所以當 [NSObject alloc]
這條消息發送給類對象的時候,運行時代碼 objc_msgSend()
會去它元類中查找能夠響應消息的方法實現,如果找到了,就會對這個類對象執行方法調用。
上圖實現是 super_class
指針,虛線時 isa
指針。而根元類的父類是 NSObject
,isa指向了自己。而 NSObject沒有父類。
最后 objc_class
中還有一個 objc_cache
,緩存,它的作用很重要,后面會提到。
Method
Method 代表類中某個方法的類型
typedef struct objc_method *Method;
?
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
objc_method 存儲了方法名,方法類型和方法實現:
方法名類型為 SEL
方法類型 method_types 是個 char 指針,存儲方法的參數類型和返回值類型
method_imp 指向了方法的實現,本質是一個函數指針
Ivar
Ivar 是表示成員變量的類型。
typedef struct objc_ivar *Ivar;
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
}
其中 ivar_offset
是基地址偏移字節
IMP
IMP在objc.h中的定義是:
typedef id (*IMP)(id, SEL, ...);
它就是一個函數指針
,這是由編譯器生成的。當你發起一個 ObjC 消息之后,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP這個函數指針就指向了這個方法的實現。
如果得到了執行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執行方法,這在后面 Cache 中會提到。
你會發現 IMP
指向的方法與 objc_msgSend
函數類型相同,參數都包含 id
和SEL
類型。每個方法名都對應一個 SEL類型的方法選擇器,而每個實例對象中的 SEL 對應的方法實現肯定是唯一的,通過一組 id和 SEL 參數就能確定唯一的方法實現地址。
而 一個確定的方法也只有唯一的一組 id 和 SEL 參數 。
Cache
Cache 定義如下:
typedef struct objc_cache *Cache
?
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache 為方法調用的 性能進行優化 , 每當實例對象接收到一個消息時,它不會直接在 isa指針指向的類的方法列表中遍歷查找能夠響應的方法,因為每次都要查找效率太低了,而是優先在 Cache 中查找 。
Runtime 系統會把被調用的方法存到 Cache中,如果一個方法被調用,那么它有可能今后還會被調用,下次查找的時候就會效率更高。就像計算機組成原理中 CPU 繞過主存先訪問 Cache 一樣。
RunTime在實際開發中的應用
利用runtime 可以做一些OC不容易實現的功能
動態交換兩個方法的實現(特別是交換系統自帶的方法)
動態添加對象的成員變量和成員方法
獲得某個類的所有成員方法、所有成員變量
*將某些OC代碼轉為運行時代碼,探究底層,比如block的實現原理
*攔截系統自帶的方法調用(Swizzle 黑魔法),比如攔截imageNamed:、viewDidLoad、alloc
*實現NSCoding的自動歸檔和自動解檔
*實現字典和模型的自動轉換。
下面通過demo 一個個來講解具體應用
一、交換兩個方法的實現,攔截系統自帶的方法調用功能
需要用到的方法
<objc/runtime.h>
-
獲得某個類的類方法
Method class_getClassMethod(Class cls , SEL name)
-
獲得某個類的實例對象方法
Method class_getInstanceMethod(Class cls , SEL name)
-
交換兩個方法的實現
void method_exchangeImplementations(Method m1 , Method m2)
想使用runtime,首先在寫運行時代碼之前,要先加上頭文件:
#import <objc/objc-runtime.h> // 模擬器
或者
#import <objc/runtime.h> // 真機
#import <objc/message.h> // 真機
一、動態添加一個類
KVO
的實現是利用了runtime能夠動態添加類)
原來當你對一個對象進行觀察時, 系統會自動新建一個類繼承自原類, 然后重寫被觀察屬性的setter方法.然后重寫的setter方法會負責在調用原setter方法前后通知觀察者. 然后把原對象的isa指針指向這個新類, 我們知道,對象是通過isa指針去查找自己是屬于哪個類, 并去所在類的方法列表中查找方法的, 所以這個時候這個對象就自然地變成了新類的實例對象.
就像KVO一樣, 系統是在程序運行的時候根據你要監聽的類, 動態添加一個新類繼承自該類, 然后重寫原類的setter方法并在里面通知observer的.
那么, 如何動態添加一個類呢?
// 創建一個類(size_t extraBytes該參數通常指定為0, 該參數是分配給類和元類對象尾部的索引ivars的字節數。)
Class clazz = objc_allocateClassPair([NSObject class], "GoodPerson", 0);
?
// 添加ivar
// @encode(aType) : 返回該類型的C字符串
class_addIvar(clazz, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
?
class_addIvar(clazz, "_age", sizeof(NSUInteger), log2(sizeof(NSUInteger)), @encode(NSUInteger));
?
// 注冊該類
objc_registerClassPair(clazz);
?
// 創建實例對象
id object = [[clazz alloc] init];
?
// 設置ivar
[object setValue:@"Tracy" forKey:@"name"];
?
Ivar ageIvar = class_getInstanceVariable(clazz, "_age");
object_setIvar(object, ageIvar, @18);
?
// 打印對象的類和內存地址
NSLog(@"%@", object);
?
// 打印對象的屬性值
NSLog(@"name = %@, age = %@", [object valueForKey:@"name"], object_getIvar(object, ageIvar));
?
// 當類或者它的子類的實例還存在,則不能調用objc_disposeClassPair方法
object = nil;
?
// 銷毀類
objc_disposeClassPair(clazz);
這樣, 我們就在程序運行時動態添加了一個繼承自NSObject的GoodPerson類, 并為該類添加了name和age成員變量.
二、通過runtime獲取一個類的所有屬性,我們可以做些什么?
1. 打印一個類的所有ivar, property 和 method(簡單直接的使用)
Person *p = [[Person alloc] init];
[p setValue:@"Kobe" forKey:@"name"];
[p setValue:@18 forKey:@"age"];
// p.address = @"廣州大學城";
p.weight = 110.0f;
?
// 1.打印所有ivars
unsigned int ivarCount = 0;
// 用一個字典裝ivarName和value
NSMutableDictionary *ivarDict = [NSMutableDictionary dictionary];
Ivar *ivarList = class_copyIvarList([p class], &ivarCount);
for(int i = 0; i < ivarCount; i++){
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivarList[i])];
id value = [p valueForKey:ivarName];
?
if (value) {
ivarDict[ivarName] = value;
} else {
ivarDict[ivarName] = @"值為nil";
}
}
// 打印ivar
for (NSString *ivarName in ivarDict.allKeys) {
NSLog(@"ivarName:%@, ivarValue:%@",ivarName, ivarDict[ivarName]);
}
?
// 2.打印所有properties
unsigned int propertyCount = 0;
// 用一個字典裝propertyName和value
NSMutableDictionary *propertyDict = [NSMutableDictionary dictionary];
objc_property_t *propertyList = class_copyPropertyList([p class], &propertyCount);
for(int j = 0; j < propertyCount; j++){
NSString *propertyName = [NSString stringWithUTF8String:property_getName(propertyList[j])];
id value = [p valueForKey:propertyName];
?
if (value) {
propertyDict[propertyName] = value;
} else {
propertyDict[propertyName] = @"值為nil";
}
}
// 打印property
for (NSString *propertyName in propertyDict.allKeys) {
NSLog(@"propertyName:%@, propertyValue:%@",propertyName, propertyDict[propertyName]);
}
?
// 3.打印所有methods
unsigned int methodCount = 0;
// 用一個字典裝methodName和arguments
NSMutableDictionary *methodDict = [NSMutableDictionary dictionary];
Method *methodList = class_copyMethodList([p class], &methodCount);
for(int k = 0; k < methodCount; k++){
SEL methodSel = method_getName(methodList[k]);
NSString *methodName = [NSString stringWithUTF8String:sel_getName(methodSel)];
?
unsigned int argumentNums = method_getNumberOfArguments(methodList[k]);
?
methodDict[methodName] = @(argumentNums - 2); // -2的原因是每個方法內部都有self 和 selector 兩個參數
}
// 打印method
for (NSString *methodName in methodDict.allKeys) {
NSLog(@"methodName:%@, argumentsCount:%@", methodName, methodDict[methodName]);
}
2. 動態變量控制
在程序中,XiaoMing的age是10,后來被runtime變成了20,來看看runtime是怎么做到的:
-(void)changeAge{
unsigned int count = 0;
//動態獲取XiaoMing類中的所有屬性[當然包括私有]
Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);
//遍歷屬性找到對應age字段
for (int i = 0; i<count; i++) {
Ivar var = ivar[I];
const char *varName = ivar_getName(var);
NSString *name = [NSString stringWithUTF8String:varName];
if ([name isEqualToString:@"_age"]) {
//修改對應的字段值成20
object_setIvar(self.xiaoMing, var, @"20");
break;
}
}
NSLog(@"XiaoMing's age is %@",self.xiaoMing.age);
}
3. 在NSObject的分類中增加方法來避免使用KVC賦值的時候出現崩潰
在有些時候我們需要通過KVC去修改某個類的私有變量,但是又不知道該屬性是否存在,如果類中不存在該屬性,那么通過KVC賦值就會crash,這時也可以通過運行時進行判斷。同樣我們在NSObject的分類中增加如下方法。
* 判斷類中是否有該屬性
*
* @param property 屬性名稱
*
* @return 判斷結果
*/
-(BOOL)hasProperty:(NSString *)property {
BOOL flag = NO;
u_int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
const char *propertyName = ivar_getName(ivars[I]);
NSString *propertyString = [NSString stringWithUTF8String:propertyName];
if ([propertyString isEqualToString:property]){
flag = YES;
}
}
}
三、動態添加方法
開發使用場景 :如果一個類方法非常多,加載類到內存的時候也比較耗費資源,需要給每個方法生成映射表,可以使用動態給某個類,添加方法解決。
經典面試題 :有沒有使用performSelector,其實主要想問你有沒有動態添加過方法。
簡單使用:
@implementation ViewController
?
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
?
Person *p = [[Person alloc] init];
?
// 默認person,沒有實現eat方法,可以通過performSelector調用,但是會報錯。
// 動態添加方法就不會報錯
[p performSelector:@selector(eat)];
?
}
@end
@implementation Person
// void(*)()
// 默認方法都有兩個隱式參數,
void eat(id self,SEL sel)
{
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
?
// 當一個對象調用未實現的方法,會調用這個方法處理,并且會把對應的方法列表傳過來.
// 剛好可以用來判斷,未實現的方法是不是我們想要動態添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
?
if (sel == @selector(eat)) {
// 動態添加eat方法
?
// 第一個參數:給哪個類添加方法
// 第二個參數:添加方法的方法編號
// 第三個參數:添加方法的函數實現(函數地址)
// 第四個參數:函數的類型,(返回值+參數類型) v:void @:對象->self :表示SEL->_cmd
class_addMethod(self, @selector(eat), eat, "v@:");
?
}
?
return [super resolveInstanceMethod:sel];
}
@end
四、方法簡單的交換
創建一個Person類,類中實現以下兩個類方法,并在.h 文件中聲明
+ (void)run {
NSLog(@"跑");
}
?
+ (void)study {
NSLog(@"學習");
}
控制器中調用,則先打印跑,后打印學習
[Person run];
[Person study];
下面通過runtime 實現方法交換,類方法用class_getClassMethod
,對象方法用class_getInstanceMethod
// 獲取兩個類的類方法
Method m1 = class_getClassMethod([Person class], @selector(run));
Method m2 = class_getClassMethod([Person class], @selector(study));
// 開始交換方法實現
method_exchangeImplementations(m1, m2);
// 交換后,先打印學習,再打印跑!
[Person run];
[Person study];
五、攔截系統方法
需求:比如iOS6 升級 iOS7后需要版本適配,根據不同系統使用不同樣式圖片(擬物化和扁平化),如何通過不去手動一個個修改每個UIImage的imageNamed:方法就可以實現為該方法中加入版本判斷語句?
步驟: 1、為UIImage建一個分類(UIImage+Category) 2、在分類中實現一個自定義方法,方法中寫要在系統方法中加入的語句,比如版本判斷
+ (UIImage *)xh_imageNamed:(NSString *)name {
double version = [[UIDevice currentDevice].systemVersion doubleValue];
if (version >= 7.0) {
// 如果系統版本是7.0以上,使用另外一套文件名結尾是‘_os7’的扁平化圖片
name = [name stringByAppendingString:@"_os7"];
}
return [UIImage xh_imageNamed:name];
}
3、分類中重寫UIImage的load方法,實現方法的交換(只要能讓其執行一次方法交換語句,load再合適不過了)
+ (void)load {
// 獲取兩個類的類方法
Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
Method m2 = class_getClassMethod([UIImage class], @selector(xh_imageNamed:));
// 開始交換方法實現
method_exchangeImplementations(m1, m2);
}
注意:自定義方法中最后一定要再調用一下系統的方法,讓其有加載圖片的功能,但是由于方法交換,系統的方法名已經變成了我們自定義的方法名(有點繞,就是用我們的名字能調用系統的方法,用系統的名字能調用我們的方法),這就實現了系統方法的攔截!
利用以上思路,我們還可以給 NSObject添加分類,統計創建了多少個對象,給控制器添加分類,統計有創建了多少個控制器,特別是公司需求總變的時候,在一些原有控件或模塊上添加一個功能,建議使用該方法!
二、在分類中設置屬性,給任何一個對象設置屬性
眾所周知,分類中是無法設置屬性的,如果在分類的聲明中寫@property 只能為其生成get 和 set方法的聲明,但無法生成成員變量,就是雖然點語法能調用出來,但程序執行后會crash,有人會想到使用全局變量呢?比如這樣:
int _age;
?
- (int )age {
return _age;
}
?
- (void)setAge:(int)age {
_age = age;
}
但是全局變量程序整個執行過程中內存中只有一份,我們創建多個對象修改其屬性值都會修改同一個變量,這樣就無法保證像屬性一樣每個對象都擁有其自己的屬性值。這時我們就需要借助runtime為分類增加屬性的功能了。
需要用到的方法 <objc/runtime.h>
- set方法,將值value 跟對象object 關聯起來(將值value 存儲到對象object 中) 參數 object:給哪個對象設置屬性 參數 key:一個屬性對應一個Key,將來可以通過key取出這個存儲的值,key 可以是任何類型:double、int 等,建議用char 可以節省字節 參數 value:給屬性設置的值 參數policy:存儲策略 (assign 、copy 、 retain就是strong)
void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)
- 利用參數key 將對象object中存儲的對應值取出來
id objc_getAssociatedObject(id object , const void *key)
步驟: 1、創建一個分類,比如給任何一個對象都添加一個name屬性,就是NSObject添加分類(NSObject+Category) 2、先在.h 中@property 聲明出get 和 set 方法,方便點語法調用
NSString *name;
3、在.m 中重寫set 和 get 方法,內部利用runtime 給屬性賦值和取值
?
- (void)setName:(NSString *)name {
// 將某個值跟某個對象關聯起來,將某個值存儲到某個對象中
objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
?
- (NSString *)name {
return objc_getAssociatedObject(self, &nameKey);
}
三、獲得一個類的所有成員變量
最典型的用法就是一個對象在歸檔和解檔的encodeWithCoder和initWithCoder:方法中需要該對象所有的屬性進行decodeObjectForKey: 和encodeObject:,通過runtime我們聲明中無論寫多少個屬性,都不需要再修改實現中的代碼了。
需要用到的方法 <objc/runtime.h>
- 獲得某個類的所有成員變量(outCount 會返回成員變量的總數) 參數: 1、哪個類 2、放一個接收值的地址,用來存放屬性的個數 3、返回值:存放所有獲取到的屬性,通過下面兩個方法可以調出名字和類型
Ivar *class_copyIvarList(Class cls , unsigned int *outCount)
- 獲得成員變量的名字
const char *ivar_getName(Ivar v)
- 獲得成員變量的類型
const char *ivar_getTypeEndcoding(Ivar v)
案例1:獲取Person類中所有成員變量的名字和類型
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([Person class], &outCount);
?
// 遍歷所有成員變量
for (int i = 0; i < outCount; i++) {
// 取出i位置對應的成員變量
Ivar ivar = ivars[I];
const char *name = ivar_getName(ivar);
const char *type = ivar_getTypeEncoding(ivar);
NSLog(@"成員變量名:%s 成員變量類型:%s",name,type);
}
// 注意釋放內存!
free(ivars);
案例2:利用runtime 獲取所有屬性來重寫歸檔解檔方法
// 設置不需要歸解檔的屬性
- (NSArray *)ignoredNames {
return @[@"_aaa",@"_bbb",@"_ccc"];
}
?
// 解檔方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
// 獲取所有成員變量
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[I];
// 將每個成員變量名轉換為NSString對象類型
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 忽略不需要解檔的屬性
if ([[self ignoredNames] containsObject:key]) {
continue;
}
// 根據變量名解檔取值,無論是什么類型
id value = [aDecoder decodeObjectForKey:key];
// 取出的值再設置給屬性
[self setValue:value forKey:key];
// 這兩步就相當于以前的 self.age = [aDecoder decodeObjectForKey:@"_age"];
}
free(ivars);
}
return self;
}
?
// 歸檔調用方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
// 獲取所有成員變量
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[I];
// 將每個成員變量名轉換為NSString對象類型
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 忽略不需要歸檔的屬性
if ([[self ignoredNames] containsObject:key]) {
continue;
}
// 通過成員變量名,取出成員變量的值
id value = [self valueForKeyPath:key];
// 再將值歸檔
[aCoder encodeObject:value forKey:key];
// 這兩步就相當于 [aCoder encodeObject:@(self.age) forKey:@"_age"];
}
free(ivars);
}
依據上面的原理我們就可以給NSObject做一個分類,讓我們不需要每次都寫這么一長串代碼,只要實現一小段代碼就可以讓一個對象具有歸解檔的能力。
注意,下面的代碼我換了一個方法名(不然會覆蓋系統原來的方法!),加了一個忽略屬性方法是否被實現的判斷,并加上了對父類屬性的歸解檔循環。
NSObject+Extension.h
#import <Foundation/Foundation.h>
?
@interface NSObject (Extension)
?
- (NSArray *)ignoredNames;
- (void)encode:(NSCoder *)aCoder;
- (void)decode:(NSCoder *)aDecoder;
?
@end
NSObject+Extension.m
#import "NSObject+Extension.h"
#import <objc/runtime.h>
?
@implementation NSObject (Extension)
?
- (void)decode:(NSCoder *)aDecoder {
// 一層層父類往上查找,對父類的屬性執行歸解檔方法
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(c, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[I];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 如果有實現該方法再去調用
if ([self respondsToSelector:@selector(ignoredNames)]) {
if ([[self ignoredNames] containsObject:key]) continue;
}
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
c = [c superclass];
}
}
?
- (void)encode:(NSCoder *)aCoder {
// 一層層父類往上查找,對父類的屬性執行歸解檔方法
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[I];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 如果有實現該方法再去調用
if ([self respondsToSelector:@selector(ignoredNames)]) {
if ([[self ignoredNames] containsObject:key]) continue;
}
id value = [self valueForKeyPath:key];
[aCoder encodeObject:value forKey:key];
}
free(ivars);
c = [c superclass];
}
}
@end
上面分類使用方法:在需要歸解檔的對象中實現下面方法即可:
// 設置需要忽略的屬性
- (NSArray *)ignoredNames {
return @[@"bone"];
}
?
// 在系統方法內來調用我們的方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
[self decode:aDecoder];
}
return self;
}
?
- (void)encodeWithCoder:(NSCoder *)aCoder {
[self encode:aCoder];
}
這樣看來,我們每次又要寫同樣的代碼,我們可以將歸解檔兩個方法封裝為宏,在需要的地方一句宏搞定,如果有不需要歸解檔的屬性就實現ignoredNames方法,具體可以看我的demo,這個也是MJExtension中那個一句宏就可以解決歸解檔的實現原理。
案例3:利用runtime 獲取所有屬性來進行字典轉模型
字典轉模型的第一步:設計模型
模型屬性,通常需要跟字典中的key一一對應
問題:一個一個的生成模型屬性,很慢?
需求:能不能自動根據一個字典,生成對應的屬性。
解決:提供一個分類,專門根據字典生成對應的屬性字符串。
@implementation NSObject (Log)
// 自動打印屬性字符串
+ (void)resolveDict:(NSDictionary *)dict{
?
// 拼接屬性字符串代碼
NSMutableString *strM = [NSMutableString string];
?
// 1.遍歷字典,把字典中的所有key取出來,生成對應的屬性代碼
[dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
?
// 類型經常變,抽出來
NSString *type;
?
if ([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) {
type = @"NSString";
}else if ([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]){
type = @"NSArray";
}else if ([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]){
type = @"int";
}else if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]){
type = @"NSDictionary";
}
// 屬性字符串
NSString *str;
if ([type containsString:@"NS"]) {
str = [NSString stringWithFormat:@"@property (nonatomic, strong) %@ *%@;",type,key];
}else{
str = [NSString stringWithFormat:@"@property (nonatomic, assign) %@ %@;",type,key];
}
?
// 每生成屬性字符串,就自動換行。
[strM appendFormat:@"\n%@\n",str];
?
}];
?
// 把拼接好的字符串打印出來,就好了。
NSLog(@"%@",strM);
?
}
@end
(字典轉模型的核心就是可以遍歷出字典中的每個屬性,json解析中大??蚣芏加昧诉@個特性,包括MJEXtension,YYModel,jsonModel都是將json轉換為字典,再遍歷字典中的每個屬性來進行modle的轉換)。
基本上主流的json 轉model都少不了,使用運行時動態獲取屬性的屬性名的方法,來進行字典轉模型替換,字典轉模型效率最高的(耗時最短的)的是KVC,其他的字典轉模型是在KVC 的key和Value 做處理,動態的獲取json 中的key 和value ,當然轉換的過程中,第三方框架需要做一些判空
啊,鑲嵌
的邏輯處理,再進行KVC
轉模型.
無論JsonModle,YYKIt,MJextension 都少不了[xx setValue:valueforKey:key];
這句代碼的,這是字典轉模型的核心方法。
以往我們都是利用KVC進行字典轉模型,但是它還是有一定的局限性,例如:模型屬性和鍵值對對應不上會crash(雖然可以重寫setValue:forUndefinedKey:方法防止報錯),模型屬性是一個對象或者數組時不好處理等問題,所以無論是效率還是功能上,利用runtime進行字典轉模型都是比較好的選擇。
字典轉模型我們需要考慮三種特殊情況: 1.當字典的key和模型的屬性匹配不上 2.模型中嵌套模型(模型屬性是另外一個模型對象) 3.數組中裝著模型(模型的屬性是一個數組,數組中是一個個模型對象)
根據上面的三種特殊情況,我們一個個處理,先是字典的key和模型的屬性不對應的情況。 不對應有兩種,一種是字典的鍵值大于模型屬性數量,這時候我們不需要任何處理,因為runtime是先遍歷模型所有屬性,再去字典中根據屬性名找對應值進行賦值,多余的鍵值對也當然不會去看了;另外一種是模型屬性數量大于字典的鍵值對,這時候由于屬性沒有對應值會被賦值為nil,就會導致crash,我們只需加一個判斷即可,JSON數據和sample如下:
JSON數據
- (void)setDict:(NSDictionary *)dict {
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(c, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[I];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 成員變量名轉為屬性名(去掉下劃線 _ )
key = [key substringFromIndex:1];
// 取出字典的值
id value = dict[key];
// 如果模型屬性數量大于字典鍵值對數理,模型屬性會被賦值為nil而報錯
if (value == nil) continue;
// 將字典中的值設置到模型上
[self setValue:value forKeyPath:key];
}
free(ivars);
c = [c superclass];
}
}
第二種情況是模型的屬性是另外一個模型對象
JSON數據
這時候我們就需要利用runtime的ivar_getTypeEncoding方法獲取模型對象類型,對該模型對象類型再進行字典轉模型,也就是進行遞歸,需要注意的是我們要排除系統的對象類型,例如NSString
,下面的方法中我添加了一個類方法方便遞歸。
打印可以看到各屬性類型
#import "NSObject+JSONExtension.h"
#import <objc/runtime.h>
?
@implementation NSObject (JSONExtension)
?
- (void)setDict:(NSDictionary *)dict {
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(c, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[I];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 成員變量名轉為屬性名(去掉下劃線 _ )
key = [key substringFromIndex:1];
// 取出字典的值
id value = dict[key];
// 如果模型屬性數量大于字典鍵值對數理,模型屬性會被賦值為nil而報錯
if (value == nil) continue;
// 獲得成員變量的類型
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 如果屬性是對象類型
NSRange range = [type rangeOfString:@"@"];
if (range.location != NSNotFound) {
// 那么截取對象的名字(比如@"Dog",截取為Dog)
type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
// 排除系統的對象類型
if (![type hasPrefix:@"NS"]) {
// 將對象名轉換為對象的類型,將新的對象字典轉模型(遞歸)
Class class = NSClassFromString(type);
value = [class objectWithDict:value];
}
}
// 將字典中的值設置到模型上
[self setValue:value forKeyPath:key];
}
free(ivars);
c = [c superclass];
}
}
?
+ (instancetype )objectWithDict:(NSDictionary *)dict {
NSObject *obj = [[self alloc]init];
[obj setDict:dict];
return obj;
}
第三種情況是模型的屬性是一個數組,數組中是一個個模型對象,例如下面的數據我就可以通過books[0].name
獲取到C語言程序設計
JSON數據
我們既然能獲取到屬性類型,那就可以攔截到模型的那個數組屬性,進而對數組中每個模型遍歷并字典轉模型,但是我們不知道數組中的模型都是什么類型,我們可以聲明一個方法,該方法目的不是讓其調用,而是讓其實現并返回模型的類型。 這塊語言可能解釋不太清楚,可以參考我的demo,直接運行即可。
NSObject+JSONExtension.h
// 返回數組中都是什么類型的模型對象
- (NSString *)arrayObjectClass ;
NSObject+JSONExtension.m
#import "NSObject+JSONExtension.h"
#import <objc/runtime.h>
?
@implementation NSObject (JSONExtension)
?
- (void)setDict:(NSDictionary *)dict {
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(c, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[I];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 成員變量名轉為屬性名(去掉下劃線 _ )
key = [key substringFromIndex:1];
// 取出字典的值
id value = dict[key];
// 如果模型屬性數量大于字典鍵值對數理,模型屬性會被賦值為nil而報錯
if (value == nil) continue;
// 獲得成員變量的類型
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 如果屬性是對象類型
NSRange range = [type rangeOfString:@"@"];
if (range.location != NSNotFound) {
// 那么截取對象的名字(比如@"Dog",截取為Dog)
type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
// 排除系統的對象類型
if (![type hasPrefix:@"NS"]) {
// 將對象名轉換為對象的類型,將新的對象字典轉模型(遞歸)
Class class = NSClassFromString(type);
value = [class objectWithDict:value];
}else if ([type isEqualToString:@"NSArray"]) {
// 如果是數組類型,將數組中的每個模型進行字典轉模型,先創建一個臨時數組存放模型
NSArray *array = (NSArray *)value;
NSMutableArray *mArray = [NSMutableArray array];
// 獲取到每個模型的類型
id class ;
if ([self respondsToSelector:@selector(arrayObjectClass)]) {
NSString *classStr = [self arrayObjectClass];
class = NSClassFromString(classStr);
}
// 將數組中的所有模型進行字典轉模型
for (int i = 0; i < array.count; i++) {
[mArray addObject:[class objectWithDict:value[i]]];
}
value = mArray;
}
}
// 將字典中的值設置到模型上
[self setValue:value forKeyPath:key];
}
free(ivars);
c = [c superclass];
}
}
?
+ (instancetype )objectWithDict:(NSDictionary *)dict {
NSObject *obj = [[self alloc]init];
[obj setDict:dict];
return obj;
}
?
@end
實現push跳轉可配
在你的開發過程中,是否遇到過如下的需求:
在tableView類型的展示列表中,點擊每個cell中人物頭像都可以跳轉到人物詳情,可參見微博中的頭像,同理包括轉發、評論按鈕、各種鏈接及linkcard。
跳轉到任意頁面
(1)產品要求,某個頁面的不同banner圖,點擊可以跳轉到任何一個頁面,可能是原生的頁面A、頁面B,或者是web頁C。
(2)在web頁面,可以跳轉到任何一個原生頁面。
(3)在遠程推送中跳轉到任意指定的頁面。
以上2種需求,我想大多數開發者都遇到過,并且可以實現這種功能。畢竟,這是比較基礎的功能。但是代碼未必那么優雅。
一般處理辦法
針對 1:一般初學者會用target或者block等方法在tableView的代理方法拿到事件,并把要執行的跳轉寫到controller里。功能是可以實現的,但問題是這種cell及相似的cell(布局有些變化,或者多幾個少幾個控件)一般出現在多個頁面。這樣的話相同的代碼就會出現在多個地方。就算把跳轉方法抽取出來寫成category,但是target或者block總是每個地方都要寫的。
針對 2: 初級的方法是每個地方寫一坨判斷及跳轉,高級一些是抽取出來寫在基類或者category。
優雅的解決辦法(利用runtime)
利用runtime動態生成對象、屬性、方法這特性,我們可以先跟服務端商量好,定義跳轉規則,比如要跳轉到A控制器,需要傳屬性id、type,那么服務端返回字典給我,里面有控制器名,兩個屬性名跟屬性值,客戶端就可以根據控制器名生成對象,再用kvc給對象賦值,這樣就搞定了。
舉例:比如根據推送規則跳轉對應界面HSFeedsViewController
HSFeedsViewController.h:
進入該界面需要傳的屬性
@interface HSFeedsViewController : UIViewController
// 注:根據下面的兩個屬性,可以從服務器獲取對應的頻道列表數據
/** 頻道ID */
@property (nonatomic, copy) NSString *ID;
/** 頻道type */
@property (nonatomic, copy) NSString *type;
@end
AppDelegate.m 中添加以下代碼片段:
推送過來的消息規則
// 這個規則肯定事先跟服務端溝通好,跳轉對應的界面需要對應的參數
NSDictionary *userInfo = @{
@"class": @"HSFeedsViewController",
@"property": @{
@"ID": @"123",
@"type": @"12"
}
};
接收推送消息
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
[self push:userInfo];
}
跳轉界面:
- (void)push:(NSDictionary *)params
{
// 類名
NSString *class =[NSString stringWithFormat:@"%@", params[@"class"]];
const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];
// 從一個字串返回一個類
Class newClass = objc_getClass(className);
if (!newClass)
{
// 創建一個類
Class superClass = [UIViewController class];
newClass = objc_allocateClassPair(superClass, className, 0);
// 注冊你創建的這個類
objc_registerClassPair(newClass);
}
// 創建對象
id instance = [[newClass alloc] init];
// 對該對象賦值屬性
NSDictionary * propertys = params[@"property"];
[propertys enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
// 檢測這個對象是否存在該屬性
if ([self checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {
// 利用kvc賦值
[instance setValue:obj forKey:key];
}
}];
// 獲取導航控制器
UITabBarController *tabVC = (UITabBarController *)self.window.rootViewController;
UINavigationController *pushClassStance = (UINavigationController *)tabVC.viewControllers[tabVC.selectedIndex];
// 跳轉到對應的控制器
[pushClassStance pushViewController:instance animated:YES];
}
檢測對象是否存在該屬性:
- (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName
{
unsigned int outCount, I;
// 獲取對象里的屬性列表
objc_property_t * properties = class_copyPropertyList([instance
class], &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property =properties[I];
// 屬性名轉成字符串
NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
// 判斷該屬性是否存在
if ([propertyName isEqualToString:verifyPropertyName]) {
free(properties);
return YES;
}
}
free(properties);
return NO;
}