Objective-C是一门动态性比较强的编程语言,跟C、C++等语言有着很大的不同
Objective-C的动态性是由Runtime API来支撑的
Runtime API提供的接口基本都是C语言的,源码由C\C++\汇编语言编写
要想学习Runtime,首先要了解它底层的一些常用数据结构,比如isa指针
在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址
从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息
位域详解
-
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的类的属性中
注意:
has_assoc是否是设置过关联对象,而不是此时是否有关联对象。如果设置过,即使取消了,那么也算设置过。weakly_referenced 同理。
class结构
class_rw_t
class_ro_t
类的初始信息,不包含分类信息。
最开始是没有rw的,运行的时候才创建rw,把ro的内容和分类的信息放在rw里。 源码(汉子注释部分):
static Class realizeClassWithoutSwift(Class cls)
{
runtimeLock.assertLocked();
const class_ro_t *ro;
class_rw_t *rw;
Class supercls;
Class metacls;
bool isMeta;
if (!cls) return nil;
if (cls->isRealized()) return cls;
assert(cls == remapClass(cls));
// fixme verify class is not in an un-dlopened part of the shared cache?
ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
// rw已经分配
rw = cls->data();
ro = cls->data()->ro;
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
// 分配rw 并进行初始赋值
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
}
isMeta = ro->flags & RO_META;
rw->version = isMeta ? 7 : 0; // old runtime went up to 6
// Choose an index for this class.
// Sets cls->instancesRequireRawIsa if indexes no more indexes are available
cls->chooseClassArrayIndex();
if (PrintConnecting) {
_objc_inform("CLASS: realizing class '%s'%s %p %p #%u %s%s",
cls->nameForLogging(), isMeta ? " (meta)" : "",
(void*)cls, ro, cls->classArrayIndex(),
cls->isSwiftStable() ? "(swift)" : "",
cls->isSwiftLegacy() ? "(pre-stable swift)" : "");
}
// Realize superclass and metaclass, if they aren't already.
// This needs to be done after RW_REALIZED is set above, for root classes.
// This needs to be done after class index is chosen, for root metaclasses.
// This assumes that none of those classes have Swift contents,
// or that Swift's initializers have already been called.
// fixme that assumption will be wrong if we add support
// for ObjC subclasses of Swift classes.
supercls = realizeClassWithoutSwift(remapClass(cls->superclass));
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()));
#if SUPPORT_NONPOINTER_ISA
// Disable non-pointer isa for some classes and/or platforms.
// Set instancesRequireRawIsa.
bool instancesRequireRawIsa = cls->instancesRequireRawIsa();
bool rawIsaIsInherited = false;
static bool hackedDispatch = false;
if (DisableNonpointerIsa) {
// Non-pointer isa disabled by environment or app SDK version
instancesRequireRawIsa = true;
}
else if (!hackedDispatch && !(ro->flags & RO_META) &&
0 == strcmp(ro->name, "OS_object"))
{
// hack for libdispatch et al - isa also acts as vtable pointer
hackedDispatch = true;
instancesRequireRawIsa = true;
}
else if (supercls && supercls->superclass &&
supercls->instancesRequireRawIsa())
{
// This is also propagated by addSubclass()
// but nonpointer isa setup needs it earlier.
// Special case: instancesRequireRawIsa does not propagate
// from root class to root metaclass
instancesRequireRawIsa = true;
rawIsaIsInherited = true;
}
if (instancesRequireRawIsa) {
cls->setInstancesRequireRawIsa(rawIsaIsInherited);
}
// SUPPORT_NONPOINTER_ISA
#endif
// Update superclass and metaclass in case of remapping
cls->superclass = supercls;
cls->initClassIsa(metacls);
// Reconcile instance variable offsets / layout.
// This may reallocate class_ro_t, updating our ro variable.
if (supercls && !isMeta) reconcileInstanceVariables(cls, supercls, ro);
// Set fastInstanceSize if it wasn't set already.
cls->setInstanceSize(ro->instanceSize);
// Copy some flags from ro to rw
if (ro->flags & RO_HAS_CXX_STRUCTORS) {
cls->setHasCxxDtor();
if (! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) {
cls->setHasCxxCtor();
}
}
// Propagate the associated objects forbidden flag from ro or from
// the superclass.
if ((ro->flags & RO_FORBIDS_ASSOCIATED_OBJECTS) ||
(supercls && supercls->forbidsAssociatedObjects()))
{
rw->flags |= RW_FORBIDS_ASSOCIATED_OBJECTS;
}
// Connect this class to its superclass's subclass lists
if (supercls) {
addSubclass(supercls, cls);
} else {
addRootClass(cls);
}
// Attach categories
methodizeClass(cls);
return cls;
}
method_t
其他:C语言的字符串,用%s.
注意:不同类中相同名字的方法,所对应的方法选择器是相同的。
types
// "i24@0:8i16f20"
// 0id 8SEL 16int 20float == 24
- (int)test:(int)age height:(float)height;
最前面代表返回值,@是隐式参数self,:是隐式参数 _cmd
i24 @0 :8 i16 f20意思是,这个函数,返回值是int类型,参数总共24字节。self参数从0开始,_cmd参数从8开始,从16开始的参数是int类型,从20开始的参数是float类型。
cache_t
因为class_rw_t的 method_array_t是二维数组,如果通过isa,superClass,每次都要通过遍历查看方法,效率太低。所以做了缓存。
每当调用方法的时候,会先去cache中查找是否有缓存的方法,如果没有缓存,在去类对象方法缓存中找,再去列表中查找,以此类推直到找到方法之后,就会将方法直接存储在cache中,下一次在调用这个方法的时候,就会在类对象的cache里面找到这个方法,直接调用了。
缓存是通过 散列表(哈希)实现的。
从源码中可以看出bucket_t中存储着SEL和_imp,通过key->value的形式,以SEL为key,函数实现的内存地址 _imp为value来存储方法。
通过一张图来展示一下cache_t的结构。
上述bucket_t列表我们称之为散列表(哈希表) 散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
那么apple如何在散列表中快速并且准确的找到对应的key以及函数实现呢?这就需要我们通过源码来看一下apple的散列函数是如何设计的。
散列函数及散列表原理
首先来看一下存储的源码,主要查看几个函数,关键代码都有注释,不在赘述。
源码位于 objc-cache.mm 中:
cache_fill 及 cache_fill_nolock 函数
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
mutex_locker_t lock(cacheUpdateLock);
cache_fill_nolock(cls, sel, imp, receiver);
#else
_collecting_in_critical();
return;
#endif
}
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
// 如果没有initialize直接return
if (!cls->isInitialized()) return;
// 确保线程安全,没有其他线程添加缓存
if (cache_getImp(cls, sel)) return;
// 通过类对象获取到cache
cache_t *cache = getCache(cls);
// 将SEL包装成Key
cache_key_t key = getKey(sel);
// 占用空间+1
mask_t newOccupied = cache->occupied() + 1;
// 获取缓存列表的缓存能力,能存储多少个键值对
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {
// 如果为空的,则创建空间,这里创建的空间为4个。
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// 如果所占用的空间占总数的3/4一下,则继续使用现在的空间
}
else {
// 如果占用空间超过3/4则扩展空间
cache->expand();
}
// 通过key查找合适的存储空间。
bucket_t *bucket = cache->find(key, receiver);
// 如果key==0则说明之前未存储过这个key,占用空间+1
if (bucket->key() == 0) cache->incrementOccupied();
// 存储key,imp
bucket->set(key, imp);
}
reallocate函数负责分配散列表空间
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
// 旧的散列表能否被释放
bool freeOld = canBeFreed();
// 获取旧的散列表
bucket_t *oldBuckets = buckets();
// 通过新的空间需求量创建新的散列表
bucket_t *newBuckets = allocateBuckets(newCapacity);
assert(newCapacity > 0);
assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
// 设置Buckets和Mash,Mask的值为散列表长度-1
setBucketsAndMask(newBuckets, newCapacity - 1);
// 释放旧的散列表
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
}
散列表初始长度是4
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
当散列表的空间被占用超过3/4的时候,散列表会调用expand ()函数进行扩展
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
// 获取旧的散列表的存储空间
uint32_t oldCapacity = capacity();
// 将旧的散列表存储空间扩容至两倍
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
// 为新的存储空间赋值
if ((uint32_t)(mask_t)newCapacity != newCapacity) {
newCapacity = oldCapacity;
}
// 调用reallocate函数,重新创建存储空间
reallocate(oldCapacity, newCapacity);
}
扩容时,会扩大到原来的两倍;
看一下散列表中如何快速的通过key找到相应的bucket呢?我们来到find函数内部
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
// 获取散列表
bucket_t *b = buckets();
// 获取mask
mask_t m = mask();
// 通过key找到key在散列表中存储的下标
mask_t begin = cache_hash(k, m);
// 将下标赋值给i
mask_t i = begin;
// 如果下标i中存储的bucket的key==0说明当前没有存储相应的key,将b[i]返回出去进行存储
// 如果下标i中存储的bucket的key==k,说明当前空间内已经存储了相应key,将b[i]返回出去进行存储
do {
if (b[i].key() == 0 || b[i].key() == k) {
// 如果满足条件则直接reutrn出去
return &b[i];
}
// 如果走到这里说明上面不满足,那么会往前移动一个空间重新进行判定,直到找到或者转了一圈,又到begin
} 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);
}
可以发现cache_hash (k, m)函数内部仅仅是进行了key & mask的按位与运算,得到下标即存储在相应的位置上。
static inline mask_t cache_hash(cache_key_t key, mask_t mask)
{
return (mask_t)(key & mask);
}
通过上面的分析我们知道_mask的值是散列表的长度减一,那么任何数通过与_mask进行按位与运算之后获得的值都会小于等于_mask,因此不会出现数组溢出的情况。
当第一次使用方法时,消息机制通过isa找到方法之后,会对方法以SEL为keyIMP为value的方式缓存在cache的_buckets中,当第一次存储的时候,会创建具有4个空间的散列表,并将_mask的值置为散列表的长度减一,之后通过SEL & mask计算出方法存储的下标值,并将方法存储在散列表中。举个例子,如果计算出下标值为3,那么就将方法直接存储在下标为3的空间中,前面的空间会留空。
当散列表中存储的方法占据散列表长度超过3/4的时候,散列表会进行扩容操作,将创建一个新的散列表并且空间扩容至原来空间的两倍,并重置_mask的值,最后释放旧的散列表,此时再有方法要进行缓存的话,就需要重新通过SEL & mask计算出下标值之后在按照下标进行存储了。
如果一个类中方法很多,其中很可能会出现多个方法的SEL & mask得到的值为同一个下标值,那么会调用cache_next函数往下标值-1位去进行存储,如果下标值-1位空间中有存储方法,并且key不与要存储的key相同,那么再到前面一位进行比较,直到找到一位空间没有存储方法或者key与要存储的key相同为止,如果到下标0的话就会到下标为_mask的空间也就是最大空间处进行比较,依次进行,直到找到合适的下标,或者回到开始的地方。
当要查找方法时,并不需要遍历散列表,同样通过SEL & mask计算出下标值,直接去下标值的空间取值即可,同上,如果下标值中存储的key与要查找的key不相同,就去前面一位查找。这样虽然占用了少量控件,但是大大节省了时间,也就是说其实apple是使用空间换取了存取的时间。
方法缓存实例
demo如下:
MJClassInfo.h 是仿写的源码,方便使用。
//
// MJClassInfo.h
// TestClass
//
// Created by MJ Lee on 2018/3/8.
// Copyright © 2018年 MJ Lee. All rights reserved.
//
#import <Foundation/Foundation.h>
#ifndef MJClassInfo_h
#define MJClassInfo_h
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# endif
#if __LP64__
typedef uint32_t mask_t;
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;
#if __arm__ || __x86_64__ || __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
#else
#error unknown architecture
#endif
struct bucket_t {
cache_key_t _key;
IMP _imp;
};
struct cache_t {
bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
IMP imp(SEL selector)
{
mask_t begin = _mask & (long long)selector;
mask_t i = begin;
do {
if (_buckets[i]._key == 0 || _buckets[i]._key == (long long)selector) {
return _buckets[i]._imp;
}
} while ((i = cache_next(i, _mask)) != begin);
return NULL;
}
};
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
};
struct method_t {
SEL name;
const char *types;
IMP imp;
};
struct method_list_t : entsize_list_tt {
method_t first;
};
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
uint32_t alignment_raw;
uint32_t size;
};
struct ivar_list_t : entsize_list_tt {
ivar_t first;
};
struct property_t {
const char *name;
const char *attributes;
};
struct property_list_t : entsize_list_tt {
property_t first;
};
struct chained_property_list {
chained_property_list *next;
uint32_t count;
property_t list[0];
};
typedef uintptr_t protocol_ref_t;
struct protocol_list_t {
uintptr_t count;
protocol_ref_t list[0];
};
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;
};
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_list_t * methods; // 方法列表
property_list_t *properties; // 属性列表
const protocol_list_t * protocols; // 协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
};
#define FAST_DATA_MASK 0x00007ffffffffff8UL
struct class_data_bits_t {
uintptr_t bits;
public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
};
/* OC对象 */
struct mj_objc_object {
void *isa;
};
/* 类对象 */
struct mj_objc_class : mj_objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;
public:
class_rw_t* data() {
return bits.data();
}
mj_objc_class* metaClass() {
return (mj_objc_class *)((long long)isa & ISA_MASK);
}
};
#endif /* MJClassInfo_h */
MJPerson、MJStudent、MJGoodStudent
@implementation MJPerson
- (void)personTest
{
NSLog(@"%s", __func__);
}
@end
@implementation MJStudent
- (void)studentTest
{
NSLog(@"%s", __func__);
}
@end
@implementation MJGoodStudent
- (void)goodStudentTest
{
NSLog(@"%s", __func__);
}
@end
main
#import <Foundation/Foundation.h>
#import "MJGoodStudent.h"
#import "MJClassInfo.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJGoodStudent *goodStudent = [[MJGoodStudent alloc] init];
mj_objc_class *goodStudentClass = (__bridge mj_objc_class *)[MJGoodStudent class];
mj_objc_class *personClass1 = (__bridge mj_objc_class *)[[MJGoodStudent superclass] superclass];
mj_objc_class *personClass2 = (__bridge mj_objc_class *)[MJPerson class];
[goodStudent goodStudentTest];
[goodStudent studentTest];
[goodStudent personTest];
[goodStudent goodStudentTest];
[goodStudent studentTest];
NSLog(@"--------------------------");
//获取缓存里的方法
cache_t cache = goodStudentClass->cache;
NSLog(@"%s %p", @selector(personTest), cache.imp(@selector(personTest)));
NSLog(@"%s %p", @selector(studentTest), cache.imp(@selector(studentTest)));
NSLog(@"%s %p", @selector(goodStudentTest), cache.imp(@selector(goodStudentTest)));
// bucket_t *buckets = cache._buckets;
//
// bucket_t bucket = buckets[(long long)@selector(studentTest) & cache._mask];
// NSLog(@"%s %p", bucket._key, bucket._imp);
//遍历方式获取
// for (int i = 0; i <= cache._mask; i++) {
// bucket_t bucket = buckets[i];
// NSLog(@"%s %p", bucket._key, bucket._imp);
// }
NSLog(@"123");
}
return 0;
}
结果:
- 断点断在第一个 [goodStudent goodStudentTest]前; 会发现 goodStudentClass的缓存里只有一个方法,是init.
- 断点断在 [goodStudent studentTest]后面,会发现,goodStudentClass的缓存有三个方法。goodStudent通过isa指针或者superclass指针找到方法后,会缓存在自己的类对象的cache_t里,而不是缓存在父类类对象的cache_t里。
- 断点断在 [goodStudent personTest]后面,会发现:
- goodStudent类对象的缓存散列表长度不够,重新分配长度=旧长度*2,此时散列表会清空,注意,清空后并不会把原来的缓存重新放入。清空后原来的缓存就没了。
- personClass1和personClass2地址一样。而且自始至终该类对象的缓存里都没有任何方法,所以印证了上面所说,并不会缓存在父类的类对象的cache_t里。
- 后面就是获取缓存方法。