官方描述: 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与其余语言不一样的一点就是,函数调用采用了消息转发
的机制,但直到程序运行以前,消息都没有与任何方法绑定起来。只有在真正运行的时候,才会根据函数的名字来,肯定该调用的函数。markdown
runtime 是有个两个版本的:数据结构
在Objective-C 1.0
使用的是legacy,在2.0
使用的是modern。 如今通常来讲runtime都是指modern。less
首先要了解它底层的一些经常使用数据结构,好比isa指针。ide
当建立一个新对象时,会为它分配一段内存,该对象的实例变量也会被初始化。第一个变量就是一个指向它的类的指针(isa)。 经过isa指针,一个对象能够访问它的类,并经过它的类来访问全部父类。函数
// 描述类中的一个方法
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
能够操做整个内存区,而位域只能操做对应的位。ui
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的类的属性中
结构体
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; // 用于获取具体的类的信息
}
复制代码
查看源码(只保留了主要代码)
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
是二维数组,是可读可写的,包含了类的初始内容、分类的内容。
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指针、名字、类型等方法信息。
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是同一个对象。
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
是一维数组,是只读的,包含了类的初始内容
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字节
用散列表来缓存曾经调用过的方法,能够提升方法的查找速度。 结构体 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
}
复制代码
// 散列表中查找方法缓存
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;
}
复制代码
当方法缓存太多的时候,超过了容量的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);
}
复制代码
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:
方法。
应用举例:
类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)
。
若是前面的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]
补充: 一、消息转发的forwardingTargetForSelector、methodSignatureForSelector、forwardInvocation
不只支持实例方法,还支持类方法。不过系统没有提示,须要写成实例方法,而后把前面的-改为+便可。
+(IMP)instanceMethodForSelector:(SEL)aSelector{
}
-(IMP)methodForSelector:(SEL)aSelector{
}
复制代码
二、只能向运行时动态建立的类添加ivars
,不能向已经存在的类添加ivars
。 这是由于在编译时,只读结构体class_ro_t
就被肯定,在运行时不可更改。