這篇文章是筆者結合一些參考文章和當初學習Runtime的心得而寫的一篇總結,主要講解Runtime在工做中的運用,沒有涉及到太底層的知識,極盡詳略,適合初中級學者,水平有限,有錯誤的地方,還請大佬在評論中指出,一塊兒快樂學習。持續更新中。。。html
Runtime 簡稱運行時,是一套C語言的API(引入 <objc/runtime.h>
或<objc/message.h>
)。OC 就是運行時機制,也就是在運行時候的一些機制,其中最主要的是 消息機制。面試
對於C語言,函數的調用在編譯的時候會決定調用哪一個函數。數組
對於OC,函數的調用稱爲消息發送,屬於動態調用過程。在編譯的時候並不能決定真正調用哪一個函數,只有在真正運行的時候纔會根據函數的名稱找到對應的函數來調用。安全
消息機制原理:對象根據方法編號SEL去映射表查找對應的方法實現。bash
驗證:數據結構
1.在main.m中建立一個對象;多線程
id object = [NSObject alloc];
object = [object init];
複製代碼
2.終端切換到該目錄下,執行命令clang -rewrite-objc main.m
,編譯後會生成一個main.cpp(C++文件);app
3.在.cpp文件中搜索autoreleasepool
,能夠找到上述對象建立的底層代碼;框架
id object = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));
object = ((id (*)(id, SEL))(void *)objc_msgSend)((id)object, sel_registerName("init"));
複製代碼
能夠看出調用方法本質就是發消息。[[NSObject alloc]init]
語句發了兩次消息,第一次發了alloc 消息,第二次發送init 消息。ide
4.咱們本身來嘗試實現,首先導入頭文件 #import <objc/message.h>
,而後讓消息機制方法有提示(【build setting
-> 搜索msg
-> objc_msgSend
(YES --> NO)】)。
id object = objc_msgSend(objc_getClass("NSObject"), sel_registerName("alloc"));
object = objc_msgSend(object, sel_registerName("init"));
/**
objc_getClass(const char *name) 獲取當前類
sel_registerName(const char *str) 註冊個方法編號
objc_msgSend(id self:誰發送消息, SEL op:發送什麼消息, ...:參數)
*/
複製代碼
換個寫法:
id objc = objc_msgSend([NSObject class], @selector(alloc));
objc = objc_msgSend(objc, @selector(init));
複製代碼
參數處理:
objc_msgSend(p, sel_registerName("height:"), 180);
複製代碼
注:
objc_msgSend:這是個最基本的用於發送消息的函數。
其實編譯器會根據狀況在objc_msgSend
,objc_msgSend_fpret
,objc_msgSend_stret
,objc_msgSendSuper
, 或 objc_msgSendSuper_stret
五個方法中選擇一個來調用。
若是消息是傳遞給超類,那麼會調用名字帶有 Super
的函數;
若是消息返回值是浮點數,那麼會調用名字帶有fpret
的函數;
若是消息返回值是數據結構而不是簡單值時,那麼會調用名字帶有stret
的函數。
Meta Class
)中方法列表)1.消息傳遞:
一個對象的方法像這樣[obj foo]
,編譯器轉成消息發送objc_msgSend(obj, foo)
,Runtime
時執行的流程是什麼樣的吶?
1.首先,經過obj
的isa
指針找到它的 class
;
2.註冊方法編號SEL
,能夠快速查找;
3.根據方法編號,在 class
的 method list
找 foo
;
3.若是 class
中沒到 foo
,繼續往它的 superclass
中找 ;
4.一旦找到 foo
這個函數,就去執行它的實現IMP
。
2.Runtime的三次轉發流程:
動態方法解析:
Objective-C
運行時會調用 +resolveInstanceMethod:
或者 +resolveClassMethod:
,讓你有機會提供一個函數實現。若是你添加了函數,那運行時系統就會從新啓動一次消息發送的過程(動態添加方法)。若是未實現方法,運行時就會移到下一步:forwardingTargetForSelector
。
備用接收者:
若是目標對象實現了-forwardingTargetForSelector:
,Runtime
這時就會調用這個方法,給你把這個消息轉發給其餘對象的機會。若是還不能處理未知消息,就會進入完整消息轉發階段。
完整消息轉發:
Runtime系統會向對象發送methodSignatureForSelector:
消息,並取到返回的方法簽名用於生成NSInvocation對象。爲接下來的完整的消息轉發生成一個 NSMethodSignature對象。NSMethodSignature 對象會被包裝成 NSInvocation 對象,forwardInvocation: 方法裏就能夠對 NSInvocation 進行處理了。若是未實現,Runtime則會發出 -doesNotRecognizeSelector:
消息,程序這時也就掛掉了。
使用場景:若是一個類方法很是多,加載類到內存的時候也比較耗費資源,須要給每一個方法生成映射表,可使用動態給某個類,添加方法解決。
經典面試題:有沒有使用performSelector,其實主要想問你有沒有動態添加過方法。
方法介紹:
// 參數1:給哪一個類添加方法
// 參數2:添加方法的方法編號SEL
// 參數3:添加方法的函數實現IMP(函數地址)
// 參數4:函數的類型,(返回值+參數類型)
class_addMethod(Class cls, SEL name, IMP imp, const char * types)
複製代碼
注:
1.class_addMethod會添加一個覆蓋父類的實現,但不會取代原有類的實現。
方法示例:
假如Person
對象調用eat
方法,而該方法並無實現,則會報錯。咱們能夠利用Runtime
在Person
類中動態添加eat
方法,來實現該方法的調用。
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
p = objc_msgSend(p, sel_registerName("init"));
[p performSelector:@selector(eat)];
}
@end
複製代碼
@implementation Person
/**
void的前面沒有+、-號,由於只是C的代碼;
必須有兩個指定參數(id self,SEL _cmd)
*/
void eat(id self, SEL sel)
{
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
// 當一個對象調用未實現的方法,會調用這個方法處理,而且會把對應的方法列表傳過來.
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat)) {
//函數的類型,(返回值+參數類型) v:void @:對象->self :表示SEL->_cmd
class_addMethod(self, sel, (IMP)eat, "v@:");
}
return [super resolveInstanceMethod:sel];
}
@end
複製代碼
使用場景:當第三方框架或者系統原生方法功能不能知足咱們的時候,咱們能夠在保持系統原有方法功能的基礎上,添加額外的功能。
方法介紹:
// 交換方法地址,交換兩個方法的實現
method_exchangeImplementations(Method m1, Method m2)
複製代碼
方法封裝:爲了後續調用方便,咱們能夠將Method Swizzling
功能封裝爲類方法,做爲NSObject
的類別。
@interface NSObject (Swizzling)
+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
bySwizzledSelector:(SEL)swizzledSelector;
@end
複製代碼
#import "NSObject+Swizzling.h"
#import <objc/message.h>
@implementation NSObject (Swizzling)
+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector
{
Class class = [self class];
//原有方法
Method originalMethod = class_getInstanceMethod(class, originalSelector);
//替換原有方法的新方法
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//先嚐試給源SEL添加IMP,這裏是爲了不源SEL沒有實現IMP的狀況
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {//添加成功:說明源SEL沒有實現IMP,將源SEL的IMP替換到交換SEL的IMP
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {//添加失敗:說明源SEL已經有IMP,直接將兩個SEL的IMP交換便可
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end
複製代碼
**方法示例:**例如咱們想要替換ViewController生命週期方法,能夠這樣作。
#import "UIViewController+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementation UIViewController (Swizzling)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self methodSwizzlingWithOriginalSelector:@selector(viewWillAppear:) bySwizzledSelector:@selector(mj_viewWillAppear:)];
});
}
- (void)mj_viewWillAppear:(BOOL)animated{
[self mj_viewWillAppear:animated];
NSLog(@"被調用了");
}
@end
複製代碼
注:
1.swizzling
建議在+load
中完成。+load
和 +initialize
是Objective-C runtime會自動調用兩個類方法。+load
是在一個類被初始加載時調用,必定會被調用;+initialize
是在應用第一次調用該類的類方法或實例方法前調用,至關於懶加載方式,可能不被調用。此外 +load
方法還有一個很是重要的特性,那就是子類、父類和分類中的 +load
方法的實現是被區別對待的。換句話說在 Objective-C runtime 自動調用 +load
方法時,分類中的 +load
方法並不會對主類中的 +load
方法形成覆蓋。
2.swizzling
應該只在dispatch_once
中完成,因爲swizzling
改變了全局的狀態,因此咱們須要確保在任何狀況下(多線程環境,或者被其餘人手動再次調用+load方法)只交換一次,防止再次調用又將方法交換回來。+load方法自己即爲線程安全,爲何仍需添加dispatch_once,其緣由就在於+load方法自己沒法保證其中代碼只被執行一次。
場景:分類是不能自定義屬性和變量的,這時候可使用runtime動態添加屬性方法;
原理:給一個類聲明屬性,其實本質就是給這個類添加關聯,並非直接把這個值的內存空間添加到類存空間。
方法:
/** 關聯對象、set方法
id object:給哪一個對象添加關聯,給哪一個對象設置屬性
const void *key:關聯的key,要求惟一,建議用char 能夠節省字節
id value:關聯的value,給屬性設置的值
objc_AssociationPolicy policy:內存管理的策略
*/
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
// 獲取關聯的對象、get方法
id objc_getAssociatedObject(id object, const void *key)
// 移除關聯的對象
void objc_removeAssociatedObjects(id object)
複製代碼
內存策略對應的屬性修飾表:
內存策略 | 屬性修飾 | 描述 |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | @property (assign) 或 @property (unsafe_unretained) | 指定一個關聯對象的弱引用。 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (nonatomic, strong) | @property (nonatomic, strong) 指定一個關聯對象的強引用,不能被原子化使用。 |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property (nonatomic, copy) | 指定一個關聯對象的copy引用,不能被原子化使用。 |
OBJC_ASSOCIATION_RETAIN | @property (atomic, strong) | 指定一個關聯對象的強引用,能被原子化使用。 |
OBJC_ASSOCIATION_COPY | @property (atomic, copy) | 指定一個關聯對象的copy引用,能被原子化使用。 |
示例:實現一個UIView
的Category
添加自定義屬性defaultColor
。
@interface UIView (Color)
@property (nonatomic, strong) UIColor *defaultColor;
@end
@implementation UIView (Color)
static char kDefaultColorKey;
- (void)setDefaultColor:(UIColor *)defaultColor
{
objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)defaultColor {
return objc_getAssociatedObject(self, &kDefaultColorKey);
}
@end
複製代碼
場景:若是一個模型有許多個屬性,實現自定義模型數據持久化時,須要對每一個屬性都實現一遍encodeObject
和 decodeObjectForKey
方法,比較麻煩。咱們可使用Runtime來解決。
原理:用runtime
提供的函數遍歷Model
自身全部屬性,並對屬性進行encode
和decode
操做。
方法實現:
#import "MJMusicModel.h"
#import <objc/runtime.h>
@implementation MJMusicModel
// 設置不須要歸解檔的屬性
- (NSArray *)ignoredNames {
return @[@"_musicUrl"];
}
// 歸檔調用方法
- (void)encodeWithCoder:(NSCoder *)encoder
{
unsigned int count = 0;
// 得到這個類的全部成員變量
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置對應的成員變量
Ivar ivar = ivars[i];
// 得到成員變量的名字
const char *name = ivar_getName(ivar);
// 將每一個成員變量名轉換爲NSString對象類型
NSString *key = [NSString stringWithUTF8String:name];
// 忽略不須要歸檔的屬性
if ([[self ignoredNames] containsObject:key]) {
continue;
}
// 歸檔
id value = [self valueForKey:key];
[encoder encodeObject:value forKey:key];
}
// 注意釋放內存!
free(ivars);
}
// 解檔方法
- (id)initWithCoder:(NSCoder *)decoder
{
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置對應的成員變量
Ivar ivar = ivars[i];
// 得到成員變量的名字
const char *name = ivar_getName(ivar);
// 將每一個成員變量名轉換爲NSString對象類型
NSString *key = [NSString stringWithUTF8String:name];
// 忽略不須要解檔的屬性
if ([[self ignoredNames] containsObject:key]) {
continue;
}
// 解檔
id value = [decoder decodeObjectForKey:key];
// 設置到成員變量身上
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
@end
複製代碼
注:咱們能夠將歸解檔兩個方法封裝爲宏,在須要的地方一句宏搞定;也能夠寫到NSObject一個分類中,方便使用。
原理:利用Runtime,遍歷模型中全部屬性,根據模型的屬性名,去字典中查找key
,取出對應的值,給模型的屬性賦值。
步驟:提供一個NSObject分類,專門字典轉模型,之後全部模型均可以經過這個分類實現字典轉模型。
接下來分別介紹一下三種狀況所實現的代碼:
1.簡單的字典轉模型
注意:模型屬性數量大於字典的鍵值對時,因爲屬性沒有對應值會被賦值爲nil,就會致使crash,因此咱們要加一個判斷,獲取到Value
時,纔給模型中屬性賦值。
NSDictionary *dict = @{
@"name" : @"xiaoming",
@"age" : @25,
@"weight" : @"60kg",
@"height" : @1.81
};
複製代碼
#import "NSObject+Model.h"
#import <objc/message.h>
@implementation NSObject (Model)
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
// 建立對應的對象
id objc = [[self alloc] init];
// 成員變量個數
unsigned int count = 0;
// 獲取類中的全部成員變量
Ivar *ivars = class_copyIvarList(self, &count);
// 遍歷全部成員變量
for (int i = 0; i < count; i++) {
// 根據角標,從數組取出對應的成員變量(Ivar:成員變量,如下劃線開頭)
Ivar ivar = ivars[i];
// 獲取成員變量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 處理成員變量名->字典中的key(去掉 _ ,從第一個角標開始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根據成員屬性名去字典中查找對應的value
id value = dict[key];
if (value) {
// 給模型中屬性賦值
[objc setValue:value forKey:key];
}
}
// 釋放ivars
free(ivars);
return objc;
}
@end
複製代碼
2.模型中嵌套模型(模型屬性是另一個模型對象)
利用runtime的ivar_getTypeEncoding
方法獲取模型對象類型,對該模型對象類型再進行字典轉模型,也就是進行遞歸,須要注意的是要排除系統的對象類型,例如NSString
。
NSDictionary *dict2 = @{
@"name" : @"xiaoming",
@"age" : @25,
@"body" :@{
@"weight" : @"65kg",
@"height" : @1.82
}
};
複製代碼
// runtime字典轉模型二級轉換:字典->字典;若是字典中還有字典,也須要把對應的字典轉換成模型
if ([value isKindOfClass:[NSDictionary class]]) {
// 獲取成員變量類型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 替換: @\"User\" -> User ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
if (![ivarType hasPrefix:@"NS"]) {
// 字典轉換成模型,根據字符串類名生成類對象
Class modelClass = NSClassFromString(ivarType);
if (modelClass) { // 有對應的模型才須要轉
// 把字典轉模型
value = [modelClass modelWithDict:value];
}
}
}
複製代碼
3.數組中裝着模型(模型的屬性是一個數組,數組中是一個個模型對象)
攔截到模型的數組屬性,進而對數組中每一個模型遍歷並字典轉模型,可是咱們不知道數組中的模型都是什麼類型,須要聲明一個方法,該方法目的不是讓其調用,而是讓其實現並返回模型的類型。
NSDictionary *dict3 = @{
@"name" : @"xiaoming",
@"age" : @25,
@"body" :@{
@"weight" : @"65kg",
@"height" : @1.82
},
@"children" : @[
@{
@"sex" : @"男",
@"love" : @"籃球",
},
@{
@"sex" : @"nv",
@"love" : @"鋼琴",
}
],
};
複製代碼
// runtime字典轉模型三級轉換:字典->數組->字典;NSArray中也是字典,把數組中的字典轉換成模型.
if ([value isKindOfClass:[NSArray class]]) {
// 判斷對應類有沒有實現字典數組轉模型數組的協議
// arrayContainModelClass 提供一個協議,只要遵照這個協議的類,都能把數組中的字典轉模型
if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
// 轉換成id類型,就能調用任何對象的方法
id idSelf = self;
// 獲取數組中字典對應的模型
NSString *type = [idSelf arrayContainModelClass][key];
// 生成模型
Class classModel = NSClassFromString(type);
NSMutableArray *arrM = [NSMutableArray array];
// 遍歷字典數組,生成模型數組
for (NSDictionary *dict in value) {
// 字典轉模型
id model = [classModel modelWithDict:dict];
[arrM addObject:model];
}
// 把模型數組賦值給value
value = arrM;
}
}
複製代碼
#import <Foundation/Foundation.h>
@protocol ModelDelegate <NSObject>
@optional
/**
提供一個協議,只要遵照這個協議的類,都能把數組中的字典轉模型
*/
+ (NSDictionary *)arrayContainModelClass;
@end
@interface NSObject (Model)
/**
dict -> model
利用runtime 遍歷模型中全部屬性,根據模型中屬性去字典中取出對應的value給模型屬性賦值
*/
+ (instancetype)modelWithDict:(NSDictionary *)dict;
@end
複製代碼
實現協議類:
+ (NSDictionary *)arrayContainModelClass
{
// 數組屬性 : 數組中的類名
return @{@"children" : @"MJChild"};
}
複製代碼
不忙的時候,就整理知識,通過幾天時間的努力,終於寫好了。途中參考大量資料,並經過Demo驗證其正確性,也算對本身的一次全面級的學習與複習。
下一篇,會深刻了解Runtime底層語言。
I’m not perfect. But I keep trying.
蘋果官方文檔
OC最實用的runtime總結
讓你快速上手Runtime
runtime詳解
iOS 模式詳解—
iOS Runtime詳解
裝逼技術RunTime的總結篇