iOS底層探索之方法緩存

分析類的時候,objc_class 結構體裏面有個 cache_t cache 成員變量。

一、cache 存的是什麼

先看下 cache 裏面存的是什麼

1.1 源碼查看
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;
    static constexpr uintptr_t maskShift = 48;
    static constexpr uintptr_t maskZeroBits = 4;
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    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;

public:
    static bucket_t *emptyBuckets();
    
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty();
    // 省略很多方法...
}

發現比較重要的成員 _buckets

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

    // Compute the ptrauth signing modifier from &_imp, newSel, and cls.
    uintptr_t modifierForSEL(SEL newSel, Class cls) const {
        return (uintptr_t)&_imp ^ (uintptr_t)newSel ^ (uintptr_t)cls;
    }

    // Sign newImp, with &_imp, newSel, and cls as modifiers.
    uintptr_t encodeImp(IMP newImp, SEL newSel, Class cls) const {
        if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        return (uintptr_t)
            ptrauth_auth_and_resign(newImp,
                                    ptrauth_key_function_pointer, 0,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (uintptr_t)newImp ^ (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (uintptr_t)newImp;
#else
#error Unknown method cache IMP encoding.
#endif
    }

public:
    inline SEL sel() const { return _sel.load(memory_order::memory_order_relaxed); }

    inline IMP imp(Class cls) const {
        uintptr_t imp = _imp.load(memory_order::memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        SEL sel = _sel.load(memory_order::memory_order_relaxed);
        return (IMP)
            ptrauth_auth_and_resign((const void *)imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(sel, cls),
                                    ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
    }

    template <Atomicity, IMPEncoding>
    void set(SEL newSel, IMP newImp, Class cls);
};

bucket_t 裏面就2個成員變量

  • _imp : 方法實現
  • _sel :方法指針
1.2 打印cache_t

實踐代碼:

@interface GLPerson : NSObject

@property (nonatomic, strong) NSString *name; /**<  8個字節  */
@property (nonatomic, strong) NSString *nickName; /**<  8個字節  */

- (void)sayHello1;
- (void)sayHello2;
- (void)sayHello3;
- (void)sayHello4;

@end
---
        GLPerson *p = [GLPerson alloc];
        [p sayHello1];
        [p sayHello2];
        [p sayHello3];
        [p sayHello4];

斷點打印 [GLPerson class] 的內存,然後通過內存偏移找到 cache_t

在分析類的時候,知道 cache_t 偏移 16 字節。所以可通過如下調試打印出 cache_t

第一次我先在調用第一個實例方法之前打斷點,[p sayHello1]; 打個斷點,然後打印:

(lldb) x/4gx GLPerson.class
0x1000022f8: 0x00000001000022d0 0x0000000100334140
0x100002308: 0x000000010032e440 0x0000802400000000
(lldb) p 0x1000022f8 + 0x10
(long) $1 = 4294976264
(lldb) p (cache_t *)$1
(cache_t *) $2 = 0x0000000100002308
(lldb) p *$2
(cache_t) $3 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x000000010032e440 {
      _sel = {
        std::__1::atomic<objc_selector *> = (null)
      }
      _imp = {
        std::__1::atomic<unsigned long> = 0
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 0
  }
  _flags = 32804
  _occupied = 0
}

然後調用 [p sayHello1] 之後:

(lldb) p *$2
(cache_t) $4 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x0000000101c8e800 {
      _sel = {
        std::__1::atomic<objc_selector *> = (null)
      }
      _imp = {
        std::__1::atomic<unsigned long> = 0
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 3
  }
  _flags = 32804
  _occupied = 1
}

再往下調用

// 調用 sayHello2 之後
(lldb) p *$2
(cache_t) $6 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x0000000101c8e800 {
      _sel = {
        std::__1::atomic<objc_selector *> = ""
      }
      _imp = {
        std::__1::atomic<unsigned long> = 12200
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 3
  }
  _flags = 32804
  _occupied = 2
}
// 調用完4個方法
(lldb) p *$2
(cache_t) $7 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x0000000101a0c160 {
      _sel = {
        std::__1::atomic<objc_selector *> = (null)
      }
      _imp = {
        std::__1::atomic<unsigned long> = 0
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 7
  }
  _flags = 32804
  _occupied = 2
}

可以看出調用方法之後,類的 cache_t 成員變量變化了。

1.3 打印方法名

再回到調用第一個方法之後

(lldb) p *$1   // 打印cache_t
(cache_t) $3 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x0000000100645710 {
      _sel = {
        std::__1::atomic<objc_selector *> = ""
      }
      _imp = {
        std::__1::atomic<unsigned long> = 11824
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 3
  }
  _flags = 32804
  _occupied = 1
}
(lldb) p $3.buckets()
(bucket_t *) $4 = 0x0000000100645710
(lldb) p *$4
(bucket_t) $5 = {
  _sel = {
    std::__1::atomic<objc_selector *> = ""
  }
  _imp = {
    std::__1::atomic<unsigned long> = 11824
  }
}
(lldb) p $5.sel()   // bucket_t的sel()方法
(SEL) $6 = "sayHello1"
(lldb) p $5.imp(pClass)   // bucket_t的imp(Class)方法
(IMP) $7 = 0x0000000100000d30 (KCObjc`-[GLPerson sayHello1])

總結:

  1. 先得到 (cache_t *)$1;
  2. p *$1 得到 (cache_t) $3;
  3. 調用 cache_tbuckets 方法,p $3.buckets(),得到 (bucket_t *) $4;
  4. p *$4 得到 (bucket_t) $5;
  5. 調用 bucket_tsel() 方法,打印出 (SEL) $6 = "sayHello1" SEL 名稱;
  6. 調用 bucket_timp(Class cls) 方法,打印出 (IMP) $7 = 0x0000000100000d30 (-[GLPerson sayHello1]) IMP實現地址;

然後調用完四個方法之後再打印:

(lldb) p $9.buckets()
(bucket_t *) $10 = 0x0000000101a07960
(lldb) p *$10
(bucket_t) $11 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null)
  }
  _imp = {
    std::__1::atomic<unsigned long> = 0
  }
}
(lldb) p $11.sel()
(SEL) $12 = <no value available>
(lldb) p *($10+1)
(bucket_t) $13 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null)
  }
  _imp = {
    std::__1::atomic<unsigned long> = 0
  }
}
(lldb) p *($10+2)
(bucket_t) $14 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null)
  }
  _imp = {
    std::__1::atomic<unsigned long> = 0
  }
}
(lldb) p *($10+3)
(bucket_t) $15 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null)
  }
  _imp = {
    std::__1::atomic<unsigned long> = 0
  }
}
(lldb) p *($10+4)
(bucket_t) $16 = {
  _sel = {
    std::__1::atomic<objc_selector *> = ""
  }
  _imp = {
    std::__1::atomic<unsigned long> = 11856
  }
}
(lldb) p $15.sel()
(SEL) $17 = <no value available>
(lldb) p $16.sel()
(SEL) $18 = "sayHello3"
(lldb) p *($10+5)
(bucket_t) $19 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null)
  }
  _imp = {
    std::__1::atomic<unsigned long> = 0
  }
}
(lldb) p *($10+6)
(bucket_t) $20 = {
  _sel = {
    std::__1::atomic<objc_selector *> = ""
  }
  _imp = {
    std::__1::atomic<unsigned long> = 11872
  }
}
(lldb) p $19.sel()
(SEL) $21 = <no value available>
(lldb) p $20.sel()
(SEL) $22 = "sayHello4"
 // 再往下繼續加,不是null就是其他東西了

可以知道,_buckets裏面就是存已經調用的方法的,調用完四個方法之後,發現這個_buckets裏面存的方法卻不是按順序排的,而是隨機的,中間可能還爲空。

二、cache 是怎麼存的

上面在打印 cache_t 的時候,發現有個變量_occupied是不是也在改變,還有 _mask 變量。

2.1 _occupied追蹤

objc 源碼全局搜索一下 _occupied,發現用到的地方很少,其中只有下面一個方法能讓 _occupied 增加。

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

只有 cache_tincrementOccupied 方法可增加 _occupied 的值。

再搜索 incrementOccupied 的調用地方,

只有 cache_tinsert 方法調用 incrementOccupied

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 了,通過打斷點可見調用方法

發送方法以後再分析,先繼續關注 cache_t

2.2 cache->insert 存方法
ALWAYS_INLINE
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 + CACHE_END_MARKER <= 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);
}
bool cache_t::isConstantEmptyCache()
{
    return 
        occupied() == 0  &&  
        buckets() == emptyBucketsForCapacity(capacity(), false);
}
---
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}
---
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
---
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),
};
---
#define CACHE_END_MARKER 1

流程分析:

  1. 賦值 newOccupied = occupied() + 1,oldCapacity = capacity = capacity();;
  2. 判斷 cache 是否爲空,爲空的話釋放舊的,開闢新的大小,新的爲4;
  3. newOccupied + CACHE_END_MARKER <= capacity / 4 * 3,如果 occupied + 1 之後還小於等於 occupied 的 3/4 的話,就還用他自己;
  4. 最後就是超過了 3/4 之後,會做 2 倍擴容,capacity * 2, 但是最大不超過 216
  5. 聲明一個 buckets() 數組,m = capacity - 1begin = cache_hash(sel, m), i = begin
  6. 加入m = 3, 可知任何一個數 & 3 得到的肯定小於等於3,也就是 begin 肯定會小於capacitycapacity 既可以理解爲 buckets 的邊界大小;
  7. do while 循環中,判斷 b[i].sel() 是否存在;
  8. 等於0,說明不存在,調用 incrementOccupied 使 _occupied++b[i].set<Atomic, Encoded>(sel, imp, cls) 緩存方法,結束
  9. 存在並且等於要存的方法,說明在其它線程已經存了,結束
  10. b[i].sel() 既不等於0,也不等於要存的方法;
  11. (i = cache_next(i, m)) != begin) , 把 i = (i + 1) & m,判斷 i 是否等於 begin,如果等於,重複 步驟11,如果不等於回到 步驟7

三、取方法

objc-runtime-new.mm 文件中 log_and_fill_cache 方法的下面,有個 IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) 方法。裏面有個cache_getImp 方法

    // Optimistic cache lookup
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

通過 _cache_getImp 搜索,有各個架構的定義,下面是 arm64架構 的。

    STATIC_ENTRY _cache_getImp

    GetClassFromIsa_p16 p0
    CacheLookup GETIMP, _cache_getImp

LGetImpMiss:
    mov p0, #0
    ret

    END_ENTRY _cache_getImp
.macro CacheLookup
    //
    // Restart protocol:
    //
    //   As soon as we're past the LLookupStart$1 label we may have loaded
    //   an invalid cache pointer or mask.
    //
    //   When task_restartable_ranges_synchronize() is called,
    //   (or when a signal hits us) before we're past LLookupEnd$1,
    //   then our PC will be reset to LCacheMiss$1 which forcefully
    //   jumps to the cache-miss codepath.
    //
    //   It is assumed that the CacheMiss codepath starts right at the end
    //   of CacheLookup2 and will re-setup the registers to meet the cache-miss
    //   requirements:
    //
    //   GETIMP:
    //     The cache-miss is just returning NULL (setting r9 to 0)
    //
    //   NORMAL and STRET:
    //   - r0 or r1 (STRET) contains the receiver
    //   - r1 or r2 (STRET) contains the selector
    //   - r9 contains the isa (reloaded from r0/r1)
    //   - other registers are set as per calling conventions
    //
LLookupStart$1:

    ldrh    r12, [r9, #CACHE_MASK]  // r12 = mask
    ldr r9, [r9, #CACHE]    // r9 = buckets
.if $0 == STRET
    and r12, r12, r2        // r12 = index = SEL & mask
.else
    and r12, r12, r1        // r12 = index = SEL & mask
.endif
    add r9, r9, r12, LSL #3 // r9 = bucket = buckets+index*8
    ldr r12, [r9, #CACHED_SEL]  // r12 = bucket->sel
6:
.if $0 == STRET
    teq r12, r2
.else
    teq r12, r1
.endif
    bne 8f
    ldr r12, [r9, #CACHED_IMP]  // r12 = bucket->imp

.if $0 == STRET
    tst r12, r12        // set ne for stret forwarding
.else
    // eq already set for nonstret forwarding by `teq` above
.endif

.endmacro

.macro CacheLookup2
#if CACHED_SEL != 0
#   error this code requires that SEL be at offset 0
#endif
8:  
    cmp r12, #1
    blo LCacheMiss$1        // if (bucket->sel == 0) cache miss
    it  eq          // if (bucket->sel == 1) cache wrap
    ldreq   r9, [r9, #CACHED_IMP]   // bucket->imp is before first bucket
    ldr r12, [r9, #8]!      // r12 = (++bucket)->sel
    b   6b

LLookupEnd$1:
LCacheMiss$1:

.endmacro

彙編代碼,看右邊註釋就行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章