Objective-C runtime运行时详解
image.png最近看了一下runtime运行时方面的文章,总结一下,加上自己的一下理解,如果文章有未全和不足的地方,欢迎各位在下方留言补充和指正。
目录:
- runtime 消息机制
- runtime 添加属性
- runtime 交换方法
- runtime 动态添加方法
- runtime Class的常见方法
1.获取成员变量列表
2.获取属性列表
3.获取方法列表
4.获取协议列表
5.获得类方法
6.获得实例方法
7.添加方法
8.替换原方法实现
9.交换两个方法
- runtime method swizzling 黑魔法
runtime 消息机制
对于OC代码,调用方法的实质就是一个消息发送,OC底层通过runtime实现
消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。
每一个 OC 的方法,底层必然有一个与之对应的 runtime 方法。
-
首先必须要导入头文件 #import <objc/message.h>
- 在使用objc_msgSend方法编译时可能出现报错的情况,对应的解决办法如下 image.png
-
新建一个Person类 Person.h中代码
/** 实例方法*/
// 无参数无返回值
- (void)run1;
// 无参数有返回值
- (NSString *)run2;
// 有参数无返回值
- (void)run3:(NSString *)string;
// 有参数有返回值
- (NSString *)run4:(NSString *)string;
/** 类方法*/
// 无参数无返回值
+ (void)run1;
// 无参数有返回值
+ (NSString *)run2;
// 有参数无返回值
+ (void)run3:(NSString *)string;
// 有参数有返回值
+ (NSString *)run4:(NSString *)string;
- Person.m中代码
- (void)run1 {
NSLog(@"实例方法 :run1");
}
- (NSString *)run2 {
return @"实例方法 :run2";
}
- (void)run3:(NSString *)string {
NSLog(@"run3 --- 参数:%@", string);
}
- (NSString *)run4:(NSString *)string {
return [NSString stringWithFormat:@"run4 --- 参数:%@", string];
}
+ (void)run1 {
NSLog(@"run1 classMethod");
}
+ (NSString *)run2 {
return @"run2 classMethod";
}
+ (void)run3:(NSString *)string {
NSLog(@"run3 classMethod --- 参数:%@", string);
}
+ (NSString *)run4:(NSString *)string {
return [NSString stringWithFormat:@"run4 classMethod --- 参数:%@", string];
}
- 利用objc_msgSend调用上面的这些方法
/** 对象方法/实例方法 */
// 底层的实际写法
Person *person = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
person = objc_msgSend(person, sel_registerName("init"));
NSLog(@"无参数无返回值:");
((void (*) (id, SEL)) (void *)objc_msgSend)(person, sel_registerName("run1"));
objc_msgSend(person, @selector(run1));
NSLog(@"无参数有返回值:");
NSString *run2Return1 = ((NSString *(*) (id, SEL)) (void *)objc_msgSend)(person, sel_registerName("run2"));
NSLog(@"%@", run2Return1);
NSString *run2Return2 = objc_msgSend(person, @selector(run2));
NSLog(@"%@", run2Return2);
NSLog(@"有参数无返回值:");
((void (*) (id, SEL, NSString *)) (void *)objc_msgSend)(person, sel_registerName("run3:"), @"3333");
NSLog(@"有参数有反回值:");
NSString *run4Return1 = ((NSString *(*) (id, SEL, NSString *)) (void *)objc_msgSend)(person, sel_registerName("run4:"), @"4444");
NSLog(@"%@", run4Return1);
NSLog(@"--------------------------");
/** 类方法 */
NSLog(@"classMethod - 无参数无返回值:");
((void (*) (id, SEL)) (void *)objc_msgSend)(Person.class, sel_registerName("run1"));
objc_msgSend(Person.class, @selector(run1));
NSLog(@"classMethod - 无参数有返回值:");
NSString *run2Return1Class = ((NSString *(*) (id, SEL)) (void *)objc_msgSend)(Person.class, sel_registerName("run2"));
NSLog(@"%@", run2Return1Class);
NSString *run2Return2Class = objc_msgSend(Person.class, @selector(run2));
NSLog(@"%@", run2Return2Class);
NSLog(@"classMethod - 有参数无返回值:");
((void (*) (id, SEL, NSString *)) (void *)objc_msgSend)(Person.class, sel_registerName("run3:"), @"3333");
NSLog(@"classMethod - 有参数有反回值:");
NSString *run4Return1Class = ((NSString *(*) (id, SEL, NSString *)) (void *)objc_msgSend)(Person.class, sel_registerName("run4:"), @"4444");
NSLog(@"%@", run4Return1Class);
- 注:
/**
错误写法(arm64崩溃偶尔发生)
*/
objc_msgSend(person, sel_registerName("run3:"), @"12345678");
/**
标准写法
*/
((void (*) (id, SEL, NSString *)) (void *)objc_msgSend)(person, sel_registerName("run3:"), @"不能直接写objc_msgSend,会出现崩溃的现象(正常应该是可以的)");
- 解释一下标准写法前面参数
((void (*) (id, SEL)) (void *)objc_msgSend)
1、第一个void代表是否有返回值
如果返回值是NSString类型的 例:无参数有返回值的方法
写法:((NSString *(*) (id, SEL)) (void *)objc_msgSend)
2、(id, SEL)
如果方法有参数,参数类型是NSString 例:有参数无返回值
写法:((void *(*) (id, SEL, NSString *)) (void *)objc_msgSend)
3、关于离objc_msgSend最近的void从互联网上还没找到具体含义,还望知道的好友留言或私信告知
应用与注意
注:使用objc_msgSend()创建对象不能自动释放,对象需要手动release。使用runtime执行初始化方法创建的对象的时候是不在ARC控制之下的,所以在该类销毁的时候需要手动release
应用:使用objc_msgSend()创建对象时好处,应用之一就是在一个控制器要跳转多个控制器的时候,不再需要每个控制器都单独写一遍初始化,也不再需要每一个控制器单独写release方法,使用runtime的话,使用一个或者根据情况使用几个回调,返回控制器的类名以及相应的参数就好了。
Class class = objc_getClass(controllerName.UTF8String); //或者 NSStringFromClass(<#Class _Nonnull __unsafe_unretained aClass#>)
id viewController = ((id(*)(id,SEL))objc_msgSend)(class,NSSelectorFromString(@"new"));
[self xpz_pushViewController:viewController];
// 导航控制器获得控制权后进行release即可
- (void)xpz_pushViewController:(__kindof UIViewController *)viewController
{
[self pushViewController:viewController];
//release
((void(*)(id,SEL))objc_msgSend)(viewController,NSSelectorFromString(@"release"));
}
category添加属性
1.面试中经常会被问到如何给category添加属性,在平时我们偶尔也会遇到想要在分类中添加属性的情况,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成,其实可以使用runtime动态添加属性方法
2.还有另外一种方法: use @dynamic or provide a method implementation in this category
1.使用runtime动态添加属性
// objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)
// object:给哪个对象添加属性
// key:属性名称
// value:属性值
// policy:保存策略
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
- 需求:在UIView上添加一个播放器
- objc_getAssociatedObject有两个参数,第一个参数为从该object中获取关联对象,第二个参数为想要获取关联对象的key;
对于第二个参数const void *key,有以下四种推荐的key值:
- 声明
static char kAssociatedObjectKey;
,使用&kAssociatedObjectKey
作为 key 值; - 声明
static void *kAssociatedObjectKey = &kAssociatedObjectKey;,
使用kAssociatedObjectKey
作为key值; - 用
selector
,使用 getter 方法的名称作为key值; - 而使用
_cmd
可以直接使用该@selector
的名称,即hideButton,并且能保证改名称不重复。(与上一种方法相同)
static char playerViewKey; // playerView
static void *playerLayerKey = &playerLayerKey; // playerLayer
/************************************************* playerView ********************************************************/
// getter
- (UIView *)playerView {
UIView *_playerView = objc_getAssociatedObject(self, &playerViewKey);
if (!_playerView) {
objc_setAssociatedObject(self, &playerViewKey, _playerView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return _playerView;
}
// setter
- (void)setPlayerView:(UIView *)playerView {
return objc_setAssociatedObject(self, &playerViewKey, playerView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
/************************************************* avplayer ********************************************************/
//getter
- (AVPlayer *)avPlayer {
AVPlayer *_avPlayer = objc_getAssociatedObject(self, @selector(avPlayer));
if (!_avPlayer) {
objc_setAssociatedObject(self, @selector(avPlayer), _avPlayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return _avPlayer;
}
//setter
- (void)setAvPlayer:(AVPlayer *)avPlayer {
objc_setAssociatedObject(self, @selector(avPlayer), avPlayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
/************************************************* playerLayer ********************************************************/
- (AVPlayerLayer *)playerLayer {
return objc_getAssociatedObject(self, playerLayerKey);
}
- (void)setPlayerLayer:(AVPlayerLayer *)playerLayer {
objc_setAssociatedObject(self, playerLayerKey, playerLayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
/************************************************* playerItem ********************************************************/
/**
getter方法
*/
- (AVPlayerItem *)playerItem {
return objc_getAssociatedObject(self, _cmd);
}
/**
setter方法
*/
- (void)setPlayerItem:(AVPlayerItem *)playerItem {
objc_setAssociatedObject(self, @selector(playerItem), playerItem, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- use @dynamic
@dynamic playerView,playerItem,playerLayer,avPlayer;
扩展:
objc_getAssociatedObject与objc_setAssociatedObject方法另一种用途,在tableViewCell中的btn点击事件中使用
在- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;方法中使用:
if (self.GroupListArr.count > 0) {
cell.hidden = NO;
CB_ActivityGroupModel *model = self.GroupListArr[0];
[cell setModel:model];
[cell.joinGroupBtn addTarget:self action:@selector(joinGroupAction:) forControlEvents:(UIControlEventTouchUpInside)];
objc_setAssociatedObject(cell.joinGroupBtn, &joinGroup, model, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
使用
- (void)joinGroupAction:(UIButton *)btn
{
CB_ActivityGroupModel *model = objc_getAssociatedObject(btn, &joinGroup);
CB_ActivityGroupDetailsVC *vc = [[CB_ActivityGroupDetailsVC alloc]init];
vc.goodModel = self.detailsModel;
vc.model = model;
[self.navigationController pushViewController:vc animated:YES];
}
runtime 交换方法
当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。
class_getInstanceMethod 得到类的实例方法
class_getClassMethod 得到类的类方法
- 给系统的imageNamed添加额外功能
#import "UIImage+Image.h"
#import <objc/runtime.h>
@implementation UIImage (Image)
+ (void)load
{
Class class = [self class];
// 类方法
// 1.获取 imageNamed方法地址
Method originalMethod = class_getClassMethod(class, sel_registerName("imageNamed:"));
// 2.获取 xpz_imageNamed方法地址
Method swizzledMethod = class_getClassMethod(class, @selector(xpz_imageNamed:));
// 交换 imageNamed:
method_exchangeImplementations(originalMethod, swizzledMethod);
}
/**
看清楚下面是不会有死循环的
调用 imageNamed => xpz_imageNamed
调用 xpz_imageNamed => imageNamed
*/
+ (nullable UIImage *)xpz_imageNamed:(NSString *)name{
UIImage *xpz_image = [UIImage xpz_imageNamed:name];
if (xpz_image) {
NSLog(@"runtime添加额外功能--加载成功");
} else {
NSLog(@"runtime添加额外功能--加载失败");
}
return xpz_image;
}
@end
/**
不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super
所以第二步,我们要 自己实现一个带有扩展功能的方法.
+ (UIImage *)imageNamed:(NSString *)name {
}
*/
- 给UIViewController的viewWillAppear添加额外功能,就可以再控制台看到每次控制器的变化
#import "UIViewController+hook.h"
#import <objc/runtime.h>
@implementation UIViewController (hook)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(hook_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(class, originalSelector);
// Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)hook_viewWillAppear:(BOOL)animated {
[self hook_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
打印
[控制器:UIViewController+hook.m -- line: 53行] viewWillAppear: <NavigationViewController: 0x1060bfc00>
[控制器:UIViewController+hook.m -- line: 53行] viewWillAppear: <GroupViewController: 0x105a6c960>
[控制器:UIViewController+hook.m -- line: 53行] viewWillAppear: <NavigationViewController: 0x10607e400>
- 为什么要添加didAddMethod判断?
先尝试添加原SEL其实是为了做一层保护,因为如果这个类没有实现originalSelector,但其父类实现了,那class_getInstanceMethod会返回父类的方法。这样method_exchangeImplementations替换的是父类的那个方法,这当然不是我们想要的。所以我们先尝试添加 orginalSelector,如果已经存在,再用 method_exchangeImplementations 把原方法的实现跟新的方法实现给交换掉。
大概的意思就是我们可以通过class_addMethod为一个类添加方法(包括方法名称(SEL)和方法的实现(IMP)),返回值为BOOL类型,表示方法是否成功添加。需要注意的地方是class_addMethod会添加一个覆盖父类的实现,但不会取代原有类的实现。也就是说如果class_addMethod返回YES,说明子类中没有方法originalSelector,通过class_addMethod为其添加了方法originalSelector,并使其实现(IMP)为我们想要替换的实现。
runtime动态地添加方法
float runtime_addMethod(id receiver, SEL sel, const void *arg1, int arg2)
{
NSLog(@"方法名:%s, 参数1:%@, 参数2:%d", __FUNCTION__, [NSString stringWithUTF8String:arg1], arg2);
return 1;
}
Person_runtimeVC *vc = objc_msgSend(objc_getClass("Person_runtimeVC"), sel_registerName("alloc"));
vc = objc_msgSend(vc, sel_registerName("init"));
/** 添加方法*/
// 动态添加run方法
// class: 给哪个类添加方法
// SEL: 添加哪个方法,即添加方法的方法编号
// IMP: 方法实现 => 函数 => 函数入口 => 函数名(添加方法的函数实现(函数地址))
// type: 方法类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
class_addMethod([vc class], NSSelectorFromString(@"runtime_addMethod"), (IMP)runtime_addMethod, "f@:r^vd");
int returnValue = ((float (*)(id, SEL, const void *, int))objc_msgSend)((id)vc, NSSelectorFromString(@"runtime_addMethod"), "参数1", 10086);
NSLog(@"返回值:%d", returnValue);
NSLog(@"%s", @encode(const void *));
/**打印结果*/
[控制器:ViewController.m -- line: 108行] 方法名:runtime_addMethod, 参数1:参数1, 参数2:10086
[控制器:ViewController.m -- line: 74行] 返回值:1
[控制器:ViewController.m -- line: 75行] r^v
- 参数:
"f@:r^vd"
第1个字符:表示函数(方法)返回值类型,这里返回值类型是 `float` ,故为 `f`
第2、3个字符:苹果解释是由于函数(方法)至少带有两个参数(self和_cmd)还记得之前的 (id,SEL) 么,所以第2、3个字符必须是 ‘@:’,其实我们当做固定写法就好了
第4个字符根据NSLog(@"%s", @encode(const void *));打印结果可以看出来r^v是第三个参数的类型,第四个参数是int,所以是d
runtime 常见方法
原著https://www.jianshu.com/p/46dd81402f63
- 获取成员变量
/** 获取类中的所有成员变量*/
Ivar *ivarList = class_copyIvarList([Person_runtimeVC class], &count);
for(int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员变量
Ivar ivar = ivarList[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
NSString *key = [ivarName substringFromIndex:1];
NSLog(@"获取类成员变量:%@ --- %@",ivarName,key);
}
- 获取属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}
- 获取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
Method method = methodList[i];
NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
- 获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}
现在有一个Person类,和person创建的xiaoming对象,有test1和test2两个方法
- 获得类方法
Class PersonClass = object_getClass([Person class]);
SEL oriSEL = @selector(test1);
Method oriMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
- 获得实例方法
Class PersonClass = object_getClass([xiaoming class]);
SEL oriSEL = @selector(test2);
Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
- 添加方法
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
- 替换原方法实现
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
- 交换两个方法
method_exchangeImplementations(oriMethod, cusMethod);
什么是 method swizzling(俗称黑魔法)
- 简单说就是进行方法交换
- 在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的
-
每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP
image
selector --> 对应的IMP
-
交换方法的几种实现方式
-
利用 method_exchangeImplementations 交换两个方法的实现
-
利用 class_replaceMethod 替换方法的实现
-
利用 method_setImplementation 来直接设置某个方法的IMP。
image
这里可以参考简友这篇:【Runtime Method Swizzling开发实例汇总】http://www.jianshu.com/p/f6dad8e1b848
runtime知识很多,后期会慢慢补充,欢迎广大简友补充学习;刚学习使用Markdown,排版上做的不是太好。
本篇笔记部分参考自以下: