iOS中的Runtime
引言
- **對於C語言,函數調用在編譯的時候會決定調用哪個函數,編譯完成之後直接順序執行 **
- 對於OC語言, 屬於動態函數調用,在編譯的時候並不能決定真正調用哪個函數,只有在真正運行的時候纔會根據函數的名稱找到對應的函數來調用
- 事實證明:在編譯階段,OC可以調用任何函數,即使這個函數並未實現,只要聲明過就不會報錯。而C語言在編譯階段就會報錯
Runtime簡介
Runtime簡稱運行時,Runtime就是執行已編譯好的代碼,在其底層,OC就是通過Runtime這個庫把方法調用轉化爲消息發送
消息驅動機制
在OC中方法調用:
[object doSomething];
在編譯時RunTime會將上述代碼轉化爲:
objc_msgSend(id object,selector);
//object:方法的調用者
//selector:方法選擇器
動態綁定
當我們在調用一個實例方法的時候,其過程:
1、Runtime系統會把方法[object doSomething];
調用轉化爲消息發送objc_msgSend(object, @selector (doSomething));
,即objc_msgSend,並且把方法的調用者、和方法選擇器,當做參數傳遞過去。
分爲以下倆種:
方法無參數:objc_msgSend(id objec,selector);
方法有參數:objc_msgSend(id objec,selector, arg1.arg2...);
@selector (doSomething)返回一個SEL數據類型,即方法選擇器。SEL主要作用是快速的通過方法名字(doSomething)查找到對應方法的函數指針,然後調用其函數。SEL其本身是一個int型的地址,地址中存放着方法的名字。在一個類中,每一個方法對應着一個SEL。iOS類中不能存在兩個名稱相同的方法,即使參數類型不同,因爲SEL是根據方法名字生成的,相同的方法名稱只能對應一個SEL。
2、在objc_msgSend函數中,首先通過obj的isa指針找到obj對應的class。在class中,有一塊最近調用的方法的指針緩存,所以先去cache通過selector查找對應的method。
3、若cache中未找到,再去method list中查找,若method list中未找到,則去superClass中查找。若能找到,則將method加入到cache中,以方便下次查找,
4、通過method中的函數指針跳轉到對應的函數中去執行。
和RunTime交互的三種方式
通過Objective-C源代碼
大部分情況下你就只管寫你的Objc代碼就行,runtime 系統自動在幕後辛勤勞作着。
還記得引言中舉的例子吧,消息的執行會使用到一些編譯器爲實現動態語言特性而創建的數據結構和函數,Objc中的類、方法和協議等在 runtime 中都由一些數據結構來定義。
通過Foundation框架中類NSObject的方法
Cocoa 中大多數類都繼承於NSObject類,也就自然繼承了它的方法。最特殊的例外是NSProxy,它是個抽象超類,它實現了一些消息轉發有關的方法,可以通過繼承它來實現一個其他類的替身類或是虛擬出一個不存在的類,說白了就是領導把自己展現給大家風光無限,但是把活兒都交給幕後小弟去幹。
有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重載它併爲你定義的類提供描述內容。NSObject還有些方法能在運行時獲得類的信息,並檢查一些特性,比如class返回對象的類;isKindOfClass:和isMemberOfClass:則檢查對象是否在指定的類繼承體系中;respondsToSelector:檢查對象能否響應指定的消息;conformsToProtocol:檢查對象是否實現了指定協議類的方法;methodForSelector:則返回指定方法實現的地址。
通過直接調用運行時系統的函數
Runtime 系統是一個由一系列函數和數據結構組成,具有公共接口的動態共享庫。頭文件存放於/usr/include/objc目錄下。許多函數允許你用純C代碼來重複實現 Objc 中同樣的功能。雖然有一些方法構成了NSObject類的基礎,但是你在寫 Objc 代碼時一般不會直接用到這些函數的,除非是寫一些 Objc 與其他語言的橋接或是底層的debug工作。在中Objective-C Runtime Reference有對 Runtime 函數的詳細文檔。
Runtime作用
動態加載屬性、方法
1、動態添加屬性
- 使用場景:準備用一個系統的類,但是系統的類並不能滿足你的需求,你需要額外添加一個屬性。這種情況的一般解決辦法就是繼承。但是,只增加一個屬性,就去繼承一個類,總是覺得太麻煩類。這個時候,runtime的關聯屬性,也叫加載屬性
- 簡單使用:
@implementation ViewController
//動態添加屬性
// 給系統NSString類動態添加屬性name
NSString *str = [[NSString alloc]init];
str.name = @"動態添加屬性";
NSLog(@"%@",str.name);
#import "NSString+AddProperty.h"
#import <objc/objc-runtime.h>
// 定義關聯的key
static const char *key = "name";
@implementation NSString (AddProperty)
- (NSString *)name
{
// 根據關聯的key,獲取關聯的值。
return objc_getAssociatedObject(self, key);
}
- (void)setName:(NSString *)name
{
// 參數1:給哪個對象添加屬性賦值
// 參數2:關聯的key,通過這個key獲取
// 參數3:關聯的value
// 參數4:關聯的策略,下面枚舉(手機開發一般都選擇NONATOMIC)
// enum {
// OBJC_ASSOCIATION_ASSIGN = 0,//assign策略
// OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
// OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
// OBJC_ASSOCIATION_RETAIN = 01401,// retain策略
// OBJC_ASSOCIATION_COPY = 01403//copy策略
// };
objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
2、動態添加方法
- 使用場景:一個類方法非常多,一次性加載到內存,比較耗費資源
- 簡單使用:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//創建Addmethod的實例,並調用不存在的方法
AddMethod *addmethod = [[AddMethod alloc]init];
//調用實例方法
[addmethod performSelector:@selector(test)];
[addmethod performSelector:@selector(eat)];
//調用類方法
[AddMethod performSelector:@selector(read)];
}
#import <objc/objc-runtime.h>
@implementation AddMethod
//有倆個隱含參數id、SEL
void test(id self,SEL sel)
{
NSLog(@"添加test實例方法");
}
void(^writeBlock)(id,SEL) = ^(id objc_self,SEL objc_cmd){
NSLog(@"添加writeBlock實例方法");
};
void(^readBlock)(id,SEL) = ^(id objc_self,SEL objc_cmd){
NSLog(@"添加readBlock類方法");
};
#pragma -mark:動態解析實例方法(如果一個實例的方法選擇器沒有再方法列表裏找打就會進入)
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
//可以添加函數指針或block
//判斷若方法名是test則添加,別則不添加
if (sel == @selector(test)) {
// 參數1:給哪個類添加方法
// 參數2:方法名
// 參數3:添加方法的函數實現(函數地址)
// 參數4:函數的類型,(返回值+參數類型) v:void @:對象->self :表示SEL->_cmd
class_addMethod([self class], @selector(test), test, "v@:");
}else{
//判斷若是其他則添加
class_addMethod([self class], sel, imp_implementationWithBlock(writeBlock), "v@:");
}
printf("%s\n",sel_getName(sel));
return [super resolveInstanceMethod:sel];
}
#pragma -mark:動態解析類方法(如果一個類的方法選擇器沒有再方法列表裏找打就會進入)
+(BOOL)resolveClassMethod:(SEL)sel{
//創建類方法的實現
/*爲指定類添加方法
*參數1:需要添加新方法的類
*參數2:方法名
*參數3:由編譯器生成的、指向實現方法的指針。也就是說,這個指針指向的方法就是我們要添加的方法。
*參數4:添加的方法的返回值和參數,函數的類型編碼(結構爲:返回值、第一個參數、第二個參數。。。)
*/
class_addMethod(object_getClass([self class]), sel, imp_implementationWithBlock(readBlock), "v@:");
printf("%s\n",sel_getName(sel));
return [super resolveInstanceMethod:sel];;
}
消息轉發
消息轉發只是將其他類引入消息鏈,而不是繼承鏈。當一個對象或類調用了一個不存在的方法,程序就會crash,爲了程序不crash,有個層級方法可以避免程序crash。
- 第一層級
給程序動態添加方法,對應的具體方法是+(BOOL)resolveInstanceMethod:(SEL)sel
和+(BOOL)resolveClassMethod:(SEL)sel
,當方法是實例方法時調用前者,當方法爲類方法時,調用後者。這個方法設計的目的是爲了給類利用 class_addMethod 添加方法的機會,在返回值中,無論返回 YES 還是 NO,系統都會嘗試用 SEL 來尋找方法實現,如果找到函數實現,則執行,所以無論返回 YES\NO都會進入第二層級。 - 第二層級
第二個層級是備援接收者階段,對象的具體方法是-(id)forwardingTargetForSelector:(SEL)aSelector
,此時,運行時詢問能否把消息轉給其他接收者處理,也就是此時系統給了個將這個 SEL 轉給其他對象的機會。在返回值中,當返回值是非self\非nil 時,消息被轉給新對象執行,就不會進入第三層級。如果是self\nil就會進入第三層級。
@interface ViewController ()
//消息轉發
ForwardInvocationA *f =[[ForwardInvocationA alloc]init];
f.objectB = [[ForwardInvocationB alloc]init];
[f performSelector:@selector(learnOC)];
------------------------------
@implementation ForwardInvocationA
//第二層---當第一層沒有添加動態方法就會進入第二層
#pragma maerk-將消息轉給某對象
-(id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"myObject---%@",NSStringFromSelector(_cmd));
if ([self.objectB respondsToSelector:aSelector]){
return nil ;
}
return [super forwardingTargetForSelector:aSelector];
}
- 第三層級
第三層級是完整消息轉發階段,對應方法-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector和
-(void)forwardInvocation:(NSInvocation *)anInvocation`,這是消息轉發流程的最後一個環節。在這個方法中,可以把 anInvocation 轉發給多個對象,與第二號層級不同,二號只能轉給一個對象。
@interface ViewController ()
//消息轉發
ForwardInvocationA *f =[[ForwardInvocationA alloc]init];
f.objectB = [[ForwardInvocationB alloc]init];
[f performSelector:@selector(learnOC)];
------------------------------
//第三層---當第二層返回值爲self或nil就會進入第三層
#pragma maerk-得到方法名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if(aSelector == @selector(learnOC)){
return [self.objectB methodSignatureForSelector:aSelector];
}
return [super methodSignatureForSelector:aSelector];
}
#pragma maerk-實現消息轉發
-(void)forwardInvocation:(NSInvocation *)anInvocation{
if (anInvocation.selector == @selector(learnOC)){
[anInvocation invokeWithTarget:self.objectB];
}
}
- 都不對
如果都不中,調用doesNotRecognizeSelector拋出異常,程序crash。
注:
最後說一下 warning 的事。編譯器很好心的報的那個 warning 咋辦呢,不管那個小黃條不是一個愛整潔的程序員的風格,所以我們要想辦法把它去掉。比較暴力,通過在配置文件中把 Complier Flag 加-w,對該類去除所有 warning。
方法交換
-
使用場景:需要擴張一個功能又不想改變原始代碼可以使用方法交換
-
概念
+(void)initialize
:當類第一次被調用的時候就會調用該方法,整個程序運行中只會調用一次
+ (void)load
:當程序啓動的時候就會調用該方法,換句話說,只要程序一啓動就會調用load方法,整個程序運行中只會調用一次 -
簡單使用:
//方法調用
NSURL *url = [NSURL URLWithString:@"http://www.baidu.com/中文"];
NSURLRequest *request = [[NSURLRequest alloc]initWithURL:url];
------------------------------
//利用runtime交換方法
#import "NSURL+ExchangeMethod.h"
#import <objc/objc-runtime.h>
@implementation NSURL (ExchangeMethod)
//當這個類被加載時調用
+(void)load{
NSLog(@"-------");
//利用runtime交換方法
// 獲取方法地址
Method urlWithStr = class_getClassMethod([NSURL class], @selector(URLWithString:));
Method XYURLWithStr = class_getClassMethod([NSURL class], @selector(XYURLWithStr:));
// 交換方法地址,相當於交換實現方式
method_exchangeImplementations(urlWithStr,XYURLWithStr);
}
//不能在分類中重寫系統方法imageNamed,因爲會把系統的功能給覆蓋掉,而且分類中不能調用super.
+(instancetype)XYURLWithStr:(NSString*)str{
/**
1. 此時調用的方法 'XYURLWithStr' 相當於調用系統的 'URLWithString' 方法,原因是在load方法中進行了方法交換.
2. 注意:此處並沒有遞歸操作.
*/
NSURL *url = [NSURL XYURLWithStr:str];//調用系統方法實現
if (url == nil){
NSLog(@"自定義url爲nil");
}
return url;
}
歸檔–反歸檔(序列化–反序列化)
- 使用場景:歸檔是保存數據方式之一,一般用於保存模型對象,模型屬性個數很多,使用runtime歸檔會簡單很多
- 簡單使用:
//方法調用
//歸檔
PersonEncode *per = [[PersonEncode alloc]init];
per.name = @"XY";
per.age = 12;
NSString *temp = NSTemporaryDirectory();
NSString *filepath = [temp stringByAppendingPathComponent:@"xy.xy"];
[NSKeyedArchiver archiveRootObject:per toFile:filepath];
//反歸檔並輸出
PersonEncode *per1 =[NSKeyedUnarchiver unarchiveObjectWithFile:filepath];
NSLog(@"%@---%d",per1.name,per1.age);
------------------------------
#import <Foundation/Foundation.h>
@interface PersonEncode : NSObject<NSCoding>
@property(nonatomic, strong)NSString *name;
@property(nonatomic, assign)NSInteger age;
@end
------------------------------
#import "PersonEncode.h"
#import <objc/objc-runtime.h>
@implementation PersonEncode
-(void)encodeWithCoder:(NSCoder *)aCoder{
//定義一個實例變量的個數
unsigned int ivarCount = 0;
//首先獲取這個類的實例變量列表
/*
*C語言中,如果傳基本數據類型的指針,那麼一般都是需要在函數內部改變他的值
*/
Ivar *vars = class_copyIvarList([self class], &ivarCount);
//遍歷實例變量列表
for (int i = 0; i< ivarCount; i++) {
//獲取實例變量名
NSString *strName = [NSString stringWithUTF8String:ivar_getName(vars[i])];
//通過KVC獲取實例變量值
id value = [self valueForKey:strName];
//以實例變量名作爲key進行歸檔
[aCoder encodeObject:value forKey:strName];
}
//在C語言中,凡是看到new,creat,copy函數需要釋放指針
//釋放內存
free(vars);
}
-(id)initWithCoder:(NSCoder *)aDecoder{
if (self = [self init]){
//首先獲取這個類的實例變量列表
//定義一個實例變量的個數
unsigned int ivarCount = 0;
Ivar *vars = class_copyIvarList([self class], &ivarCount);
//遍歷實例變量列表
for (int i = 0; i< ivarCount; i++) {
//獲取實例變量名
NSString *strName = [NSString stringWithUTF8String:ivar_getName(vars[i])];
//以實例變量的名字作爲key進行反歸檔
id value = [aDecoder decodeObjectForKey:strName];
//通過KVC對實例變量進行賦值
[self setValue:value forKey:strName];
}
//在C語言中,凡是看到new,creat,copy函數需要釋放指針
//釋放內存
free(vars);
}
return self;
}
@end
最後,附上以上的demo,git:(https://github.com/hejiasu/Runtime)