iOS Runtime 基础实用篇

前言

本篇博文主要以初学者学习 Runtime 的角度介绍,由浅入深同时结合简单的示例,让读者更快捷的学习。【注】若读者对本文中的内容有所疑惑或者不对的地方,请不要吝啬您的留言,让我们一起成长

本文结构

  • Runtime 介绍
  • OC 类基础结构介绍
  • OC 函数是如何调用的
  • Runtime 消息转发

Runtime 介绍

  • Runtime 是什么?

        Objective-C 拓展了 C 语言,添加了 面向对象特性消息传递机制,拓展的核心是一个用 C 语言和汇编语言写的 Runtime 系统,它是 OC 面向对象和动态机制的基石。

  • Runtime 发展历程

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

  • OC 语言动态性的体现

        本质体现:一些高级编程语言,像 C/C++ 语言,想要成为可执行文件,需要经过编译(预处理 & 编译),汇编成机器语言。机器语言是计算机唯一能识别的语言,但是 OC 并不能直接编译转换为汇编语言,而是要先转为 C 语言在进行编译和汇编的处理,OC->C语言->在进行编译汇编的操作。从OC到C的过渡就是由Runtime来实现的,将面向对象的类开发转换成面向过程的结构体开发。

        这就给动态化留出足够多的舞台,运行时动态的创建类和对象,进行消息传递和转发等等。

  • 源代码

        Runtime 基本使用 C 和汇编写的。苹果维护了自己的 runtime 源代码。GNU也维护了自己的开源的 runtime 版本,两个版本之间都在尽力保持一直,平时的业务中主要使用官方API,解决框架性的需求。

OC 类基础结构介绍

  • 类对象 (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 结构体的定义如下:

结构体里面定义了很多变量

  • isa
  • super_class:指向父类的指针
  • name:类的名字
  • instance_size:实例大小
  • struct objc_ivar_list *ivars:实例变量列表
  • struct objc_method_list *methodlists:方法列表
  • cache:缓存
  • protocols:遵守的协议

类对象其实就是一个结构体 struct objc_class,结构体里面存放的数据成为元数据 (metaclass)。该结构体的第一个成员变量也是 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;

元类(Meta Class)

元类是一个类对象的类,元类中保存了创建类对象以及类方法所需的所有信息。任何 NSObject 继承体系下的 meta-class 都使用 NSObject 的 meta-class 作为自己的所属类,而基类的 meta-class 的isa 指针是指向它自己的。

 首先我们先举个例子来验证一下

@interface Person : NSObject
{
    @public
    int _age;
}
@property (nonatomic, assign)int height;
@end

@implementation Person

@end

int main(int argc, char * argv[]) {
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        Person *p1 = [[Person alloc] init];
        p1->_age = 3;
        
        Person *p2 = [[Person alloc] init];
        p2->_age = 4;
    }
    return 0;
}

显而易见,p1 和 p2 必然是不一样的,因为他们的存放位置不一样

 那么p1 指向的实例对象的isa 指针 和 p2 指向的实例对象的isa 指针式一样的吗?可想而知他们都指向Person类对象,是一样的

 

Method(objc_method)

先来看一下定义 

runtime.h
/// An opaque type that represents a method in a class definition.代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;

Method和我们平时理解的函数是一致的,就是表示能够独立完成一个功能的一段代码,比如:

- (void)logName
{
	NSLog(@"name");
}

我们来看下objc_method这个结构体的内容:

  • SEL method_name 方法名
  • char *method_types 方法类型
  • IMP method_imp 方法实现

SELIMP其实都是Method的属性。

SEL(obj_selector)

Objc.h
/// An opaque type that represents a method selector.代表一个方法的不透明类型
typedef struct objc_selector *SEL;

OC 中调用一个方法 [obj func],编译器会转成消息转发 objc_msgSend(obj, func),其中这个func 就是一个 SEL。从定义中可以发现 SEL 是 selector 在OC 中表示的类型。selector 是方法选择器,可以理解为是区分方法的 ID,而这个 ID 的数据结构就是 SEL

@property SEL selector;

可以看到 selector 是SEL 的一个实例

A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.
其实 selector 就是一个映射到方法的 C 字符串,可以用 Objective-C 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个 SEL 类型的方法选择器。

selector 既然是一个 string,可以想象一下这个string 应该是类似 className + method 的组合,这样的话就会产生两条规则

  • 同一个类,selector 不能重复
  • 不同的类,selector 可以重复

这样其实会产生一个弊端:比如在C 和 C++ 中,会存在函数重载,函数名相同单参数不同,这在OC 中是不行的,因为selector 只记录了 method 的 name,没有参数。

例如

- (void)caculate(NSInteger)num;
- (void)caculate(CGFloat)num;

在OC 中只能通过函数命名来区分。

IMP

/// A pointer to the function of a method implementation.  指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...); 
#endif

就是指向最终实现程序的内存地址的指针。

OC 函数是如何调用的呢?

上文说过 [obc func] 其实是 objc_msgSend(obj, func),在 Runtime 中执行的流程是:

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
  • 首先通过 obj 的 isa 指针找到他的 class
  • 在 class 的method list 中找 func;
  • 如果 class 中没找到 func,继续往 superclass 中找;
  • 当找到 这个函数后,就去执行他的 IMP
//对象
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;
}

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

类缓存(objc_cache)

基于上述的问题,类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入它的缓存。所以当objc_msgSend查找一个类的选择器,它首先搜索类缓存。

为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度。

Runtime 消息转发

前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了要怎么处理呢?那就是进行消息转发:他会有三次机会进行处理

  • 动态方法解析
  • 备用接收者
  • 完整的消息转发

 

动态方法解析

首先,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 NO;
}

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

@end

可以看到我们通过forwardingTargetForSelector把当前ViewController的方法转发给了Person去执行了。

完整消息转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。 首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nilRuntime则会发出 -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 NO;
}

- (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

 

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

以上就是Runtime的三次转发流程。

Runtime 的一些应用

  • 关联对象(Objective-C Associated Objects)给分类增加属性
  • 方法魔法(Method Swizzling)方法添加和替换和KVO实现
  • 消息转发(热更新)解决Bug(JSPatch)
  • 实现NSCoding的自动归档和自动解档
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值