Objective-C RunTime概览

本文为入门介绍,希望能让第一次接触Runtime概念的朋友有一个概貌了解。

一篇文章,不可能讲完Runtime的全部,但是,分成很多篇讲,又有点「见树木不见森林」的迷糊感觉——自己就是看了很多关于Runtime的文章,看完还是「迷雾重重」(当然,也可能因为资质太过平庸)。

所以,这一篇,尽量涉及。

一句话概括

什么是Runtime?作以下引述。但也不要太奢望看完这些说明后,就会豁然开朗。

官方文档Objective-C Runtime

The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps.

Objective-C的runtime是一个「运行时库」,为OC这门语言提供动态的特性,所有OC应用程序都与之相关联。

The down low on Objective-C Runtime

The Objective-C Runtime is an open source library written in C and Assembler that adds the Object Oriented capabilities to C to create the Objective-C language.

Objective-C的Runtime,是一个用C和汇编写的「开源库」,它为C添加了面向对象的特性,从而成就了Objrctive-C这门语言。

The Objective-C languages defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language, it’s what makes the language work.

Objective-C可以从『编译时』、『链接时』再到『运行时』,hold住尽可能多的决策。只要有可能,它都是动态地干活儿的。这就意味着,这门语言不仅需要一个编译器,还需要一个runtime系统,用来执行编译的代码。这个runtime系统就好比如是Objective-C的「操作系统」,(runtime系统)让这门语言能工作起来。

简单点理解,Runtime就是一个C和汇编写的代码库——是Objective-C之所以成为Objective-C的一个库。

用一图以助理解:

Runtime概览

另外,可参考: 重识 Objective-C Runtime - Smalltalk 与 C 的融合

Runtime的三个头文件

Runtime这个库是开源的。有兴(能)趣(力)的朋友可以仔细研究。

而平时我们会用到的Runtime函数,基本上在runtime.h, objc.h, message.h这三个头文件中。代码2500行+(主要是runtime.h)

runtime.h

runtime.h中定义了若干「类型(Types)」和「函数(Functions)」。

有我们比较熟悉的MethodIvarCategoryobjc_property_tobjc_class类型,都在这里定义。

另外还有106个函数。如常见的:

object_copy(), class_respondsToSelector(), class_copyMethodList等都在这里面。

objc.h

objc.h中定义了Class, id, SEL, IMP类型。

另外还有6个函数。

message.h

声明了一系列的方法执行函数。

objc_msgSend()objc_msgSendSuper()都定义在这里。

名词解释

isa

isa是一个指针,隐式地存在于实例对象、类中,对象的isa指针指向所属类——因此实例对象能知道自己属于哪个类;类的isa指针指向一个叫「元类(Meta Class))」的玩意儿。

isa指针在三个地方有定义:

  • objc_class结构体有声明,指向类的meta类。(在runtime.h)
  • objc_object结构体有声明,指向对象所属类(在objc.h)
  • NSObject类有有声明,指向对象所属类;(在NSObject.h)

Class

Class定义在objc.h中第37、38行,是一个指向objc_class结构体的指针。

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

So, Class就是一个「指针变量」。

objc_class结构体在runtime.h第55-70行中有定义:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;// isa指针, 指向一个meta类,侧面印证:「类也是对象」

#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;
/* Use `Class` instead of `struct objc_class *` */

这个结构体,包括:isa指针、父类指针、类名、成员变量、方法列表、缓存以及协议列表等。

解读一下部分成员:

isa指针
上面介绍isa的时候,说过类也有一个isa指针,我们可以理解为:类本身也是一个对象——「类对象」。是「元类(Meta Class)」的实例(每个类的isa指针指向元类)。

我们熟知的「类方法」,也可以理解为是「类对象」的实例方法。

而这些「元类(Meta Class)」则是「根源类(Root Meta Class)」的实例——所有元类的isa指针最终都指向根元类。根元类的isa指针指向自己,最终完成闭环。

画了一张示意图帮助理解:

isa的指针的指向

struct objc_ivar_list
struct objc_ivar_list(ivars),是实例变量列表,保存类所声明的所有实例变量。

objc_method_list
struct objc_method_list(methodLists)是方法列表,给某个对象发送消息,就是来这个列表中查找是否有相应方法实现的。

可以动态修改methodLists的值来添加成员方法,这也是Category的实现原理。

struct objc_cache
struct objc_cache(cache),用于缓存方法,调用过的方法会缓存到这里,方便以后索引,提高速度。

Method

定义在runtime.h第44行,表示一个方法。

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

objc_method结构体存储了方法名、方法类型和方法实现。

SEL

定义在objc.h第49、50行中,表示一个方法选择器(可以简单点,理解为方法名,一个C语言的字符串)。

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

IMP

定义在objc.h第54行,表示一个方法的实现。由这个函数指针决定最终执行哪段代码。

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

MethodSELIMP有什么区别?

  • Method:表示一个方法,本质是一个指向objc_method结构体的指针。
  • SEL(Selector):在运行时用来代表一个方法的名字。
  • IMP(Implementation):表示方法的实现部分。第一个参数id指向调用方法的自身,第二个参数是方法的名字seletor,方法的参数紧随其后。

在消息发送的过程中,这三个概念是可以互相转换的。

可以这样理解:

Runtime中,Class维护了一份分发列表(dispatch table),用于消息分发;列表中每个入口,就是一个方法(Method),这份列表的key是selector(SEL),value是implementation(IMP)。

而后面介绍到的Method Swizzling,就是改变这份列表某两个方法的SEL和IMP的对应关系,让seletor对应一个不同的implementation。

(也有人比喻:SLE是门牌号码,IMP是住户)

id

定义在objc.h第45、46行中,表示一个类的实例对象。

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

objc_object这个结构体,定义在objc.h中,这个结构体只有一个指向类的isa指针。

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

Ivar

定义在runtime.h第44行,表示一个实例变量。

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;

Cache

定义在runtime.h第1841行。

typedef struct objc_cache *Cache OBJC2_UNAVAILABLE;

Cache的存在,是为方法调用时的性能优化:实例对象收到消息后,会先从Cache中查找,看是否有方法的实现——Runtime会把调用过的方法缓存到Cache中。

objc_property_t

定义在runtime.h第52,53行。

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;

表示Objective-C中的属性。

Category

runtime.h第49,50行:

/// An opaque type that represents a category.
typedef struct objc_category *Category;

objc_category结构体定义在第1784-1790行。

struct objc_category {
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

Category可以动态的地为已存在的类添加新的方法。

self & super

先做个实验:

打印:

NSLog(@"[self class]:%@; [super class]:%@", NSStringFromClass([self class]), NSStringFromClass([super class]));

结果,[self class][super class]的值是一样的。 Why?不应该打印一个子类,一个父类吗?

self,是一个隐藏参数,隐藏在objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)函数中,发送的所有方法,第一个参数都是self。

super不是隐藏参数,是一个「编译器标示符」,它告诉编译器,调用父类的方法,而不是本类的方法。但是,这时候实际上的消息的接收者,还是self。

详解解读:

  • 执行[super class],会先调用objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)函数;

  • 再根据objc_super结构体的super_class去查找方法实现,,最后调用objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)执行方法的实现。 所以,最后的接收器还是self。

因此,上述打印结果的值是一样的。

消息的传递流程

关于OC中的消息传递流程,画了一张图以帮助理解(流程由下往上):

消息传递流程

Objective-C的消息传递流程,个人划分为三部分

  • 正常的消息传递(Messaging)
  • 消息动态解析(Dynamic Method Resolution)
  • 消息转发(Message Forwarding)又分2小步:
    • Fast forwarding
    • Normal forwarding

第一部分,叫做「正常的消息传递」,那理所当然,后面的就是「不正常」了。事实是:如果能找到方法的实现(IMP/implementation),就不会跳到后面。

Runtime应用

1.获取类的相关情况

比如,我想创建一个类似UITableView的类,然后打算参考一下官方的这个类都声明了哪些方法,可以用以下方式查看(头文件声明的方法并不是全部方法):

    /* 获取某个类的方法列表(所有方法) */
    // 这样获取是实例方法(不是类方法)
    Method *methods = class_copyMethodList([UITableView class], &outCount);
    for (NSUInteger methodIndex = 0 ; methodIndex < outCount; methodIndex ++) {
        SEL name = method_getName(methods[methodIndex]);
        NSLog(@"Human-例法方实-%@",NSStringFromSelector(name));
    }

还有很多其他函数:class_getInstanceVariable(), objc_getMetaClass(), class_getClassVariable()等等。

2.动态添加方法的实现

比如,我们用了某个闭源的框架,不幸地,有个bug是:某方法没有实现,导致crash:

[Animal jump]: unrecognized selector sent to instance

这时候如果等闭源框架的debug更新,比较被动。而利用Runtime,可以动态地添加方法的实现,防止crash:

#import "Bird.h"
#import <objc/runtime.h>

// 创建Animal的子类Bird
@implementation Bird

// 如果没有找到实例方法的实现, 就会回调跳到这里
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(jump)) {
        // 利用Runtime的class_addMethod()函数, 动态添加方法的实现
        class_addMethod(self, sel, (IMP)jumpImp, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void jumpImp(id obj, SEL _cmd) {
    NSLog(@"执行了jumpImp(动态添加的方法实现)");
}

@end

3.Method Swizzling

Method Swizzling,可以理解为「交换方法的实现(IMP)」,这是网友的说法,官方并没有这种说法,可见苹果官方应该是不提倡这样做的。

假如有个需求:需要记录App每个页面进入的次数(这个需求和Method Swizzling介绍的一样)

我们可以在viewWillAppear:方法中作一些计数处理。但是,每个页面都要写重复的代码。在这里就可以使用Method Swizzling,「动态地」在官方的基础上增加一些代码,以实现需求。

需要新建一个UIViewController的Category,在load方法中实现互换。

#import "UIViewController+Tracking.h"
#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        // 拿到两个Method对象
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(antony_viewWillAppear:);
        
        Method orignalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod == YES) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(orignalMethod), method_getTypeEncoding(orignalMethod));
        }
        else {
            // 利用method_exchangeImplementations()函数交换两个Method的实现
            method_exchangeImplementations(orignalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling
- (void)antony_viewWillAppear:(BOOL)animated {
    // 因为互换了方法, 这里实际调用的是viewWillAppear:的IMP(不会造成递归)
    [self antony_viewWillAppear:animated];

    // 在这里增加你要的功能
    NSLog(@"这是在viewWillAppear:新增的内容");
}

@end

核心就是用method_exchangeImplementations()函数,互换了viewWillAppear:和antony_viewWillAppear:的实现。

而如果现在创建控制器对象,实际流程是这样的:

  • viewWillAppea:被执行(实际上执行上述Category的antony_viewWillAppear:方法)
  • antony_viewWillAppear:方法内又调用了antony_viewWillAppear:(实际上执行的是系统的viewWillAppear:方法——因为互换了)
  • 最后再执行我们自己添加的代码——这样就实现了需求:所有UIViewController在执行 viewWillAppear:时, 都会调用你增加的代码。从而无须在所有的UIViewController中重复写这部分代码。

Github有个框架:Aspects,就是用Runtime的Method Swizzling实现的,它允许你往任意现存类或实例添加额外的代码。

4.动态添加属性 - 利用Associated Objects(Associative References)

Associative References(关联引用/对象),在runtime.h中定义的三个相关函数:

  • objc_setAssociatedObject()
  • objc_getAssociatedObject()
  • objc_removeAssociatedObjects()

有什么作用呢?

网上有种说法:OC中的Category不能添加属性。

其实严格来说:Category不能添加的是「实例变量」,而属性其实是可以添加的:

  • 不能为Category添加实例变量;否则报错:Instance variables may not be placed in categories

  • 但是可以为Category添加属性,也可以自定义setter、getter,外部也可以访问;但是,这个属性是无意义的,因为不能保存数据(可以返回值,但是不能赋值)。而不能保存数据的原因,是因为没有实例变量「装」数据;

而Associated Objects(关联对象),则可以为Category提供保存数据的地方。

因此Associated Objects(关联对象)就可以:给已有类(封闭的类)添加真正有意义的属性——可以保存数据的属性。

比如,我们要为一个叫做Human的类添加一个属性nickName,就可以:

#import "Human+AdditionalProperties.h"
#import <objc/runtime.h>

@implementation Human (AdditionalProperties)
@dynamic nickName;

// 如果要删除该属性,调用objc_setAssociatedObject()赋值为nil即可,
// 不要用objc_removeAssociatedObjects(), 该函数会删除所有添加的属性
- (void)setNickName:(NSString *)nickName {
    // 参数1: 为哪个对象实现的关联
    // 参数2: 这个关联的key(可以用SEL作为key)
    // 参数3: 需要与对应key(参数2)关联的值(就是外部传入的值)
    // 参数4: 关联的策略(和属性的attribute相对应)
    objc_setAssociatedObject(self, @selector(nickName), nickName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)nickName {
    // 参数1: 为哪个对象实现的关联
    // 参数2: 该关联的key
    return objc_getAssociatedObject(self, @selector(nickName));
}
@end

需要再次强调的是:通过Associated Objects为类添加有意义的属性,事实上并不是添加了实例变量,而是通过关联,使属性有保存数据的能力。(可以用class_copyPropertyList()验证,并没有增加实例变量。或者断点看该类的实例,并不会看到有添加了实例变量——虽然能用该属性来存取数据。)

5.归档和解档 一键序列化:

有用过NSKeyedArchiver固化自定义对象到沙盒的朋友应该了解,当一个自定义对象有很多属性,需要一个一个encode(编码)或者decode(解码),是很琐屑的,比如:

自定义类有很多属性

而利用Runtime,则可以简化这个过程——无论类有多少属性:

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    if (self) {
        unsigned int count = 0;
        // 利用class_copyIvarList()拿到类的所有实例变量
        Ivar *ivars = class_copyIvarList([self class], &count);
        // 再用for循环一次性解档
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int count = 0;
    // 利用class_copyIvarList()拿到类的所有实例变量
    Ivar *ivars = class_copyIvarList([self class], &count);
    // 再用for循环一次性归档
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        id object = [self valueForKey:key];
        [aCoder encodeObject:object forKey:key];
    }
}

Runtime还有很多应用,有兴趣可以继续找相关资料学习。不过:

Objective-C的Runtime就像一把双刃剑,使用它,风险高,回报也高。它赋予你很大的权力,但只要你犯了哪怕一丁点儿错误,都有可能让程序挂掉。

所以,总原则:能不用,尽量不用。

Conclusion

到这里,估计还是有很多黑人问号:Runtime究竟是什么玩意儿? What the hell is Runtime??

这很正常,学习本来就是一个重复的过程——特别是面对学习曲线还比较陡峭的知识。继续实践、温故知新,相信后面会有更好的了解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容