深刻淺出 Runtime(二):數據結構

Runtime 系列文章

深刻淺出 Runtime(一):初識
深刻淺出 Runtime(二):數據結構
深刻淺出 Runtime(三):消息機制
深刻淺出 Runtime(四):super 的本質
深刻淺出 Runtime(五):相關面試題html

1. objc_object

Objective-C的面向對象都是基於C/C++的數據結構——結構體實現的。
咱們平時使用的全部對象都是id類型,id類型對象對應到runtime中,就是objc_object結構體。面試

// A pointer to an instance of a class.
typedef struct objc_object *id;
複製代碼
struct objc_object {
private:
    isa_t isa;
    /*... isa操做相關 弱引用相關 關聯對象相關 內存管理相關 ... */
};
複製代碼

2. objc_class

Class指針用來指向一個 Objective-C 的類,它是objc_class結構體類型,因此class、meta-class底層結構都是objc_class結構體,objc_class繼承自objc_object,因此它也有isa指針,它也是對象。數組

// An opaque type that represents an Objective-C class.
typedef struct objc_class *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();
    }
};
複製代碼

2.1 class_data_bits_t

  • class_data_bits_t主要是對class_rw_t的封裝,能夠經過bits & FAST_DATA_MASK得到class_rw_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);
    }
};
複製代碼
  • class_rw_t表明了類相關的讀寫信息,它是對class_ro_t的封裝;
  • class_rw_t中主要存儲着類的方法列表、屬性列表、協議列表等;
  • class_rw_t裏面的methodspropertiesprotocols都繼承於list_array_tt二維數組,是可讀可寫的,包含了類的初始內容、分類的內容。
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;
};
複製代碼
  • class_ro_t表明了類相關的只讀信息;
  • class_ro_t中主要存儲着類的成員變量列表、類名等;
  • class_ro_t裏面的baseMethodListbaseProtocolsivarsbaseProperties是一維數組,是隻讀的,包含了類的初始內容;
  • 一開始類的信息都存放在class_ro_t裏,當程序運行時,通過一系列的函數調用棧,在realizeClass()函數中,將class_ro_t裏的東西和分類的東西合併起來放到class_rw_t裏,並讓bits指向class_rw_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;
    }
};
複製代碼
  • method_array_tmethod_list_t

method_array_t 與 method_list_t

2.2 cache_t

  • 用於快速查找方法執行函數;
  • 是可增量擴展的哈希表結構,用哈希表來緩存曾經使用過的方法,能夠提升方法的查找速度(空間換時間:犧牲內存空間來換取執行效率);
  • 是局部性原理的最佳應用(好比一些方法調用的頻率高,存放到cache中,下一次調用這些方法的命中率就會更高些);
  • hash 函數式爲 f(@selector()) = index, @selector() & _mask
  • 當咱們調用過一個方法後,runtime會將這個方法緩存到cache中,下次再調用此方法的時候,runtime會優先去cache中查找。
struct cache_t {
    struct bucket_t *_buckets;  // 哈希表
    mask_t _mask;               // 哈希表的長度 - 1
    mask_t _occupied;           // 已經緩存的方法數量
};

struct bucket_t {
private:
    cache_key_t _key;  // SEL
    IMP _imp;          // IMP 函數的內存地址
};
複製代碼

2.2.1 緩存查找流程

//objc-cache.mm(objc4)
bucket_t * cache_t::find(cache_key_t k, id receiver)  // 根據 k 即 @selector 進行查找
{
    assert(k != 0);

    bucket_t *b = buckets();          // 獲取_buckets
    mask_t m = mask();                // 獲取_mask
    mask_t begin = cache_hash(k, m);  // 計算起始索引
    mask_t i = begin;
    do {
        // 根據索引 i 從 _buckets 哈希表中取值
        // 若是取出來的 bucket_t 的 _key = 0,說明在索引的位置上尚未緩存過方法,返回該 bucket_t,停止緩存查詢,用於 cache_fill_nolock() 函數
        // 若是取出來的 bucket_t 的 _key = k,說明查詢成功,返回該 bucket_t
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
      // 在 __arm64__ 下將索引 i -1,繼續查找,反向遍歷 _buckets 哈希表
      // 直到 i 指向首個元素即索引 = 0 時,將 mask 賦值給 i,使其指向哈希表最後一個元素,繼續反向遍歷
      // 若是此時尚未找到 k 對應的 bucket_t ,或者是空的 bucket_t ,則循環結束,查找失敗,調用 bad_cache() 函數
      // 接下來去類對象中 class_rw_t 中的 methods 查找
    } 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);
}


static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}
static inline mask_t cache_next(mask_t i, mask_t mask) {
    // return (i+1) & mask; // __arm__ || __x86_64__ || __i386__
    return i ? i-1 : mask;   // __arm64__
}
複製代碼

2.2.2 緩存添加流程

//objc-cache.mm(objc4)
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();

    // Never cache before +initialize is done
    if (!cls->isInitialized()) return;   // 若是類還未初始化,直接返回

    // Make sure the entry wasn't added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;  // 可能有其它線程搶先將該方法緩存了,因此要檢查一次緩存,若是存在,直接返回

    cache_t *cache = getCache(cls);  // ️取出該 class 的 cache_t
    cache_key_t key = getKey(sel);   // ️根據 sel 得到 _key

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;  // 將 cache_t 的 _occupied 即已經緩存的方法數量 + 1,這裏只是爲了判斷 +1 後緩存容量是否滿
    mask_t capacity = cache->capacity();  // 得到緩存容量 = _mask + 1
    if (cache->isConstantEmptyCache()) {  // 若是緩存是隻讀的,從新申請緩存空間
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);  // 申請新的緩存空間,並釋放舊的
    }
    else if (newOccupied <= capacity / 4 * 3) {  // ️若是當前已經緩存的方法數量 +1 <= 緩存容量的 3/4,就繼續往下操做
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {  // ️若是以上條件不知足,說明緩存已滿,進行緩存擴容
        // Cache is too full. Expand it.
        cache->expand();
    }

    // Scan for the first unused slot and insert there. // 掃描第一個未使用的插槽(bucket_t)並將其插入
    // There is guaranteed to be an empty slot because the // 必然會有一個空的插槽(bucket_t)
    // minimum size is 4 and we resized at 3/4 full. // 由於最小大小是4,咱們調整爲3/4滿
    bucket_t *bucket = cache->find(key, receiver);       // ️調用 find() 函數進行一次緩存查找,必然會獲得一個空的 bucket_t
    if (bucket->key() == 0) cache->incrementOccupied();  // ️若是該 bucket_t 爲空,將 _occupied 即已經緩存的方法數量 + 1
    bucket->set(key, imp);  // ️添加緩存
}

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
}
複製代碼

2.2.3 緩存擴容流程

  • ① 設置新的緩存bucket_t,容量 = 舊的兩倍;
  • ② 設置新的_mask=bucket_t長度 - 1;
  • ③ 釋放舊的緩存(在runtime動態交換方法實現時也會釋放緩存)。
//objc-cache.mm(objc4)
void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    // ️將緩存擴容爲原來的兩倍,若是是首次調用,設置緩存容量的初始值爲 4
    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);  // ️申請新的緩存空間,並釋放舊的
}

enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
};

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    bool freeOld = canBeFreed();  // ️判斷一下緩存是否是空的,若是爲空,就不必釋放空間

    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this

    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}

bool cache_t::canBeFreed()
{
    return !isConstantEmptyCache();
}

bool cache_t::isConstantEmptyCache()
{
    return 
        occupied() == 0  &&  
        buckets() == emptyBucketsForCapacity(capacity(), false);
}
複製代碼

3. isa 指針

  • isa指針用來維護對象和類之間的關係,並確保對象和類可以經過isa指針找到對應的方法、實例變量、屬性、協議等;
  • 在 arm64 架構以前,isa就是一個普通的指針,直接指向objc_class,存儲着ClassMeta-Class對象的內存地址。instance對象的isa指向class對象,class對象的isa指向meta-class對象;
  • 從 arm64 架構開始,對isa進行了優化,變成了一個共用體(union)結構,還使用位域來存儲更多的信息。將 64 位的內存數據分開來存儲着不少的東西,其中的 33 位纔是拿來存儲classmeta-class對象的內存地址信息。要經過位運算將isa的值& ISA_MASK掩碼,才能獲得classmeta-class對象的內存地址。
struct objc_object {
    Class isa;  // 在 arm64 架構以前
};

struct objc_object {
private:
    isa_t isa;  // 在 arm64 架構開始
};

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA

    // extra_rc must be the MSB-most field (so it matches carry/overflow flags)
    // nonpointer must be the LSB (fixme or get rid of it)
    // shiftcls must occupy the same bits that a real class pointer would
    // bits + RC_ONE is equivalent to extra_rc + 1
    // RC_HALF is the high bit of extra_rc (i.e. half of its range)

    // future expansion:
    // uintptr_t fast_rr : 1; // no r/r overrides
    // uintptr_t lock : 2; // lock for atomic property, @synch
    // uintptr_t extraBytes : 1; // allocated with extra bytes

# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL // 用來取出 Class、Meta-Class 對象的內存地址
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;  // 0:表明普通的指針,存儲着 Class、Meta-Class 對象的內存地址
                                          // 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;  // 若是爲1,表明引用計數過大沒法存儲在 isa 中,那麼超出的引用計數會存儲在一個叫 SideTable 結構體的 RefCountMap(引用計數表)哈希表中
        uintptr_t extra_rc          : 19; // 裏面存儲的值是引用計數 retainCount - 1
# define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18) }; }; 複製代碼

3.1 isa 與 superclass 指針指向

isa 與 superclass 指針指向

3.2 類對象(class)與元類對象(meta-class)

  • classmeta-class底層結構都是objc_class結構體,objc_class繼承自objc_object,因此它也有isa指針,因此它也是對象;
  • class中存儲着實例方法、成員變量、屬性、協議等信息, meta-class中存儲着類方法等信息;
  • isa指針和superclass指針的指向(如上圖);
  • 基類的meta-classsuperclass指向基類的class, 決定了一個性質:當咱們調用一個類方法,會經過classisa指針找到meta-class,在meta-class中查找有無該類方法,若是沒有,再經過meta-classsuperclass指針逐級查找父meta-class,一直找到基類的meta-class若是還沒找到該類方法的話,就會去找基類的class中同名的實例方法的實現。

3.3 得到 class 或者 meta-class 的方式

  • 得到 class 有 3 種方式
- (Class)class;
+ (Class)class;
Class object_getClass(id obj);  // 傳參:instance 對象
複製代碼
  • 得到 meta-class 只有 1 種方式
Class object_getClass(id obj);  // 傳參:Class 對象
複製代碼

示例代碼以下緩存

NSObject *object1 = [NSObject alloc] init];
    NSObject *object2 = [NSObject alloc] init];
    // objectClass1 ~ objectClass5 都是 NSObject 的類對象
    Class objectClass1 = [object1 class];
    Class objectClass2 = [object2 class];
    Class objectClass3 = [NSObject class];
    Class objectClass4 = object_getClass(object1);
    Class objectClass5 = object_getClass(object2);  
    // objectMetaClass1 ~ objectMetaClass4 都是 NSObject 的元類對象
    Class objectMetaClass1 = object_getClass([object1 class];    
    Class objectMetaClass2 = object_getClass([NSObject class]);    
    Class objectMetaClass3 = object_getClass(object_getClass(object1));    
    Class objectMetaClass4 = object_getClass(objectClass5);    
複製代碼

方法實現bash

- (Class)class {
    return object_getClass(self);
}

+ (Class)class {
    return self;
}

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
objc_object::getIsa() 
{
    if (!isTaggedPointer()) return ISA();
    ......
}
objc_object::ISA() 
{
    assert(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}
#if __ARM_ARCH_7K__ >= 2
# define SUPPORT_INDEXED_ISA 1
#else
# define SUPPORT_INDEXED_ISA 0
#endif
複製代碼

3.4 爲何要設計 meta-class ?

目的是將實例和類的相關方法列表以及構建信息區分開來,方便各司其職,符合單一職責設計原則。markdown

4. method_t

  • Method就是method_t類型的指針;
  • method_t是對方法/函數的封裝(函數四要素:函數名、返回值、參數、函數體)。
typedef struct method_t *Method;
複製代碼
struct method_t {
    SEL name;  // 方法名
    const char *types;  // 編碼(返回值類型、參數類型)
    IMP imp;   // 方法的地址/實現
};
複製代碼

4.1 SEL

  • SEL 又稱「選擇器」,它是一個指向方法的selector的指針,表明方法/函數名;
  • SEL 維護在一個全局的 Map 中,因此它是全局惟一的,不一樣類中相同名字的方法的 SEL 是相同的。
typedef struct objc_selector *SEL;
複製代碼
  • SEL 能夠經過如下方式得到
SEL sel1 = @selector(selector);
    SEL sel2 = sel_registerName("selector");
    SEL sel3 = NSSelectorFromString(@"selector");
複製代碼
  • SEL 能夠經過如下方式轉換成字符串
char *string1 = sel_getName(sel1);
    NSString *string2 = NSStringFromSelector(sel1);
複製代碼

4.2 IMP

  • IMP 是指向方法實現的函數指針;
  • 咱們調用方法,實際上就是根據方法 SEL 查找 IMP;
  • method_t實際上至關於在 SEL 和 IMP 之間作了一個映射。
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif
複製代碼

4.3 Type Encodings

  • Type Encodings 編碼技術就是配合runtime的技術,把一個方法的返回值類型、參數類型經過字符串的形式描述;
  • @encode()指令能夠將類型轉換爲 Type Encodings 字符串編碼, 如@encode(int)=i
  • OC方法都有兩個隱式參數,方法調用者(id)self和方法名(SEL) _cmd,因此咱們才能在方法中使用self_cmd
  • -(void)test,它的編碼爲「v16@0:8」,能夠簡寫爲「v@:
    v:表明返回值類型爲 void
    @:表明參數 1 類型爲 id
    ::表明參數 2 類型爲 SEL
    16:表明全部參數所佔的總字節數
    0:表明參數 1 從第幾個字節開始存儲
    8:表明參數 2 從第幾個字節開始存儲
  • 下圖爲類型對應的 Type Encodings 編碼:

Objective-C type encodings

  • Type Encodings 在runtime的消息轉發中會使用到;
  • 更多關於 Type Encodings 的內容,能夠查看官方文檔 Type Encodings
相關文章
相關標籤/搜索