一直想弄明白runtime是怎麼回事,因為面試的時候這是一道必備問題,但是平時用的機會真的少之又少,我一度以為runtime只是用來裝13的利器,沒什麼卵用。但是隨著學習的增多,發現runtime真的很有用,但也沒那麼神秘。我相信看了我這篇博客,您對runtime肯定會有自己的理解。
先說說OC與C的對比:
1.OC是對OC的面向對象的封裝,OC中的對象只是C中指向結構體的指針。
2.OC的方法,本質上就是C語言中的函數,OC中的任意一個方法,在runtime中都會有一個與之對應的函數。eg:[objc sendMessage:@"I am back"];
-> objc_msg(self,@selector(sendMessage),"I am back");
所以說在OC中對象調用方法,到運行的時候,都會變成向對象發送消息,這就是runtime中最著名的消息機制。
3.既然本質都是函數,那是不是和C語言的函數沒有區別呢?絕對不是。
(1)C語言只能調用實現過的函數,只聲明了是不行的,編譯是不能通過的。
(2)OC無所謂,只要聲明了就能調用,即時你沒聲明都能調用,編譯階段都不會報錯,只會報警告。
- (id)performSelector:(SEL)aSelector;
這樣據說是保證了編程的靈活性,反正大家都這麼說,但是我覺得這就是不夠嚴謹,因為真要是需要這個方法執行了,程序就得崩潰,在編譯的時候就能解決的問題,為什麼要等到程序崩潰再修改代碼呢,有點浪費時間啊。
下面列舉了幾個runtime的應用實例,先用起來,用的多了,自然就理解了。
runtime可以動態獲取一個對象的成員變量、屬性、方法、遵守的協議。
#import <Foundation/Foundation.h>
#import "ProtocolTest1.h"
#import "ProtocolTest2.h"
@interface Person : NSObject <ProtocolTest1,ProtocolTest2>
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *age;
- (NSString *)getName;
- (NSString *)getAge;
+ (void)classMethodTest;
@end
#import "Person.h"
@implementation Person
- (NSString *)getName {
return @"I am Tomcat";
}
- (NSString *)getAge {
return @"I will be 18 years old forever";
}
+ (void)classMethodTest {
NSLog(@"This is a class method");
}
- (NSString *)description {
return [NSString stringWithFormat:@"name=%@,age=%@",_name,_age];
}
@end
先看看我們的小白鼠Person類,我們就拿他做實驗,動態獲取他的成員變量、屬性、方法、遵守的協議。
#import "FirstViewController.h"
#import <objc/runtime.h>
#import "Person.h"
@interface FirstViewController ()
@end
@implementation FirstViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *tomcat = [Person new];
unsigned int count;
NSLog(@"\n1.獲得屬性列表");
// 1.get describes of all properties (獲得屬性列表)
objc_property_t *propertyList = class_copyPropertyList([tomcat class], &count);
for (unsigned int i = 0; i < count; i++) {
const char *propertyName = property_getName(propertyList[i]);
printf("property = %s\n",propertyName);
}
// 2.get describes of all methods (獲得方法列表)
NSLog(@"\n\n2.獲得方法列表");
Method *methodList = class_copyMethodList([tomcat class], &count);
for (unsigned int i = 0; i < count; i++) {
SEL methodName = method_getName(methodList[i]);
NSLog(@"methodName = %@",NSStringFromSelector(methodName));
}
// 3.get describes of all variables (獲得成員變量列表)
NSLog(@"\n\n3.獲得成員變量列表");
Ivar *ivarList = class_copyIvarList([tomcat class], &count);
for (unsigned int i = 0; i < count; i++) {
const char *ivarNmae = ivar_getName(ivarList[i]);
printf("ivarNmae = %s\n",ivarNmae);
// 動態變量控制
object_setIvar(tomcat, ivarList[i], @"哈哈,你被我改了");
}
NSLog(@"動態變量控制: name = %@",tomcat.name);
//4.get describes of all protocols adopted by a class (獲得當前對象遵守的協議列表)
NSLog(@"\n\n4.獲得協議列表");
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([tomcat class], &count);
for (unsigned int i = 0; i < count; i++) {
const char *protocolNmae = protocol_getName(protocolList[i]);
printf("protocolNmae = %s\n",protocolNmae);
}
注意:我們要使用runtime庫,首先要 #import <objc/runtime.h>。
想要攔截和替換方法,首先要找到方法,根據什麼找呢?方法名。
//5. 通過方法名獲得類方法
Class personClass = object_getClass([Person class]);
SEL classSel = @selector(classMethodTest);
Method classMethod = class_getInstanceMethod(personClass, classSel);
//6. 通過方法名獲得實例方法
SEL objSel1 = @selector(getName);
Method objMethod1 = class_getInstanceMethod([tomcat class], objSel1);
SEL objSe2 = @selector(getAge);
Method objMethod2 = class_getInstanceMethod([tomcat class], objSe2);
我們還可以交換這兩個方法的實現
//7. 交換兩個方法的實現
NSLog(@"\n\n交換兩個方法的實現");
NSLog(@"交換之前 --- getName = %@,getAge = %@", [tomcat getName],[tomcat getAge]);
method_exchangeImplementations(objMethod1, objMethod2);
NSLog(@"交換之後 --- getName = %@,getAge = %@", [tomcat getName],[tomcat getAge]);
攔截並替換方法,多用於給系統方法添加新的功能和修改第三方庫。我們現在實現一個功能,就是給計算按鈕的點擊計數。
#import "UIButton+Count.h"
#import <objc/runtime.h>
#import "Tool.h"
@implementation UIButton (Count)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class selfClass = [self class];
// 1.原來的方法
SEL oriSEL = @selector(sendAction:to:forEvent:);
Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);
// 2.現在的方法
SEL cusSel = @selector(mySendAction:to:forEvent:);
Method cusMethod = class_getInstanceMethod(selfClass, cusSel);
// 3.給原來的方法添加實現,防止原來的方法沒有實現,只有聲明崩潰
BOOL addSuc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
if (addSuc) {
// 添加成功,用現在的方法的實現替換原來方法的實現
class_replaceMethod(selfClass, cusSel, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else {
// 沒添加成功,證明原來的方法有實現,直接交換兩個方法
method_exchangeImplementations(oriMethod, cusMethod);
}
});
}
// 現在的方法
- (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
[[Tool shareInstance] countClicks];
[super sendAction:action to:target forEvent:event];
}
@end
Tool.h
#import <Foundation/Foundation.h>
@interface Tool : NSObject
@property (nonatomic, assign) NSInteger count;
+ (instancetype)shareInstance;
- (void)countClicks;
@end
Tool.m
#import "Tool.m"
@implementation Tool
static id _instance;
+ (instancetype)shareInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[Tool alloc] init];
});
return _instance;
}
- (void)countClicks {
_count += 1;
NSLog(@"您點擊了%ld次",_count);
}
@end
NSCoding用於存儲模型對象,必須實現代理NSCoding。
#import "Student.h"
#import <objc/runtime.h>
@implementation Student
- (void)encodeWithCoder:(NSCoder *)aCoder {
// 如果不用runtime
// [aCoder encodeObject:self.name forKey: @"name"];
// [aCoder encodeObject:self.stuID forKey: @"stuID"];
// [aCoder encodeObject:self.score forKey: @"score"];
// [aCoder encodeObject:self.name forKey: @"myFriend"];
unsigned int count = 0;
// 獲取變量列表
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
// 獲取變量名
const char *name = ivar_getName(ivars[i]);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
// 用變量名歸檔變量
[aCoder encodeObject:value forKey:key];
}
free(ivars);
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
// 如果不用runtime
// self.name = [aDecoder decodeObjectForKey:@"name"];
// self.stuID = [aDecoder decodeObjectForKey:@"stuID"];
// self.score = [aDecoder decodeObjectForKey:@"score"];
// self.myFriend = [aDecoder decodeObjectForKey:@"myFriend"];
unsigned int count = 0;
// 獲取變量列表
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
// 獲取變量名
const char *name = ivar_getName(ivars[i]);
NSString *key = [NSString stringWithUTF8String:name];
// 用變量名解檔變量
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
- (NSString *)description {
return [NSString stringWithFormat:@"name=%@\nstuID=%@\nscore=%@\nmyFriend:%@",_name,_stuID,_score,_myFriend];
}
@end
如果模型屬性少無所謂,如果多的話,最好用runtime。有100個屬性,你不可能寫100次解檔歸檔啊。
其實這個用系統方法就很好了,這次我用runtime實現一下,估計系統方法也是這個邏輯。
#import "NSObject+Model.h"
#import <objc/runtime.h>
@implementation NSObject (Model)
+ (instancetype)allocWithDic: (NSDictionary *)dic {
id objc = [[self alloc] init];
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(self, &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivarList[i];
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
NSString *key = [ivarName substringFromIndex:1];
id value = dic[key];
NSLog(@"type = %@",ivarType);
// 把模型轉化的字典再轉換成模型
// 先判斷value類型,value類型是自字典,但是ivarType又不是NSDictionary,說明value實際上是一個模型轉化的字典
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType containsString:@"NS"]) {
// @"Person" -> "Person"
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
// "Person" -> Person
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
// 生成模型
Class modelClass = NSClassFromString(ivarType);
if (modelClass) {
// 給模型裡的屬性賦值
value = [modelClass allocWithDic:value];
}
}
if (value) {
[objc setValue:value forKey:key];
}else {
NSLog(@"沒找到value");
}
}
return objc;
}
@end
rumtime庫是一個非常強大的,我列舉的這幾個用法是比較常用和基礎的。例外runtime是開源的,不過看起來應該很難,基本上是用c語言和匯編寫的,現在懂匯編的應該很少。本文完整案例已經上傳到Github,歡迎下載。