iOS Runtime详解(新手也看得懂)

前言

Runtime的特性主要是消息(方法)传递,如果消息(方法)在对象中找不到,就进行转发,具体怎么实现的呢。我们从下面几个方面探寻Runtime的实现机制。

  • Runtime介绍
  • Runtime消息传递
  • Runtime消息转发
  • Runtime应用

Runtime介绍

Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。

Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime ,要先了解它的核心 - 消息传递 (Messaging)。

Runtime其实有两个版本: “modern” 和 “legacy”。我们现在用的 Objective-C 2.0 采用的是现行 (Modern) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 之后的 64 位程序中。而 macOS 较老的32位程序仍采用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。

Runtime 基本是用 C 和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的 runtime版本,这两个版本之间都在努力的保持一致。

平时的业务中主要是使用官方Api,解决我们框架性的需求。

高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。

Runtime消息传递

一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj,foo),Runtime时执行的流程是这样的:

  • 首先,通过obj的isa指针找到它的 class ;
  • 在 class 的 method list 找 foo ;
  • 如果 class 中没到 foo,继续往它的 superclass 中找 ;
  • 一旦找到 foo 这个函数,就去执行它的实现IMP 。

但这种实现有个问题,效率低。但一个class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是objc_class 中另一个重要成员objc_cache 做的事情 - 再找到foo 之后,把foo 的method_name 作为key ,method_imp作为value 给存起来。当再次收到foo 消息的时候,可以直接在cache 里找到,避免去遍历objc_method_list。从前面的源代码可以看到objc_cache是存在objc_class 结构体中的。

objec_msgSend的方法定义如下:

OBJC_EXPORT id objc_msgSend(id self,SEL op,...)

那消息传递是怎么实现的呢?我们看看对象(object),类(class),方法(method)这几个的结构体:

//对象
struct objc_object {
 Class isa OBJC_ISA_AVAILABILITY;
};
//类
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
} OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
 struct objc_method_list *obsolete   OBJC2_UNAVAILABLE;
 int method_count      OBJC2_UNAVAILABLE;
#ifdef __LP64__
 int space      OBJC2_UNAVAILABLE;
#endif
 /* variable length structure */
 struct objc_method method_list[1]   OBJC2_UNAVAILABLE;
}        OBJC2_UNAVAILABLE;
//方法
struct objc_method {
 SEL method_name      OBJC2_UNAVAILABLE;
 char *method_types     OBJC2_UNAVAILABLE;
 IMP method_imp      OBJC2_UNAVAILABLE;
}
  1. 系统首先找到消息的接收对象,然后通过对象的isa找到它的类。
  2. 在它的类中查找method_list,是否有selector方法。
  3. 没有则查找父类的method_list。
  4. 找到对应的method,执行它的IMP。
  5. 转发IMP的return值。

下面讲讲消息传递用到的一些概念:

  • 类对象(objc_class)
  • 实例(objc_object)
  • 元类(Meta Class)
  • Method(objc_method)
  • SEL(objc_selector)
  • IMP
  • 类缓存(objc_cache)
  • Category(objc_category)

类对象(objc_class)

Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。

typedef struct objc_class *Class;

查看objc/runtime.h中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;

struct objc_class结构体定义了很多变量,通过命名不难发现,
结构体里保存了指向父类的指针、类的名字、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等,
一个类包含的信息也不就正是这些吗?没错,类对象就是一个结构体struct objc_class,这个结构体存放的数据称为元数据(metadata),

该结构体的第一个成员变量也是isa指针,这就说明了Class本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生用于创建实例对象,是单例。

实例(objc_object)

/// Represents an instance of a class.
struct objc_object {
 Class isa OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

类对象中的元数据存储的都是如何创建一个实例的相关信息,那么类对象和类方法应该从哪里创建呢?
就是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(metaclass),
元类中保存了创建类对象以及类方法所需的所有信息,因此整个结构应该如下图所示:

动态方法解析

首先,Objective-C运行时会调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。

实现一个动态方法解析的例子如下:

- (void)viewDidLoad {
 [super viewDidLoad];
 // Do any additional setup after loading the view,typically from a nib.
 //执行foo函数
 [self performSelector:@selector(foo:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
 if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
  class_addMethod([self class],sel,(IMP)fooMethod,"v@:");
  return YES;
 }
 return [super resolveInstanceMethod:sel];
}

void fooMethod(id obj,SEL _cmd) {
 NSLog(@"Doing foo");//新的foo函数
}

打印结果:

2018-04-01 12:23:35.952670+0800 ocram[87546:23235469] Doing foo

可以看到虽然没有实现foo:这个函数,但是我们通过class_addMethod动态添加fooMethod函数,并执行fooMethod这个函数的IMP。从打印结果看,成功实现了。

如果resolve方法返回 NO ,运行时就会移到下一步:forwardingTargetForSelector。

备用接收者

如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。

实现一个备用接收者的例子如下:

#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
 NSLog(@"Doing foo");//Person的foo函数
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
 [super viewDidLoad];
 // Do any additional setup after loading the view,typically from a nib.
 //执行foo函数
 [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
 return YES;//返回YES,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
 if (aSelector == @selector(foo)) {
  return [Person new];//返回Person对象,让Person对象接收这个消息
 }

 return [super forwardingTargetForSelector:aSelector];
}

@end

打印结果:

2018-04-01 12:45:04.757929+0800 ocram[88023:23260346] Doing foo

可以看到我们通过forwardingTargetForSelector把当前ViewController的方法转发给了Person去执行了。打印结果也证明我们成功实现了转发。

完整消息转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。

首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil ,Runtime则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation 对象并发送 -forwardInvocation:消息给目标对象。

实现一个完整转发的例子如下:

#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
 NSLog(@"Doing foo");//Person的foo函数
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
 [super viewDidLoad];
 // Do any additional setup after loading the view,typically from a nib.
 //执行foo函数
 [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
 return YES;//返回YES,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
 return nil;//返回nil,进入下一步转发
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
 if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
  return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
 }

 return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
 SEL sel = anInvocation.selector;

 Person *p = [Person new];
 if([p respondsToSelector:sel]) {
  [anInvocation invokeWithTarget:p];
 }
 else {
  [self doesNotRecognizeSelector:sel];
 }

}

@end

打印结果:

2018-04-01 13:00:45.423385+0800 ocram[88353:23279961] Doing foo

从打印结果来看,我们实现了完整的转发。通过签名,Runtime生成了一个对象anInvocation,发送给了forwardInvocation,我们在forwardInvocation方法里面让Person对象去执行了foo函数。签名参数v@:怎么解释呢,这里苹果文档Type Encodings有详细的解释。

以上就是Runtime的三次转发流程。下面我们讲讲Runtime的实际应用。

Runtime应用

Runtime简直就是做大型框架的利器。它的应用场景非常多,下面就介绍一些常见的应用场景。

  • 关联对象(Objective-C Associated Objects)给分类增加属性
  • 方法魔法(Method Swizzling)方法添加和替换和KVO实现
  • 消息转发(热更新)解决Bug(JSPatch)
  • 实现NSCoding的自动归档和自动解档
  • 实现字典和模型的自动转换(MJExtension)

关联对象(Objective-C Associated Objects)给分类增加属性

我们都是知道分类是不能自定义属性和变量的。下面通过关联对象实现给分类添加属性。

关联对象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)

参数解释

id object:被关联的对象
const void *key:关联的key,要求唯一
id value:关联的对象
objc_AssociationPolicy policy:内存管理的策略

内存管理的策略

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. */
};

下面实现一个UIView的Category添加自定义属性defaultColor。

#import "ViewController.h"
#import "objc/runtime.h"

@interface UIView (DefaultColor)

@property (nonatomic,strong) UIColor *defaultColor;

@end

@implementation UIView (DefaultColor)

@dynamic defaultColor;

static char kDefaultColorKey;

- (void)setDefaultColor:(UIColor *)defaultColor {
 objc_setAssociatedObject(self,&kDefaultColorKey,defaultColor,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)defaultColor {
 return objc_getAssociatedObject(self,&kDefaultColorKey);
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
 [super viewDidLoad];
 // Do any additional setup after loading the view,typically from a nib.

 UIView *test = [UIView new];
 test.defaultColor = [UIColor blackColor];
 NSLog(@"%@",test.defaultColor);
}

@end

打印结果:

2018-04-01 15:41:44.977732+0800 ocram[2053:63739] UIExtendedGrayColorSpace 0 1

打印结果来看,我们成功在分类上添加了一个属性,实现了它的setter和getter方法。

通过关联对象实现的属性的内存管理也是有ARC管理的,所以我们只需要给定适当的内存策略就行了,不需要操心对象的释放。

我们看看内存测量对于的属性修饰。

内存策略 属性修饰 描述
OBJC_ASSOCIATION_ASSIGN @property (assign) 或 @property (unsafe_unretained) 指定一个关联对象的弱引用。
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic,strong) @property (nonatomic,strong) 指定一个关联对象的强引用,不能被原子化使用。
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic,copy) 指定一个关联对象的copy引用,不能被原子化使用。
OBJC_ASSOCIATION_RETAIN @property (atomic,strong) 指定一个关联对象的强引用,能被原子化使用。
OBJC_ASSOCIATION_COPY @property (atomic,copy) 指定一个关联对象的copy引用,能被原子化使用。

方法魔法(Method Swizzling)方法添加和替换和KVO实现

方法添加

实际上添加方法刚才在讲消息转发的时候,动态方法解析的时候就提到了。

//class_addMethod(Class _Nullable __unsafe_unretained cls,SEL _Nonnull name,IMP _Nonnull imp,const char * _Nullable types)
class_addMethod([self class],"v@:");
  • cls 被添加方法的类
  • name 添加的方法的名称的SEL
  • imp 方法的实现。该函数必须至少要有两个参数,self,_cmd
  • 类型编码

方法替换

下面实现一个替换ViewController的viewDidLoad方法的例子。

@implementation ViewController

+ (void)load {
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken,^{
    Class class = [self class];
    SEL originalSelector = @selector(viewDidLoad);
    SEL swizzledSelector = @selector(jkviewDidLoad);

    Method originalMethod = class_getInstanceMethod(class,originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);

    //judge the method named swizzledMethod is already existed.
    BOOL didAddMethod = class_addMethod(class,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));
    // if swizzledMethod is already existed.
    if (didAddMethod) {
      class_replaceMethod(class,swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));
    }
    else {
      method_exchangeImplementations(originalMethod,swizzledMethod);
    }
  });
}

- (void)jkviewDidLoad {
  NSLog(@"替换的方法");

  [self jkviewDidLoad];
}

- (void)viewDidLoad {
  NSLog(@"自带的方法");

  [super viewDidLoad];
}

@end

swizzling应该只在+load中完成。 在 Objective-C 的运行时中,每个类有两个方法都会自动调用。+load 是在一个类被初始装载时调用,+initialize 是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。

swizzling应该只在dispatch_once 中完成,由于swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch 的 dispatch_once满足了所需要的需求,并且应该被当做使用swizzling 的初始化单例方法的标准。

实现图解如下图。

微信公众号搜索 “ 程序精选 ” ,选择关注!
精选程序员所需精品干货内容!