原文地址原文
Runtime在Objective-C中被称为“运行时系统”。
预热几个知识点
一、所有都是对象,方法都是消息
1、OC中所有id类型都被设计成对象,类本身也是一个对象。OC代码在运行时会动态转化为C代码。
2、所有方法调用都是发消息,例如
[self init];
被转化为objc_msgSend(self,@selector(init))
在OC中id指针,可以代表所有对象,其实id是结构体,我们看看id的具体结构:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
这是属性都是什么意思呢?下面一一介绍
- .Class 的isa指针,指向元类
- .super_class :指向超类
- .name是类名
- .version是类版本信息
- .info是这个类的详情信息
- .instance_size是这个类实例对象的大小
- .ivars是类成员变量
- .methodLists类的方法列表
- .cache是,存储被调用过的方法,方便下次使用
- .protocols是类的协议列表
二、方法调用顺序
- 调用一个
方法A
,首先runtime把方法转为消息发送,可以简单理解为objc_msgSend
。 - 在
cache
里查找,找到执行,否则 - 在本类的
methodLists
中查找,找到执行,否则 - 在父类中重复2、3步骤
- 直到
根类NSObject
都没找到,转向方法拦截
- 动态解析
方法A
,判断是不是系统忽略方法,例如retain、release等 - 判断
调用者target
是不是nil,OC语法允许nil对象调用不存在方法而不Crash
。 - 进入第二阶段,
消息转发
- 进入
resolveInstanceMethod:
方法。如果返回YES,调用class_addMethod
,执行方法完成。如果返回NO,向下 - 进入重定向
forwardingTargetForSelector:
,指定一个可以实现方法A
对象,完成这次调用。如果返回nil,向下 - 进入方法签名操作
methodSignatureForSelector:
,如果签名成功,有返回值,这时会调用消息转发方法forwardInvocation:
。 - 在
forwardInvocation:
中可以修改实现方法、修改响应对象。
三、获取参数
runtime可以获取类的各种参数,方法如下:
- class_copyPropertyList:获取属性列表
- class_copyMethodList:获取方法列表
- class_copyIvarList:获取成员变量
- class_copyProtocolList:获取协议列表
Runtime用途
一、直接通过C发送消息,来调用方法
- objc_msgSend:调用普通方法
- objc_msgSend_stret:消息返回值是数据结构
- objc_msgSend_fpret:消息返回值是浮点数
- objc_msgSendSuper:调用父类方法
- objc_msgSendSuper_stret:父类消息返回值是数据结构
通过代码来说明下:
新建一个Person类
@interface Person : NSObject
@property (nonatomic,strong) NSString *name;
@property (nonatomic,strong) NSString *idcard;
@property (nonatomic) NSInteger age;
person有下面方法
- (void)hello:(NSString*)name andAge:(NSInteger)age;
- (NSString*)goodMornig:(NSString*)name;
- (float)getHeight;
正常情况下,OC调用方法如下
Person *person = [[Person alloc] init];
[person hello:@"Dave" andAge:12];
在runtime机制下,可以直接用c方法调用,如下
1、((void (*) (id, SEL)) objc_msgSend) (person, sel_registerName("hello:andAge:"));
2、NSString *str = ((NSString* (*) (id, SEL)) objc_msgSend) (person, sel_registerName("goodMornig:"));
3、float f = ((float (*) (id, SEL)) objc_msgSend_fpret) (objct, sel_registerName("getHeight"));
二、关联对象
允许开发者对已经存在的对象在 Category 中添加自定义的属性:
设置关联对象核心方法是:
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
参数解析:
- .object:源对象
- .value :被关联的对象
- .key :关联键
- .plicy :关联行为,是个枚举
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
移除关联
objc_removeAssociatedObjects
通过代码来说明下:
例如给UIButton的Category,添加属性(判断按钮是否被点击了),正常Category只能扩展方法不能添加属性,但是关联对象打破了这个限制。
.m代码如下:
#import "UIButton+Tap.h"
#import
static const void *associatedKey = "associatedKey";
@implementation UIButton (Tap)
- (void)setTapButton:(Tap_button)tapButton{
objc_setAssociatedObject(self, associatedKey, tapButton, OBJC_ASSOCIATION_COPY_NONATOMIC);
[self removeTarget:self action:@selector(buttonTap:) forControlEvents:UIControlEventTouchUpInside];
if (tapButton) {
[self addTarget:self action:@selector(buttonTap:) forControlEvents:UIControlEventTouchUpInside];
}
}
- (Tap_button)tapButton{
return objc_getAssociatedObject(self, associatedKey);
}
- (void)buttonTap:(UIButton*)sender{
if (self.tapButton) {
self.tapButton();
}
}
调用扩展属性
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.tapButton = ^{
NSLog(@"button tap");
};
三、自动归档
归档和解档是iOS中的序列化和反序列化操作,需要遵循NSCoding协议。
例如对上面的Person类进行归档和解档。初级操作如下:
pragma mark --- 归档
- (void)encodeWithCoder:(NSCoder *)aCoder{
//设置归档属性
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeObject:self.idcard forKey:@"idCord"];
[aCoder encodeInteger:self.age forKey:@"age"];
[aCoder encodeFloat:self.height forKey:@"height"];
}
pragma mark --- 解档
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
self = [super init];
if (self) {
self.name = [aDecoder decodeObjectForKey:@"name"];
self.idcard = [aDecoder decodeObjectForKey:@"idCord"];
self.age = [[aDecoder decodeObjectForKey:@"age"] integerValue];
self.height = [[aDecoder decodeObjectForKey:@"height"] floatValue];
}
return self;
}
BUT,BUT,BUT这种写法太没有技术含量了,一旦模型属性数量增加,工作量就成倍增加,有了Runtime就可以轻松搞定了。
pragma mark --- 归档
- (void)encodeWithCoder:(NSCoder *)aCoder{
unsigned int count = 0;//属性个数
Ivar *ivars = class_copyIvarList([Person class], &count);
for (int i=0; i
在VC中调用方法如下
//---自动归档
Person *person = [[Person alloc] init];
person.name = @"Deve";
person.idcard = @"123456";
person.age = 18;
person.height = 170.0;
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"person.data"];//名字取什么都行
[NSKeyedArchiver archiveRootObject:person toFile:filePath];//归档
//---解档
//获取归档地址
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"person.data"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
NSLog(@"person's name is %@,and age is = %ld",person.name,person.age);
四、模型与字典转换
这其实是最常见的场景,我们从后台拿来数据,需要对数据进行处理,往往会建立模型,也就是Model。那么用字典生成模型是怎么操作的呢?一般方法如下:
- (instancetype)initWithDictionary:(NSDictionary*)dict{
if(self = [super init]){
self.name = dict[@"name"];
self.idCard = dict[@"idCard"];
...
}
return self;
}
看上去合情合理,但是属性一多,就会很麻烦,要写很多重复类似的赋值语句。
Runtime来解决!
简单说下原理:
- 字典转模型:利用objc_msgSend方法主动调用setter方法为Model赋值
- 模型转字典:利用objc_msgSend方法主动调用getter方法获取属性值生成字典
- kvc也可以替换setter或getter方法
通过代码来说明下:
-
首先,建立NSObject的Category,如下:
@interface NSObject (KeyValue)
-
然后,建立两个方法,分别是字典转模型和模型转字典,如下:
.h定义方法 //字典转模型 +(id)objectInitWithDictionary:(NSDictionary*)dic; //模型转字典 - (NSDictionary*)dictionaryWithObject; .m方法实现 //字典转模型 +(id)objectInitWithDictionary:(NSDictionary*)dic{ id objc = [[self alloc] init]; for (NSString *key in dic.allKeys) { id value = dic[key];//取值 //1、判断属性是不是Model,如果是Model递归改方法,如果不是向下 objc_property_t property = class_getProperty(self, key.UTF8String);//获取模型属性 unsigned int count = 0;//属性数量 objc_property_attribute_t *attributeList = property_copyAttributeList(property, &count); objc_property_attribute_t att = attributeList[0];//获取属性 NSString *attString = [NSString stringWithUTF8String:att.value];//转OC字符串 if ([attString isEqualToString:@"@\"Person\""]) { value = [self objectInitWithDictionary:value];//递归 } //2、用objc_msgSend调用setter方法,进行赋值 NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]];//例如setName: SEL setter= sel_registerName(methodName.UTF8String); if ([objc respondsToSelector:setter]) { ((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);//runtime,发消息方法 } free(attributeList); } return objc; } //模型转字典 - (NSDictionary*)dictionaryWithObject{ unsigned int count = 0; NSMutableDictionary *dic = [NSMutableDictionary dictionary]; objc_property_t *propertyList = class_copyPropertyList([self class], &count); for (int i=0; i
-
最后,在VC里面调用
pragma mark --- 六、字典和模型互转 - (void)keyValueExchange{ NSDictionary *dic = @{@"name":@"李磊", @"idcard":@"888888", @"age":@5, @"height":@170.0, @"student":@{ @"name":@"Halen" } }; //字典转模型 Person *person = [Person objectInitWithDictionary:dic]; NSLog(@"\n person's name is %@,\n person'age is %ld,\n person'height is %f",person.name,[person.age integerValue],[person.height floatValue]); //模型转字典 NSDictionary *dict = [person dictionaryWithObject]; NSLog(@"转换后的字典是\n%@",dict); }
五、动态解析
现在暂停,回到文章开头预热知识点
,第二部分我们讲到方法调用顺序
,在调启不存在方法时,系统会Crash,为了避免崩溃,我们可以利用runtime动态解析
。
通过代码来说明下:
首先我们建立两个对象School和Teacher,如下
-
school
#import
@interface School : NSObject -(void)RecruitmentTeacher;//招聘老师 @end -
teacher
#import
@interface Teacher : NSObject - (void)haveClass;//上课 @end -
VC里调用方法如下:
School *school = [[School alloc] init]; //调用 ((void (*) (id, SEL)) objc_msgSend) (school,sel_registerName("haveClass"));
可以看到,School
对象,调用了haveClass
方法,但是这个方法是Teacher
对象的。这个时候如果不做特殊处理程序就会崩溃。此时就该动态解析
出场的了。
有以下几种情况:
- 用class_addMethod方法动态添加一个方法,避免崩溃
- 没有动态添加方法,进行重定向forwardingTargetForSelector
- 重定向失败,配合签名方法methodSignatureForSelector,进行消息转发,在转发方法forwardInvocation里进行处理。
附上具体的.m文件代码
#import "School.h"
#import "Teacher.h"
#import
@implementation School
-(void)RecruitmentTeacher{
NSLog(@"recruitment a teacher");
}
#pragma mark --- 1
/*
如果当前对象调用了一个不存在的方法
Runtime会调用resolveInstanceMethod:来进行动态方法解析
我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作
返回NO,则进入下一步forwardingTargetForSelector:
*/
+(BOOL)resolveInstanceMethod:(SEL)sel{
#if 0
return NO;
#else
class_addMethod(self, sel, class_getMethodImplementation(self, sel_registerName("RecruitmentTeacher")), "v@:");
return [super resolveInstanceMethod:sel];
#endif
}
#pragma mark ---2
/*
在消息转发机制执行前,Runtime 系统会再给我们一次 “重定向” 的机会
通过重载forwardingTargetForSelector:方法来替换消息的接受者为其他对象
返回nil则进步下一步forwardInvocation:
*/
-(id)forwardingTargetForSelector:(SEL)aSelector{
#if 0
return nil;
#else
// return nil;
return [[Teacher alloc] init];//找到可以实现方法的对象,进行替换
#endif
}
#pragma mark ---3
/*
进行方法签名,
返回nil,表示不做签名处理,
若返回方法签名,进入下一步,消息转发
*/
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
// return nil;
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
#pragma mark ---4
/*
消息转发
可以做很多操作,修改实现方法,修改相应对象
*/
-(void)forwardInvocation:(NSInvocation *)anInvocation{
return [anInvocation invokeWithTarget:[[Teacher alloc] init]];//修改相应对象
}
@end
Demo下载地址Demo下载