Runtime是一套底層純C語言API,OC代碼最終都會被編譯器轉化爲運行時代碼,經過消息機制決定函數調用方式,這也是OC做爲動態語言使用的基礎。
複製代碼
在Object-C的語言中,對象方法調用都是相似[receiver selector]
的形式,其本質:就是讓對象在運行時發送消息的過程。
json
而方法調用[receiver selector]
分爲兩個過程:數組
[receiver selector]
方法被編譯器轉化,分爲兩種狀況:緩存
1.不帶參數的方法被編譯爲:objc_msgSend(receiver,selector)
2.帶參數的方法被編譯爲:objc_msgSend(recevier,selector,org1,org2,…)
複製代碼
消息接收者recever
尋找對應的selector
,也分爲兩種狀況:bash
1.接收者能找到對應的selector,直接執行接收receiver對象的selector方法。
2.接收者找不到對應的selector,消息被轉發或者臨時向接收者添加這個selector對應的實現內容,不然崩潰
複製代碼
總而言之:數據結構
OC調用方法[receiver selector],編譯階段肯定了要向哪一個接收者發送message消息,可是接收者如何響應決定於運行時的判斷
複製代碼
1.3.1 objc_msgSendapp
全部 Objective-C 方法調用在編譯時都會轉化爲對 C 函數 objc_msgSend 的調用。objc_msgSend(receiver,selector); 是 [receiver selector]; 對應的 C 函數
複製代碼
1.3.2 Object(對象)框架
在 objc/runtime.h 中
,Object(對象)
被定義爲指向 objc_object
結構體 的指針,objc_object
結構體 的數據結構以下:函數
//runtime對objc_object結構體的定義
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
//id是一個指向objc_object結構體的指針,即在Runtime中:
typedef struct objc_object *id;
//OC中的對象雖然沒有明顯的使用指針,可是在OC代碼被編譯轉化爲C以後,每一個OC對象其實都是擁有一個isa的指針的
複製代碼
1.3.2 Class(類)測試
在 objc/runtime.h 中
,Class(類)
被定義爲指向 objc_class
結構體 的指針,objc_class
結構體 的數據結構以下:ui
//runtime對objc_class結構體的定義
struct objc_class {
Class _Nonnull isa; // objc_class 結構體的實例指針
#if !__OBJC2__
Class _Nullable super_class; // 指向父類的指針
const char * _Nonnull name; // 類的名字
long version; // 類的版本信息,默認爲 0
long info; // 類的信息,供運行期使用的一些位標識
long instance_size; // 該類的實例變量大小;
struct objc_ivar_list * _Nullable ivars; // 該類的實例變量列表
struct objc_method_list * _Nullable * _Nullable methodLists; // 方法定義的列表
struct objc_cache * _Nonnull cache; // 方法緩存
struct objc_protocol_list * _Nullable protocols; // 遵照的協議列表
#endif
};
//class是一個指向objc_class結構體的指針,即在Runtime中:
typedef struct objc_class *Class;
複製代碼
1.3.3 SEL (方法選擇器)
在 objc/runtime.h 中
,SEL (方法選擇器)
被定義爲指向 objc_selector
結構體 的指針:
typedef struct objc_selector *SEL;
//Objective-C在編譯時,會依據每個方法的名字、參數序列,生成一個惟一的整型標識(Int類型的地址),這個標識就是SEL
複製代碼
注意:
1.不一樣類中相同名字的方法對應的方法選擇器是相同的。
2.即便是同一個類中,方法名相同而變量類型不一樣也會致使它們具備相同的方法選擇器。
複製代碼
一般獲取SEL有三種方法:
1.OC中,使用@selector(「方法名字符串」)
2.OC中,使用NSSelectorFromString(「方法名字符串」)
3.Runtime方法,使用sel_registerName(「方法名字符串」)
複製代碼
1.3.4 Ivar
在 objc/runtime.h 中
,Ivar
被定義爲指向 objc_ivar
結構體 的指針,objc_ivar
結構體 的數據結構以下:
struct objc_ivar {
char * Nullable ivar_name OBJC2UNAVAILABLE;
char * Nullable ivar_type OBJC2UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef LP64
int space OBJC2_UNAVAILABLE;
#endif
}
//Ivar表明類中實例變量的類型,是一個指向ojbcet_ivar的結構體的指針
typedef struct objc_ivar *Ivar;
複製代碼
在objc_class
中看到的ivars
成員列表,其中的元素就是Ivar
,能夠經過實例查找其在類中的名字,這個過程被稱爲反射,下面的class_copyIvarList
獲取的不只有實例變量還有屬性:
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i= 0; i<count; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
複製代碼
1.3.5 Method(方法)
在 objc/runtime.h 中
,Method(方法)
被定義爲指向 objc_method
結構體 的指針,在objct_class
定義中看到methodLists
,其中的元素就是Method
,objc_method
結構體 的數據結構以下:
struct objc_method {
SEL _Nonnull method_name; // 方法名
char * _Nullable method_types; // 方法類型
IMP _Nonnull method_imp; // 方法實現
};
//Method表示某個方法的類型
typedef struct objc_method *Method;
複製代碼
OC代碼會在編譯階段被編譯器轉化。OC中的類、方法和協議等在Runtime中都由一些數據結構來定義。
因此在平常的項目開發過程當中,使用OC語言進行編碼時,這已是在和Runtime進行交互了,只是這個過程對於開發者而言是無感的
複製代碼
Runtime的最大特徵就是實現了OC語言的動態特性。
複製代碼
做爲大部分Objective-C
類繼承體系的根類的NSObject
,其自己就具備了一些很是具備運行時動態特性的方法, 好比:
1. -respondsToSelector:方法能夠檢查在代碼運行階段當前對象是否能響應指定的消息
2. -description:返回當前類的描述信息
3. -isKindOfClass: 和 -isMemberOfClass: 檢查對象是否存在於指定的類的繼承體系中
4. -conformsToProtocol: 檢查對象是否實現了指定協議類的方法;
5. -methodForSelector: 返回指定方法實現的地址。
複製代碼
Runtime系統是一個由一系列函數和數據結構組成,具備公共接口的動態共享庫。頭文件存放於/usr/include/objc目錄下。
複製代碼
在項目工程代碼裏引用Runtime
的頭文件,一樣可以實現相似OC代碼的效果:
//至關於:Class class = [UIView class];
Class viewClass = objc_getClass("UIView");
//至關於:UIView *view = [UIView alloc];
UIView *view = ((id (*)(id, SEL))(void *)objc_msgSend)((id)viewClass, sel_registerName("alloc"));
//至關於:UIView *view = [view init];
((id (*)(id, SEL))(void *)objc_msgSend)((id)view, sel_registerName("init"));
複製代碼
Runtime足夠強大,可以在運行時動態添加一個未實現的方法,這個功能主要有兩個應用場景:
1. 動態添加未實現方法,解決代碼中由於方法未找到而報錯的問題;
2. 利用懶加載思路,若一個類有不少個方法,同時加載到內存中會耗費資源,可使用動態解析添加方法
複製代碼
方法動態解析主要用到的方法以下:
//OC方法:
//類方法未找到時調起,可於此添加類方法實現
+ (BOOL)resolveClassMethod:(SEL)sel
//實例方法未找到時調起,可於此添加實例方法實現
+ (BOOL)resolveInstanceMethod:(SEL)sel
//Runtime方法:
/**
運行時方法:向指定類中添加特定方法實現的操做
@param cls 被添加方法的類
@param name selector方法名
@param imp 指向實現方法的函數指針
@param types imp函數實現的返回值與參數類型
@return 添加方法是否成功
*/
BOOL class_addMethod(Class _Nullable cls,
SEL _Nonnull name,
IMP _Nonnull imp,
const char * _Nullable types)
複製代碼
執行OC方法其實就是一個發送消息的過程,若方法未實現,能夠利用方法動態解析與消息轉發來避免程序崩潰,這主要涉及下面一個處理未實現消息的過程:
在這個過程當中,可能還會使用到的方法有:
#import "ViewController.h"
#import <objc/runtime.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 執行 fun 函數
[self performSelector:@selector(fun)];
}
// 重寫 resolveInstanceMethod: 添加對象方法實現
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(fun)) { // 若是是執行 fun 函數,就動態解析,指定新的 IMP
class_addMethod([self class], sel, (IMP)funMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void funMethod(id obj, SEL _cmd) {
NSLog(@"funMethod"); //新的 fun 函數
}
@end
//日誌輸出:
2019-09-01 23:24:34.911774+0800 XKRuntimeKit[3064:521123] funMethod
複製代碼
從執行任務的輸出日誌中,能夠看到:
雖然沒有實現 fun 方法,可是經過重寫 resolveInstanceMethod: ,利用 class_addMethod 方法添加對象方法實現 funMethod 方法,並執行。從打印結果來看,成功調起了funMethod 方法。
複製代碼
若是上一步中 +resolveInstanceMethod:
或者 +resolveClassMethod
: 沒有添加其餘函數實現,運行時就會進行下一步:消息接受者重定向。
若是當前對象實現了 -forwardingTargetForSelector:
,Runtime
就會調用這個方法,容許將消息的接受者轉發給其餘對象,其主要方法以下:
//重定向類方法的消息接收者,返回一個類
- (id)forwardingTargetForSelector:(SEL)aSelector
//重定向實例方法的消息接受者,返回一個實例對象
- (id)forwardingTargetForSelector:(SEL)aSelector
複製代碼
#import "ViewController.h"
#import <objc/runtime.h>
@interface Person : NSObject
- (void)fun;
@end
@implementation Person
- (void)fun {
NSLog(@"fun");
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 執行 fun 方法
[self performSelector:@selector(fun)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES; // 爲了進行下一步 消息接受者重定向
}
// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(fun)) {
return [[Person alloc] init];
// 返回 Person 對象,讓 Person 對象接收這個消息
}
return [super forwardingTargetForSelector:aSelector];
}
//日誌輸出:
2019-09-01 23:24:34.911774+0800 XKRuntimeKit[3064:521123] fun
複製代碼
從執行任務的輸出日誌中,能夠看到:
雖然當前 ViewController 沒有實現 fun 方法,+resolveInstanceMethod: 也沒有添加其餘函數實現。
可是咱們經過 forwardingTargetForSelector 把當前 ViewController 的方法轉發給了 Person 對象去執行了
複製代碼
經過forwardingTargetForSelector
能夠修改消息的接收者,該方法返回參數是一個對象,若是這個對象是否是 nil
,也不是 self
,系統會將運行的消息轉發給這個對象執行。不然,繼續進行下一步:消息重定向流程
若是通過消息動態解析、消息接受者重定向,Runtime 系統仍是找不到相應的方法實現而沒法響應消息,Runtime 系統會利用 -methodSignatureForSelector:
方法獲取函數的參數和返回值類型。
其過程:
1. 若是 -methodSignatureForSelector: 返回了一個 NSMethodSignature 對象(函數簽名),Runtime 系統就會建立一個 NSInvocation 對象,
並經過 -forwardInvocation: 消息通知當前對象,給予這次消息發送最後一次尋找 IMP 的機會。
2. 若是 -methodSignatureForSelector: 返回 nil。則 Runtime 系統會發出 -doesNotRecognizeSelector: 消息,程序也就崩潰了
複製代碼
因此能夠在-forwardInvocation:
方法中對消息進行轉發。
其主要方法:
// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;
// 獲取函數的參數和返回值類型,返回簽名
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;
複製代碼
#import "ViewController.h"
#import <objc/runtime.h>
@interface Person : NSObject
- (void)fun;
@end
@implementation Person
- (void)fun {
NSLog(@"fun");
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 執行 fun 函數
[self performSelector:@selector(fun)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES; // 爲了進行下一步 消息接受者重定向
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil; // 爲了進行下一步 消息重定向
}
// 獲取函數的參數和返回值類型,返回簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector; // 從 anInvocation 中獲取消息
Person *p = [[Person alloc] init];
if([p respondsToSelector:sel]) { // 判斷 Person 對象方法是否能夠響應 sel
[anInvocation invokeWithTarget:p]; // 若能夠響應,則將消息轉發給其餘對象處理
} else {
[self doesNotRecognizeSelector:sel]; // 若仍然沒法響應,則報錯:找不到響應方法
}
}
@end
//日誌輸出:
2019-09-01 23:24:34.911774+0800 XKRuntimeKit[30032:8724248] fun
複製代碼
從執行任務的輸出日誌中,能夠看到:
在 -forwardInvocation: 方法裏面讓 Person 對象去執行了 fun 函數
複製代碼
既然 -forwardingTargetForSelector:
和 -forwardInvocation:
均可以將消息轉發給其餘對象處理,那麼二者的區別在哪?
區別就在於 -forwardingTargetForSelector: 只能將消息轉發給一個對象。而 -forwardInvocation: 能夠將消息轉發給多個對象。
複製代碼
實現動態方法交換(Method Swizzling )是Runtime中最具盛名的應用場景,其原理是:
經過Runtime獲取到方法實現的地址,進而動態交換兩個方法的功能
複製代碼
//獲取類方法的Mthod
Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
//獲取實例對象方法的Mthod
Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
//交換兩個方法的實現
void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
複製代碼
#import "RuntimeKit.h"
#import <objc/runtime.h>
@implementation RuntimeKit
- (instancetype)init
{
self = [super init];
if (self) {
//交換方法的實現,並測試打印
Method methodA = class_getInstanceMethod([self class], @selector(testA));
Method methodB = class_getInstanceMethod([self class], @selector(testB));
method_exchangeImplementations(methodA, methodB);
[self testA];
[self testB];
}
return self;
}
- (void)testA{
NSLog(@"我是A方法");
}
- (void)testB{
NSLog(@"我是B方法");
}
@end
日誌輸出:
2019-09-01 21:25:32.858860+0800 XKRuntimeKit[1662:280727] 我是B方法
2019-09-01 21:25:32.859059+0800 XKRuntimeKit[1662:280727] 我是A方法
複製代碼
#import "UIViewController+xk.h"
#import <objc/runtime.h>
@implementation UIViewController (xk)
+ (void)load{
//獲取系統方法地址
Method sytemMethod = class_getInstanceMethod([self class], @selector(viewWillAppear:));
//獲取自定義方法地址
Method customMethod = class_getInstanceMethod([self class], @selector(run_viewWillAppear:));
//判斷存在與否
if (!class_addMethod([self class], @selector(viewWillAppear:), method_getImplementation(customMethod), method_getTypeEncoding(customMethod))) {
method_exchangeImplementations(sytemMethod, customMethod);
}
else{
class_replaceMethod([self class], @selector(run_viewWillAppear:), method_getImplementation(sytemMethod), method_getTypeEncoding(sytemMethod));
}
}
- (void)run_viewWillAppear:(BOOL)animated{
[self run_viewWillAppear:animated];
NSLog(@"我是運行時替換的方法-viewWillAppear");
}
- (void)run_viewWillDisappear:(BOOL)animated{
[self run_viewWillDisappear:animated];
NSLog(@"我是運行時替換的方法-viewWillDisappear");
}
@end
日誌輸出:
2019-09-01 21:36:55.610385+0800 XKRuntimeKit[1921:310118] 我是運行時替換的方法-viewWillAppear
複製代碼
將該分類引入,從執行結果能夠看到,但系統的控制器執行viewWillAppear
時,則會進入已經替換的方法run_viewWillAppear
之中。
在平常開發過程當中,經常會使用類目Category
爲一些已有的類擴展功能。雖然繼承也可以爲已有類增長新的方法,並且相比類目更是具備增長屬性的優點,可是繼承畢竟是一個重量級的操做,添加沒必要要的繼承關係無疑增長了代碼的複雜度。
遺憾的是,OC的類目並不支持直接添加屬性
複製代碼
爲了實現給分類添加屬性,還需藉助 Runtime的關聯對象(Associated Objects)
特性,它可以幫助咱們在運行階段將任意的屬性關聯到一個對象上:
/**
1.給對象設置關聯屬性
@param object 須要設置關聯屬性的對象,即給哪一個對象關聯屬性
@param key 關聯屬性對應的key,可經過key獲取這個屬性,
@param value 給關聯屬性設置的值
@param policy 關聯屬性的存儲策略(對應Property屬性中的assign,copy,retain等)
OBJC_ASSOCIATION_ASSIGN @property(assign)。
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property(strong, nonatomic)。
OBJC_ASSOCIATION_COPY_NONATOMIC @property(copy, nonatomic)。
OBJC_ASSOCIATION_RETAIN @property(strong,atomic)。
OBJC_ASSOCIATION_COPY @property(copy, atomic)。
*/
void objc_setAssociatedObject(id _Nonnull object,
const void * _Nonnull key,
id _Nullable value,
objc_AssociationPolicy policy)
/**
2.經過key獲取關聯的屬性
@param object 從哪一個對象中獲取關聯屬性
@param key 關聯屬性對應的key
@return 返回關聯屬性的值
*/
id _Nullable objc_getAssociatedObject(id _Nonnull object,
const void * _Nonnull key)
/**
3.移除對象所關聯的屬性
@param object 移除某個對象的全部關聯屬性
*/
void objc_removeAssociatedObjects(id _Nonnull object)
複製代碼
注意:
key與關聯屬性一一對應,咱們必須確保其全局惟一性,經常使用咱們使用@selector(methodName)做爲key
複製代碼
在UIViewController+xk.h
中新增一個name
屬性:
@interface UIViewController (xk)
//新增屬性:名稱
@property(nonatomic,copy)NSString * name;
- (void)clearAssociatedObjcet;
@end
複製代碼
在UIViewController+xk.m
中補充對應的實現:
#import "UIViewController+xk.h"
#import <objc/runtime.h>
@implementation UIViewController (xk)
//set方法
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self,
@selector(name),
name,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//get方法
- (NSString *)name{
return objc_getAssociatedObject(self,
@selector(name));
}
//添加一個自定義方法,用於清除全部關聯屬性
- (void)clearAssociatedObjcet{
objc_removeAssociatedObjects(self);
}
@end
複製代碼
執行任務:
ViewController * vc = [ViewController new];
vc.name = @"我是根控制器";
NSLog(@"獲取關聯屬性name:%@",vc.name);
[vc clearAssociatedObjcet];
NSLog(@"獲取關聯屬性name:%@",vc.name);
日誌輸出:
2019-09-01 21:50:05.162915+0800 XKRuntimeKit[2066:335327] 獲取關聯屬性name:我是根控制器
2019-09-01 21:50:05.163080+0800 XKRuntimeKit[2066:335327] 獲取關聯屬性name:(null)
複製代碼
一樣的,使用運行時還能夠爲類目新增一些自身沒有的方法,好比給UIView
新增點擊事件:
#import <objc/runtime.h>
static char onTapGestureKey;
static char onTapGestureBlockKey;
@implementation UIView (Gesture)
//添加輕拍手勢
- (void)addTapGestureActionWithBlock:(onGestureActionBlock)block{
UITapGestureRecognizer *gesture = objc_getAssociatedObject(self, &onTapGestureKey);
self.userInteractionEnabled = YES;
if (!gesture){
gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(xk_handleActionForTapGesture:)];
[self addGestureRecognizer:gesture];
objc_setAssociatedObject(self, &onTapGestureKey, gesture, OBJC_ASSOCIATION_RETAIN);
}
//添加點擊手勢響應代碼塊屬性
objc_setAssociatedObject(self, &onTapGestureBlockKey, block, OBJC_ASSOCIATION_COPY);
}
//點擊回調
- (void)xk_handleActionForTapGesture:(UITapGestureRecognizer*)sender{
onGestureActionBlock block = objc_getAssociatedObject(self, &onTapGestureBlockKey);
if (block) block(sender);
}
@end
複製代碼
可是使用運行時給類目新增代理屬性時,須要注意循環應用問題,因爲運行時執行添加的屬性都是retain操做,因此每每在執行過程會致使對應的 delegate
得不到釋放,於是會致使崩潰,對此,能夠進行如下修改操做:
emptyDataDelegate
空頁面代理,以處理一些異常狀況的顯示在UIView+EmptyDataSet.h
中新增一個emptyDataDelegate
屬性,:
//頁面無數據代理
@protocol XKEmptyDataSetDelegate <NSObject>
@optional
//佔位文字
- (NSString*)placeholderForEmptyDataSet:(UIScrollView*)scrollView;
@end
//空頁面設置
@interface UIView (EmptyDataSet)
@property (nonatomic,weak) id<XKEmptyDataSetDelegate>emptyDataDelegate;
@end
複製代碼
在UIView+EmptyDataSet.m
中藉助XKEmptyDataWeakObjectContainer
實現其方法:
//弱引用代理
@interface XKEmptyDataWeakObjectContainer : NSObject
@property (nonatomic,weak,readonly)id weakObject;
- (instancetype)initWithWeakObject:(id)object;
@end
@implementation XKEmptyDataWeakObjectContainer
- (instancetype)initWithWeakObject:(id)object{
self = [super init];
if (self) {
_weakObject = object;
}
return self;
}
@end
static char xk_EmptyDataSetDelegateKey;
//空視圖設置
@implementation UIView (EmptyDataSet)
- (void)setEmptyDataDelegate:(id<XKEmptyDataSetDelegate>)emptyDataDelegate{
objc_setAssociatedObject(self, &xk_EmptyDataSetDelegateKey, [[XKEmptyDataWeakObjectContainer alloc] initWithWeakObject:emptyDataDelegate], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id<XKEmptyDataSetDelegate>)emptyDataDelegate{
XKEmptyDataWeakObjectContainer * container = objc_getAssociatedObject(self, &xk_EmptyDataSetDelegateKey);
return container.weakObject;
}
@end
複製代碼
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"PropertyName(%d): %@",i,[NSString stringWithUTF8String:propertyName]);
}
free(propertyList);
複製代碼
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i= 0; i<count; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
複製代碼
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i = 0; i<count; i++) {
Method method = methodList[i];
SEL mthodName = method_getName(method);
NSLog(@"MethodName(%d): %@",i,NSStringFromSelector(mthodName));
}
free(methodList);
複製代碼
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (int i=0; i<count; i++) {
Protocol *protocal = protocolList[i];
const char *protocolName = protocol_getName(protocal);
NSLog(@"protocol(%d): %@",i, [NSString stringWithUTF8String:protocolName]);
}
free(propertyList); //C語言中使用Copy操做的方法,要注意釋放指針,防止內存泄漏
複製代碼
Runtime源碼中的IMP
做爲函數指針,指向方法的實現。經過它,能夠繞開發送消息的過程來提升函數調用的效率。當須要持續大量重複調用某個方法的時候,會十分有用,以下:
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
複製代碼
咱們使用第三方框架裏的Person類,在特殊需求下想要更改其私有屬性nickName,這樣的操做咱們就可使用Runtime能夠動態修改對象屬性。
複製代碼
Person *ps = [[Person alloc] init];
NSLog(@"nickName: %@",[ps valueForKey:@"nickName"]); //null
//第一步:遍歷對象的全部屬性
unsigned int count;
Ivar *ivarList = class_copyIvarList([ps class], &count);
for (int i= 0; i<count; i++) {
//第二步:獲取每一個屬性名
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSString *propertyName = [NSString stringWithUTF8String:ivarName];
if ([propertyName isEqualToString:@"_nickName"]) {
//第三步:匹配到對應的屬性,而後修改;注意屬性帶有下劃線
object_setIvar(ps, ivar, @"allenlas");
}
}
NSLog(@"nickName: %@",[ps valueForKey:@"nickName"]); //allenlas
複製代碼
歸檔是一種經常使用的輕量型文件存儲方式,可是它有個弊端:
在歸檔過程當中,若一個Model有多個屬性,咱們不得不對每一個屬性進行處理,很是繁瑣
複製代碼
歸檔操做主要涉及兩個方法: encodeObject
和 decodeObjectForKey
,對於這兩個方法,能夠利用Runtime
來進行改進:
//原理:使用Runtime動態獲取全部屬性
//解檔操做
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
self = [super init];
if (self) {
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:ivarName];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivarList); //釋放指針
}
return self;
}
//歸檔操做
- (void)encodeWithCoder:(NSCoder *)aCoder{
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (NSInteger i = 0; i < count; i++) {
Ivar ivar = ivarList[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(ivarList); //釋放指針
}
複製代碼
//--測試歸檔
Person *ps = [[Person alloc] init];
ps.name = @"allenlas";
ps.age = 20;
NSString *temp = NSTemporaryDirectory();
NSString *fileTemp = [temp stringByAppendingString:@"person.archive"];
[NSKeyedArchiver archiveRootObject:ps toFile:fileTemp];
//--測試解檔
NSString *temp = NSTemporaryDirectory();
NSString *fileTemp = [temp stringByAppendingString:@"person.henry"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:fileTemp];
NSLog(@"person-name:%@,person-age:%ld",person.name,person.age);
//person-name:allenlas,person-age:20
複製代碼
在平常項目開發中,常常會使用YYModel
或 MJExtension
等對接口返回的數據對象實現轉模型操做。對於此,能夠利用KVC
和Runtime
來進行相似的功能實現,在這個過程當中須要解決的問題有:
利用Runtime
實現的思路大致以下:
藉助Runtime能夠動態獲取成員列表的特性,遍歷模型中全部屬性,而後以獲取到的屬性名爲key,在JSON字典中尋找對應的值value;再將每個對應Value賦值給模型,就完成了字典轉模型的目的。
複製代碼
{
"id":"10089",
"name": "Allen",
"age":"20",
"position":"iOS開發工程師",
"address":{
"country":"中國",
"province": "廣州"
},
"tasks":[{
"name":"Home",
"desc":"app首頁開發"
},{
"name":"Train",
"desc":"app培訓模塊開發"
},{
"name":"Me",
"desc":"完成我的頁面"
}
]
}
複製代碼
NSObject
的類目 NSObject+model
,用於實現字典轉模型//在NSObject+model.h中
NS_ASSUME_NONNULL_BEGIN
//AAModel協議,協議方法能夠返回一個字典,代表特殊字段的處理規則
@protocol AAModel<NSObject>
@optional
+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;
@end;
@interface NSObject (model)
+ (instancetype)xk_modelWithDictionary:(NSDictionary *)dictionary;
@end
NS_ASSUME_NONNULL_END
複製代碼
#import "NSObject+model.h"
#import <objc/runtime.h>
@implementation NSObject (model)
+ (instancetype)xk_modelWithDictionary:(NSDictionary *)dictionary{
//建立當前模型對象
id object = [[self alloc] init];
//1.獲取當前對象的成員變量列表
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
//2.遍歷ivarList中全部成員變量,以其屬性名爲key,在字典中查找Value
for (int i= 0; i<count; i++) {
//2.1獲取成員屬性
Ivar ivar = ivarList[i];
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)] ;
//2.2截取成員變量名:去掉成員變量前面的"_"號
NSString *propertyName = [ivarName substringFromIndex:1];
//2.3以屬性名爲key,在字典中查找value
id value = dictionary[propertyName];
//3.獲取成員變量類型, 由於ivar_getTypeEncoding獲取的類型是"@\"NSString\""的形式
//因此咱們要作如下的替換
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];// 替換:
//3.1去除轉義字符:@\"name\" -> @"name" ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
//3.2去除@符號
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
//4.對特殊成員變量進行處理:
//判斷當前類是否實現了協議方法,獲取協議方法中規定的特殊變量的處理方式
NSDictionary *perpertyTypeDic;
if([self respondsToSelector:@selector(modelContainerPropertyGenericClass)]){
perpertyTypeDic = [self performSelector:@selector(modelContainerPropertyGenericClass) withObject:nil];
}
//4.1處理:字典的key與模型屬性不匹配的問題,如id->uid
id anotherName = perpertyTypeDic[propertyName];
if(anotherName && [anotherName isKindOfClass:[NSString class]]){
value = dictionary[anotherName];
}
//4.2.處理:模型嵌套模型
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
Class modelClass = NSClassFromString(ivarType);
if (modelClass != nil) {
//將被嵌套字典數據也轉化成Model
value = [modelClass xk_modelWithDictionary:value];
}
}
//4.3處理:模型嵌套模型數組
//判斷當前Vaue是一個數組,並且存在協議方法返回了perpertyTypeDic
if ([value isKindOfClass:[NSArray class]] && perpertyTypeDic) {
Class itemModelClass = perpertyTypeDic[propertyName];
//封裝數組:將每個子數據轉化爲Model
NSMutableArray *itemArray = @[].mutableCopy;
for (NSDictionary *itemDic in value) {
id model = [itemModelClass xk_modelWithDictionary:itemDic];
[itemArray addObject:model];
}
value = itemArray;
}
//5.使用KVC方法將Vlue更新到object中
if (value != nil) {
[object setValue:value forKey:propertyName];
}
}
free(ivarList); //釋放C指針
return object;
}
@end
複製代碼
UserModel
、AddressModel
、TasksModel
對json處理進行處理:#import "NSObject+model.h"
#import "AddressModel.h"
#import "TasksModel.h"
NS_ASSUME_NONNULL_BEGIN
@interface UserModel : NSObject<AAModel>
//普通屬性
@property (nonatomic, copy) NSString * uid;
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSString * position;
@property (nonatomic, assign) NSInteger age;
//嵌套模型
@property (nonatomic, strong) AddressModel *address;
//嵌套模型數組
@property (nonatomic, strong) NSArray *tasks;
@end
NS_ASSUME_NONNULL_END
@implementation UserModel
+ (NSDictionary<NSString *,id> *)modelContainerPropertyGenericClass{
//須要特別處理的屬性
return @{@"tasks" : [TasksModel class],@"uid":@"id"};
}
@end
複製代碼
#import "NSObject+model.h"
NS_ASSUME_NONNULL_BEGIN
@interface AddressModel : NSObject
@property (nonatomic, copy) NSString * country;
@property (nonatomic, copy) NSString * province;
@end
NS_ASSUME_NONNULL_END
@implementation AddressModel
@end
複製代碼
#import "NSObject+model.h"
NS_ASSUME_NONNULL_BEGIN
@interface TasksModel : NSObject
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSString * desc;
@end
NS_ASSUME_NONNULL_END
@implementation TasksModel
@end
複製代碼
- (void)viewDidLoad {
[super viewDidLoad];
//讀取JSON數據
NSDictionary * jsonData = @{
@"id":@"10089",
@"name": @"Allen",
@"age":@"20",
@"position":@"iOS開發工程師",
@"address":@{
@"country":@"中國",
@"province":@"廣州"
},
@"tasks":@[@{
@"name":@"Home",
@"desc":@"app首頁開發"
},@{
@"name":@"Train",
@"desc":@"app培訓模塊開發"
},@{
@"name":@"Me",
@"desc":@"完成我的頁面"
}
]
};
//字典轉模型
UserModel * user = [UserModel xk_modelWithDictionary:jsonData];
TasksModel * task = user.tasks[0];
NSLog(@"%@",task.name);
}
複製代碼
其執行結果,數據結構以下: