1. 概念
runtime(運行時系統),是一套基于C語言API,包含在 <objc/runtime.h>和<objc/message.h>中,運行時系統的功能是在運行期間(而不是編譯期或其他時機)通過代碼去動態的操作類(獲取類的內部信息和動態操作類的成員),如創建一個新類、為某個類添加一個新的方法或者為某個類添加實例變量、屬性,或者交換兩個方法的實現、獲取類的屬性列表、方法列表等和Java中的反射技術類似。
2. 探索
程序最終運行的是二進制的可執行文件,編譯器需要將OC代碼轉換為運行時代碼,再將運行時代碼經過一些處理成最終的二進制可執行文件
mian.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
[[NSObject alloc] init];
}
return 0;
}
使用終端命令行切換到mian.m文件所在的目錄下并執行: clang -rewrite-objc main.m
在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常用的數據類型
OC源代碼最終會翻譯成運行時代碼,而runtime是一套C語言API,也就是說OC的數據類型最終也會翻譯成C語言中的數據類型
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, "v@:");
// 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;
}`