runtime介紹及基本使用
阿新 • • 發佈:2019-01-08
1. 概念
runtime(執行時系統),是一套基於C語言API,包含在 <objc/runtime.h>和<objc/message.h>中,執行時系統的功能是在執行期間(而不是編譯期或其他時機)通過程式碼去動態的操作類(獲取類的內部資訊和動態操作類的成員),如建立一個新類、為某個類新增一個新的方法或者為某個類新增例項變數、屬性,或者交換兩個方法的實現、獲取類的屬性列表、方法列表等和Java中的反射技術類似。2. 探索
程式最終執行的是二進位制的可執行檔案,編譯器需要將OC程式碼轉換為執行時程式碼,再將執行時程式碼經過一些處理成最終的二進位制可執行檔案使用終端命令列切換到mian.m檔案所在的目錄下並執行: clang -rewrite-objc main.mmian.m int main(int argc, const char * argv[]) { @autoreleasepool { [[NSObject alloc] init]; } return 0; }
在mian.m檔案目錄所在的位置會有一個 main.cpp檔案,開啟檔案可以看到OC程式碼都被轉換成執行時runtime程式碼了
runtime.h和message.h中的方法一般以objc_、 class_、 method_、 property_、 ivar_、 protocol_、object_、 sel_等作為字首,用字首表明操作的物件
3.常用功能
1.動態交換兩個方法的實現2.動態新增物件的成本變數和成員方法
3.獲取某個類的所有成員變數和成員方法
4.實現NSCoding的自動歸檔和自動解檔
5.實現字典和模型的自動轉換
6.為類別新增屬性(我們知道類別是不能擴充套件屬性的,只能擴充套件方法,但可以執行時可以實現,通過為類增加屬性)
4.runtime常用的資料型別
Objective-C -------> runtime
類(Class) objc_class*
id objc_object*
方法(Method) objc_method
變數(Ivar) objc_ivar*
struct objc_class {
Class isa;
Class super_class
const char *name
long version
long info
long instance_size
struct objc_ivar_list *ivars
struct objc_method_list **methodLists
struct objc_cache *cache
struct objc_protocol_list *protocols
};
typedef struct objc_class* Class;
struct objc_object {
Class isa;
};
typedef struct objc_object* id;
struct objc_method {
SEL method_name, // 方法名
char* method_types, // 方法的引數型別
IMP method_imp // 方法實現程式碼的指標
};
typedef objc_method Method;
struct objc_ivar {
char *ivar_name
char *ivar_type
int ivar_offset
int space
}
typedef struct objc_ivar* Ivar;
5.runtime 常用API
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
// 新增方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
// 新增屬性
BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types)
// 註冊類對
void objc_registerClassPair(Class cls)
// 向某個物件傳送某個訊息
id objc_msgSend(id self, SEL op, ...)
// 獲取某個類的類方法
Method class_getClassMethod(Class cls, SEL name)
// 獲取某個類的例項方法
Method class_getInstanceMethod(Class cls, SEL name)
// 為類新增一個屬性
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
// 獲取屬性對應的值
id objc_getAssociatedObject(id object, const void *key)
// 獲取例項變數列表
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
// 獲取方法的型別(方法的簽名,返回值型別,引數型別)
const char *method_getTypeEncoding(Method m)
6.使用runtime
示例1:交換兩個方法的實現
<span style="font-weight: normal;">#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *address;
@property (assign, nonatomic)int age;
@property (assign, nonatomic)double hight;
@property (assign, nonatomic)BOOL gender;
+ (void)run;
- (void)study;
@end
//--------------------------------------------------------------------------------------
#import "Person.h"
@implementation Person
+ (void)run {
NSLog(@"run。。。");
}
- (void)study {
NSLog(@"study...");
}
@end
//--------------------------------------------------------------------------------------
int main(int argc, const char * argv[]) {
@autoreleasepool {
Method runMethod = class_getClassMethod([Person class], @selector(run));
Method studyMethod = class_getInstanceMethod([Person class], @selector(study));
method_exchangeImplementations(runMethod, studyMethod);
[Person run]; // 列印 study...
[[[Person alloc] init] study]; // 列印 run。。。
}
return 0;
}</span>
例項2:為類新增屬性
分類(類別)是用來擴充套件方法的,不能擴充套件屬性,但並不是說類別中不能寫 @proprty, 如果類別中有 @proprty,意思是說為該屬性生產getter&&setter方法,但是不生成帶下劃線的例項變數。新建一個類別為列表新增屬性,並重寫getter&&setter
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface Person (AddProperty)
@property (copy, nonatomic) NSString *name;
@end
#import "Person+AddProperty.h"
@implementation Person (AddProperty)
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, "name");
}
@end
//------------------------------------------------------------------------------
#import <Foundation/Foundation.h>
#import "Person+AddProperty.h"
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.name = @"小紅";
NSLog(@"使用類別(分類)間接為類新增屬性, person.name = %@", person.name);
}
return 0;
}
程式解釋:當程式通過點語法呼叫 person.name = @"小紅" 的時候實際上是執行的[person setName:@"小紅"];
即呼叫相應的Set方法,該方法使用runtime動態的為類關聯一個屬性並賦值;當執行person.name 的時候,
實際上是執行[person name]; 即呼叫相應的Get方法,使用runtime獲取關聯的屬性值。
使用類別也是可以做到為類新增屬性的。(當會的知識增多時,就會發現之前認為對的可能都是錯的)
示例3:獲取例項變數列表
int main(int argc, const char * argv[]) {
@autoreleasepool {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([Person class], &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
const char* name = ivar_getName(ivar);
const char* type = ivar_getTypeEncoding(ivar);
NSLog(@"%s :%s", name, type);
}
free(ivars);
}
return 0;
}
2016-07-19 17:48:28.788 Runtime[13864:3404989] _gender :c
2016-07-19 17:48:28.789 Runtime[13864:3404989] _age :i
2016-07-19 17:48:28.789 Runtime[13864:3404989] _address :@"NSString"
2016-07-19 17:48:28.789 Runtime[13864:3404989] _hight :d
Program ended with exit code: 0
示例4:動態建立類並新增方法
使用執行時系統API以動態方式建立類的步驟:1.新建一個類及元類
2.向這個類新增方法和例項變數
3.註冊新建的類
static void display(id self, SEL _cmd){
NSLog(@"invoke method with selector %@ on %@ instance", NSStringFromSelector(_cmd), [self class]);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1. 建立一個類對
Class WidgetClass = objc_allocateClassPair([NSObject class], "Widget", 0);
// 2. 為該類新增一個方法
class_addMethod(WidgetClass, @selector(display), (IMP)display, "[email protected]:");
// 3. 為該類新增一個例項變數
class_addIvar(WidgetClass, "height", sizeof(id), rint(log2(sizeof(id))), @encode(id));
// 4. 註冊類對
objc_registerClassPair(WidgetClass);
// 5. 建立例項變數並賦值、呼叫方法
id widge = [[WidgetClass alloc] init];
[widge setValue:@(15) forKey:[NSString stringWithUTF8String:"height"]];
NSLog(@"Widge instance height = %@", [widge valueForKey:[NSString stringWithUTF8String:"height"]]);
objc_msgSend(widge, NSSelectorFromString(@"display"));
// 6. 動態方式新增一個屬性
objc_setAssociatedObject(widge, @"width", @(10), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 7. 獲取
id result = objc_getAssociatedObject(widge, @"width");
NSLog(@"Widget instance width = %@", result);
}
return 0;
}
示例5:歸檔解檔
先來看一個常用的寫法:
#import <Foundation/Foundation.h>
@interface Student : NSObject <NSCoding>
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic)int age;
@property (assign, nonatomic)double weight;
@property (copy, nonatomic)NSArray *hobby;
@property (copy, nonatomic)NSDictionary *others;
@end
#import "Student.h"
#define knameKey @"name"
#define kageKey @"age"
#define kweightKey @"weight"
#define khobbyKey @"hobby"
#define kothersKey @"others"
@implementation Student
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:_name forKey:knameKey];
[aCoder encodeInt:_age forKey:kageKey];
[aCoder encodeDouble:_weight forKey:kweightKey];
[aCoder encodeObject:_hobby forKey:khobbyKey];
[aCoder encodeObject:_others forKey:kothersKey];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
_name = [aDecoder decodeObjectForKey:knameKey];
_age = [aDecoder decodeIntForKey:kageKey];
_weight = [aDecoder decodeDoubleForKey:kweightKey];
_hobby = [aDecoder decodeObjectForKey:khobbyKey];
_others = [aDecoder decodeObjectForKey:kothersKey];
}
return self;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *student = [[Student alloc] init];
student.name = @"小紅";
student.age = 25;
student.weight = 100.5;
student.hobby = @[@"吃", @"喝", @"玩", @"樂"];
student.others = @{@"phone": @"1234567890", @"wechat": @"123456"};
NSString *path = @"/Users/macmini/Documents/Test/Student.plist";
[NSKeyedArchiver archiveRootObject:student toFile:path];
Student *xiaoming = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
NSLog(@"xiaoming:%@", xiaoming);
}
return 0;
}
普通的寫法每個類如果要歸檔解檔的話都要實現NSCoding的協議方法,該方法的實現都很類似,可以使用runtime進行簡化操作,儘可能的達到至簡
#import "Student.h"
#import <objc/runtime.h>
@implementation Student
// 獲取所有成員變數進行迴圈編碼
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i++) {
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(ivars);
}
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i++) {
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
id value = [self valueForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
@end
普通寫法每個類都要寫一次,使用runtime雖然每個類都要寫一次,但是程式碼都是完全一樣的,可以直接貼上複製,既然都是一樣的,我們可以使用類別(分類)給NSObject增加這兩個方法,這樣就能簡化程式碼
//該實現也實現了對父類屬性進行歸檔解檔的實現
#import "NSObject+Archive.h"
#import <objc/runtime.h>
@implementation NSObject (Archive)
// 先對當前類進行編碼,然後對父類進行編碼,如果父類是NSObject就結束編碼
- (void)encode:(NSCoder *)aCoder {
Class clazz = self.class;
while (clazz && clazz != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(clazz, &outCount);
for (int i = 0; i < outCount; i++) {
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(ivars);
clazz = [clazz superclass];
}
}
- (void)decode:(NSCoder *)aDecoder {
Class clazz = self.class;
while (clazz && clazz != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(clazz, &outCount);
for (int i = 0; i < outCount; i++) {
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
clazz = [clazz superclass];
}
}
@end
#import <Foundation/Foundation.h>
#import "Person.h"
// 繼承Person類
@interface Student : Person <NSCoding>
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic)double weight;
@property (copy, nonatomic)NSArray *hobby;
@property (copy, nonatomic)NSDictionary *others;
@end
#import "Student.h"
#import "NSObject+Archive.h"
#import <objc/runtime.h>
@implementation Student
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
[self decode:aDecoder];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[self encode:aCoder];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *student = [[Student alloc] init];
student.name = @"小紅";
student.age = 25;
student.weight = 100.5;
student.hobby = @[@"吃", @"喝", @"玩", @"樂"];
student.others = @{@"phone": @"1234567890", @"wechat": @"123456"};
student.address = @"父類屬性address";
student.hight = 180.5;
student.gender = YES;
NSString *path = @"/Users/macmini/Documents/Test/Student.plist";
[NSKeyedArchiver archiveRootObject:student toFile:path];
Student *xiaoming = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
NSLog(@"xiaoming:%@", xiaoming);
}
return 0;
}
該方式可以簡化每個類中歸檔和解檔的程式碼量,但仍可以進行再簡化,就是將NSCoding的實現定義成巨集
建立一個.h檔案
#ifndef Coding_h
#define Coding_h
#import "NSObject+Archive.h"
#define CodingImplemention \
- (instancetype)initWithCoder:(NSCoder *)aDecoder {\
if (self = [super init]) {\
[self decode:aDecoder];\
}\
return self;\
}\
\
- (void)encodeWithCoder:(NSCoder *)aCoder {\
[self encode:aCoder];\
}
#endif /* Coding_h */
#import "Student.h"
#import "Coding.h"
@implementation Student
CodingImplemention
@end
在Student.m 檔案中只需一個單詞即可實現歸檔解檔,可以看到已經達到至簡了示例6:使用runtime將字典轉為模型字典轉模型需要考慮三種特殊情況
1.當字典中的key和模型的屬性匹配不上
2.模型中巢狀模型
3.陣列中的元素是模型
第一種情況分:當字典中欄位多個類中的屬性時,不用做任何處理,因為runtime是獲取類中的所有 屬性並迴圈的,字典中多的就不用管了。當類的欄位多於字典中的欄位,根據該欄位去屬性中獲取值如果獲取不到就繼續下次迴圈;
第二種情況:利用runtime的ivar_getTypeEncoding方法獲取例項變數的資料型別,如果是自定義的類也需要呼叫轉換
第三種情況:第二種情況能夠判斷資料型別,就可以知道是否為陣列,但是我們不知道數組裡面的資料型別,我們可以提供一個函式,讓使用者指定陣列元素的資料型別
@interface School : NSObject
@property (copy, nonatomic)NSString * ID;
@property (copy, nonatomic)NSString * name;
@end
#import "School.h"
@implementation School
@end
#import <Foundation/Foundation.h>
@interface Address : NSObject
@property (copy, nonatomic)NSString * ID;
@property (copy, nonatomic)NSString * address;
@end
#import "Address.h"
@implementation Address
@end
#import <Foundation/Foundation.h>
#import "School.h"
@interface User : NSObject
@property (copy, nonatomic)NSString * ID;
@property (copy, nonatomic)NSString * name;
@property (assign, nonatomic)int age;
@property (strong, nonatomic)School *school;
@property (copy, nonatomic)NSArray *address;
@end
#import "User.h"
@implementation User
- (NSDictionary *)eleTypeForArray {
return @{@"address": @"Address"};
}
@end
#import <Foundation/Foundation.h>
@interface NSObject (Dict2Model)
- (void)setDict:(NSDictionary *)dict;
+ (instancetype)initWithDict:(NSDictionary *)dict;
- (NSDictionary *)eleTypeForArray;
@end
#import "NSObject+Dict2Model.h"
#import <objc/runtime.h>
@implementation NSObject (Dict2Model)
- (void)setDict:(NSDictionary *)dict {
Class clazz = self.class;
while (clazz && clazz != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(clazz, &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];
NSLog(@"%d: key:%@ value:%@", i, key, value);
// 1. 如果類的例項變數多於字典中key
if (value == nil) {
continue;
}
// 2. 例項變數型別以@開頭並且字首不是NS開頭的(排除系統類) "@Student"
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
NSRange range = [type rangeOfString:@"@"];
if (range.location != NSNotFound) {
type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
if (![type hasPrefix:@"NS"]) {
Class class = NSClassFromString(type);
value = [class initWithDict:value];
} else if ([type isEqualToString:@"NSArray"]) {
NSArray *array = (NSArray *)value;
NSMutableArray *mArray = [NSMutableArray array];
id class;
if ([self respondsToSelector:@selector(eleTypeForArray)]) {
NSString *eleTypeStr = [[self eleTypeForArray] objectForKey:key];
class = NSClassFromString(eleTypeStr);
} else {
NSLog(@"陣列型別不明確!");
return;
}
for (int i = 0; i < array.count; i++) {
id obj = [class initWithDict:value[i]];
[mArray addObject:obj];
}
value = mArray;
}
}
[self setValue:value forKeyPath:key];
}
free(ivars);
clazz = [clazz superclass];
}
}
+ (instancetype)initWithDict:(NSDictionary *)dict {
NSObject *obj = [[self alloc] init];
[obj setDict:dict];
return obj;
}
@end
Student.json
{
"name" : "Tom",
"age" : 20,
"weight" : "181",
"school":{
"ID":1,
"name":"北京大學"
},
"address" : [
{
"ID":1,
"address":"上海市"
},
{
"ID":2,
"address":"北京市"
}
]
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSData *jsonData = [NSData dataWithContentsOfFile:@"/Users/macmini/Documents/Test/RuntimeWidget/RuntimeWidget/Student.json"];
NSDictionary *userDict = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:NULL];
User *user = (User *)[User initWithDict:userDict];
School *school = user.school;
Address *address = user.address[0];
NSLog(@"User: %@, %d, %@, %@", user.name, user.age, school.name, address.address);
}
return 0;
}
2016-07-21 08:57:26.089 Runtime[15421:3827096] User: Tom, 20, 北京大學, 上海市Program ended with exit code: 0