iOS 底層學(xué)習(xí)6

iOS 底層第6天的學(xué)習(xí)。今天又學(xué)了一個新的技能,原來還能把底層源碼拉出來在局部進行分析,真是受益匪淺啊。

cache 探索

  • 我們已經(jīng)知道在類的底層結(jié)構(gòu)中有 isa,superclass,bit,cache
name desc
isa isa指針,objc isa-> Class,Class isa -> metal Class
superclass 父類
bit bit 里有 ro,rw,里有屬性,方法,協(xié)議 等等
cache 緩存

而這里 cache 里到底存了有哪些東西呢?

  • 我們先 lldb 獲取到 cache 的內(nèi)存地址,經(jīng)過一番操作如下所示
(lldb) p/x pClass
(Class) $0 = 0x00000001000085d0 Person
(lldb) p/x 0x00000001000085d0 + 0x10 // 平移16字節(jié)
(long) $1 = 0x00000001000085e0
(lldb) p (cache_t *)0x00000001000085e0 
(cache_t *) $2 = 0x00000001000085e0
(lldb) p *$2
(cache_t) $3 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4298515360
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 0
        }
      }
      _flags = 32800
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0000802000000000
      }
    }
  }
}
  • 接下里就是要去分析 cache_t 源碼,目的是為了找到相應(yīng)的方法。
struct cache_t {

  ..... 省略部分代碼
public:
    // The following four fields are public for objcdt's use only.
    // objcdt reaches into fields while the process is suspended
    // hence doesn't care for locks and pesky little details like this
    // and can safely use these.
    unsigned capacity() const;

    struct bucket_t *buckets() const;

    Class cls() const;

#if CONFIG_USE_PREOPT_CACHES
    const preopt_cache_t *preopt_cache() const;
#endif

    mask_t occupied() const;
    void initializeToEmpty();

#if CONFIG_USE_PREOPT_CACHES
    bool isConstantOptimizedCache(bool strict = false, uintptr_t empty_addr = (uintptr_t)&_objc_empty_cache) const;
    bool shouldFlush(SEL sel, IMP imp) const;
    bool isConstantOptimizedCacheWithInlinedSels() const;
    Class preoptFallbackClass() const;
    void maybeConvertToPreoptimized();
    void initializeToEmptyOrPreoptimizedInDisguise();
#else
    inline bool isConstantOptimizedCache(bool strict = false, uintptr_t empty_addr = 0) const { return false; }
    inline bool shouldFlush(SEL sel, IMP imp) const {
        return cache_getImp(cls(), sel) == imp;
    }
    inline bool isConstantOptimizedCacheWithInlinedSels() const { return false; }
    inline void initializeToEmptyOrPreoptimizedInDisguise() { initializeToEmpty(); }
#endif

    void insert(SEL sel, IMP imp, id receiver);

}
  • 我們看到 有 一個 struct bucket_t *buckets() const; buckets的結(jié)構(gòu)體,
    點擊進入 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
}

發(fā)現(xiàn)了 _imp_sel ,由此我們可以知道 buckets 里面存儲了 _imp 好 `_sel,

  • 接下來我們回到 lldb 去驗證一下 buckets
(lldb) p  $3.buckets()
(bucket_t *) $5 = 0x00000001003623a0
(lldb) p *$5
(bucket_t) $6 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = nil
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}
(lldb) p $3.buckets()[1]
(bucket_t) $6 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 48880
    }
  }
}
  • 查找bucket_t源碼 ,發(fā)現(xiàn)2個方法
  inline SEL sel() const { return _sel.load(memory_order_relaxed); }
  inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        SEL sel = _sel.load(memory_order_relaxed);
        return (IMP)
            ptrauth_auth_and_resign((const void *)imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, 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
    }

繼續(xù) lldb 輸出 sel & imp

(lldb) p $6.sel()
(SEL) $8 = "doSomething"
(lldb) p $6.imp(nil,pClass)
(IMP) $12 = 0x0000000100003b00 (KCObjcBuild`-[Person doSomething])
  • 驗證成功。但是每次都要去 lldb 打印 感覺真的好麻煩,有沒有更方便的方法呢?接下來我們用底層局部代碼塊的形式繼續(xù)分析,用 NSLog 日志方式進行打印。

局部代碼分析 cache

  • 把獲取的源碼更改成自己需要的 struct
struct x_bucket_t {
    SEL _sel;
    IMP _imp;
};
 struct x_cache_t {
    uintptr_t _bucketsAndMaybeMask;
    mask_t    _maybeMask;
    uint16_t  _flags;
    uint16_t  _occupied;
};
struct x_class_data_bits_t {
    uintptr_t bits;
};
struct x_objc_class {
    // Class ISA;
    Class isa;
    Class superclass;
    struct x_cache_t cache;
    struct x_class_data_bits_t bits;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [Person alloc];
        Class p_class = p.class;
        // 進行 x_objc_class 的強轉(zhuǎn)
        struct x_objc_class *x_class = (__bridge struct x_objc_class *)p_class;
        NSLog(@"x_class is %@",x_class);      
}
  • 打印結(jié)果
x_class is Person
  • 說明這樣是可以底層源碼更進行 NSLog 輸出的,我們繼續(xù)往下走。
  • 我們的目的是要打印 x_cache_t 里的 buckets ,可現(xiàn)在x_cache_t 里沒有,我想能不能在 x_cache_t 加一個x_bucket_t試試呢,我們把_bucketsAndMaybeMask 替換成 struct x_bucket_t *_buckets;
struct x_cache_t {
    struct x_bucket_t *_buckets;
//    uintptr_t _bucketsAndMaybeMask;
    mask_t    _maybeMask;
    uint16_t  _flags;
    uint16_t  _occupied;
};
  Class p_class = p.class;
  [p readBook1]; //
 // 進行 x_objc_class 的強轉(zhuǎn)
 struct x_objc_class *x_class = (__bridge struct x_objc_class *)p_class;
 for (mask_t i = 0 ; i<x_class->cache._maybeMask; i++) {
      struct x_bucket_t bucket = x_class->cache._buckets[I];
      NSLog(@"sel is %@, imp is %pf",NSStringFromSelector(bucket._sel),bucket._imp);
 }
   NSLog(@"_occupied is %hu,_maybeMask is %u",x_class->cache._occupied,x_class->cache._maybeMask);

  • 打印輸出結(jié)果,我們發(fā)現(xiàn) _bucketsAndMaybeMask 替換成 struct x_bucket_t *_buckets 可以輸出結(jié)果打印 selimp
  • 已經(jīng)成功找到 selreadBook1
 sel is (null), imp is 0x0f
 sel is (null), imp is 0x0f
 sel is readBook1, imp is 0xbcb0f
_occupied is 1,_maybeMask is 3 
  • 我們繼續(xù)測試多打印幾個方法調(diào)用,看看 cache_buckets會有什么變化
  Class p_class = p.class;
  [p readBook1]; //
  [p readBook2]; //
  [p readBook3]; //
 // 進行 x_objc_class 的強轉(zhuǎn)
 struct x_objc_class *x_class = (__bridge struct x_objc_class *)p_class;
 for (mask_t i = 0 ; i<x_class->cache._maybeMask; i++) {
      struct x_bucket_t bucket = x_class->cache._buckets[I];
      NSLog(@"sel is %@, imp is %pf",NSStringFromSelector(bucket._sel),bucket._imp);
 }
 >>> readBook1
 >>> readBook2
 >>> readBook3
 sel is (null), imp is 0x0f // 1
 sel is (null), imp is 0x0f // 2
 sel is (null), imp is 0x0f // 3
 sel is (null), imp is 0x0f // 4
 sel is (null), imp is 0x0f // 5
 sel is (null), imp is 0x0f // 6 
 sel is readBook3, imp is 0xbc50f // 7
_occupied is 1,_maybeMask is 7

疑問1:為什么我調(diào)用了 3 個 方法,buckets 里的 maybeMask 都變成7個了呢?
疑問2:readBook2,readBook3 為什么沒有呢?

  • 我們只調(diào)用 readBook1readBook2 看看輸出如何
sel is readBook2, imp is 0xbc78f
sel is (null), imp is 0x0f
sel is readBook1, imp is 0xbc08f
_occupied is 2,_maybeMask is 3
  • 我們可以看到如果是打印 2個方法 occupied2, 但 maybeMask3 沒有增加, 打印 1個方法, occupied1, maybeMask 還是 3,也沒有增加。
  • 我們帶著上面的機個疑問,去分析下cache底層源碼到底是如何實現(xiàn)的。
  • 分析前先要思考下我們在調(diào)用 方法的時候是在從 cache把,那到底是怎么進去的呢?
struct cache_t { 
      ...
      void insert(SEL sel, IMP imp, id receiver);
      ...
}
  • 我源碼里找到了 insert 插入 = 把,我們進入 insert 方法里繼續(xù)分析。
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    // ... 省略部分代碼
    // Use the cache as-is if until we exceed our expected fill ratio.
    mask_t newOccupied = occupied() + 1; // occupied 默認(rèn) 0 + 1
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    // 先從這里開始分析
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE; // 4
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) { // <=  capacity * 3/4
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
    }
#if CACHE_ALLOW_FULL_UTILIZATION // 1
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) { 
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }
#endif
    else { // newOccupied +1 >  capacity * 3/4 會進來   
       // 4*2 = 8
        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; // 4-1=3
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

   
  //. 通過 do while 尋找合適的下標(biāo)操作
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, 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));

}
  • isConstantEmptyCache 判斷是否有緩存,沒緩存直接進入 reallocate 方法開辟內(nèi)存
ALWAYS_INLINE
/*
  oldCapacity = 0
  newCapacity = 4
  freeOld = false
**/
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);
    // ... 省略部分代碼 
    // newCapacity = 4
    setBucketsAndMask(newBuckets, newCapacity - 1);
  
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity); // 回收清空
    }
}
  • 當(dāng) (3 = newOccupied +1) = {4 > 3} = (4 = capacity * 3/4 ) 時會進入else 這個方法里。這里的3/4 只針對__arm__|| __x86_64__ || __i386__

  • 當(dāng) capacity = capacity ? capacity * 2 -> capacity = 8 -> reallocate(oldCapacity, capacity, true) -> collect_free(oldBuckets, oldCapacity);

  • 當(dāng) capacity = 8 -> mask_t m = capacity - 1 = 7 就能解釋 疑問1:為什么我調(diào)用了 3 個 方法,buckets 里的 maybeMask 都變成7個了

  • 調(diào)用3 個 methods =>occupied = 2 -> else 代碼 -> freeOld = true -> 重新梳理緩存 , 這樣就能解釋疑問2:readBook2,readBook3 沒有的原因了。

  • cache 分析圖如下


總結(jié)

  • 當(dāng)cache 里的 occupied + 2> capacity * 3/4, 就會進行一次擴容,并把原有的舊方法都清空。而capacity 無值的時默認(rèn)4, 擴容后的 capacity = 8,擴容的算法就是當(dāng)前 capacity * 2。 而maybeMask = capacity - 1

知識點補充

  • 脫離源碼進行小規(guī)模底層分析到底有什么好處呢?
    1:解決了有時源碼無法調(diào)試的問題。
    2:解決了lldb 操作麻煩的問題。
    3:小規(guī)模取樣 ,讓你研究的東西 更加的簡單和清晰。

ps: 真機架構(gòu) - arm64
模擬器 - i386
Mac - x86_64
_LP64 -> Unix 和 Unix類系統(tǒng)(Linux,Mac OS X)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容