一.什么是runtime(运行时)
Objective-C
是一个动态语言,一个由Objective-C
编写的程序跑起来分为编译过程和运行过程,当程序在运行的时候,才能做一些处理,比如说类的创建,方法的调用,消息的传递和转发等。也就是说runtime
就是程序正在运行的时候的状态。(C
是一种静态语言,它在编译的时候就完成这些处理)
可以简单的理解为:Objective-C
编写的程序在运行的过程,其实就是runtime
状态下的C
语言代码。
实例说明:在.h
文件声明一个对象方法,如果在.m
文件中不做任何实现的话,然后在外部对这个对象方法进行调用。程序编译的时候不会报错,运行的时候再报错。
二.runtime中的class(类)
在Objective-C
中,任何类的定义都是对象。类和类的实例(对象)在本质上是没有区别的。
1.打开Xcode
,然后按下 shift + command + O
,搜索 objc.h
。
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
可以看出 Class
(类)是一个 objc_class
结构类型的指针。
2.搜索 objc_class
。
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;
三.runtime中消息的传递
对象方法的调用:[obj method]
,在runtime
机制中,其实就是消息的发送objc_msgSend(obj, method)
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
- (void)setName:(NSString *)name;
- (void)setAge:(NSString *)age;
@end
NS_ASSUME_NONNULL_END
Person *p = [[Person alloc] init];
[p setName:@"wxh"];
当程序运行中,执行到[p setName:@"wxh"];
这条语句时候,在runtime
中C
做了这样的消息传递objc_msgSend(p, setName:)
,具体如下:
1.通过 p
(obj)
的isa
指针找到Person
这个类(class)
。
注:isa
指针 是一个Class
类型的指针,每个实例对象有个isa
的指针,他指向对象的类。
2.在Person
这个类(class)
的methodLists
(存放这个类对象函数的一个数组列表)中找到setName:
这个函数(method)
,如果找不到,就到Person
的父类(super_class)
中去找。
3.找到setName:
这个函数(method)
,通过setName:
这个函数(method)
的IMP
指针 ,调用这个函数(method)
。
注:IMP
指针 IMP
是一个函数指针,指向函数实现的地址。
4.另外一个重要成员, objc_cache
类型的cache
。它把经常调用的函数缓存起来,当再次收到setName:
的消息的时候,直接从cache
里面找,大大提高了效率。
总结:在Objective-C
中,函数的调用,其实就是在runtime
中C
做了消息传递。runtime
中C
做消息传递的函数。
objc_msgSend(void /* id self, SEL op, ... */ )
说到这里,顺便看一下底层中的定义函数
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp
}
SEL
是指向 函数名的指针 IMP
是指向函数实现地址的指针 char
函数的类型
由此可以看出,在Objective-C
底层中定义一个函数的时候,并没有涉及到参数,这就是为什么Objective-C
中不可以使用函数重载的原因。
四.runtime的应用
1. 关联对象。
runtime
给出的方法有
//关联对象
①void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
②id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
③void objc_removeAssociatedObjects(id object)
(1)给分类category
添加属性
举个例子:给NSObject
添加一个分类 NSObject+wxh
,记得#import "objc/runtime.h"
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (wxh)
@property (nonatomic,copy)NSString *name;
@property (nonatomic,strong)NSArray *datas;
@end
NS_ASSUME_NONNULL_END
#import "NSObject+wxh.h"
#import "objc/runtime.h"
@implementation NSObject (wxh)
// 用一个字节来存储key值,设置为静态私有变量,避免外界修改
static char nameKey;
- (void)setName:(NSString *)name{
// 将某个值与某个对象关联起来,将某个值存储到某个对象中
objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name{
return objc_getAssociatedObject(self, &nameKey);
}
static char datasKey;
- (void)setDatas:(NSArray *)datas{
objc_setAssociatedObject(self, &datasKey, datas, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSArray *)datas{
return objc_getAssociatedObject(self, &datasKey);
}
@end
NSString *string = [[NSString alloc] init];
string.name = @"123";
NSLog(@"%@",string.name);
string.datas = @[@"w",@"x",@"h"];
NSLog(@"%@",string.datas);
解释:跟KVC
的字典转模型差不多逻辑。
- 先通过
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
:设置关联对象的方法,相当于setValue:forKey
。 - 再使用
id objc_getAssociatedObject(id object, const void *key)
:获取关联对象。相当于valueForKey:
。 -
objc_AssociationPolicy policy
:关联策略。
OBJC_ASSOCIATION_ASSIGN 等价于 @property(assign)。
OBJC_ASSOCIATION_RETAIN_NONATOMIC 等价于 @property(strong, nonatomic)。
OBJC_ASSOCIATION_COPY_NONATOMIC 等价于 @property(copy, nonatomic)。
OBJC_ASSOCIATION_RETAIN 等价于 @property(strong,atomic)。
OBJC_ASSOCIATION_COPY 等价于 @property(copy, atomic)。
(2)使分散的代码通过block
的形式集中到一起。这样可读性更强,代码更简洁。
举个例子:button
添加点击事件。
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
[btn setBackgroundColor:[UIColor redColor]];
[self.view addSubview:btn];
[btn addTarget:self action:@selector(btnClick) forControlEvents:UIControlEventTouchUpInside];
}
- (void)btnClick{
NSLog(@"我被点击了");
}
也就是说我们创建按钮
和按钮点击事件
是分开的处理,如果页面逻辑比较多,调试的时候就会比较麻烦,有时候找一个按钮的点击事件
找了大半天。下面将两者关联起来。
#import <UIKit/UIKit.h>
typedef void (^event)(id sender);
NS_ASSUME_NONNULL_BEGIN
@interface UIButton (wxh)
- (void)handelWithEvent:(event)block;
@end
NS_ASSUME_NONNULL_END
#import "UIButton+wxh.h"
#import <objc/runtime.h>
@implementation UIButton (wxh)
- (void)handelWithEvent:(event)block {
if (block) {
objc_setAssociatedObject(self, @selector(btnAction:), block, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[self addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)btnAction:(id)sender{
event block = objc_getAssociatedObject(self,@selector(btnAction:));
if (block) {
block(sender);
}
}
@end
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
[btn setBackgroundColor:[UIColor redColor]];
[self.view addSubview:btn];
[btn handelWithEvent:^(id sender) {
NSLog(@"我被点击了");
}];
也就是说在开发中,我们可以利用这一点代替比较繁琐的代理事件。
2. 交换方法
class_getClassMethod
得到类的类方法
class_getInstanceMethod
得到类的对象方法
- 交换类方法
import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSURL (wxh)
+ (instancetype)wxhURLWithString:(NSString *)URLString;
@end
NS_ASSUME_NONNULL_END
#import "NSURL+wxh.h"
#import <objc/runtime.h>
@implementation NSURL (wxh)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = object_getClass([NSURL class]);
SEL SEL_old = @selector(URLWithString:);
SEL SEL_new = @selector(wxhURLWithString:);
Method method_old = class_getClassMethod([NSURL class], SEL_old);
Method method_new = class_getClassMethod([NSURL class], SEL_new);
IMP imp_old = method_getImplementation(method_old);
IMP imp_new = method_getImplementation(method_new);
BOOL beMethod_old = class_addMethod(cls, SEL_old, imp_new, method_getTypeEncoding(method_new));
if (beMethod_old) {
class_replaceMethod(cls, SEL_new, imp_old, method_getTypeEncoding(method_old));
}
else{
method_exchangeImplementations(method_old, method_new);
}
});
}
+ (instancetype)wxhURLWithString:(NSString *)URLString{
if (!URLString.length) {
URLString = @"www.baidu.com";
}
NSURL *url = [NSURL wxhURLWithString:URLString];
return url;
}
@end
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSURL *url = [NSURL URLWithString:@""];
NSLog(@"打印URL:%@",url);
}
@end
打印结果:打印URL:www.baidu.com
- 交换对象方法
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSMutableArray (wxh)
- (void)wxhAddObject:(id)object;
@end
NS_ASSUME_NONNULL_END
#import "NSMutableArray+wxh.h"
#import <objc/runtime.h>
@implementation NSMutableArray (wxh)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSMutableArray *obj = [[NSMutableArray alloc] init];
[obj exchangeImplementationsMethod:@selector(addObject:) WithMethod:@selector(wxhAddObject:)];
});
}
- (void)wxhAddObject:(id)object{
if (object) {
[self wxhAddObject:object];
}
else{
NSLog(@"=== object is nil ===");
}
}
- (void)exchangeImplementationsMethod:(SEL)SEL_old WithMethod:(SEL)SEL_new{
Class cls = [self class];
Method method_old = class_getInstanceMethod(cls, SEL_old);
Method method_new = class_getInstanceMethod(cls, SEL_new);
IMP imp_old = method_getImplementation(method_old);
IMP imp_new = method_getImplementation(method_new);
BOOL beMethod_old = class_addMethod(cls, SEL_old, imp_new, method_getTypeEncoding(method_new));
if (beMethod_old) {
class_replaceMethod(cls, SEL_new, imp_old, method_getTypeEncoding(method_old));
}
else{
method_exchangeImplementations(method_old, method_new);
}
}
@end
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *r = nil;
NSMutableArray *arr = [NSMutableArray array];
[arr addObject:r];
}
@end
打印结果:=== object is nil ===
解释:
(1)全局效果:不需要引入头文件,直接调用原来的方法就可。
(2)只实现一次dispatch_once
:交换方法效果是全局的,只能让它实现一次。虽然系统加载程序时+ (void)load
只调用一次,但+ (void)load
是一个公开的类函数,有可能被手动调用。因此加上dispatch_once
是必要的。
(3)class_addMethod
:给class
动态添加方法sel
,如果sel
在class
中已经存在的,返回值是NO
,否则返回值为YES
。
(4)class_replaceMethod
:修改函数的IMP
指针,使其指向新的实现地址。
(5)原理:首先,通过dispatch_once
保证方法交换有且只有一次,然后根据class_addMethod
的返回值判断要被交换的方法本身是否存在。如果为NO
,说明存在,通过exchangeImplementationsMethod
将两个函数的IMP
指针指向的函数实现地址交叉互换。如果为YES
,说明要在交换的方法本身是不存在的,但是这时候已经通过class_addMethod
将它添加到这个class
里面,而且将他的IMP
指向新的方法。那么最后只需要再通过class_replaceMethod
将新的方法的IMP
指向要被交换的方法即可。(其实最后这一步是可以省略的,因为如果要被交换的方法本身是不存在的,那这整个过程其实就相当于给class
添加方法而已。)
(6)object_getClass
获取父类的Class
-
应用场景 一般都是用来改变一些系统方法的实现。
(1)例如上面NSMutableArray
的例子,达到防奔溃处理。
(2)改变viewDidLoad
打印出当前控制器的名称等。
3.动态添加函数
class_addMethod
返回BOOL
值,YES
表示添加成功,NO
表示添加不成功。
+ (BOOL)resolveInstanceMethod:(SEL)sel
对象函数动态决议方法。
+ (BOOL)resolveClassMethod:(SEL)sel
类函数动态决议方法。
- 动态添加对象方法:创建一个
Person
继承于NSObject
,不添加任何属性和方法,直接在外部给Person
添加一个对象方法。
#import "Person.h"
#import <objc/runtime.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
class_addMethod([Person class], @selector(say:WithName:), class_getMethodImplementation([ViewController class], @selector(personSay:WithName:)), method_getTypeEncoding(class_getInstanceMethod([ViewController class], @selector(personSay:WithName:))));
[p performSelector:@selector(say:WithName:) withObject:@"hello" withObject:@"wxh"];
}
- (void)personSay:(NSString *)string WithName:(NSString *)name{
NSLog(@"%@,%@",name,string);
}
打印结果为wxh,hello
- 动态添加类方法
#import "ViewController.h"
#import "Person.h"
#import <objc/runtime.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
class_addMethod(object_getClass([Person class]), @selector(drive), class_getMethodImplementation(object_getClass([ViewController class]), @selector(personDrive)), method_getTypeEncoding(class_getClassMethod(object_getClass([ViewController class]), @selector(personDrive))));
[Person performSelector:@selector(drive)];
}
+ (void)personDrive{
NSLog(@"===");
}
打印结果为===
解释:
(1)如果是一个类class
要添加对象方法,直接用类class
添加。如果一个类class
要添加类方法,只需要通过这个类的父类object_getClass
,添加成父类的对象方法,也就是该类的类方法。
(2)方法添加成功之后,必须要通过performSelector
调用,不能直接调用。因为class_addMethod
是在程序运行状态下才执行的,而编译状态下,还找不到动态添加的方法。
(3)当一个对象cls
去调用一个函数method
的时候,cls
通过自身的ipa
指针找到该对象的类class
,然后通过class
中的函数列表methodLists
查找method
的函数名sel
,如果找到sel
,通过IMP
指针指向的'method'的实现地址,调用该method
。那如果找不到呢,这时候就会去到动态决议方法那里,并返回NO
值。
(4)class_addMethod
当要添加的方法在改类或者该类的父类中已经存在时,class_addMethod
返回NO
。
-
应用场景
(1)在类的外部动态给类class
添加方法。
(2)重写动态协议,防止调用不存在的方法时,程序崩溃crash
。
(3)通过class_addMethod
的返回值判断某个类中是否有某个方法。
4.获取属性,修改属性和获取方法。
-
获取属性
class_copyIvarList
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([UITextField class], &count);
for (int i = 0; i<count; i++) {
// 取出成员变量
Ivar ivar = *(ivars + i);
// 打印成员变量名字 数据类型
NSLog(@"%s--%s", ivar_getName(ivar),ivar_getTypeEncoding(ivar));
}
// 释放
free(ivars);
-
获取方法
class_copyMethodList
unsigned int methCount = 0;
Method *meths = class_copyMethodList([UITextField class], &methCount);
for(int i = 0; i < methCount; i++) {
Method meth = meths[i];
SEL sel = method_getName(meth);
const char *name = sel_getName(sel);
NSLog(@"%s", name);
}
free(meths);
- 修改属性
通过KVC的方式进行修改即可
[textField setValue:[UIColor greenColor] forKeyPath:@"_placeholderLabel.textColor"];