方法调用 objc_msgSend

调用方法(函数)是语言经常使用的功能,在 Objective-C 中专业一点的叫法是 传递消息(pass a message)。Objective-C 的方法调用都是 动态绑定 ,而C语言中函数调用方式是 静态绑定 ( static binding ),也就是说,在编译时期就能决定和知道在运行时所调用的函数。

以下面代码为例:

void sayHello(){
}
void sayGoodBye(){
}
void saySomething(int type){if(type == 0){sayHello();}else{sayGoodBye();}
}

基本上,上面的代码在编译的时候编译器就知道 sayHellosayGoodBye 两个函数的存在,函数地址是硬编码在指令之中的。但是如果换一种写法:

void sayHello(){
}
void sayGoodBye(){
}
void saySomething(int type){void (*something) ();if(type == 0){something = sayHello;}else{something = sayGoodBye;}something();
}

这就得使用 动态绑定 ,待调用的函数地址需要到运行时才能读取出来。
在 Objective-C 中,对某一个对象传递消息,会用动态绑定机制来决定到底是调用哪个方法。而Objective-C是 C 的超集,底层是由 C语言实现,但是对象接收消息后会调用哪个方法都是在运行期决定。

给对象发送消息可以这么来写:

id object = [list objectAtIndex:1];

在这行代码中, list 称为 接收者objectAtIndex 叫做 选择器, 选择器和参数合起来称为消息。当编译器看到这行代码的时候,会换成标准的C语言函数调用:

void objc_msgSend(id self, SEL cmd, ...);
id lastObject = objc_msgSend(list, @selector(objectAtIndex:), parameter);

objc_msgSend 这个函数可以接收两个及两个以上的参数,第一个参数是接收者,第二个参数是选择器,后面的参数是保持顺序的原来消息传递的参数,objc_msgSend会依据接收者和选择器来决定调用哪个方法,首先在接收者的方法列表中寻找,如果找不到就会沿着继承体系去向上一层一层的寻找,如果仍旧找不到就会执行消息转发(message forwarding)
当消息第一次传递之后,objc_msgSend 会将匹配结果进行缓存,下次会直接调用方法。消息传递除了objc_msgSend之外在特殊情况下还会有其他的方法来处理:

  • objc_msgSend_stret 如果待发送的消息返回一个结构体,就会调用这个函数来处理。

  • objc_msgSend_fpret 如果消息返回的是浮点数,就会调用这个函数进行处理。

  • objc_msgSendSuper 如果要传递消息给父类。

总结:

  • 消息由 接收者、选择器及参数构成,给某对象 发送消息( invoke a message ) 也就相当于在该对象上调用方法。

  • 发送给某对象的全部消息都要有动态消息派发系统( dynamic message dispatch system ) 来处理。

消息转发

在上面介绍了运行时的消息传递机制,但是却没有说对象收到消息却无法解读该怎么办。本篇博客就着重介绍当消息传递时无法解读的时候就会启动的 消息转发机制( message forwarding )

开发可能经常会遇到这种情况:

2016-04-20 13:14:07.391 runtime[1096:22076] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[AutoDictionary setDate:]: unrecognized selector sent to instance 0x100302f50'
*** First throw call stack:
(0   CoreFoundation                      0x00007fff9f2d94f2 __exceptionPreprocess + 1781   libobjc.A.dylib                     0x00007fff90db3f7e objc_exception_throw + 482   CoreFoundation                      0x00007fff9f3431ad -[NSObject(NSObject) doesNotRecognizeSelector:] + 2053   CoreFoundation                      0x00007fff9f249571 ___forwarding___ + 10094   CoreFoundation                      0x00007fff9f2490f8 _CF_forwarding_prep_0 + 1205   runtime                             0x0000000100001c1c main + 1246   libdyld.dylib                       0x00007fff91df85ad start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

这个异常信息是由 NSObjectdoesNotRecognizeSelector: 方法抛出来的,本来是给 AutoDictionary 的一个实例对象发送消息,但是该对象并没有 setDate: 方法,所以消息转发给了 NSObject ,最后抛出异常。

先看下消息处理机制流程图:

消息转发分为两阶段三步,第一阶段先看接受消息的对象能不能自己处理这个无法解读的消息,这一步可以动态的添加方法去解读接受这个消息;第二阶段是先看看对象自己不能处理这个消息,能不能交给其他对象来进行处理,在这一步如果仍然无法解读消息,那么就会走最后一步:把和消息有关的所有细节封装到一个 NSInvocation 中,再询问一次对象是否能解决。
看下三个方法:

// 询问对象是否自己处理,是返回YES,一般会在这个方法里面动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;// 这一步询问对象把消息交给哪个对象来进行处理
- (id)forwardingTargetForSelector:(SEL)aSelector;// 如果走到这一步的话,就把消息的所有信息封装成 NSInvocation 对象进行 "最后通牒"
- (void)forwardInvocation:(NSInvocation *)anInvocation;

来一段代码示例:
新建一个 AutoDictionary 类,添加一个 NSDate 类型的 date 属性,在实现文件里面用 @dynamic date; 禁止自动生成存取方法,这样当代码中给 AutoDictionary 实例对象的 date属性赋值时就会出现消息无法解读的现象。
.h 文件:

@interface AutoDictionary : NSObject@property (nonatomic, strong) NSDate *date;@end

.m 实现文件代码内容:

@interface AutoDictionary()
@property (nonatomic, strong) NSMutableDictionary *backingStore;/***  该类仅在实现文件 实现了*  - (NSDate *)date*  - (void)setDate:(NSDate *)date*  两个方法,用于处理 AutoDictionary 无法解读的消息*/
@property (nonatomic, strong) MethodCreator *methodCreator;
@end
@implementation AutoDictionary@dynamic date;- (instancetype)init{if (self = [super init]) {self.backingStore = [NSMutableDictionary dictionary];self.methodCreator = [MethodCreator new];}return self;
}#pragma mark - 消息转发机制 :1.动态添加方法 2.后备消息接收者 3.封装NSInvocation,最后通牒
// 3. 封装NSInvocation,最后通牒
- (void)forwardInvocation:(NSInvocation *)anInvocation{}
// 2. 无法接受消息,选择由谁来接受
- (id)forwardingTargetForSelector:(SEL)aSelector{return self.methodCreator;
}
// 1. 动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{NSString *selString = NSStringFromSelector(sel);if ([selString hasPrefix:@"set"]) {class_addMethod(self, sel, (IMP)autoDictSetter, "");}else{class_addMethod(self, sel, (IMP)autoDictGetter, "");}return YES;
}id autoDictGetter (id self, SEL _cmd){AutoDictionary *dict = self;NSString *key = NSStringFromSelector(_cmd);return [dict.backingStore objectForKey:key];
}void autoDictSetter (id self, SEL _cmd, id value){AutoDictionary *dict = self;NSString *selString = NSStringFromSelector(_cmd);NSString *key = [selString substringWithRange:NSMakeRange(3, selString.length-4)];key = [key lowercaseStringWithLocale:[NSLocale currentLocale]];if (value) {[dict.backingStore setObject:value forKey:key];}else{[dict.backingStore removeObjectForKey:key];}
}@end

测试代码:

AutoDictionary *dict = [AutoDictionary new];
dict.date = [NSDate date];
NSLog(@"dict.date = %@",dict.date);

给对象、分类添加实例变量

在开发中有时候想给对象实例添加个变量来存储数据,但又无法直接声明,比如说既有类的分类。这个时候我们就可以通过 关联对象 在运行时给对象关联一个 对象 来存储数据。(注意:并不是真实的添加了一个实例变量)

关联对象 可以给某个对象关联其他对象并用key来区分其他对象。需要注意的是,存储对象的时候要指明 存储策略,用来维护对象的内存管理语义。存储策略是 objc_AssociationPolicy 枚举定义,以下是存储策略对应的 @property属性:

存储策略类型 对应的@property属性
OBJC_ASSOCIATION_ASSIGN weak
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong
OBJC_ASSOCIATION_COPY copy

用下面的方法可以管理关联对象:

// 这个方法可以根据指定策略给对象关联对象值
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)

对于关联对象这个OC特性,我们可以把对象想象成一个 NSDictionary,关联对象需要一个 key( 类型是 opaque pointer,无类型的指针 ) 来区分,我们可以把要添加的变量名作为 key ,把变量的值作为关联的对象来存储到 ”对象“ 这个 NSDictionary 中。
所以,关联对象的

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

方法类似于字典的

[dict setObject: forKey:]

方法。

在存储和获取关联对象时需要用一个相等的 key ,因为是给 Class 的实例对象关联对象,所以一般用静态变量来做 key

说的再多,不如上段代码!

比如说,我们给 NSString 实例加上个 NSDate 类型的 date 变量。什么?给字符串加个日期变量是要干袅?我要给字符串过个生日不行吗! 别闹,举个栗子嘛!(捂脸逃跑~~~)

首先,我们先给 NSString 新建个名为 RT 的 category。
在头文件中有个 NSDate 类型的 date 属性:

//  NSString+RT.h
//  runtime
#import <Foundation/Foundation.h>@interface NSString (RT)@property (nonatomic, strong) NSDate *date;@end

在分类中的属性只会生成 getset 方法,并不会生成变量。
所以我们需要重写 getset 方法,关联对象以变相实现添加变量,在现实文件中:

//  NSString+RT.m
//  runtime
#import <objc/runtime.h>
#import "NSString+RT.h"@implementation NSString (RT)static void *runtime_date_key = "date";
- (NSDate *)date{return objc_getAssociatedObject(self, runtime_date_key);
}- (void)setDate:(NSDate *)date{objc_setAssociatedObject(self, runtime_date_key, date, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}@end

需要注意的是,关联对象用到的 key 是个无类型的指针,一般来说是静态来修饰。
另外,给对象关联的只能是对象,如果是 intfloat 等类型需要 NSNumber 进行包装。
因为 date 是强引用和非原子属性,所以关联策略用 OBJC_ASSOCIATION_RETAIN_NONATOMIC

然后执行代码:

NSString *string = @"runtimeTestString";
string.date = [NSDate date];
NSLog(@"string.date = %@",string.date);

输出结果:

2016-04-12 21:27:31.099 runtime[2837:103727] string.date = 2016-04-12 13:27:31 +0000

注意:

  • 定义关联对象时需要指定内存管理语义,用来模拟对象对变量的拥有关系

  • 尽量避免使用关联对象,因为如果出现bug不易于问题排查

iOS 开发中的 AOP

Objective-C 中,类的方法列表会把选择器的名称映射到方法的实现上,这样 动态消息转发系统 就可以以此找到需要调用的方法。这些方法是以函数指针的形式来表示,这种指针叫做 IMP
如下:

id (*IMP) (id, SEL, ...)

Objective-C 的 runtime 机制以此提供了获取和交换映射IMP的的接口:

// 获取方法
Method class_getInstanceMethod(Class cls, SEL name);// 交换两个方法
void method_exchangeImplementations(Method m1, Method m2)

我们可以通过上面两个方法来进行选择器和所映射的IMP进行交换:

来,直接上代码示例,比如我们的要实现功能是在每个控制器的viewDidLoad方法里面log一下,一般有三种实现方式:

  1. 直接修改每个页面的 view controller 代码,简单粗暴;

  2. 子类化 view controller ,并让我们的 view controller 都继承这些子类;

  3. 使用 Method Swizzling 进行 hook,以达到 AOP 编程的思想

第一种实现的代码是在每个类的里面都这么写:

- (void)viewDidLoad {[super viewDidLoad];DDLog();
}

第二种是只在基类里面写。然后所有的控制器都继承这个基类。
最后一种是最佳的解决方案:

@implementation UIViewController (Log)+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{Class class = [self class];SEL originalSelector = @selector(viewDidLoad);SEL swizzledSelector = @selector(log_viewDidLoad);Method originalMethod = class_getInstanceMethod(class, originalSelector);Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));if (success) {class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));} else {method_exchangeImplementations(originalMethod, swizzledMethod);}});
}#pragma mark - Method Swizzling- (void)log_viewDidLoad{[self log_viewDidLoad];DDLog(...);
}@end

注意:

  • 为什么使用 + (void)load ?因为父类、子类和分类的该方法是分别调用,互不影响,而且是在类被加载的时候必定会调用的方法。



本文首发于https://iosgg.cn/tags/#Objc/runtime

runtime 入门与简介相关推荐

  1. 进程 Android架构  Android Runtime  Libraries framework简介 Framework的范围 framework简介范围基础 Linux入门介绍

    目录 FrameWork框架之进程 Android简介 Android架构 Android Runtime Libraries framework简介

  2. kafka入门:简介、使用场景、设计原理、主要配置及集群搭建

    为什么80%的码农都做不了架构师?>>>    kafka入门:简介.使用场景.设计原理.主要配置及集群搭建(转) 问题导读: 1.zookeeper在kafka的作用是什么? 2. ...

  3. TypeScript基础入门 - 函数 - 简介

    2019独角兽企业重金招聘Python工程师标准>>> 转载 TypeScript基础入门 - 函数 - 简介 项目实践仓库 https://github.com/durban89/ ...

  4. GBDT(MART) 迭代决策树入门教程 | 简介 .

     GBDT(MART) 迭代决策树入门教程 | 简介 2013-12-23 16:26 2802人阅读 评论(0) 收藏 举报 本文章已收录于: 分类: 数据挖掘相关(13) 作者同类文章X GB ...

  5. TypeScript基础入门 - 接口 - 简介

    转载地址 TypeScript基础入门 - 接口 - 简介 项目实践仓库 https://github.com/durban89/typescript_demo.git tag: 1.0.6 为了保证 ...

  6. MoveIt!入门教程-简介

    转载: http://www.ncnynl.com/archives/201610/1028.html MoveIt!入门教程-简介 说明 MOVEit!是目前针对移动操作最先进的软件. 它结合了运动 ...

  7. 代理服务器(代理IP)的入门与简介

    代理服务器(代理IP)的入门与简介 一.什么是代理服务器 代理IP即代理服务器,英文全称是Proxy Server.其功能就是代理网络用户去获取网络信息,形象的说,它是网络信息的中转站. 一般情况下, ...

  8. 码云 Git 入门篇 ---简介、安装、使用

    码云 Git 入门篇 -简介.安装.使用 这篇文章可以对刚入门gitee的朋友起到一点帮助的作用,也是我自己的一点点总结,能力一般,水平有限,如有不正确的地方请大家指正,如果觉得有帮助,请点一个赞,谢 ...

  9. JMeter入门操作简介

    JMeter入门操作简介 1.JMter简介及作用 JMter是Apache公司用Java开发的一款测试工具 接口测试.性能测试.数据库测试.java程序测试. 可以对服务器.网络或者对象模拟巨大的负 ...

最新文章

  1. linux kafka进程挂了 自动重启
  2. CSS之布局(盒子模型—边框)
  3. Quartz 实现分布式任务调度
  4. 四步法分析定位生产环境下MySQL上千条SQL中的问题所在
  5. Xilinx IP解析之FIFO Generator v13.2
  6. 美颜相机window 开源_X-Window系统| 免费和开源软件
  7. PHP如何让apache支持.htaccess 解决Internal Server Error The server …错误
  8. 揭秘OCR的策略规则
  9. Problem 2128 最长子串
  10. selenium报错TypeError: 'FirefoxWebElement' object is not iterable
  11. python对象的深复制与浅复制
  12. 拓端tecdat|R语言社区主题检测算法应用案例
  13. 计算机全息图的制作与在线,基于Matlab的计算全息图的制作与数字再现的研究精选.doc...
  14. pdf怎么删除页码 页码删除介绍
  15. 灵异问题,使用EasyConnect后,用navicat可以访问数据库,但是用Idea跑项目连接超时怎么办?
  16. uni-app基础入门
  17. 常见因子的Fama-Macbeth回归:EAP.fama_macbeth
  18. 盘点SaaS在高歌猛进路上的16个坑,总有一个你会遇到
  19. 安装office的ISO版本,虚拟光驱
  20. 【专栏推荐】朱赟的技术管理课

热门文章

  1. Echarts各种图表案例(代码+效果)
  2. 趁室友洗澡的时候华丽的给他戒网瘾(教不会打我)
  3. 20X29 FCPX插件50种可爱流行手绘图形MG元素包 Hand Painted
  4. MemoryError的处理方式
  5. WiFi爆惊天漏洞!KRACK可攻陷所有WiFi网络
  6. 设计模式——模版方法模式详解(论沉迷LOL对学生的危害)
  7. 常用的画图工具(UML图、业务流程图、时序图)
  8. [Daozy][区块链 EOS 课程]第3课 如何创建EOS钱包
  9. 人工智能机器人电销,AI语音机器人系统
  10. SQL数据类型对照表