通过Runtime源码看Objective-C的动态性  

写过Objective-C的同学都应该知道,Objective-C里有一个id类型,可以在编写程序时指代任何对象类型,在程序运行起来后再具体确定到底是什么类型。给一个对象发送一个未定义在它的类中的方法在编译期间也不会报错,因为在运行时可以给类再添加方法,也可以改变方法的接收者,甚至是交换两个方法的实现过程。还有一点,我们一直在用,但可能忽略了它也是动态性的一个基础特性,就是可以根据不同的屏幕尺寸加载不同的启动图片,还可以根据不同屏幕的分辨率加载不同分辨率的资源图片,比如在Retain屏上加载@2x图片,在一些老设备上加载原图,在后来的plus系列设备上加载@3x图片。总结一下,Objective-C动态性体现在3个方面:动态类型、动态绑定、动态加载。下面主要说一下动态类型和动态绑定。

动态类型

类和对象的原型

Objective-C的动态性大多是由Runtime提供的,Runtime是用C/C++和汇编写的,苹果开源了它的代码。源码地址:https://opensource.apple.com/source/objc4/,代码下载地址:https://opensource.apple.com/tarballs/objc4/,其中列出了所有的历史版本。我下面的分析基于objc4-750里的代码。

我们所写的Objective-C代码经过编译器编译会先转换成低级的C/C++代码,通过Runtime的源码,可以看一下对象、类到底是如何定义的,以及类的层次结构是怎样的。

先来看一下对象是如何定义的,下载源码后,可以在Public Header/objc.h中找到对应的定义。

1
2
3
4
5
6
7
8
9
10
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

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

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

由此可知,我们写代码时定义的实例对象实际上是一个结构体指针,每一个实例对象都包含了一个isa指针,指向了它所属的类。这也是为什么我们定义实例对象变量时前面都要加上*号。id类型在定义的时候就将它定义为了objc_object *类型,所以我们在用的时候就不用再加*号了。

那类又是如何定义的呢?在Project Headers/objc-runtime-new.h中找到:

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
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();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}

//此处略去其余部分,详细的可以看源代码
}

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;

//此处略去其余部分,详细的可以看源代码
}

所以类也是一个结构体指针,其中包含了描述它的实例对象的一切内容,如它的实例对象拥有的属性、能够调用的方法、遵循的协议等等。而objc_class又继承自objc_object,所以类其实也是对象,也包含了一个isa指针,指向了它所属的类。与普通的实例对象不同的是,类对象还有超类superclass

类的层次结构

那么类对象的isa指针又指向了谁呢?struct objc_class中还有如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool isMetaClass() {
assert(this);
assert(isRealized());
return data()->ro->flags & RO_META;
}

// NOT identical to this->ISA when this is a metaclass
Class getMeta() {
if (isMetaClass()) return (Class)this;
else return this->ISA();
}

bool isRootClass() {
return superclass == nil;
}
bool isRootMetaclass() {
return ISA() == (Class)this;
}

这里又出现了几个类:元类、根类与根元类,并且根类的超类为nil,根元类的isa指针指向了自己。在Source/objc-runtime-new.mm中可以找到他们是如何关联起来的。

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
Class objc_allocateClassPair(Class superclass, const char *name, 
size_t extraBytes)
{
// 类和元类是一一对应的,有一个类就有一个对应的元类
Class cls, meta;

mutex_locker_t lock(runtimeLock);

// Fail if the class name is in use.
// Fail if the superclass isn't kosher.
if (getClass(name) || !verifySuperclass(superclass, true/*rootOK*/)) {
return nil;
}

// Allocate new classes.
cls = alloc_class_for_subclass(superclass, extraBytes);
meta = alloc_class_for_subclass(superclass, extraBytes);

// fixme mangle the name if it looks swift-y?
objc_initializeClassPair_internal(superclass, name, cls, meta);

return cls;
}

static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta)
{
// 只列出了关键代码
// Connect to superclasses and metaclasses
// 类对象的`isa`指针指向了元类
cls->initClassIsa(meta);
if (superclass) {
// 元类对象的`isa`指针指向了根元类,这一点从根类往下推,就可以得到了。
meta->initClassIsa(superclass->ISA()->ISA());
// 类的继承关系在元类中也有对应的继承关系
cls->superclass = superclass;
meta->superclass = superclass->ISA();

addSubclass(superclass, cls);
addSubclass(superclass->ISA(), meta);
} else { // superclass为nil,下面是该类为根类时的关系
// 根元类对象的`isa`指针指向自己
meta->initClassIsa(meta);
// 根类的超类为nil
cls->superclass = Nil;
// 根元类继承自根类(NSObject)
meta->superclass = cls;
addRootClass(cls);
addSubclass(cls, meta);
}
}

由此可以得到类的层次结构图了,例如Student继承Person,Person继承NSObject,则它们的层次结构图如下:

像类描述了它的实例对象一样,元类描述了类对象,元类中定义了那些只有类对象能够调用的方法,也就是类方法。所以如果调用的实例方法,就沿着类的继承链从下往上找,如果是类方法,就沿着元类的继承链从下往上查找。

从上面的源码的实现过程其实也可以看出,无论我们在Objective-C中定义了什么对象,最终都会被编译成同一个结构体指针类型,只要在运行时改变isa指针就改变了它所属的类。对于类也是,我们甚至可以在运行时创建一个类。所以对于Objective-C语言来说它是动态的,但是到了C/C++层面,就是静态的了,我们不可能在运行时,创建一个struct

用内省检查判断对象所属的类

NSObject提供了两个方法来判断对象所属的类:

1
2
3
4
// 判断一个对象是否是某个类的,或者任何继承自它的类的实例
- (BOOL)isKindOfClass:(Class)aClass;
// 判断一个对象是否是某个类的实例
- (BOOL)isMemberOfClass:(Class)aClass;

例如:

1
2
3
4
5
NSMutableDictionary *dict = [NSMutableDictionary new];
[dict isMemberOfClass:[NSDictionary class]]; ///< NO
[dict isMemberOfClass:[NSMutableDictionary class]]; ///< YES
[dict isKindOfClass:[NSDictionary class]]; ///< YES
[dict isKindOfClass:[NSArray class]]; ///< NO

动态绑定

消息是如何发送的

当给一个对象发送一个消息时,哪个方法会被调用完全是在运行时决定的,这就是Objective-C所采用的动态绑定机制,这使得Objective-C真正是动态的。在OC中给一个对象发送消息通常像下面这样:

1
id returnValue = [someObject messageName:parameter];

编译器会把上面的语句转换成一个C函数:objc_msgSend,在Public Headers/message.h中可以找到其定义,只摘出了关键代码:

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
/* Basic Messaging Primitives
*
* On some architectures, use objc_msgSend_stret for some struct return types.
* On some architectures, use objc_msgSend_fpret for some float return types.
* On some architectures, use objc_msgSend_fp2ret for some float return types.
*
* These functions must be cast to an appropriate function pointer type
* before being called.
*/
#if !OBJC_OLD_DISPATCH_PROTOTYPES
OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

#else
/**
* Sends a message with a simple return value to an instance of a class.
*
* @param self A pointer to the instance of the class that is to receive the message.
* @param op The selector of the method that handles the message.
* @param ...
* A variable argument list containing the arguments to the method.
*
* @return The return value of the method.
*
* ...
*/
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#endif

OBJC_OLD_DISPATCH_PROTOTYPES这里是一个编译选项,详细的可以看这篇文章。如果没有开启就会转换成if中的写法,注释中的These functions must be cast to an appropriate function pointer type before being called.告诉我们需要在调用之前强转成合适的函数指针类型。上面的OC方法调用会被转成如下:

1
2
SEL sel = @selector(messageName:)
id returnValue = ((id(*)(id, SEL))objc_msgSend)(someObject, sel);

如果开启了会转换成else中的写法,此时上面的OC方法调用会被转成如下:

1
2
SEL sel = @selector(messageName:)
id returnValue = objc_msgSend(someObject, sel);

objc_msgSend的实现是用汇编写的,不同的架构函数体内部的写法也不同,但做的事情大致一样,我们这里看一下Source/objc-msg-i386.s里的实现:

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
/********************************************************************
*
* id objc_msgSend(id self, SEL _cmd,...);
*
********************************************************************/


ENTRY _objc_msgSend
CALL_MCOUNTER

// load receiver and selector
movl selector(%esp), %ecx
movl self(%esp), %eax

// check whether receiver is nil
testl %eax, %eax
je LMsgSendNilSelf

// receiver (in %eax) is non-nil: search the cache
LMsgSendReceiverOk:
movl isa(%eax), %edx // class = self->isa
CacheLookup WORD_RETURN, MSG_SEND, LMsgSendCacheMiss
xor %edx, %edx // set nonstret for msgForward_internal
jmp *%eax

// cache miss: go search the method lists
LMsgSendCacheMiss:
MethodTableLookup WORD_RETURN, MSG_SEND
xor %edx, %edx // set nonstret for msgForward_internal
jmp *%eax // goto *imp

// message sent to nil: redirect to nil receiver, if any
LMsgSendNilSelf:
// %eax is already zero
movl $0,%edx
xorps %xmm0, %xmm0
LMsgSendDone:
ret

// guaranteed non-nil entry point (disabled for now)
// .globl _objc_msgSendNonNil
// _objc_msgSendNonNil:
// movl self(%esp), %eax
// jmp LMsgSendReceiverOk

LMsgSendExit:
END_ENTRY _objc_msgSend

通过注释可以看出,objc_msgSend根据receiverselector,要找到一个可以跳转到的imp指针。

那这个imp指针又是什么呢?在Public Headers/objc.h可以找到对其的定义:

1
2
3
4
5
6
/// 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

由此可知,IMP是一个指向函数的指针,这个函数就是OC里定义的方法的实现。所以objc_msgSend找到合适的IMP,就可以通过IMP去真正地执行方法了。可以看到IMPobjc_msgSend的使用几乎一样,如何我们能在编码时就很确定要调用哪个方法,就可以通过NSObject的实例方法methodForSelector来得到IMP,然后直接执行了,省去了objc_msgSend的查找过程。

从这里也可以看到,其实可以在运行时给类添加一个IMP,也就添加了一个方法,或者交换两个方法的IMP,那这个两个方法的方法体就可以交换了。也就是说,方法A和方法B的IMP指向交换,那当调用A时,实际执行的是B中的内容。所以完全可以动态地控制OC的方法。

Runtime提供了很多方法来直接操作类/实例对象/方法/属性等等,可以在Public Headers/runtime.h中查看。但是为了代码的可读性,以及避免少出错,还是优先使用OC给我们封装的上层方法。

消息转发

OC通过封装Runtime中的一些方法,在NSObject中给我们提供了一系列方法,可以在运行时灵活地处理那些在定义类时还没有给到的方法。这里分3个阶段来给开发者处理找不到的方法:

  1. 首先会调用下面其中之一的方法来询问是否给对象的类添加了实例/类方法,如果返回YES,会重新给对象发一次消息,后面的转发流程就不走了。用@dynamic实现给属性动态添加getter/setter方法就可以在此处,通过class_addMethod来添加,
1
2
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel
  1. 如果上面的方法返回了NO,就会接着执行下面的方法,找到新的消息接收者。返回值是新的receiver
1
- (id)forwardingTargetForSelector:(SEL)aSelector;
  1. 如果上面的方法返回了nil,就会走到下面的方法,将整个消息完整的转发出去。在NSInvocation被创建之前,需要一个NSMethodSignature,所以methodSignatureForSelector会被调用。如果该方法是当前对象没有直接实现的,或者需要它的代理也能去处理,则需要重写这个方法,给一个新的NSMethodSignature。多路代理正是这样实现的。
1
- (void)forwardInvocation:(NSInvocation *)anInvocation;

经过了上面3个阶段依旧没有找到消息要怎么被处理,就抛错了。上面的过程可以用图表示如下:

参考

[1] https://onevcat.com/2012/04/objective-c-runtime/

[2] https://blog.csdn.net/hou_manager/article/details/79373616

[3]《Effective Objective-C 2.0》

本文作者:意林
本文链接:http://www.shinancao.cn/2019/06/01/iOS-Runtime-1
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!