什么是runtime
官方描述:
The Objective-C language defers as many decisions as it can from compile time and link time to runtime.
“尽量将决定放到运行的时候,而不是在编译和链接过程”
runtime是一个C语言库,包含了很多底层的纯C语言API。 平时编写的OC代码中,程序运行,其实最终都是转成了runtime的C语言代码,runtime算是OC的幕后工作者 。
- 特点
OC与其他语言不同的一点就是,函数调用采用了消息转发
的机制,但直到程序运行之前,消息都没有与任何方法绑定起来。只有在真正运行的时候,才会根据函数的名字来,确定该调用的函数。
runtime 是有个两个版本的:
在Objective-C 1.0
使用的是legacy,在2.0
使用的是modern。
现在一般来说runtime都是指modern。
1. isa指针
首先要了解它底层的一些常用数据结构,比如isa指针。
当创建一个新对象时,会为它分配一段内存,该对象的实例变量也会被初始化。第一个变量就是一个指向它的类的指针(isa)。
通过isa指针,一个对象可以访问它的类,并通过它的类来访问所有父类。
- 一个实例对象,在runtime中用结构体表示
// 描述类中的一个方法
typedef struct objc_method *Method;
// 实例变量
typedef struct objc_ivar *Ivar;
// 类别Category
typedef struct objc_category *Category;
// 类中声明的属性
typedef struct objc_property *objc_property_t;
查看runtime源码可以看到关于isa结构。
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
ISA_BITFIELD; // defined in isa.h
};
};
下面的代码对isa_t
中的结构体进行了位域声明,地址从nonpointer
起到extra_rc
结束,从低到高进行排列。位域也是对结构体内存布局进行了一个声明,通过下面的结构体成员变量可以直接操作某个地址。位域总共占8字节,所有的位域加在一起正好是64位。
小提示:union
中bits
可以操作整个内存区,而位域只能操作对应的位。
define ISA_BITFIELD \
uintptr_t nonpointer : 1; //指针是否优化过 \
uintptr_t has_assoc : 1; //是否有设置过关联对象,如果没有,释放时会更快 \
uintptr_t has_cxx_dtor : 1; //是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快 \
uintptr_t shiftcls : 33; //存储着Class、Meta-Class对象的内存地址信息 \
uintptr_t magic : 6; //用于在调试时分辨对象是否未完成初始化 \
uintptr_t weakly_referenced : 1; //是否有被弱引用指向过,如果没有,释放时会更快 \
uintptr_t deallocating : 1; //对象是否正在释放 \
uintptr_t has_sidetable_rc : 1; //引用计数器是否过大无法存储在isa中 \
uintptr_t extra_rc : 19 //里面存储的值是引用计数器减1
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
- nonpointer
0:代表普通的指针,存储着Class、Meta-Class
对象的内存地址。
1:代表优化过,使用位域存储更多的信息。- has_assoc
是否有设置过关联对象。如果没有,释放时会更快。- has_cxx_dtor
是否有C++的析构函数.cxx_destruct
如果没有,释放时会更快。- shiftcls
存储着Class、Meta-Class
对象的内存地址信息- magic
用于在调试时,分辨对象是否未完成初始化- weakly_referenced
是否有被弱引用指向过。如果没有,释放时会更快- deallocating
对象是否正在释放- extra_rc
里面存储的值是引用计数器减1- has_sidetable_rc
引用计数器是否过大无法存储在isa中
如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
2. class结构
结构体
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {
// Class ISA;
Class superclass; // 父类
cache_t cache; //方法缓存
class_data_bits_t bits; // 用于获取具体的类的信息
}
查看源码(只保留了主要代码)
- class_rw_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;
}
其中的methods、properties、protocols
是二维数组,是可读可写的,包含了类的初始内容、分类的内容。
- method_array_t
class method_array_t :
public list_array_tt<method_t, method_list_t>
{
typedef list_array_tt<method_t, method_list_t> Super;
public:
method_list_t **beginCategoryMethodLists() {
return beginLists();
}
method_list_t **endCategoryMethodLists(Class cls);
method_array_t duplicate() {
return Super::duplicate<method_array_t>();
}
};
方法列表 中存放着很多一维数组method_list_t,而每一个method_list_t中存放着method_t。method_t中是对应方法的imp指针、名字、类型等方法信息。
- method_t
struct method_t {
SEL name; //函数名
const char *types; //编码(返回值类型,参数类型)
MethodListIMP 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; }
};
};
IMP
:代表函数的具体实现
SEL
:代表方法、函数名,一般叫做选择器。
types
:包含了函数返回值、参数编码的字符串
关于SEL:
可以通过@selector()
和sel_registerName()
获得
可以通过sel_getName()
和NSStringFromSelector()
转成字符串
不同类中相同名字的方法,所对应的方法选择器是相同的。即,不同类的相同SEL是同一个对象。
- class_ro_t
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;//instance对象占用的内存空间
#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_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties
是一维数组,是只读的,包含了类的初始内容
3. Type Encoding
iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码。比如:
+(int)testWithNum:(int)num{
return num;
}
上面的方法可以用 i20@0:8i16
来表示:
i表示返回值是int类型,20是参数总共20字节
@表示第一个参数是id类型,0表示第一个参数从第0个字节开始
:表示第二个参数是SEL类型。8表示第二个参数从第8个字节开始。
i表示第三个参数是int类型,16表示第三个参数从第16个字节开始
第三个参数从第16个字节开始,是Int类型,占用4字节。总共20字节
4. 方法缓存
用散列表来缓存曾经调用过的方法,可以提高方法的查找速度。
结构体 cache_t
struct cache_t {
struct bucket_t *_buckets; // 散列表
mask_t _mask; //散列表的长度 -1
mask_t _occupied; //已经缓存的方法数量
}
// 其中的 散列表
struct bucket_t {
MethodCacheIMP _imp; //函数的内存地址
cache_key_t _key; //SEL作为Key
}
- cache_t中如何查找方法
// 散列表中查找方法缓存
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
mask_t begin = cache_hash(k, m);
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
其中,根据key和散列表长度减1 mask 计算出下标 key & mask,取出的值如果key和当初传进来的Key相同,就说明找到了。否则,就不是自己要找的方法,就有了hash冲突,把i的值加1,继续计算。如下代码:
// 计算下标
static inline mask_t cache_hash(cache_key_t key, mask_t mask)
{
return (mask_t)(key & mask);
}
//hash冲突的时候
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
- cache_t的扩容
当方法缓存太多的时候,超过了容量的3/4s时候,就需要扩容了。扩容是,把原来的容量增加为2倍。
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
...
if (cache->isConstantEmptyCache()) {
// Cache is read-only. Replace it.
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// Cache is less than 3/4 full. Use it as-is.
}
else {
// 来到这里说明,超过了3/4,需要扩容
cache->expand();
}
...
}
// 扩容
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
// cache_t的扩容
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
uint32_t oldCapacity = capacity();
// 扩容为原来的2倍
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
if ((uint32_t)(mask_t)newCapacity != newCapacity) {
// mask overflow - can't grow further
// fixme this wastes one bit of mask
newCapacity = oldCapacity;
}
reallocate(oldCapacity, newCapacity);
}
5. 消息转发机制
OC方法调用的本质是,消息转发机制。比如:
对象instance 调用dotest方法[instance1 dotest];
底层会转化为:objc_msgSend(instance1, sel_registerName("dotest"));
OC中方法的调用,其实都是转换为objc_msgSend
函数的调用。
实例对象中存放着 isa 指针以及实例变量。由 isa 指针找到实例对象所属的类对象 (类也是对象)。类中存放着实例方法列表。在这个列表中,方法的保存形式是SEL
作 key,IMP
作value。
这是在编译时根据方法名,生成唯一标识
SEL
,IMP
其实就是函数指针 ,指向最终的函数实现。
整个 Runtime 的核心就是 objc_msgSend(receiver, @selector (message))
函数,通过给类发送 SEL
以传递消息,找到匹配的IMP
再获取最终的实现。
执行流程可以分为3大阶段:消息发送->动态方法解析->消息转发
- 消息发送阶段:
首先判断receiver是否为空
如果不为空,从receiverClass的缓存中,查找方法。(找到了就调用)
如果没找到,就从receiverClass的class_rw_t
中查找方法。(找到就调用,并缓存)
如果没找到,就去receiverClassd的父类的缓存中查找。
如果没找到,就从父类的class_rw_t
中查找方法。
如果没找到,就看是否还有父类,有就继续查父类的缓存,方法列表。
由上述知道,去查缓存、方法列表、查父类等这些操作之后,都没有找到这个方法的实现,这时如果后面不做处理,必然抛出异常:
...due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[xxx xxxx]: unrecognized selector sent to instance 0x100f436c0’
如果没有父类,说明消息发送阶段结束,那么就进入第二阶段,动态方法解析阶段。
- 动态方法解析:
在此,可以给未找到的方法,动态绑定方法实现。或者给某个方法重定向。
源码:
// 动态方法解析
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) { //如果不是元类对象
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else { // 是元类对象
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
其中的resolveClassMethod
和resolveInstanceMethod
默认是返回NO
+ (BOOL)resolveClassMethod:(SEL)sel {
return NO;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
- 在动态解析阶段,可以重写
resolveInstanceMethod
并添加方法的实现。
假如,没有找到run
这个方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector( run )) {
// 获取其他方法 实例方法 或类方法,作为run的实现
Method method = class_getInstanceMethod(self, @selector(test));
// 动态添加test方法的实现
class_addMethod(self, sel,
method_getImplementation(method),
method_getTypeEncoding(method));
// 返回YES代表有动态添加方法 其实这里返回NO,也是可以的,返回YES只是增加了一些打印
return NO;
}
return [super resolveInstanceMethod:sel];
}
上面的代码,就相当于,调用run的时候,实际上调用的是test。
如果前面消息发送 和动态解析阶段,都没有对方法进行处理,我们还有最后一个阶段。如下
- 消息转发
____forwarding___
这个函数中,交代了消息转发的逻辑。但是不开源。
先判断forwardingTargetForSelector
的返回值。有,就向这个返回值发送消息,让它调用方法。
如果返回nil
,就调用methodSignatureForSelector
方法,有就调用forwardInvocation
。
其中的参数是一个
NSInvocation
对象,并将消息全部属性记录下来。NSInvocation
对象包括了Selector、target
以及其他参数。其中的实现仅仅是改变了target
指向,使消息保证能够调用。
倘若发现本类无法处理,则继续查找父类,直至 NSObject
。如果methodSignatureForSelector
方法返回nil
,就调用doesNotRecognizeSelector:
方法。
应用举例:
场景1:
类Person只定义了方法run但没有实现,另外有类Car实现了方法run。
现在Person中,重写forwardingTargetForSelector
返回Car对象
// 消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(run)) {
return [[Car alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
这时,当person实例调用run方法时,会变成car实例调用run方法。
证明forwardingTargetForSelector
返回值不为空的话,就向这个返回值发送消息,也就是objc_msgSend(返回值, SEL)
。
场景2:
如果前面的forwardingTargetForSelector
返回为空。底层就会调用 methodSignatureForSelector
获取方法签名后,再调用 forwardInvocation
。
因此:可以重写这两个方法:
// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(run)) {
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
[anInvocation invokeWithTarget:[[Car alloc] init]];
}
这样,依然可以调用到car的run方法。
NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
anInvocation.target 方法调用者
anInvocation.selector 方法名
[anInvocation getArgument:NULL atIndex:0]
补充:
1、消息转发的forwardingTargetForSelector、methodSignatureForSelector、forwardInvocation
不仅支持实例方法,还支持类方法。不过系统没有提示,需要写成实例方法,然后把前面的-改成+即可。
+(IMP)instanceMethodForSelector:(SEL)aSelector{
}
-(IMP)methodForSelector:(SEL)aSelector{
}
2、只能向运行时动态创建的类添加ivars
,不能向已经存在的类添加ivars
。 这是因为在编译时,只读结构体class_ro_t
就被确定,在运行时不可更改。
网友评论