RunTime简称运行时。就是系统在运行的时候的一些机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。编译完成之后直接顺序执行,无任何二义性。OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编译阶段OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。那OC是怎么实现动态调用的呢?下面我们来看看OC通过发送消息来达到动态调用的秘密。
首先当我们调用一个方法时,例如
[obj doSomeThing];
其中obj是一个对象,doSomeThing是一个函数名称。对于这样一个简单的调用。在编译时RunTime会将上述代码转化成
objc_msgSend(obj,@selector(doSomeThing));
我们知道iOS中所有的对象object都是继承自NSObject,我们点进去NSObject里面看看
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
可以看到NSObject的类申明里面存在一个Class的isa指针。然后我们在看看Class
typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
}
可以看到Class是一个objc_class的结构体,而objc_class的结构体里面又存在很多东西,下面就看看这些都是什么。
Class isa: 指向metaclass,也就是静态的Class。一般一个Obj对象中的isa会指向普通的Class,这个Class中存储普通成员变量和对象方法(“-”开头的方法),普通Class中的isa指针指向静态Class,静态Class中存储static类型成员变量和类方法(“+”开头的方法)。注意:所有metaclass中isa指针都指向跟metaclass。而跟metaclass则指向自身。Root metaclass是通过继承Root class产生的。与root class结构体成员一致,也就是前面提到的结构。不同的是Root metaclass的isa指针指向自身。
Class super_class:指向父类,如果这个类是根类,则为NULL。
const char *name : 类名
long version:类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
long info:一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法
long instance_size:该类的实例变量大小(包括从父类继承下来的实例变量)
+struct objc_ivar_list *ivars :用于存储每个成员变量的地址
struct objc_method_list *methodLists:与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法
struct objc_cache *cache :指向最近使用的方法的指针,用于提升效率;
struct objc_protocol_list *protocols: 存储该类遵守的协议
下面我们就来看看具体消息发送之后是怎么来动态查找对应的方法的。
首先,编译器将代码[obj doSomeThing];转化为objc_msgSend(obj, @selector
(doSomeThing));,在objc_msgSend函数中。首先通过obj的isa指针找到obj对应的class。在Class中先去cache中通过SEL查找对应函数method(猜测cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若cache中未找到。再去methodList中查找,若methodlist中未找到,则取superClass中查找。若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
最后补充一下@selector (doSomeThing):
这是一个SEL方法选择器。SEL其主要作用是快速的通过方法名字(doSomeThing)查找到对应方法的函数指针,然后调用其函数。SEL其本身是一个Int类型的一个地址,地址中存放着方法的名字。对于一个类中。每一个方法对应着一个SEL。所以iOS类中不能存在2个名称相同的方法,即使参数类型不同,因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。
runtime的一下常见使用
- 设置关联获取对象和为类别添加属性
- 方法的交换swizzling
- 获取私有属性和方法
- 对私有属性修改
- 归档和解档
- 动态添加方法
- 字典转模型
1.设置关联获取对象和为类别添加属性
首先是为一个值设置关联
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
- id object 设置关联的对象,一般传self就行啦。
- const void *key 设置key,通过这个key来获取关联的值
- id value 关联的值。
- objc_AssociationPolicy policy 关联值的属性是用assign ,retain ,copy等
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_EXPORT id objc_getAssociatedObject(id object, const void *key)
- id object 对应设置关联的id object 。
- const void *key key当然是设置关联的key
移除关联对象
OBJC_EXPORT void objc_removeAssociatedObjects(id object)
使用示例
//定义全局的char
static const char *yoyo;
//设置关联
objc_setAssociatedObject(self, &yoyo, @"哈哈哈哈", OBJC_ASSOCIATION_COPY_NONATOMIC);
//获取关联值
objc_getAssociatedObject(self,&yoyo000);
//移除关联
objc_removeAssociatedObjects(self);
为类别添加属性
我们都知道,类别不能直接添加属性不然会报一下警告提示,如果不重新set和get方法直接使用的话还会crash。
下面我们为NSObject类添加一个property字符串属性
首先是.h文件
#import <Foundation/Foundation.h>
@interface NSObject (addProperty)
@property (nonatomic,strong)NSString *property;
@end
然后是在.m文件中重写set方法和get方法,但是要利用runtime来写
#import "NSObject+addProperty.h"
#import <objc/runtime.h>
static const char *kProperty;
@implementation NSObject (addProperty)
-(void)setProperty:(NSString *)property
{
objc_setAssociatedObject(self, &kProperty, property, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)property
{
return objc_getAssociatedObject(self, &kProperty);
}
@end
2.方法的交换swizzling
利用runtime我们可以截取方法的响应,然后交换两个方法,在我们添加的方法中加入我们想要的操作,例如我们可以截取button的响应事件,交换成我们自定义的方法,那么button在每次响应之前都会先响应我们的方法,这样子可以实现button的点击统计事件等之类的操作。
首先获取方法
OBJC_EXPORT Method class_getInstanceMethod(Class cls, SEL name)
- Class cls 传入你想获取的方法的类
- SEL name 方法名。方法的获取首先会在该类的class_copyMethodList中寻找,如果没有就在其父类的class_copyMethodList中寻找,也没有就返回空。
交换方法,用前面获取到的响应方法和自定义的方法,进行互换,使用method_exchangeImplementations需要注意的是要在dispatch_once_t中执行一次就好,不然偶数执行的话又会调换回来原先的方法
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2)
下面实例一下button的点击事件的截取交换,首先给button添加一个类别.h文件中不用做什么操作
#import <UIKit/UIKit.h>
@interface UIButton (buttonHook)
@end
然后在.m文件中执行交换,可以在类方法initialize或者load中执行交换的,initialize方法是类第一次初始化的时候调用一次。需要注意的是要在dispatch_once_t中执行一次就好,不然偶数执行的话又会调换回来原先的方法
#import "UIButton+buttonHook.h"
#import <objc/runtime.h>
@implementation UIButton (buttonHook)
+(void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//获取button的点击响应事件方法
Method sendAction = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
//获取自定义的方法
Method hookSendAction = class_getInstanceMethod(self, @selector(hookSendAction:to:forEvent:));
//交换两个方法
method_exchangeImplementations(sendAction, hookSendAction);
});
}
//自定义的方法
- (void)hookSendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
//do something.
//这里执行的是button的响应方法即@selector(sendAction:to:forEvent:)
[self hookSendAction:action to:target forEvent:event];
}
@end
3. 获取属性和方法
获取属性
方法1
OBJC_EXPORT Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
- Class cls 类,想获取那个类就传那个类
- unsigned int *outCount int类型指针,这里会返回属性个数的长度给该int数据
返回值为lvar数组。里面包含类的属性,lvar是一个结构体,看看里面有什么
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
- char *ivar_name 属性的名称
- char *ivar_type 属性的类型
- int ivar_offset 属性的偏移? 如果是object类型的是8字节,基本数据类型4字节
方法2,使用class_copyPropertyList获取,这个获取和方法1的获取类似,参数都一样
OBJC_EXPORT objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
typedef struct objc_property *objc_property_t;
获取方法 使用类似于获取属性
OBJC_EXPORT Method *class_copyMethodList(Class cls, unsigned int *outCount)
- Class cls 类,想获取那个类就传那个类
- unsigned int *outCount int类型指针,这里会返回属性个数的长度给该int数据
返回值为Method数组。里面包含类的方法,Method是一个结构体,看看里面有什么
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
- SEL method_name 方法名
- char *method_types 方法类型
- IMP method_imp 一个函数指针,保存了方法的地址
下面我们看一下具体的使用,首先先创建一个Property的类,在.h文件中添加一些属性和方法,在.m文件中也添加一个私有属性和私有方法,看看我们能不能获取得到
#import <Foundation/Foundation.h>
@interface Property : NSObject
@property (nonatomic,strong)NSString *name;
@property (nonatomic,assign)int age;
@property (nonatomic,assign)float height;
@property (nonatomic,assign)float weight;
@property (nonatomic,assign)NSInteger integer;
@property (nonatomic,strong)NSDictionary *dic;
@property (nonatomic,strong)NSArray *array;
-(void)doSomething;
@end
#import "Property.h"
@interface Property ()
//私有变量
@property (nonatomic,assign)int sex;
@end
@implementation Property
//私有方法
-(void)privateDoSomething
{
}
-(void)doSomething
{
}
@end
接着我们在viewcontroller里面获取Property类的属性和方法
#pragma mark - 获取所有的属性(包括私有的)
- (void)getAllIvar {
unsigned int count = 0;
//Ivar:定义对象的实例变量,包括类型和名字。
//获取所有的属性(包括私有的)
Ivar *ivars= class_copyIvarList([Property class], &count);
for (int i = 0; i < count; i++) {
//取出成员变量
Ivar ivar = ivars[i];
//获取属性名
NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
//获取属性类型
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
NSLog(@"属性 --> %@ 和 %@ 和 %td ",name,type,ivar_getOffset(ivar));
}
}
#pragma objc_property_t - 获取所有的属性(包括私有的)
-(void)getAllProperty
{
unsigned int count = 0;
objc_property_t *propertys = class_copyPropertyList([Property class], &count);
for (int i = 0; i<count; i++) {
objc_property_t property = propertys[i];
NSString *name = [NSString stringWithUTF8String:property_getName(property)];
NSLog(@"哈哈哈:%@",name);
}
}
#pragma mark - 获取所有的方法(包括私有的)
- (void)getAllMethod {
unsigned int count = 0;
//获取所有的方法(包括私有的)
Method *memberFuncs = class_copyMethodList([Property class], &count);
for (int i = 0; i < count; i++) {
//获取方法
SEL address = method_getName(memberFuncs[i]);
//获取方法指针
IMP imp = method_getImplementation(memberFuncs[i]);
//获取方法类型
const char *type = method_getTypeEncoding(memberFuncs[i]);
NSString *methodName = [NSString stringWithCString:sel_getName(address) encoding:NSUTF8StringEncoding];
NSLog(@"方法 : %@ --->类型%s",methodName,type);
}
}
最后我们看看打印出来的日志,完美获取到所有的属性和方法(包括私有)。而且属性的set和get方法也都可以获取到。
2017-06-22 11:03:30.573646+0800 runtime[4880:1603707] 属性 --> _age 和 i 和 8
2017-06-22 11:03:30.573848+0800 runtime[4880:1603707] 属性 --> _height 和 f 和 12
2017-06-22 11:03:30.574219+0800 runtime[4880:1603707] 属性 --> _weight 和 f 和 16
2017-06-22 11:03:30.574411+0800 runtime[4880:1603707] 属性 --> _sex 和 i 和 20
2017-06-22 11:03:30.574807+0800 runtime[4880:1603707] 属性 --> _name 和 @"NSString" 和 24
2017-06-22 11:03:30.575005+0800 runtime[4880:1603707] 属性 --> _integer 和 q 和 32
2017-06-22 11:03:30.575115+0800 runtime[4880:1603707] 属性 --> _dic 和 @"NSDictionary" 和 40
2017-06-22 11:03:30.575217+0800 runtime[4880:1603707] 属性 --> _array 和 @"NSArray" 和 48
2017-06-22 11:03:30.575361+0800 runtime[4880:1603707] 方法 : privateDoSomething --->类型v16@0:8
2017-06-22 11:03:30.575459+0800 runtime[4880:1603707] 方法 : doSomething --->类型v16@0:8
2017-06-22 11:03:30.575552+0800 runtime[4880:1603707] 方法 : dic --->类型@16@0:8
2017-06-22 11:03:30.575645+0800 runtime[4880:1603707] 方法 : setDic: --->类型v24@0:8@16
2017-06-22 11:03:30.575759+0800 runtime[4880:1603707] 方法 : .cxx_destruct --->类型v16@0:8
2017-06-22 11:03:30.575856+0800 runtime[4880:1603707] 方法 : setName: --->类型v24@0:8@16
2017-06-22 11:03:30.576947+0800 runtime[4880:1603707] 方法 : name --->类型@16@0:8
2017-06-22 11:03:30.577344+0800 runtime[4880:1603707] 方法 : array --->类型@16@0:8
2017-06-22 11:03:30.577455+0800 runtime[4880:1603707] 方法 : setArray: --->类型v24@0:8@16
2017-06-22 11:03:30.577550+0800 runtime[4880:1603707] 方法 : height --->类型f16@0:8
2017-06-22 11:03:30.580284+0800 runtime[4880:1603707] 方法 : setHeight: --->类型v20@0:8f16
2017-06-22 11:03:30.580562+0800 runtime[4880:1603707] 方法 : weight --->类型f16@0:8
2017-06-22 11:03:30.580663+0800 runtime[4880:1603707] 方法 : setWeight: --->类型v20@0:8f16
2017-06-22 11:03:30.580757+0800 runtime[4880:1603707] 方法 : age --->类型i16@0:8
2017-06-22 11:03:30.582048+0800 runtime[4880:1603707] 方法 : setAge: --->类型v20@0:8i16
2017-06-22 11:03:30.582122+0800 runtime[4880:1603707] 方法 : integer --->类型q16@0:8
2017-06-22 11:03:30.582185+0800 runtime[4880:1603707] 方法 : setInteger: --->类型v24@0:8q16
2017-06-22 11:03:30.582453+0800 runtime[4880:1603707] 方法 : setSex: --->类型v20@0:8i16
2017-06-22 11:03:30.582519+0800 runtime[4880:1603707] 方法 : sex --->类型i16@0:8
4.对私有属性修改
上面讲到获取类的属性,我们可以利用这个在外部修改类的私有属性,因为私有属性我们在外部是不能直接访问私有属性的。
用kvo