Object-C
采用"消息结构"而非”函数调用“。对于函数调用的语言,是由编译器决定的。而消息结构的语言,其运行所执行的代码由运行环境来决定。 而这个运行环境,就是Runtime
。
一. 消息机制
1.1. 消息传递
消息机制是Runtime
的核心,方法调用的过程可以看做是消息传递的过程。
先来熟悉下类的基本结构,在iOS
中,基本上所有类都直接或者间接继承于NSObject
(也有NSProxy
这种例外),那么来看下NSObject
:
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
NSObject
中持有一个Class
类型的isa
指针,那么这个Class
是什么呢?来看一下:
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; // 指向metaclass
Class _Nullable super_class OBJC2_UNAVAILABLE; // 指向其父类
const char * _Nonnull name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
long info OBJC2_UNAVAILABLE; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小(包括从父类继承下来的实例变量);
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; // 用于存储每个成员变量的地址
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
// 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; //指向最近使用的方法的指针,用于提升效率;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; // 存储该类遵守的协议
}
可以看到,objc_class
中也有多个元素,除了类型父类等能够理解顾名思义
的元素,特别需要注意的是isa
和cache
两个元素。cache
是将用过的方法存储到其内,优先查找,是典型的时空装换。而对于类的isa
, 它是指向元类的,也就是说:
mateClass
(元类)生成Class
(类/类对象), Class
(类)生成obj
(对象)。用一张经典图来说明:
有了以上的基础,那么消息传递就会容易理解很多。例如我们调用一个实例方法:
[obj test];
转化为汇编代码:
objc_msgSend(obj,sel_registerName("test"));
接下来会调用_class_lookupMethodAndLoadCache3
方法,看下其具体实现:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.lock();
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
retry:
runtimeLock.assertLocked();
imp = cache_getImp(cls, sel);
if (imp) goto done;
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break;
}
}
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
triedResolver = YES;
goto retry;
}
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
即开始先从cache
查找:
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
如果缓存命中,直接返回imp
。如果没有命中,继续往下走,先判断类有没有加载到内存,如果没有,先加载类:
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
判断是否实现了initialize
,如果有实现,先调用initialize
:
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
在类对象的方法列表查找imp
:
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
如果没有找到,继续在父类的缓存的方法列表中查找imp
。
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break;
}
}
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
imp
还没有找到,则尝试做一次动态方法解析:
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);//这里做一次动态方法解析。
runtimeLock.lock();
triedResolver = YES;
goto retry;
}
最终没有找到imp
,并且方法解析也没有处理,那么则进入消息转发流程:
imp = (IMP)_objc_msgForward_impcache;
1.2. 消息转发
在调用对象拿到对应的selector
之后,如果自己无法执行这个方法,那么该条消息要被转发。或者临时动态的添加方法实现。如果转发到最后依旧没法处理,程序就会崩溃。
如以下例子:
新建一个Person
类继承于NSObject
,并声明一个msgTest
方法(不实现);
@interface Person : NSObject
- (void)msgTest;
@end
调用该方法:
- (void)viewDidLoad {
[super viewDidLoad];
Person * p = [Person new];
[p msgTest];
}
此时我们将项目跑起来就会发现,项目是能通过编译的,但是会崩溃掉:
-[Person msgTest]: unrecognized selector sent to instance 0x6000020543e0
在方法在调用时,系统会查看这个对象能否接收这个消息(没有实现这个方法),如果不能接收,就会调用下面这几个方法,会采用拯救模式,给你“补救”的机会。
第一次补救: 动态方法解析
/*
cls:要添加方法的类
name:选择器
imp:方法实现,IMP在objc.h中的定义是:typedef id (*IMP)(id, SEL, ...);该方法至少有两个参数,self(id)和_cmd(SEL)
types:方法,参数和返回值的描述,"v@:"表示返回值为void,没有参数
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(msgTest)){
return class_addMethod([self class],sel, (IMP)reTest, "v@:");
}
return [super resolveInstanceMethod:sel];
}
void reTest(id self, SEL _cmd) {
NSLog(@"test");
}
可看到打印数据:
learn[47237:860941] test
注: resolveInstanceMethod
处理对象方法,resolveClassMethod
处理类方法。
第二次补救: 消息重定向
我们继续以实例方法举例:
创建一个新的类RePerson
,该类包含有msgTest
的实现方法。
#import "RePerson.h"
@implementation RePerson
- (void)msgTest{
NSLog(@"rePerson");
}
@end
在Person
类中进行下两步操作:
- resolveInstanceMethod返回值设为NO。
- forwardingTargetForSelector返回值为RePerson对象。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(msgTest)){
return NO;
}
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(msgTest)){
return [RePerson new];
}
return [super forwardingTargetForSelector:aSelector];
}
这样就可以得到结果:
learn[47519:906599] rePerson
第三次补救: 消息转发
关于消息转发,希望您对Type Encodings 、NSMethodSignature 、NSInvocation
已经有基本的认知,可查看本人呢另一篇文章Type Encodings 、NSMethodSignature 、NSInvocation三部曲。
也是改变调用对象,使该消息在新对象上调用;不同是forwardInvocation
方法带有一个NSInvocation
对象,这个对象保存了这个方法调用的所有信息,包括SEL
,参数和返回值描述等。
同样的,我们利用上文中描述的RePerson
类,实现以下方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
if (anInvocation.selector == @selector(msgTest)){
[anInvocation invokeWithTarget:[RePerson new]];
return;
}
[super forwardInvocation:anInvocation];
}
同样的,我们也可以拿到如下答案:
learn[47574:911006] rePerson
经典图:
消息转发也是我们处理unrecognized selector crash
的主要方案,减少对应的崩溃。
1.3. 关于NSProxy
说到消息转发这一问题,NSProxy
才是消息转发、消息分发的终极答案。
对比上面的一套消息查找过程,NSProxy
就简单多了,接收到 unkonwn selector
后,直接调用- (NSMethodSignature *)methodSignatureForSelector:
和 - (void)forwardInvocation:
进行消息转发。看下YYWeakProxy
的源码:
@implementation YYWeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[YYWeakProxy alloc] initWithTarget:target];
}
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
return [_target respondsToSelector:aSelector];
}
- (BOOL)isEqual:(id)object {
return [_target isEqual:object];
}
- (NSUInteger)hash {
return [_target hash];
}
- (Class)superclass {
return [_target superclass];
}
- (Class)class {
return [_target class];
}
- (BOOL)isKindOfClass:(Class)aClass {
return [_target isKindOfClass:aClass];
}
- (BOOL)isMemberOfClass:(Class)aClass {
return [_target isMemberOfClass:aClass];
}
- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
return [_target conformsToProtocol:aProtocol];
}
- (BOOL)isProxy {
return YES;
}
- (NSString *)description {
return [_target description];
}
- (NSString *)debugDescription {
return [_target debugDescription];
}
@end
其实就是简单的实现这两种方法而已。
它的主要功能之一就是避免循环引用:
@implementation MyView {
NSTimer *_timer;
}
- (void)initTimer {
YYWeakProxy *proxy = [YYWeakProxy proxyWithTarget:self];
_timer = [NSTimer timerWithTimeInterval:0.1 target:proxy selector:@selector(tick:) userInfo:nil repeats:YES];
}
- (void)tick:(NSTimer *)timer {...}
@end
如上例子, MyView
持有Timer
, Timer
强引用Proxy
, Proxy
虽然能发送消息到MyView
却不会形成强引用。
二. Runtime 应用
Runtime
的是iOS
中的高频词,具体的使用大致分为以下几个类别:
- 关联对象(
Objective-C Associated Objects
)添加对象。 - 方法交换
Method Swizzling
。 - 字典和模型的自动转换。
2.1. 关联对象
首先抛出一个问题:分类Category
为什么不能直接添加属性。
从逻辑角度来说,Category
本来就不是一个真实的类,是在Runtime
期间,动态的为相关类添加方法。在编译期间连相关对象都没拿到,如何添加属性?
另一方面,从Category
的结构体组成也能证明这一点:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods; // 对象方法
struct method_list_t *classMethods; // 类方法
struct protocol_list_t *protocols; // 协议
struct property_list_t *instanceProperties; // 属性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
虽然其中包括了属性的list
,但是并不包含成员变量的list
, 属性是要自动合成相关的成员变量的,而其明显不具备这一特点。so,该如何做呢 ? 当然还是回到Runtime
。
Runtime
提供了三个函数进行属性关联:
// 关联对象 setter
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// objec: 被关联对象。key:关联key, 唯一标识。 value:关联的对象。policy: 内存管理的策略。
// 获取关联的对象 getter
id objc_getAssociatedObject(id object, const void *key);
// 移除关联对象 delloc
void objc_removeAssociatedObjects(id object);
内存策略:
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)。
如我们给一个UIViewController
分类添加一个params
字典用户接受传递过来的参数:
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIViewController (Base)
@property (nonatomic, strong) NSDictionary * params;
@end
NS_ASSUME_NONNULL_END
#import "UIViewController+Base.h"
#import <objc/runtime.h>
static const void * jParamsKey = &jParamsKey;
@implementation UIViewController (Base)
- (void)setParams:(NSDictionary *)params{
objc_setAssociatedObject(self, jParamsKey, params, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSDictionary *)params{
return objc_getAssociatedObject(self, jParamsKey);
}
@end
2.2. 方法交换 (Method Swizzling)
Method Swizzling
被称为黑魔法, 在iOS
编程具有不可动摇的核心地位,修改原有方法指向的特性使其能够十分出色完成以下任务:
-
hook
系统方法,例如hook
系统字体设置动态修改不同屏幕下字体大小,hook
系统生命周期方法达到埋点统计的目的。 - 在
debug
过程中hook
原方法来进行bug
修复。hook
例如NSArray
的indexof
去防崩溃。 - 实现
KVO
类的观察者方案。
先看代码吧,如果我们实现UIFont
的动态方案:
#import "UIFont+Adapt.h"
@implementation UIFont (Adapt)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self exchangeMethod];
});
}
+ (void)exchangeMethod{
Class class = [self class];
SEL originalSelector = @selector(systemFontOfSize:);
SEL swizzledSelector = @selector(runTimeFitFont:);
Method systemMethod = class_getClassMethod(class, originalSelector );
Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
method_exchangeImplementations(systemMethod, swizzledMethod);
}
+ (UIFont *)runTimeFitFont:(CGFloat)fontSize{
UIFont *fitFont = nil;
//这里并不会造成循环调用,方法已经被交换
fitFont = [UIFont runTimeFitFont:fontSize * (Main_Screen_Width / 375 )];
return fitFont;
}
@end
这里解释下这些代码:
一般情况下,都会写一个分类来实现Method Swizzling
。 一般情况下会在load
方法里调用,保证在该方法调用之前,已经完成了方法交换。
load
方法在不同系统下有不同表现,在iOS10
或者其它情况下,会出现多次调用的情况,所以使用dispatch_once
方案保证方法交换只实现一次。
在hook
完成后,我们调用原方法,最终就会调用到交换后的方法,而在交换方法如需调用原方法,类似上面的本来该调用systemFontOfSize:
的,但是systemFontOfSize:
已经被交换了,所以调用runTimeFitFont:(CGFloat)fontSize
就是调用systemFontOfSize:
,并不会引起循环调用。
另一个需要注意点是在hook
父类的方法时候存在的问题,比如我们有一个HookViewController
继承于 BaseViewController
继承于UIViewController
,如果我们想hook
它的viewDidAppear
,如果我们直接hook
:
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// 原方法名和替换方法名
SEL originalSelector = @selector(viewDidAppear:);
SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
// 原方法结构体和替换方法结构体
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 调用交互两个方法的实现
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
就会报错:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[JTabbarController swizzle_viewDidAppear:]: unrecognized selector sent to instance 0x7fb191811400'
修改成:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// 原方法名和替换方法名
SEL originalSelector = @selector(viewDidAppear:);
SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
// 原方法结构体和替换方法结构体
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 如果当前类没有原方法的实现IMP,先调用class_addMethod来给原方法添加默认的方法实现IMP
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
// 添加方法实现IMP成功后,修改替换方法结构体内的方法实现IMP和方法类型编码TypeEncoding
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
// 添加失败,调用交互两个方法的实现
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
当然如果我们重写这个方法,也是可以的。
为什么会这样呢?
根据方法的的查找路径,没有重写的话实质会去调用父类的方法,但是父类没有实现Imp
,就会失败。
2.3. 字典和模型的自动转换
根据上文,我们已经明白, 类的结构体中包含了成员变量的list
, 那么在这个前提下,我们就很轻松的做到字典到模型或者说json
到模型的转换。
具体方案如下:
+ (instancetype)modelWithDict:(NSDictionary *)dict{
id objc = [[self alloc] init];
//1.获取成员变量
unsigned int count = 0;
//获取成员变量数组
Ivar *ivarList = class_copyIvarList(self, &count);
for (int i = 0; i < count; i++) {
//获取成员变量
Ivar ivar = ivarList[i];
//获取成员变量名称
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
//获取成员变量类型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
//获取key
NSString *key = [ivarName substringFromIndex:1];
id value = dict[key];
// 二级转换:判断下value是否是字典,如果是,字典转换层对应的模型
// 并且是自定义对象才需要转换
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]){
//获取class
Class modelClass = NSClassFromString(ivarType);
value = [modelClass modelWithDict:value];
}
if (value) {
[objc setValue:value forKey:key];
}
}
return objc;
}
搭配Type Encodings 、NSMethodSignature 、NSInvocation三部曲,相信就能轻松理解这段代码,就不多叙。
三. 总结
这篇文章算是写的比较快的,大致就是想到哪就写一写,Runtime
这个话题其实也有无数人写过了,我只是想用自己的思路把这个话题顺一下,有什么问题,欢迎留言。