iOS Objective-C的骨架 -- Runtime

我们常说Objective-C 是一门动态语言, 或者运行时语言. 动态和静态是相对的. 相对于静态语言来说, Objective-C不是在编译的时候就确定了所有东西. 可以在运行期间动态的添加类、变量、方法等. 这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发. Objective-C是基于C语言的超集,要想实现运行时这一特性, 需要有一个桥梁, 而runtime就是这个桥梁.

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

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

我将和大家一起从下边几个方面一起来深入认知Runtime的实现:

  • 一: Runtime的消息传递过程
  • 二: Runtime的消息转发过程
  • 三: Category的底层实现
  • 四: Runtime在日常工作中的应用

一: Runtime的消息传递过程:

Objective-C是一门面向对象的语言, 要想理解Runtime在其中的作用, 首先我们先要了解我们口中的对象(object), 类(calss), 方法(method), 在Runtime中是都是什么构造. 通过阅读objc/runtime.h中源码, 我们可以找到:

//!< 对象
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;
}

下面讲解在消息传递过程中用到的概念:

  • 类(类对象 objc_class);
  • 对象(实例 objc_object);
  • 元类(Meta Class);
  • Method(objc_method);
  • methodList(方法列表 objc_method_list);
  • objc_cache (类缓存);
  • SEL(objc_selector);
  • IMP;
  • Category;
类(类对象 objc_class):

Objective-C中, 类是由Class来表示的, 由源码可知, 其实际上是一个指向objc_class 结构体的指针

typedef struct objc_class *Class;

objc_class 结构体的定义如下:

//!< 类
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;

通过结构体中的变量的命名, 我们不难明白每个变量的含义及作用.
它保存了指向父类的指针类的名字版本实例大小实例变量列表方法列表缓存遵守的协议列表等信息. 这个结构体中保存的数据称为元数据(metaData) . 而结构体的第一个变量为isa指针, 这说明类本身也是一个对象. 我们称之为类对象. 类对象在编译期产生, 用于创造实例对象, 是单利.

对象(实例 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;

类对象中, 保存了创建一个实例所需的数据, 那么创建类对象所需要的数据保存在何处? 这就引出了一个新的概念元类(metaclass). 实际上, 实例对象isa指针指向它的, 而类对象isa指针, 指向保存了创建类对象所需数据的类:元类, 而元类isa指针指向根元类.
如下图所示:

元类(Meta Class):

在上一节中, 我们提出了metaclass元类概念. 很显然, 元类也是实例, 那么它是谁的实例呢? 通过上图我们不难看出, 元类根元类的实例. 元类的isa指针指向根元类, 根元类是其自身的实例, 它的isa指针指向自身, 这样就形成了一个巧妙的闭环. 元类super_class指针指向它的父类的元类, 任何NSObject 继承体系下的meta-class都使用NSObjectmeta-class作为自己的所属类,而基类的meta-class(根元类)的isa指针是指向它自己.

Method(objc_method):
/// 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;
}

objc_method结构体中, 可以看出一个Method的构成: SEL method_name方法名、char *method_types方法类型、 IMP method_imp方法实现.

methodList(方法列表 objc_method_list);

从命名可以很明显的得知, methodList是用来存储一个类的所有method.

//方法列表
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;
} 

由源码可知, methodList中存储的是一个个struct objc_method. 那么我们每次需要进行objc_msgSend时都要在methodList进行遍历查找吗? 这样的效率未免也太低下了. 要知道, 一个类中很可能只有20%的常用方法. 这就用到了struct objc_cache来减少常用方法的查找时间.

objc_cache (类缓存):

Objective-C运行时通过跟踪它的isa指针检查对象时,它可以找到一个实现许多方法的对象。然而,你可能只调用它们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义。所以类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器(SEL),它把它放入它的缓存。所以当objc_msgSend查找一个类的selector,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。
为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度。

SEL:
typedef struct objc_selector *SEL;

objc_msgSend函数第二个参数类型为SEL,它是selectorObjective-C中的表示类型. selector是方法选择器,可以理解为区分方法的 Identifer,而这个 Identifer 的数据结构是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. 方法选择器是已使用Objective-C运行时注册(或“映射”)的C字符串。 加载类时,编译器生成的选择器将由运行时自动映射。

其实selector就是个映射到方法的C字符串,你可以用 Objective-C编译器命令@selector()或者 sel_registerName函数来获得一个 SEL 类型的方法选择器。
selector既然是用来区分方法的Identifer, 那么猜想应该有以下规则:

  • 同一个类中, selector不能重复.
  • 不同类中, selector可以重复.
    实际上, 我们在日常工作中会发现, 同一个类中如果有相同的selector, 那么在编译时就会报错. 然而在Category中是可以有selector和类中重复, 是Category中的selector将类中的selector重写了吗? 我们会在后边详细讲解Category的具体实现.
IMP:

看下IMP的定义 :

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

就是指向最终实现方法的内存地址指针.

Category(objc_category):

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;
};

name :是指 class_name 而不是 category_name.
cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象.
instanceMethodscategory中所有给类添加的实例方法的列表.
classMethodscategory中所有添加的类方法的列表.
protocolscategory实现的所有协议的列表.
instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObjectobjc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的.

从上面的category_t的结构体中可以看出,分类中可以添加实例方法类方法,甚至可以实现协议添加属性,不可以添加成员变量(ivar). 具体原因以及底层实现我们在后边详细探讨.

经过上述概念解释, 我们可以总结出Runtime消息传递流程:

实例方法:

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

  • 通过objisa 指针找到它的 class ;
  • class结构体的objc_cache中找foo;
  • 如果缓存中没找到, 继续在class结构体的 method list中找 foo;
  • 如果 class 中没找到 foo,通过它的 superclass指针去class父类 中找, 一直查找到基类NSObject;
  • 一旦找到 foo这个函数, 通过objc_method结构体中的IMP指针找到最终实现方法的内存地址, 去实现.
类方法:

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

  • 通过classisa指针, 找到classmetaclass元类,
  • metaclass中的objc_cache中查找类方法foo;
  • 若缓存中没找到, 继续在methodlist中查找foo.
  • metaclass中没有找到foo, 通过metaclasssuperclass指针去class的父类的metaclass中查找, 一直到NSObject类metaclass;
  • 一旦找到 foo这个函数, 通过objc_method结构体中的IMP指针找到最终实现方法的内存地址, 去实现.

这个时候又有新的问题产生: 在消息传递的过程中, 如果一直到最后都没有找到foo呢? 接下来我们来看看当没有找到foo时会发生什么.

二: Runtime的消息转发过程:

上文在消息传递的过程中, 我们产生了新的问题: 如果一直到最后都没有找到foo? 实际上, 如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:方法, 抛出unrecognized selector异常. 那么消息转发到底是什么呢?接下来将会逐一介绍消息转发的三个步骤, 这也是在抛出unrecognized selector异常前, 我们可以拯救程序的唯三步骤.

  • 动态方法解析: +resolveInstanceMethod: / +resolveClassMethod:
  • 备用接收者: -forwardingTargetForSelector:
  • 完整消息转发: -methodSignatureForSelector: ; -forwardInvocation:
动态方法解析: +resolveInstanceMethod: / +resolveClassMethod::

消息转发的第一步, Runtime会调用 +resolveInstanceMethod:(实例方法)或者 +resolveClassMethod:(类方法), 让你有机会提供一个函数实现(IMP)。如果你添加了IMP并返回YES, 那Runtime就会重新启动一次消息发送的过程.
实现一个动态方法解析的例子如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //!<  执行foo函数, 这时消息发送过程肯定找不到IMP, 就会进入消息转发
    [self performSelector:@selector(foo:)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo:)) {
          //!< 如果是执行foo函数,就动态解析,指定一个新的IMP
          //!< "v@:" 方法类型
        class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//!< 将要执行这个新的IMP
void fooMethod(id obj, SEL _cmd) {
    NSLog(@"new foo has executed");
}

如果+resolveInstanceMethod:或者+resolveClassMethod: return NO, 那么将会进行消息转发的第二个步骤: 备用接收者: -forwardingTargetForSelector:.

备用接收者: -forwardingTargetForSelector::

如果目标对象实现了-forwardingTargetForSelector:方法, 那么Runtime允许我们指定一个对象, 将这个方法转发给指定的对象.

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

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

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@" foo has executed"); //!< 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函数, 这时消息发送过程肯定找不到IMP, 就会进入消息转发
    [self performSelector:@selector(foo)];
}

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

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

可以看到我们通过-forwardingTargetForSelector:把当前ViewController的方法转发给了Person去执行.
如果这个对象实现了foo那么就会重新走一遍消息发送的过程. 如果return nil 或者这个对象没有实现foo, 进行第三个步骤--完整消息转发.

完整消息转发: -methodSignatureForSelector: ; -forwardInvocation: :

如果在上一步还不能处理未知消息, 则唯一能做的就是启用完整的消息转发机制了.
首先, 会发送-methodSignatureForSelector:函数, 来获取函数的参数返回值类型, 如果-methodSignatureForSelector: return nil, Runtime会发送doesNotRecognizeSelector:, 抛出unrecognized selector, 这个时候程序就会崩溃. 如果-methodSignatureForSelector:返回一个函数签名(NSMethodSignature), Runtime就会创建一个NSInvocation 对象并发送 -forwardInvocation:消息给目标对象.

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

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

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"foo has executed"); //!< 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函数, 这时消息发送过程肯定找不到IMP, 就会进入消息转发
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO; //!< 返回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完整的三次转发流程.

三: Category的底层实现

在这里, 我们一起思考以下问题:

  • Category 的实现原理是什么?
  • Category 可以添加属性吗?可以添加实例变量吗?
  • Category 中添加的方法, 在消息发送的过程中是如何找到的?
  • Category中添加了一个与本类中方法相同名字的方法, 为什么本类中的方法不会再调用? 是重写还是被覆盖?

接下来我们通过一段代码来进行分析:

#import <Foundation/Foundation.h>
@interface Preson : NSObject
{
    int _age;
}
- (void)run;
@end

//!<  Presen.m
#import "Preson.h"
@implementation Preson
- (void)run
{
    NSLog(@"Person - run");
}
@end

//!< Presen分类1
//!<  Presen+Test.h
#import "Preson.h"
@interface Preson (Test) <NSCopying>
- (void)test;
+ (void)abc;
@property (assign, nonatomic) int age;
- (void)setAge:(int)age;
- (int)age;
@end

//!<  Presen+Test.m
#import "Preson+Test.h"
@implementation Preson (Test)
- (void)test
{
}

+ (void)abc
{
}
- (void)setAge:(int)age
{
}
- (int)age
{
    return 10;
}
@end

Presen分类2
//!<  Preson+Test2.h
#import "Preson.h"
@interface Preson (Test2)
@end

//!<  Preson+Test2.m
#import "Preson+Test2.h"
@implementation Preson (Test2)
- (void)run
{
    NSLog(@"Person (Test2) - run");
}
@end

通过查看分类的源码我们可以找到category_t 结构体:

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);
};

从源码基本可以看出我们平时使用Categroy的方式,对象方法,类方法,协议,和属性都可以找到对应的存储方式. 并且我们发现分类结构体中是不存在实例变量的, 因此分类中是不允许添加实例变量的. 在分类中添加property, instanceProperties中会添加一个property, 不过分类中添加的property并不会自动生成实例变量, 也不会生成get set方法的声明, 需要我们自己去实现.

通过源码我们发现,分类的方法,协议,属性等好像确实是存放在Categroy结构体里面的,那么他又是如何存储在类对象中的呢?
我们回到Runtime的源码, 来分析分类的方法是如何添加到类的method list中的:

所用到的runtime源码为objc4-680.tar.gz , runtime 的初始化函数在 objc-os.mm中.

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();
        
    // Register for unmap first, in case some +load unmaps something
    _dyld_register_func_for_remove_image(&unmap_image);
    dyld_register_image_state_change_handler(dyld_image_state_bound,
                                             1/*batch*/, &map_2_images);
    dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}

接着我们来到 &map_images读取模块(images这里代表资源模块),来到map_images_nolock函数中找到_read_images函数,在_read_images函数中我们找到category相关代码

objc-runtime-new.mm中能找到相关源码

// Discover categories. 
    for (EACH_HEADER)  {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Disavow any knowledge of this category.
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category. 
            // First, register the category with its target class. 
            // Then, rebuild the class's method lists (etc) if 
            // the class is realized. 
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

            if (cat->classMethods  ||  cat->protocols  
                /* ||  cat->classProperties */) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }

从上述代码中我们可以知道这段代码是用来查找有没有分类的. 通过_getObjc2CategoryList函数获取到分类列表之后, 进行遍历,获取其中的方法,协议,属性等. 可以看到最终都调用了remethodizeClass(cls);函数. 我们来到remethodizeClass(cls)函数内部查看.

objc-runtime-new.mm中找到源码:

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertWriting();

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

通过上述代码我们发现attachCategories函数最终接收了类对象cls和分类数组cats, 如我们一开始写的代码所示, 一个类可以有多个分类. 之前我们说到分类信息存储在category_t结构体中, 那么多个分类则保存在category_list中.

我们来看attachCategories函数:

objc-runtime-new.mm中能找到相关源码

// Attach method lists and properties and protocols from categories to a class.
//将类别中的方法列表,属性和协议附加到类。
// Assumes the categories in cats are all loaded and sorted by load order,
//假设cat中的类别都按加载顺序加载和排序,
// oldest categories first.
// 最老的类别最先进行 
static void  attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {
        auto& entry = cats->list[I];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist = entry.cat->propertiesForMeta(isMeta);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

从注释中我们可以看出这个函数的作用是用来将类别中的方法列表,属性和协议附加到类. 以及它的顺序是最老的类别最先进行.
我们继续看具体如何附加到类,
来到attachLists()函数:

objc-runtime-new.h中能找到相关源码

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

源码中有两个重要的数组:
array()->lists: 类对象原来的方法列表,属性列表,协议列表.
addedLists: 传入的所有分类的方法列表,属性列表,协议列表.
attachLists函数中最重要的两个方法为memmove内存移动memcpy内存拷贝. 我们先来分别看一下这两个函数:

// memmove :内存移动。
/*  __dst : 移动内存的目的地
*   __src : 被移动的内存首地址
*   __len : 被移动的内存长度
*   将__src的内存移动__len块内存到__dst中
*/
void    *memmove(void *__dst, const void *__src, size_t __len);

// memcpy :内存拷贝。
/*  __dst : 拷贝内存的拷贝目的地
*   __src : 被拷贝的内存首地址
*   __n : 被移动的内存长度
*   将__src的内存移动__n块内存到__dst中
*/
void    *memcpy(void *__dst, const void *__src, size_t __n);

下面我们图示经过memmovememcpy方法过后的内存变化:

未经过内存移动和拷贝时, 指针位置

经过memmove方法之后,内存变化为:

// array()->lists 原来方法、属性、协议列表数组
// addedCount 分类数组长度
// oldCount * sizeof(array()->lists[0]) 原来数组占据的空间
memmove(array()->lists + addedCount, array()->lists, 
                  oldCount * sizeof(array()->lists[0]));
memmove后指针位置

经过memmove方法之后,我们发现,虽然本类的方法,属性,协议列表会分别后移,但是本类的对应数组的指针依然指向原始位置. 这时候会进行memcopy:
memcpy方法之后,内存变化:

// array()->lists 原来方法、属性、协议列表数组
// addedLists 分类方法、属性、协议列表数组
// addedCount * sizeof(array()->lists[0]) 原来数组占据的空间
memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
memcopy后指针位置

至此, 分类中的方法,属性,协议等被完全附加到本类上.
我们发现原来指针位置并没有改变, 至始至终指向开头的位置. 并且经过memmovememcpy方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面. 这样做的目的, 是为了保证分类方法优先调用.

category总结:

Category 可以添加属性. 但是runtime不会生产对应的ivarsetter getter方法. 不可以添加实例变量?
Category 中添加的方法, 在runtime初始化的时候, 会被附加到本类methodlist中, 并且在本类方法前边, 所以消息发送过程和对象的消息发送过程相同.
Category中添加了一个与本类中方法相同名字的方法, 为什么本类中的方法不会再调用? 是重写还是被覆盖? 既不是重写, 也不是覆盖, 而是在附加到本类methodlist中的过程中被放在本类方法前边. 在消息发送的过程中, 会优先找到分类的方法,并且执行其IMP, 所以本类方法不会再执行.

四: Runtime在日常工作中的应用:

洋洋洒洒写了这么多, 接下来才是大家最关心的环节. Runtime在日常工作中到底有什么应用呢?

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

用来给Category添加属性.

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

class_addMethod() method_exchangeImplementations

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