文章

iOS消息转发机制

简介

Objective-C是一门动态语言,得力于他的objc/runtime.h,要理解runtime的实现原理,我们就需要对oc的消息发送、查找消息、消息转发进行理解。

C中的函数调用方式,是使用的静态绑定(static binding),即在编译期就能决定运行时所应调用的函数。而在Objective-C中,如果向某对象传递消息,就会使用动态绑定机制来决定需要调用的方法。而对于Objective-C的底层实现,都是C的函数。对象在收到消息之后,调用了哪些方法,完全取决于Runtime来决定,甚至可以在Runtime期间改变。

什么是消息转发?

这个概念可能容易和消息发送混淆,先解释下什么是消息发送:

消息发送简单理解就是向对象发送消息,OC的方法在编译之后就会变成C写的消息发送。

而消息发送过程中,如果找不到响应的函数,就会进入到消息转发机制中。

看个例子

一段oc代码

[test by_eat5111];

使用clang进行编译clang -rewrite-objc main.m -o main.cpp之后变成

((void (*)(id, SEL))(void *)objc_msgSend)((id)test, sel_registerName("by_eat5111"));
// 简写如下
objc_msgSend(test,sel_registerName("by_eat5111"));
// 其实就是向test对象发送by_eat5111的sel消息

objc_msgSend的执行内容下面会讲到

源码分析

Class和id的结构源码

下文涉及知识点主要会对Object-C 2.0的源码进行分析

所有方法调用在编译之后基本都是和objc_msgSend之类相关方法

知识点:

1
2
3
4
5
struct objc_object
struct objc_class : objc_object

typedef struct objc_class *Class;
typedef struct objc_object *id;

OC是分两个版本

  • Objective-C 1.0,已经废弃了不用了
  • Objective-C 2.0,现在在使用的

被误解的-objc-class

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// objc-private.h
struct objc_object {
    // isa结构体
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();
    // 下面一堆其他方法
}

// objc-runtime-new.h
// objc_class继承于objc_object,因此
// objc_class中也有isa结构体
struct objc_class : objc_object {
    // ISA占8位
    // Class ISA;
    // superclass占8位
    Class superclass;
    // 缓存的是指针和vtable,目的是加速方法的调用  cache占16位
    cache_t cache;             // formerly cache pointer and vtable
    // class_data_bits_t 相当于是class_rw_t 指针加上rr/alloc标志
    class_data_bits_t bits; 
	// 类的方法、属性、协议等信息都保存在class_rw_t结构体中
    class_rw_t *data() {
        // 这里的bits就是class_data_bits_t bits;
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    // 下面一堆其他方法
}

// 类的方法、属性、协议等信息都保存在class_rw_t结构体中
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;
	// class_ro_t结构体存储了类在编译期就已经确定的属性、方法以及遵循的协议
    const class_ro_t *ro;
    
    // 方法信息
    method_array_t methods;
    // 属性信息
    property_array_t properties;
    // 协议信息
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif

    void setFlags(uint32_t set) 
    {
        OSAtomicOr32Barrier(set, &flags);
    }

    void clearFlags(uint32_t clear) 
    {
        OSAtomicXor32Barrier(clear, &flags);
    }

    // set and clear must not overlap
    void changeFlags(uint32_t set, uint32_t clear) 
    {
        assert((set & clear) == 0);

        uint32_t oldf, newf;
        do {
            oldf = flags;
            newf = (oldf | set) & ~clear;
        } while (!OSAtomicCompareAndSwap32Barrier(oldf, newf, (volatile int32_t *)&flags));
    }
};

// class_ro_t结构体存储了类在编译期就已经确定的属性、方法以及遵循的协议
// 因为在编译期就已经确定了,所以是ro(readonly)的,不可修改
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;
    }
};

总结:如何找到一个类的方法

对象(objc_object)-> getIsa() -> Class(objc_class)-> data() -> class_rw_t(动态添加的方法在这里) -> ro(class_ro_t) -> baseMethodList

简写版objc_class和objc_object

被问过,对象占多大内存这种问题,蜜汁疑惑,可能是问objc_class的占内存大小吧(40)。

后来发现问的不是这个问的是alloc,详细看这边文章 iOS内存

下面贴下简写版本的 objc_class和objc_object,

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//
//  main.cpp
//  RAMCPPDemo
//
//  Created by rambo on 2020/11/24.
//

#include <iostream>

typedef void (*IMP)(void /* id, SEL, ... */ );
struct objc_class;
struct objc_object;

typedef struct objc_class *Class;

int main(int argc, const char * argv[]) {
    struct Test {
        int a;
        long b;
        void test();
        void test2();
    };
    
    union isa_t {
        Class cls;
        unsigned long bits;
        struct {
            unsigned long nonpointer        : 1;
            unsigned long has_assoc         : 1;
            unsigned long has_cxx_dtor      : 1;
            unsigned long shiftcls          : 33;
            unsigned long magic             : 6;
            unsigned long weakly_referenced : 1;
            unsigned long deallocating      : 1;
            unsigned long has_sidetable_rc  : 1;
            unsigned long extra_rc          : 19;
        };
    };
    
    struct bucket_t {
        private:
            // IMP-first is better for arm64e ptrauth and no worse for arm64.
            // SEL-first is better for armv7* and i386 and x86_64.
        #if __arm64__
            MethodCacheIMP _imp;
            cache_key_t _key;
        #else
            unsigned long _key;
            IMP _imp;
        #endif
    };
    struct cache_t {
        struct bucket_t *_buckets;
        unsigned int _mask;
        unsigned int _occupied;
    };
    
    struct class_data_bits_t {
        unsigned long bits;
    };
    
	// 类的方法、属性、协议等信息都保存在class_rw_t结构体中
	struct class_rw_t {
	    // Be warned that Symbolication knows the layout of this structure.
	    uint32_t flags;
	    uint32_t version;
	    // class_ro_t结构体存储了类在编译期就已经确定的属性、方法以及遵循的协议
	    const class_ro_t *ro;
	    
	    // 方法信息
	    method_array_t methods;
	    // 属性信息
	    property_array_t properties;
	    // 协议信息
	    protocol_array_t protocols;
	
	    Class firstSubclass;
	    Class nextSiblingClass;
	
	    char *demangledName;
	}
    // class_ro_t结构体存储了类在编译期就已经确定的属性、方法以及遵循的协议
	// 因为在编译期就已经确定了,所以是ro(readonly)的,不可修改
	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;
	    }
	};

    struct objc_object {
        // isa结构体
        isa_t isa;
    };
    struct objc_class : objc_object {
        // ISA占8位
        // Class ISA;
        // superclass占8位
        Class superclass;
        // 缓存的是指针和vtable,目的是加速方法的调用  cache占16位
        cache_t cache;             // formerly cache pointer and vtable
        // class_data_bits_t 相当于是class_rw_t 指针加上rr/alloc标志  占8位
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

	    class_rw_t *data() {
	        // 这里的bits就是class_data_bits_t bits;
	        return bits.data();
	    }
    };
    
    
    printf("%lu\n", sizeof(objc_class));//共占 40
    
    return 0;
}

消息发送 - objc_msgSend

一段oc代码

[test by_eat5111];

使用clang进行编译clang -rewrite-objc main.m -o main.cpp之后变成

((void (*)(id, SEL))(void *)objc_msgSend)((id)test, sel_registerName("by_eat5111"));
// 简写如下
objc_msgSend(test,sel_registerName("by_eat5111"));
// 其实就是向test对象发送by_eat5111的sel消息
1
void objc_msgSend(id self, SEL cmd, ...)

SEL选择器,也就是我们经常使用的@selector(),这里在使用上就能发现,OC的方法只和方法名有关,和参数无关系,所以没有像C++、C#那样的函数重载特性,因为选择子并不由参数和函数名共同决定

选择器SEL

使用 @selector(hello) 生成的选择子,是否会因为类的不同而不同?

使用 @selector() 生成的选择子不会因为类的不同而改变,其内存地址在编译期间就已经确定了。也就是说向不同的类发送相同的消息时,其生成的选择子是完全相同的。(感觉就像是个固定的字符数组)

选择子有以下的特性:

  1. Objective-C 为我们维护了一个巨大的选择子表
  2. 在使用 @selector() 时会从这个选择子表中根据选择子的名字查找对应的 SEL。如果没有找到,则会生成一个 SEL 并添加到表中
  3. 在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 @selector() 生成的选择子加入到选择子表中

objc_msgSend 汇编实现部分 - 先尝试的从缓存表中(也就是常说的快速映射表)查询缓存

详细的汇编讲解看这两篇文章objc_msgSend流程分析对象方法消息传递流程

objc_msgSend函数是使用汇编语言实现的,其中我们先尝试的从缓存表中(也就是常说的快速映射表)查询缓存,倘若查询失败,则会将具体的类对象、选择子、接收者在指定的内存单元中存储,并调用__class_lookupMethodAndLoadCache3函数。

走进C部分实现 - 在方法列表中进行查找 lookUpImpOrForward

objc_msgSend 的汇编实现,最后调用到

1
2
3
4
5
6
7
8
9
// objc-runtime-new.mm 
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)

这是一个仅提供给派发器(dispatcher汇编实现部分)用于方法查找的函数,其它的代码都应该使用 lookUpImpOrNil()(不会进行方法转发)。_class_lookupMethodAndLoadCache3 会传入 cache = NO 避免在没有加锁的时候对缓存进行查找,因为派发器(汇编实现部分)已经做过这件事情了。

我们看下lookUpImpOrForward的实现源码,总共步骤如下

  1. 无锁的缓存查找
  2. 如果类没有实现(isRealized)或者初始化(isInitialized),实现或者初始化类
  3. 加锁
  4. 缓存以及当前类中方法的查找
  5. 尝试查找父类的缓存以及方法列表
  6. 没有找到实现,尝试方法解析器
  7. 进行消息转发
  8. 解锁、返回实现
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver) {
    Class curClass;
    IMP imp = nil;
    Method meth;
    bool triedResolver = NO;
// ----------- 1. 无锁的缓存查找
// ----------- 在没有加锁的时候对缓存进行查找,提高缓存使用的性能
// ----------- 因为 _class_lookupMethodAndLoadCache3 传入的 cache = NO,所以这里会直接跳过 if 中代码的执行,在 objc_msgSend 中已经使用汇编代码查找过了。
    runtimeLock.assertUnlocked();

    // 检查是否添加缓存锁,如果没有进行缓存查询。
    // 查到便返回IMP指针
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }
// ----------- 1. end
// ----------- 2. 类的实现和初始化
// ----------- 在 Objective-C 运行时 初始化的过程中会对其中的类进行第一次初始化也就是执行 realizeClass 方法,为类分配可读写结构体 class_rw_t 的空间,并返回正确的类结构体。而 _class_initialize 方法会调用类的 initialize 方法

 _class_initialize 方法会调用类的 initialize 方法
    // 通过调用realizeClass方法,分配可读写`class_rw_t`的空间
    if (!cls->isRealized()) {
        rwlock_writer_t lock(runtimeLock);
        realizeClass(cls);
    }

    // 倘若未进行初始化,则初始化
    if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));
    }
// ----------- 2. end
// ----------- 3. 加锁
// ----------- 保证方法查询以及缓存填充(cache-fill)的原子性,保证在运行以下代码时不会有新方法添加导致缓存被冲洗(flush)。
    retry:
    runtimeLock.read();
// ----------- 3. end

    // 是否忽略GC垃圾回收机制(仅用在macOS中)
    if (ignoreSelector(sel)) {
        imp = _objc_ignored_method;
        cache_fill(cls, sel, imp, inst);
        goto done;
    }
// ----------- 4. 在当前类中查找实现
    // 当前类的缓存列表中进行查找,汇编实现为了加速过程,在类的 cache 中寻找对应的实现,做了一些性能上的优化。
    // cache 字段见上文 struct objc_class : objc_object { 部分
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // 没有命中缓存,开始从类的方法列表中进行线性查询cls->data()->methods
    // 查找对应的方法的结构体指针 method_t
    meth = getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        // 加入缓存中,走进cache_fill_nolock实现
        log_and_fill_cache(cls, meth->imp, sel, inst, cls);
        imp = meth->imp;
        goto done;
    }
// ----------- 4. end
// ----------- 5. 尝试查找父类的缓存以及方法列表, 这一部分与上面的实现基本上是一样的,只是多了一个循环用来判断根类:
     // 1. 查找缓存
     // 2. 搜索方法列表
    // 从父类中循环遍历
    curClass = cls;
    while ((curClass = curClass->superclass)) {
        // 父类的缓存列表中查询
        imp = cache_getImp(curClass, sel);
        if (imp) {
            if (imp != (IMP)_objc_msgForward_impcache) {
                // 如果在父类中发现方法,则填充到该类缓存列表
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done;
            }
            else {
                break;
            }
        }

        // 从父类的方法列表中查询
        meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }
// ----------- 5. end
// ----------- 6. 没有找到实现,尝试方法解析器,动态方法解析,下文会提到

    // 进入method resolve过程
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        // 调用_class_resolveMethod,解析没有实现的方法
        // 判断当前类是否实现了 resolveInstanceMethod: 或者 resolveClassMethod: 方法,然后用 objc_msgSend 执行上述方法,并传入需要决议的选择子。如果有实现,就是动态的添加了没有找到的方法,并走进 retry 也就是第3步,进行重新的执行,但不会在走一次动态方法解析,因为triedResolver = YES;
        _class_resolveMethod(cls, sel, inst);
        // 进行二次尝试
        triedResolver = YES;
        goto retry;
    }
// ----------- 6. end
// ----------- 7. 消息转发 - msgForward

    // 没有找到方法,启动消息转发
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);
// ----------- 7. end
 done:
    runtimeLock.unlockRead();
    return imp;
}

static method_t *getMethodNoSuper_nolock(Class cls, SEL sel) {
    runtimeLock.assertLocked();
    // 遍历所在类的methods,这里的methods是List链式类型,里面存放的都是指针
    for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists != end; ++mlists) {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

void _class_resolveMethod(Class cls, SEL sel, id inst) {
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        // 针对于对象方法的操作
        // 这个方法是动态方法解析中,当收到无法解读的消息后调用。
        // 这个方法也会用在@dynamic,以后会在消息转发机制的源码分析中介绍
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        // 针对于类方法的操作,说明同上
        _class_resolveClassMethod(cls, sel, inst);
        // 再次启动查询,并且判断是否拥有缓存中消息标记_objc_msgForward_impcache
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) {
            // 说明可能不是 metaclass 的方法实现,当做对象方法尝试
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

总结来说,也是先找缓存,再找方法列表,再找父类的缓存和方法列表,最后没找到就是调用msgForward

objc_objcet.isa ⇒ objc_class.bits.data ⇒ class_rw_t.ro ⇒ class_ro_t.baseMethodList

20220210-1

20220210-1

动态特性Runtime - 方法解析和消息转发

上文提到的C部分实现的源码里面的 6和7部分

没有方法的实现,程序会在运行时挂掉并抛出 unrecognized selector sent to … 的异常。但在异常抛出前,Objective-C 的运行时会给你三次拯救程序的机会:

  • Method resolution
  • Fast forwarding
  • Normal forwarding

动态方法解析 - Method Resolution

由上面objc_msgSend的源码分析知道,会走到_class_resolveMethod中,这一部分就是Method resolution

Objective-C 运行时会调用 + (BOOL)resolveInstanceMethod:或者 + (BOOL)resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。还是以 foo 为例,你可以这么实现:

void fooMethod(id obj, SEL _cmd)  
{
    NSLog(@"Doing foo");
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if(aSEL == @selector(foo:)){
        class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod];
}

这里第一字符v代表函数返回类型void,第二个字符@代表self的类型id,第三个字符:代表_cmd的类型SEL。这些符号可在Xcode中的开发者文档中搜索Type Encodings就可看到符号对应的含义,更详细的官方文档传送门 在这里,此处不再列举了。

消息转发 - Fast Rorwarding 快速转发

主要是复写方法

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(foo:)){
        return [[BackupClass alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

消息转发 - Normal Forwarding 正常转发

使用NSMethodSignatureNSInvocation进行消息转发

- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL sel = invocation.selector;
    if([alternateObject respondsToSelector:sel]) {
        [invocation invokeWithTarget:alternateObject];
    } else {
        [self doesNotRecognizeSelector:sel];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
    if (!methodSignature) {
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    return methodSignature;
}

NSInvocation的其他实战用法可以见文章

总结

  1. oc [test eat]
  2. 经过编译,变成c objc_msgSend(test,sel_registerName("eat"))
  3. 汇编部分从缓存池中查找方法(快速映射表)
  4. C部分先找缓存,再找方法列表,再找父类的缓存和方法列表,知道NSObject,找到就加入到缓存中,没找到就是调用方法Runtime
  5. 方法解析 - + (BOOL)resolveInstanceMethod:或者 + (BOOL)resolveClassMethod:
  6. 消息转发 快速转发forwardingTargetForSelector和正常转发NSMethodSignatureNSInvocation

会被问到的问题

  1. Objective-C 对象是什么?Class 是什么?id 又是什么?

    答:对象是 struct objc_object。Class是objc_class 的指针,id是objc_object 的指针。

    struct objc_object

    struct objc_class : objc_object

    typedef struct objc_class *Class;

    typedef struct objc_object *id;

  2. isa 是什么?为什么要有 isa?

    答:isa是struct objc_object仅有的一个成员变量,存储的是当前对象的类和一些所属类之外的其他信息。见文章分析

  3. 为什么在 Objective-C 中,所以的对象都用一个指针来追踪?

    答:OC的对象是创建在堆上的,我们需要指针指向它。见文章分析

  4. Objective-C 对象是如何被创建(alloc)和初始化(init)的?

    答:对象是动态创建的,第一步:通过alloc方法,为对象以及集成关系中的属性分配空间,并同时进行该空间的清空处理,避免内存中残留的之前的垃圾信息。第二步:通过init方法,确保对象在创建之后各属性有合适的初始值。参考文章

  5. Objective-C 对象的实例变量是什么?为什么不能给 Objective-C 对象动态添加实例变量?

    答:实例变量是存储在class_ro_t.ivars中的。runtime中可以使用 class_addIvar为动态新建的类创建实例,但是必须在objc_allocateClassPair实现之后,和objc_registerClassPair实现之前使用。程序在编译的时候,编译器会生成实例变量的内存布局ivar layout,告诉运行时去哪里访问类的实例变量。一旦完成了类定义,就不能再添加成员变量了。编译后的类,在程序启动后,就被runtime加载了,就没有机会调用class_addIvar了。

  6. Objective-C 对象的属性是什么?属性跟实例变量的区别?

    答:属性是使用@property进行声明的,例如@property (nonatomic, copy)NSString *userName;,在编译器中,会自动跟_useName的实例变量进行关联。属性会自动创建同名实例变量的get和set方法,在分类中的属性不会自动创建。

    属性是存在class_rw_t.properties里面的,他对应生成的实例变量是存在class_ro_t.baseProperties中,对应生成的get和set方法存放在class_ro_t.baseMethodList中,实例是在class_ro_t.ivars,详细验证可以看文章

  7. Objective-C 对象的方法是什么?Objective-C 对象的方法在内存中的存储结构是什么样的?

    答:结构体 struct method_t { SEL name; const char *types; MethodListIMP imp; } , class_rw_t.methods中会存放运行期见的方法,例如分类的方法,class_ro_t.baseMethodList存放的是编译期间确认的方法

  8. 什么是 IMP?什么是选择器 selector ?

    答:答案见问题7,imp是函数的实现,selector是选择子,对应的是方法名。

  9. 消息发送和消息转发

    答:消息发送[test eat],编译之后就变成objc_msgSend(test,sel_registerName("eat"));,所以实际消息发送调用的就是c的方法,objc_msgSend。消息转发是消息发送过程中没有找到方法实现之后的提供给用户进行兜底的机制。如果没有实现相应的转发就会程序崩溃。

  10. Method Swizzling

答:利用动态化特性,可以将方法的imp进行互换。用的比较多的就是热修复。

  1. Category

    答:分类中不能添加实例变量,添加是属性也不会自动生成对应的实例变量和set以及get,需要使用关联对象进行操作。在本类和分类有同名方法时,优先调用分类的方法。同名方法调用的优先级为分类 > 本类 > 父类。因为分类的编译顺序靠后。如果多个分类中都有和原有类中同名的方法,那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法。参考文章

  2. Associated Objects 的原理是什么?到底能不能在 Category 中给 Objective-C 类添加属性和实例变量?

    答:使用的是hashmap,不能

  3. Objective-C 中的 Protocol 是什么?

    答:编译期就生成的存储在class_ro_t.baseProtocols中。

    option和require,不同类都可以实现协议做到有相同的方法的能力,他们之间相互不影响。在使用上,可以通过判断此类是否实现这个协议。可以不需要知道类是哪个,知道她实现了某个协议就能调用这个协议的方法例如

    1
    2
    3
    
    id <XYZFrameworkUtility> utility = 
    [frameworkObject anonymousUtility];
    NSUInteger count = [utility numberOfSegments];
    

    协议(protocol)是Objective-c中一个非常重要的语言特性,从概念上讲,非常类似于JAVA中接口. 一个协议其实就是一系列有关联的方法的集合(为方便后面叙述,我们把这个协议命名为myProtocol)。协议中的方法并不是由协议本身去实现,相反而是由遵循这个协议的其他类来实现。换句话说,协议myProtocol只是完成对协议函数的声明而并不管这些协议函数的具体实现。

  4. self 和 super 的本质

    @implementation Son : Father
    - (id)init {
        self = [super init];
        if (self) {
            NSLog(@"%@", NSStringFromClass([self class]));
            NSLog(@"%@", NSStringFromClass([super class]));
        }
    		return self;
    }
    @end
    

    两个log输出都是Son,根据消息转发机制,会去找到IMP,自己没有就会找父类的,然后交由receiver执行,super的消息转发有个参数receiver就是self。由于class方法在NSObject处,最终objc_msgSend(self, @selector(class))objc_msgSendSuper(objc_super, @selector(class))查找到是同一个方法实现。

    可以参考这篇文章

  5. load 方法和 initialize 方法

    答:load是runtime最后调用的,一个类只会执行一次。initialize 是使用的时候才会触发调用。

  6. OC是否支持多继承?好,你说不支持多继承,那你有没有模拟多继承特性的办法?

    答:可以使用消息转发的机制实现多继承,将为实现的方法转发到需要指向多继承的类中,但是isKindOfClass不能够

参考文章

冬瓜的博客

iOS开发·runtime原理与实践: 消息转发篇(Message Forwarding) (消息机制,方法未实现+API不兼容奔溃,模拟多继承)

iOS 从源码解析Runtime (二):聚焦 isa、objc_object(ISA_BITFIELD相关内容篇)

读 objc4 源码,深入理解 Objective-C Runtime

从源代码看 ObjC 中消息的发送

iOS 消息发送与转发详解

理解oc消息传递机制

objc_msgSend流程分析

本文由作者按照 CC BY 4.0 进行授权