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;
}