引言
Objective-C是一门动态语言,在OC中方法的调用在编译期时并不能真正决定调用的是哪个方法。只有在真正运行时才会根据方法的名称找到对应的函数调用。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime library) 来执行编译后的代码。而Objective-C语言的动态特性正是基于runtime
。
目前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;
-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;
继续在源码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
objc_object
里面又包含了一个isa
指针,类型为isa_t
的联合体(union)联合体和结构体的区别。更多关于isa_t
的内容可以参考Objective-C 引用计数原理。根据 isa
指针我们能够找到对象所属的类,但苹果官方并不推荐我们使用isa
来判断对象的类型。
Class
Class
在源码是一个指向 objc_class
结构体的指针:
typedef struct objc_class *Class;
在源码中结构体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
}
要注意到结构体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;
// ....
}
cache_t
中又包含了一个bucket_t
的结构体和两个unsigned int
类型的_mask
、_occupied
。_mask
,_occupied
分别表示分配用来缓存bucket的总数和已被分配的数量。
bucket_t
结构:
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
// ...
}
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);
}
//...
}
当我们用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
其中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;
}
};
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; }
};
};
objc_method
存储了方法名,方法类型和方法实现:
SEL
是方法选择器(method selector
)在OC中的类型,一个方法选择器(method selector
)就是C的字符串通过OC的runtime
映射得到。- types是Type Encoding类型编码,更多参考Type Encoding
- IMP是一个函数指针,指向的是函数的具体实现。
runtime
中消息传递和转发的目的就是为了找到IMP。
typedef struct objc_selector *SEL;
SEL
是方法选择器selector
的类型,有两种方式:@selector()
或者是sel_registerName()
把C的字符串映射成在method selector
。不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。
IMP
指向方法实现的首地址指针:
id (*IMP)(id, SEL, ...)
这个数据类型是一个指向实现该方法的函数开始的指针。参数都包含 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;
}
};
Ivar中包含了实例变量的名称、类型等。我们可以通过class_copyIvarList(Class cls, unsigned int *outCount)
来获取类的实例变量(包括由@property的属性,但会添加_
前缀)。
objc_property_t
objc_property_t
是一个指向property_t
结构体的指针。
typedef struct property_t *objc_property_t;
我们可以通过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);
};
Category
为现有的类提供了扩展的途径让我们可以在类原有的基础上,通过Category
给类扩展添加上实例方法、类方法、协议等。
[参考材料]: