runtime面试指北——基本数据结构

引言

Objective-C是一门动态语言,在OC中方法的调用在编译期时并不能真正决定调用的是哪个方法。只有在真正运行时才会根据方法的名称找到对应的函数调用。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime library) 来执行编译后的代码。而Objective-C语言的动态特性正是基于runtime。

>

runtime官方指南

>

runtime API介绍

目前runtime存在两个版本Legac和Modern。Modern版本是在 Objective-C 2.0时候引入的。相对于Legac版本,Modern最值得注意的新特性是:当你对某个类的实例变量进行重新布局,编译器不需要重新编译该类的子类。即在Legac版本中,如果你更改了类的实例变量布局,编译器会重新对该类的子类重新编译。目前在iPhone的程序是使用的是Modern版本的runtime。而OSX上从v10.5及之后的版本开始在64位的程序使用Modern 版本,而其它程序(32位的Mac 程序)使用的是Legac版本。

runtime交互

在Objective-C中有三种完全不同层次的交互方式:

Objective-C 源代码

使用Foundation框架内的NSObject定义的方法。

runtime的函数。

Objective-C 源代码

在平时开发中,我们很少用到或者接触到直接调用runtime的API的情况,大多数情况下App的开发者一般只需要关心OC的代码如何编写、编译,而runtime会自动在幕后把我们写的源代码在编译阶段转换成运行时代码,在运行时确定对应的数据结构和调用具体哪个方法。

使用Foundation框架内的NSObject定义的方法

在Cocoa中的大多数类都是继承于NSObect,这些继承于NSObject的类同时继承了NSObject的方法。需要特别注意的是NSProxy它并不在上述的类之中,关于NSProxy更多信息可以参考Message Forwarding。

在NSObect有些方法仅仅作为抽象接口提供,NSObect本身的实现可以本子类重载。比如NSObect的description方法,NSObect的实现是仅返回该类内容的字符串,我们可以通过重写子类的description方法。提供更多的信息,例如:重写NSArray的description方法我们可以打印出数组中所有元素的内容。

在官方指南中还提到了NSObect以下方法就是通过“质询”runtime来获取信息的。d

- (Class)class OBJC_SWIFT_UNAVAILABLE("use 'anObject.dynamicType' instead");

- (BOOL)isKindOfClass:(Class)aClass;

- (BOOL)isMemberOfClass:(Class)aClass;

- (BOOL)conformsToProtocol:(Protocol *)aProtocol;

- (BOOL)respondsToSelector:(SEL)aSelector;

- (IMP)methodForSelector:(SEL)aSelector;

1

2

3

4

5

6

-class方法返回对象的类;

-isKindOfClass: 和 -isMemberOfClass: 方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);

-respondsToSelector: 检查对象能否响应指定的消息;

-conformsToProtocol:检查对象是否实现了指定协议类的方法;

-methodForSelector:返回指定方法实现的地址IMP

直接使用Runtime函数

Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。关于Runtime函数可以在Objective-C Runtime Reference中查看 Runtime 函数的详细文档。

Runtime的基本数据结构

在上篇文章iOS面试题库——KVC与KVO的末尾我们提到了struct objc_class * isa;,在runtime中有很多类似的结构体指针,在此我们将承接上文展开的介绍下Runtime的其它数据结构,想要更深入的了解runtime的同学必须对它里面的数据结构有所了解。本次源码下载自objc4-723。

id

id在OC中是一个指向任一对象的指针,在runtime的源码中可以看到对id的定义:

typedef struct objc_object *id;

1

继续在源码objc-runtime-new.h中寻找objc_object的结构体的结构:

struct objc_object {

private:

    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object

    Class ISA();

    // getIsa() allows this to be a tagged pointer object

    Class getIsa();

    // ... more code

1

2

3

4

5

6

7

8

9

10

11

12

objc_object里面又包含了一个isa指针,类型为isa_t的联合体(union)联合体和结构体的区别。更多关于isa_t的内容可以参考Objective-C 引用计数原理。根据 isa 指针我们能够找到对象所属的类,但苹果官方并不推荐我们使用isa来判断对象的类型。

Class

Class在源码是一个指向 objc_class 结构体的指针:

typedef struct objc_class *Class;

1

2

在源码中结构体objc_class包含了很多方法和结构体成员:

struct objc_class : objc_object {

    // Class ISA;

    Class superclass;

    cache_t cache;            // formerly cache pointer and vtable

    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() {

        return bits.data();

        //....more code

    }

1

2

3

4

5

6

7

8

9

10

11

要注意到结构体objc_class继承于objc_object,也就是说一个 ObjC 类本身同时也是一个对象,对象是以类为模板创建的,那么类对象的类又是什么呢?答案是为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class)的东西,类对象所属类型就是元类。所以当发出[NSObject alloc]消息时,实际上消息是发给了一个类对象。这个类对象必须是一个元类的实例。而这个元类同时也是一个根元类 (root meta class) 的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。这条消息发给类对象的时候,objc_msgSend() 会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。接着上图:

上图实线是 superclass 指针,虚线是isa指针。 有趣的是根元类的超类是 NSObject,而 isa 指向了自己,而 NSObject 的超类为 nil,也就是它没有超类。

cache_t

cache_t在runtime中的定义:

struct cache_t {

    struct bucket_t *_buckets;

    mask_t _mask;

    mask_t _occupied;

    // ....

}

1

2

3

4

5

6

cache_t中又包含了一个bucket_t的结构体和两个unsigned int类型的_mask、_occupied。_mask,_occupied分别表示分配用来缓存bucket的总数和已被分配的数量。

bucket_t结构:

struct bucket_t {

private:

    cache_key_t _key;

    IMP _imp;

    // ...

}

1

2

3

4

5

6

bucket_t里面包含了IMP函数指针和unsigned long类型的_key,IMP指向了一个方法的具体实现。

cache主要用来优化方法的调用,按照计算机的理论上来讲,如果一个方法被调用,那么很大概率这个方法之后还会被调用。所以runtime将被调用的方法存到cache中,下次方法被调用时,首先会在cache寻找。如果没有命中再从isa指向的类的方法列表中遍历查找能够响应消息的方法。

class_data_bits_t

class_data_bits_t:

struct class_data_bits_t {

    // Values are the FAST_ flags above.

    uintptr_t bits;

public:

    class_rw_t* data() {

        return (class_rw_t *)(bits & FAST_DATA_MASK);

    }

    //...

}

1

2

3

4

5

6

7

8

9

10

11

当我们用bits与不同的FAST_宏定义做按位与操作。可以获得不同的数据。

32位情况下以 FAST_ 开头的宏定义含义:

#if !__LP64__

// class is a Swift class

#define FAST_IS_SWIFT        (1UL<<0)

// class or superclass has default retain/release/autorelease/retainCount/

//  _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference

#define FAST_HAS_DEFAULT_RR  (1UL<<1)

// data pointer

#define FAST_DATA_MASK        0xfffffffcUL

#elif 1

#endif

1

2

3

4

5

6

7

8

9

10

11

其中FAST_DATA_MASK代表一块存储区域,存放着指向class_rw_t的指针。data()返回的就是(bits & FAST_DATA_MASK)得到的结果。

class_rw_t && class_ro_t

struct class_rw_t {

    // Be warned that Symbolication knows the layout of this structure.

    uint32_t flags;

    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;

    property_array_t properties;

    protocol_array_t protocols;

    Class firstSubclass;

    Class nextSiblingClass;

    char *demangledName;

    // ...

}

struct class_ro_t {

    uint32_t flags;

    uint32_t instanceStart;

    uint32_t instanceSize;

#ifdef __LP64__

    uint32_t reserved;

#endif

    const uint8_t * ivarLayout;

    const char * name;

    method_list_t * baseMethodList;

    protocol_list_t * baseProtocols;

    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;

    property_list_t *baseProperties;

    method_list_t *baseMethods() const {

        return baseMethodList;

    }

};

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

class_rw_t还包含了class_ro_t指针,class_ro_t的 method_list_t, ivar_list_t, property_list_t 结构体都继承自entsize_list_tt<Element, List, FlagMask>。protocol_list_t 与前三个不同,它存储的是 protocol_t * 指针列表,class_ro_t中的 method_list_t, ivar_list_t, property_list_t 、protocol_list_t存放了在编译器就能决定的属性、方法、实例变量和遵守的协议。

在某个类初始化之前类的结构中的 class_data_bits_t *data 指向的其实是一个 class_ro_t * 指针,等到static Class realizeClass(Class cls) 静态方法在类第一次初始化时被调用:

从 class_data_bits_t 调用 data 方法,将结果从 class_rw_t 强制转换为 class_ro_t 指针

初始化一个 class_rw_t 结构体

设置结构体 ro 的值以及 flag

返回真正的类

Method && SEL && IMP

typedef struct method_t *Method;

struct method_t {

    SEL name;

    const char *types;

    IMP imp;

    struct SortBySELAddress :

        public std::binary_function<const method_t&,

                                    const method_t&, bool>

    {

        bool operator() (const method_t& lhs,

                        const method_t& rhs)

        { return lhs.name < rhs.name; }

    };

};

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

objc_method 存储了方法名,方法类型和方法实现:

SEL是方法选择器(method selector)在OC中的类型,一个方法选择器(method selector)就是C的字符串通过OC的runtime映射得到。

types是Type Encoding类型编码,更多参考Type Encoding

IMP是一个函数指针,指向的是函数的具体实现。runtime 中消息传递和转发的目的就是为了找到IMP。

typedef struct objc_selector *SEL;

1

SEL是方法选择器selector的类型,有两种方式:@selector()或者是sel_registerName()把C的字符串映射成在method selector。不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。

IMP指向方法实现的首地址指针:

id (*IMP)(id, SEL, ...)

1

这个数据类型是一个指向实现该方法的函数开始的指针。参数都包含 id 和 SEL 类型,每个方法对应一个SEL,而每个 id 是一个self指针(在对象方法中self是对象的内存指针,在类方法中self是指向元类的指针)。所以一组id 和 SEL对应的方法实现肯定是唯一的。

Ivar

Ivar是类中实例变量的类型,被定义为:

typedef struct objc_ivar *Ivar;

truct ivar_t {

    int32_t *offset;

    const char *name;

    const char *type;

    // alignment is sometimes -1; use alignment() instead

    uint32_t alignment_raw;

    uint32_t size;

    uint32_t alignment() const {

        if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;

        return 1 << alignment_raw;

    }

};

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

Ivar中包含了实例变量的名称、类型等。我们可以通过class_copyIvarList(Class cls, unsigned int *outCount)来获取类的实例变量(包括由@property的属性,但会添加_前缀)。

objc_property_t

objc_property_t是一个指向property_t结构体的指针。

typedef struct property_t *objc_property_t;

1

2

我们可以通过class_copyPropertyList方法获取类的所有属性变量,但此方法返回的变量中不包含成员变量,且获取的属性变量不带_。

Category

Category在runtime源码中定义为指向category_t结构体的指针:

typedef struct category_t *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);

};

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

Category为现有的类提供了扩展的途径让我们可以在类原有的基础上,通过Category

给类扩展添加上实例方法、类方法、协议等。

[参考材料]:

神经病院 Objective-C Runtime 入院第一天—— isa 和 Class

Objective-C Runtime

ObjCRuntimeGuide

---------------------

作者:FY_Chao

来源:CSDN

原文:https://blog.csdn.net/yuwuchaio/article/details/80860965

版权声明:本文为博主原创文章,转载请附上博文链接!

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    萌萌的小伟哥阅读 728评论 0 9
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 1,233评论 0 9
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 1,215评论 0 7
  • 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的...
    lylaut阅读 549评论 0 4
  • Objective-C语言是一门动态语言,他将很多静态语言在编译和链接时期做的事情放到了运行时来处理。这种动态语言...
    tigger丨阅读 562评论 0 7