iOS OC 類原理

iOS OC底層探索

注: 本文使用的環境是objc4-779.1 Xcode 11.5 (11E608c)

1. 類

根據前面幾篇文章的分析,我們知道Objective -C的對象通過isa與類關聯起來,那么到底什么是類呢?下面我們來探索一下。

我們知道Objective-C的基類是NSObject,日常開發中我們我們使用到的類基本都是用NSObject派生來的,那么在編譯后,他到底是什么樣子呢?

在這篇文章中我們說道Class在底層是一個objc_class那么它到底是如何實現的呢?我們來到objc源碼中一探究竟。我們知道objc_class是一個結構體我們搜索struct objc_class,我們發現會有很多結果,那么我們到底去分析那個版本呢,我們應該知道runtime有old和new兩個版本,那么新版本當然作為我們的首選,所有我們打開objc-runtime-new.h進行一探究竟。

image
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() const {
        return bits.data();
    }
}

在這里我們再次看到了objc_object所以說在面向對象里面真的是萬物皆對象。OC中的NSObject就是對底層objc_object的封裝。

C Objective-C
objc_object NSObject
objc_class Nsobjcet(Class)

2. 類中包含的內容

通過objc_class的源碼我們知道類中包含:

  • ISA //isa指針 ,繼承自objc_object
  • superclass // 父類指針
  • cache // cache_t類型的結構體
  • bits // class_data_bits_t結構體
特別提醒

isa 在源碼中是以注釋的形式體現出來的,并不是沒有寫,而是繼承自objc_object

struct objc_object {
private:
    isa_t isa;
}

2.1 ISA指針

在以前的文章中我們已經詳細的介紹了isa,在對象初始化的時候通過isa使對象和類關聯起來,那么類里面為什么還會有isa呢,通過我們的isa走位分析那篇文章我就可以知道,類里面的isa指向元類。元類與類同樣通過isa進行了關聯。

2.2 superclass

顧名思義,superclass就是指向父類,繼承自哪個父類,一般來說根父類基本都是NSObject,根元類的父類也是NSObject。

2.3 cache

顧明思議,cach是緩存的意思,肯定存儲的是類中的一些緩存。cache是一個cache_t類型的結構體。在objc-runtime-new.h中查看cache_t源碼如下:

主要有bucket_t的結構體指針,mask_tmaskuint16_t_flags_occupied

類型 占用空間
bucket_t* 8字節
mask_t(uint32_t) 4字節
uint16_t 2 字節

總計是8+4+2+2=16字節

cache_t 源碼實現:

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
    
    // How much the mask is shifted by.
    static constexpr uintptr_t maskShift = 48;
    
    // Additional bits after the mask which must be zero. msgSend
    // takes advantage of these additional bits to construct the value
    // `mask << 4` from `_maskAndBuckets` in a single instruction.
    static constexpr uintptr_t maskZeroBits = 4;
    
    // The largest mask value we can store.
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    
    // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
    
    // Ensure we have enough bits for the buckets pointer.
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    // _maskAndBuckets stores the mask shift in the low 4 bits, and
    // the buckets pointer in the remainder of the value. The mask
    // shift is the value where (0xffff >> shift) produces the correct
    // mask. This is equal to 16 - log2(cache_size).
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;

    static constexpr uintptr_t maskBits = 4;
    static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
    static constexpr uintptr_t bucketsMask = ~maskMask;
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;
    、
    、
    、
    、省略代碼
}

通過objc源碼查看 cache_t 的實現,我們發現主要有

  • _buckets bucket_t類型的結構體指針
  • _mask mask_t類型的結構體
  • _flags
  • _occupied

bucket_t 源碼實現:

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
}

mask_t 源碼實現:

#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

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; }
    };
};

bucket_t源碼我們大概就能夠知道它是存儲方法的,因為方法的本質就是SEL和IMP,這個有method_t源碼也可以證實,所以cache的主要作用就是存儲我們的方法的,下面我們通過lldb來進行驗證一下:

  • bucket_t分析:
    首先我們在objc源碼中實現一個LGPerson類,代碼如下:
@interface LGPerson : NSObject{
    NSString *hobby;
}

@property (nonatomic, copy) NSString *nickname;

- (void)sayHello;

- (void)sayCode;

- (void)sayMaster;

- (void)sayNB;

+ (void)sayHappy;

@end

main代碼:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [[LGPerson alloc] init];
        Class pClass = [LGPerson class];
 
        [person sayHello];
        [person sayCode];
        [person sayNB];
        
    }
    return 0;
}

分別在sayHello,sayCode, sayNB處斷點進行lldb查看,結果如下:


沒執行方法前

執行方法后

執行多個方法后
執行多個方法2
特別提醒!

類的isa占用8字節,superclass占用8字節,所以我們對cache的分析由首地址加16進行分析,16字節在16進制中就是加10。

根據圖一我們可以看到在沒有執行方法前我們的cache_tbuckets()里面是取不出數據的直接是個null,當我們執行完sayHello方法后再buckets里面取出的數據,并通過打印sel獲取到了一個叫sayHello的SEL,在后面的兩個圖里面我們分別執行了sayCodesayNB方法后也分別獲取到了sayCodesayNB的SEL,當我們越界獲取的時候就是空了,所以我們通過lldb分析可知,cahce主要是緩存方法的。那么為什么沒有找到allocclass方法呢?因為它們是類方法,會存儲在元類里面,在本文的后續過程中我們會進一步分析。

  • 補充init
init方法

在我們沒有執行任何自定義方法的時候,我們會發現cache里面有了一個數據,通過lldb打印我們看到其實是init,因為init是個實例方法,所以當我們調用了init后也可以在cache里面找到init方法。

  • _ocuupied

lldb分析:

未執行任何方法

當我們沒有執行任何方法的時候,我們通過lldb打印cache,我們發現_ocuupied的值為0,_mask的值為0。

執行一個方法

當我們執行了一個方法后再次通過lldb打印cache,我們發現_ocuupied的值為1_msak的值為3,那么_ocuupied是不是記錄了我們緩存方法的個數呢?

執行兩個方法

當我們執行了兩個方法后再次通過lldb打印cache,我們發現_ocuupied的值為2_mask的值為3,這個時候我們肯定會覺得_ocuupied大概率是記錄了我們緩存方法的個數,下面我們繼續進行探索。

執行三個方法

當我們執行了三個方法后再次通過lldb打印cache,我們發現_ocuupied的值為3_mask的值為3,這個時候我們基本確定_ocuupied記錄了我們緩存方法的個數,下面我們繼續進行探索。

執行四個方法

當我們執行了四個方法后再次通過lldb打印cache,我們發現_ocuupied的值為1_mask的值為7,這個時候按照我們的猜想_ocuupied的值應該為4,但是他卻成了1,那么到底是什么原因導致了這個情況呢?雖然_ocuupied的值變成了1但是_mask的值也變了,并且為7,剛剛一直是3的mask現在變成可7,而我們的ocuupied剛剛也是三,好像這個3記錄在了mask里面,ocuupied重新開始計數一樣,mask開始為3,當ocuupied為3后mask就滿了,將mask進行擴容后,繼續重新對ocuupied進行計數。這個很像哈希表這種數據結構,并且為了解決哈希沖突,使用的是開放尋址法,而開放尋址法必然要在合適的時機進行擴容,這個時機應該是表快滿的時候。為了驗證我們的猜想,還是查看cache_t的源碼進行分析吧。

在源碼中我們發現了maskoccupied兩個函數。

    mask_t mask();
    mask_t occupied();

跳轉進occupied函數后,源碼如下,緊隨其后的還有incrementOccupied()函數。

mask_t cache_t::occupied() 
{
    return _occupied;
}

void cache_t::incrementOccupied() 
{
    _occupied++;
}

根據上面的源碼,我們進行全局搜索,查找調用occupiedincrementOccupied()地方,發現occupied的調用有三處incrementOccupied()的調用有一處,但他們兩個都同事出現在了一個insert函數中,看到這個函數后我們的第一想法就是,這個函數是cache緩存的核心函數,下面我們做進一步的驗證,再分析一下mask函數。mask()函數的實現如下:主要就是返回_mask的值。

mask_t cache_t::mask() 
{
    return _mask.load(memory_order::memory_order_relaxed);
}

下面我們就搜索一下mask(),發現共有三處調用,有兩處在同一函數內,有一處是返回值,所以我們重點分析兩處在一起的那個函數,函數實現如下:

unsigned cache_t::capacity()
{
    return mask() ? mask()+1 : 0; 
}

既然沒找到mask直接在cahce中的調用與影響,那么我們可以繼續搜索一下capacity()函數,這里的mask()被間接調用的可能性很大,通過搜索capacity()函數后發現共有四處調用,其中一處就在insert函數內,這時我們上面的猜想又得到了一些可能性。下面我們直接上insert函數的源碼作進一步的分析吧。

insert函數源碼:

void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
#if CONFIG_USE_CACHE_LOCK
    cacheUpdateLock.assertLocked();
#else
    runtimeLock.assertLocked();
#endif

    ASSERT(sel != 0 && cls->isInitialized());

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    else if (fastpath(newOccupied <= capacity / 4 * 3)) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the
    // minimum size is 4 and we resized at 3/4 full.
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(sel, imp, cls);
            return;
        }
        if (b[i].sel() == sel) {
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));

    cache_t::bad_cache(receiver, (SEL)sel, cls);
}
    1. 排除前面的加鎖和斷言
    1. 首先獲取了occupied的值在加1,在獲取mask的值,放在變量capacity內
    1. 先判斷cache是否為空,如果為空則初始化一個值INIT_CACHE_SIZE,這里的初始化值為4,源碼放在后面,就是1左移2位,二進制為 100, 10進制為4,然后調用reallocate 函數開辟空間。
    1. 如果不大于四分之三則不作處理,(這里應該是個擴容算法,后面則進一步驗證了)
    1. 其他情況,也就是大于四分之三后,則對capacity進行擴容,擴容為當前值的兩倍,并且如果擴容后的值大于最大值MAX_CACHE_SIZE,也就是1左移16位,1 0000 0000 0000 0000, 對應的10進制的值是65536。則不再進行擴容。擴容完畢后調用reallocate 函數開辟空間。
    1. 執行完上述操作后,獲取bucket_tmask,并通過cache_hash函數計算出一個begin(應該是緩存新調用方法的位置下標),把begin的值賦值給變量i
    1. 通過一個do while 循環,判斷計算出的位置是否為空,不為空則occupied自增,通過set方法將改類的方法進行緩存到上面初始化的bucket里面,如果不為空則判斷bucket內的sel是否等于要緩存的sel,直到通過cache_next計算出下一個位置不等于begin

上面提到的源碼

INIT_CACHE_SIZEMAX_CACHE_SIZE

/* Initial cache bucket count. INIT_CACHE_SIZE must be a power of two. */
enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),
    MAX_CACHE_SIZE_LOG2  = 16,
    MAX_CACHE_SIZE       = (1 << MAX_CACHE_SIZE_LOG2),
};

isConstantEmptyCache

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

reallocate

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    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_hashcache_next

// Class points to cache. SEL is key. Cache buckets store SEL+IMP.
// Caches are never built in the dyld shared cache.

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}

#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

buckets

struct bucket_t *cache_t::buckets() 
{
    return _buckets.load(memory_order::memory_order_relaxed);
}

通過對核心方法insert的分析我們大概知道了cache的基本原理與實現,下面我們總結一下:
1.當方法調用的時候會進行緩存
2.緩存時需要判斷緩存是否為空,空則初始化空間,不空則判斷是否到達擴容臨界點,到了則擴容,不到則直接緩存
3.緩存時則通過哈希計算緩存位置進行存儲

我們知道在調用方法的時候會觸發方法的緩存,那么這倒地是怎樣一個調用堆棧呢,我么通過搜索insert(進行查看,我們發現在cache_fill函數內調用了insert

cache_fill源碼

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
    runtimeLock.assertLocked();

#if !DEBUG_TASK_THREADS
    // Never cache before +initialize is done
    if (cls->isInitialized()) {
        cache_t *cache = getCache(cls);
#if CONFIG_USE_CACHE_LOCK
        mutex_locker_t lock(cacheUpdateLock);
#endif
        cache->insert(cls, sel, imp, receiver);
    }
#else
    _collecting_in_critical();
#endif
}

通過cache_fill源碼我們發現主要就是獲取cache然后調用insert函數緩存方法,當我們想進一步通過搜索cache_fill進行查找調用關系時,卻發現并沒有相應的源碼了,但是在``文件的注釋中發下了一些東西:其中cache_fillcache_expandcache_createbcopyflush_cachescache_flushcache_collect_free、等,這些在新的objc-cache.mm中并沒有什么蹤跡,但是在objc-cache-old.mm中卻能看見其蹤影,其實在 objc4-756中這些實現還是在的,應該是蘋果通過一些優化或者代碼的整合成了現在的樣子,雖然代碼在修改,但是原理基本是不變的,都是為了緩存方法。也都是通過擴容和哈希去實現的。

/***********************************************************************
* objc-cache.m
* Method cache management
* Cache flushing
* Cache garbage collection
* Cache instrumentation
* Dedicated allocator for large caches
**********************************************************************/


/***********************************************************************
 * Method cache locking (GrP 2001-1-14)
 *
 * For speed, objc_msgSend does not acquire any locks when it reads 
 * method caches. Instead, all cache changes are performed so that any 
 * objc_msgSend running concurrently with the cache mutator will not 
 * crash or hang or get an incorrect result from the cache. 
 *
 * When cache memory becomes unused (e.g. the old cache after cache 
 * expansion), it is not immediately freed, because a concurrent 
 * objc_msgSend could still be using it. Instead, the memory is 
 * disconnected from the data structures and placed on a garbage list. 
 * The memory is now only accessible to instances of objc_msgSend that 
 * were running when the memory was disconnected; any further calls to 
 * objc_msgSend will not see the garbage memory because the other data 
 * structures don't point to it anymore. The collecting_in_critical
 * function checks the PC of all threads and returns FALSE when all threads 
 * are found to be outside objc_msgSend. This means any call to objc_msgSend 
 * that could have had access to the garbage has finished or moved past the 
 * cache lookup stage, so it is safe to free the memory.
 *
 * All functions that modify cache data or structures must acquire the 
 * cacheUpdateLock to prevent interference from concurrent modifications.
 * The function that frees cache garbage must acquire the cacheUpdateLock 
 * and use collecting_in_critical() to flush out cache readers.
 * The cacheUpdateLock is also used to protect the custom allocator used 
 * for large method cache blocks.
 *
 * Cache readers (PC-checked by collecting_in_critical())
 * objc_msgSend*
 * cache_getImp
 *
 * Cache writers (hold cacheUpdateLock while reading or writing; not PC-checked)
 * cache_fill         (acquires lock)
 * cache_expand       (only called from cache_fill)
 * cache_create       (only called from cache_expand)
 * bcopy               (only called from instrumented cache_expand)
 * flush_caches        (acquires lock)
 * cache_flush        (only called from cache_fill and flush_caches)
 * cache_collect_free (only called from cache_expand and cache_flush)
 *
 * UNPROTECTED cache readers (NOT thread-safe; used for debug info only)
 * cache_print
 * _class_printMethodCaches
 * _class_printDuplicateCacheEntries
 * _class_printMethodCacheStatistics
 *
 ***********************************************************************/

留下一些問題?

為什么要在3/4處擴容?

因為此處緩存使用的是哈希這種數據結構,哈希中有一個叫做裝載因子的概念,表示空位的大小,在3/4處擴容則說明裝載因子是1/4,裝載因子越大說明可能產生的沖突越多,這里取1/4應該是蘋果評估的一個合理的數值。

方法緩存是有序的嗎?

因為用了哈希,所以肯定無序,這里也稍微做了一些驗證,簡單說說吧,就不上圖已進行說了,驗證的對錯也不太敢保證,只是自己的一些想法。其實在擴容的時候原來緩存是清除了的,開辟了新的緩存來保存,在objc4-756中我記得是拷貝到新的緩存里,但是在objc4-779.1中我發現原來調用的方法已經不再緩存內了,就是通過上面的lldb驗證的,而且第一次擴容后的第一方法會存儲在最后一個位置,而不是擴容后的第一個位置,驗證了幾次都是這樣,然后也沒仔細探究了。我的想法是:

  • 擴容說明方法夠多,如果都調用則需要這么多空間進行緩存
  • 但是既然用到了擴容則說明有些方法沒有頻繁調用,則觸發的緩存
  • 擴容前緩存的方法再次被調用的概率不高了,所以就沒有拷貝到新的緩存內,如果再次調用應該會存儲到緩存內

總結

  • cache_t就是緩存我們OC方法的,每調用一個OC方法他就會將該方法緩存;
  • 緩存的開辟從4個開始,到了3/4就開始擴容2倍,直到65536
  • 緩存內主要存儲sel和imp

2.4 bits

bits是一個class_data_bits_t的結構體,在objc_class源碼中很多方法的返回值都是bits中的例如:

class_rw_t *data() const {
        return bits.data();
}

void setData(class_rw_t *newData) {
        bits.setData(newData);
}

#if FAST_HAS_DEFAULT_RR
    bool hasCustomRR() const {
        return !bits.getBit(FAST_HAS_DEFAULT_RR);
    }
    void setHasDefaultRR() {
        bits.setBits(FAST_HAS_DEFAULT_RR);
    }
    void setHasCustomRR() {
        bits.clearBits(FAST_HAS_DEFAULT_RR);
    }
#else
    bool hasCustomRR() const {
        return !(bits.data()->flags & RW_HAS_DEFAULT_RR);
    }
    void setHasDefaultRR() {
        bits.data()->setFlags(RW_HAS_DEFAULT_RR);
    }
    void setHasCustomRR() {
        bits.data()->clearFlags(RW_HAS_DEFAULT_RR);
    }
#endif

放眼望去,還是那個data()最顯眼,下面我們就來研究一下它。首先讓我們來看看class_rw_t的源碼:里面除了flagsversionwitness這些,主要還有個ro以及methodspropertiesprotocols,這個ro是一個class_ro_t類型的結構體指針,其他看樣子是個數組,methods應該是存儲方法的,properties應該是存儲屬性的,protocols應該是存儲協議的。下面我們來進行驗證。

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t version;
    uint16_t witness;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif

    void setFlags(uint32_t set) 
    {
        __c11_atomic_fetch_or((_Atomic(uint32_t) *)&flags, set, __ATOMIC_RELAXED);
    }

    void clearFlags(uint32_t clear) 
    {
        __c11_atomic_fetch_and((_Atomic(uint32_t) *)&flags, ~clear, __ATOMIC_RELAXED);
    }

    // set and clear must not overlap
    void changeFlags(uint32_t set, uint32_t clear) 
    {
        ASSERT((set & clear) == 0);

        uint32_t oldf, newf;
        do {
            oldf = flags;
            newf = (oldf | set) & ~clear;
        } while (!OSAtomicCompareAndSwap32Barrier(oldf, newf, (volatile int32_t *)&flags));
    }
};

工程還是當前的工程,分析方法依舊使用lldb,斷點打在獲取pClass之后。首先說明一下,要想獲取到class_data_bits_t的首地址,就要先獲取到類的首地址,然后向下偏移32個字節,為什么呢?因為類的 isa 指針占用8字節,superClass占用8字節,cache通過我們的分析占用16字節所以拿到首地址后加32就是我們的bits的首地址。首先我們先打印一下data()的內容

data內容.png

打印完我們看到了源碼中的很多東西都打印出來了,迫不及待的我們趕緊打印一下methods看看,看到打印出來的是個method_array_t的類型,里面還有個list我們不妨看看這個list里面是否存儲的就是我們的方法,打印list后得到的是一個method_list_t類型的指針,既然是list指針,那么首地址應該是第一元素吧,我們通過p *去打印,發現確實打印出了我們的sayHello方法,我么繼續打印,分別打印出了sayCodesayNBsayMaster以及一些C++的方法,還有屬性的setter和get方法,那么我們的方法原理是什么呢,為什么要把方法存儲在這里呢,我們還不知道,在后面的探索中我們會繼續研究這些。

rw-methods.jpg

下面我們看看屬性 properties,按照上述步驟打印,第一個就是我們的nickname,后面就沒有了,那么我們測成員變量hobby去哪了呢?

rw-properties.png

暫時不探索protocols

通過上面的探索,感覺確實是這樣存儲的,但是沒有找到類方法sayHappy,我們的成員變量hobby也沒有出現在其中。這個時候突然想起,剛才我們在class_rw_t中還發現了一個class_ro_t類型的ro,那么我們在探索一下這個ro吧,首先看看class_ro_t的源碼吧:

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;

    // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
    _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];

    _objc_swiftMetadataInitializer swiftMetadataInitializer() const {
        if (flags & RO_HAS_SWIFT_INITIALIZER) {
            return _swiftMetadataInitializer_NEVER_USE[0];
        } else {
            return nil;
        }
    }

    method_list_t *baseMethods() const {
        return baseMethodList;
    }

    class_ro_t *duplicate() const {
        if (flags & RO_HAS_SWIFT_INITIALIZER) {
            size_t size = sizeof(*this) + sizeof(_swiftMetadataInitializer_NEVER_USE[0]);
            class_ro_t *ro = (class_ro_t *)memdup(this, size);
            ro->_swiftMetadataInitializer_NEVER_USE[0] = this->_swiftMetadataInitializer_NEVER_USE[0];
            return ro;
        } else {
            size_t size = sizeof(*this);
            class_ro_t *ro = (class_ro_t *)memdup(this, size);
            return ro;
        }
    }
};

這個源碼跟剛才的class_rw_t有很多類似的地方,也有方法、屬性和協議相關的東西,那么是不是這個ro也存儲了一些方法和屬性相關的東西呢?下面我們繼續通過lldb去查看。

查看ro.png

通過lldb打印我們看見與其源碼內的內容一樣,下面我們來探索一下方法baseMethodList

ro-baseMethodList.jpg

通過上面的圖片我們看到ro內部存儲的方法月rw一樣,也沒有類方法,那么他為什么要存儲兩份呢?這個只能通過我們后續的探索進行考證了。那么類方法倒地存儲在了哪里呢?其實山重水復疑無路,柳暗花明又一村啊,我們還有元類沒探索呢,實例方法存儲在類中,類方法是不是存儲在元類中呢?

我們通過上面的方法lldb去元類里面看看
1.首先打印類的地址
2.然后取出類的isa,類的isa指向元類
3.&上isa_mask,就是元類
4.查看元類的bits中的rwro

元類探索.jpg

果然我們在元類中找到了我們的sayHappy方法,這回就可以盡情happy了。其實實例方法都是由對象調用的,類方法由類調用,實例方法存儲在類中,類方法存儲在元類中也就不難理解了。

這回我們就找到了實例方法和類方法的存儲位置,下面繼續在ro里面探索一下屬性,以及我們還未找到的成員變量hobby

ro-baseProperties.png

通過查看存儲在ro里面的baseProperties其內容跟rw也是一樣的,依舊沒有我們的hobby,這個和類方法的思路不太一樣,成員變量也是類里面的,我們暫不考慮去元類里面找成員變量。這時候我們發現還有個ivars可能存在我們想要的東西,下面我們查看一下ivars:

ro-ivars.png

結論:
果不其然,我們的成員變量hobby就存在于這里,并且我們還發現了_nickname,所以成員變量和屬性自動生成的帶下劃線的成員變量都存儲在ivars里面。

至此我們的探索已經差不多了,下面我們通過代碼來驗證一下我們的上面的探索:

首先我們打印一下ivarsproperties

實現代碼:

void testObjc_copyIvar_copyProperies(Class pClass){
    
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList(pClass, &count);
    for (unsigned int i=0; i < count; i++) {
        Ivar const ivar = ivars[i];
        //獲取實例變量名
        const char*cName = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:cName];
        NSLog(@"class_copyIvarList:%@",ivarName);
    }
    free(ivars);

    unsigned int pCount = 0;
    objc_property_t *properties = class_copyPropertyList(pClass, &pCount);
    for (unsigned int i=0; i < pCount; i++) {
        objc_property_t const property = properties[i];
        //獲取屬性名
        NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
        //獲取屬性值
        NSLog(@"class_copyProperiesList:%@",propertyName);
    }
    free(properties);
}

方法調用:

Class pClass = [LGPerson class];
testObjc_copyIvar_copyProperies(pClass);

打印結果:

ivars和properties.png

實例方法打印:

實現代碼:

void testObjc_copyMethodList(Class pClass){
    unsigned int count = 0;
    Method *methods = class_copyMethodList(pClass, &count);
    for (unsigned int i=0; i < count; i++) {
        Method const method = methods[i];
        //獲取方法名
        NSString *key = NSStringFromSelector(method_getName(method));
        
        NSLog(@"Method, name: %@", key);
    }
    free(methods);
}

打印結果:

imethods.png

判斷該類是否包含該實例方法:
實現代碼:

void testInstanceMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
    Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));

    Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
    Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
    
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    NSLog(@"%s",__func__);
}

打印結果:

實例方法是否在類里面.png

這里我們用到了LGPerson的實例方法sayHello和其類方法sayHappy,在獲取實例方法的時候,在類中獲取到了實例方法sayHello,在元類在獲取到了sayHappy,說明類方法也是以實例方法的形式存儲在元類中。

下面我們在看看類中是否包含類方法:
實現代碼:

void testClassMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(pClass, @selector(sayHello));
    Method method2 = class_getClassMethod(metaClass, @selector(sayHello));

    Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
    Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
    
    // 類方法形式
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    NSLog(@"%s",__func__);
}

打印結果:

類方法打印查看.png

由于sayHello不是類方法,所前兩個打印是0x0,但是后面的就有些出乎我們的意料了,在上面我們通過lldb查看時,并沒有在類中找到類方法,下載打印是居然是有的,那么這到底是為什么呢?我們查看了class_getClassMethod的源碼

源碼:

/***********************************************************************
* class_getClassMethod.  Return the class method for the specified
* class and selector.
**********************************************************************/
Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

// NOT identical to this->ISA when this is a metaclass
Class getMeta() {
    if (isMetaClass()) return (Class)this;
    else return this->ISA();
 }

源碼一看,一目了然,獲取類方法的實質就是去元類里面查找元類的實例方法,上面我們也提到了,類方法本來就是已實例方法的形式存儲在元類中,并且在獲取元類的時候做了判斷,如果是元類就直接返回Class,如果不是就返回類的isa,其實類的isa就是元類。所以為什么打印結果是剛才的樣子也就清楚了。

全篇總結

Objective-C類有四個屬性

  1. isa 指向元類;
  2. superClass 指向父類;
  3. cache 緩存調用過的方法,并通過哈希這種數據結構進行擴容,從4到65536,其中的mask作為一個掩碼,用作哈希計算時的鹽,避免哈希沖突,一直是減一的狀態,所以一直不會滿,保證哈希安全,也用作記錄緩存大小,mask一直是緩存大小減1,所以獲取到mask加上1就是緩存的大小。occupied作為開辟新空間(新緩存方法)的計數,以及判斷是否到了臨界點3/4處需要擴容的重要條件;
  4. bits 其中有rw存儲了類的實例方法(methods)和屬性(properties),rw中有個ro存儲了類的實例方法(baseMethodList)、屬性(baseProperties)和成員變量(ivars);
  5. 類的類方法以實例方法的形式存儲在元類中。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370