iOS底层探索之类的结构—cache分析(上)

394 阅读5分钟

1. 回顾

iOS底层探索之类的结构(上) 中介绍了类中的isa,在iOS底层探索之类的结构(中)介绍了类中的bits,还有一个cache没有探索和分析,这次主要是分析cache属性。

2. cache 结构

我们的目的是探索cache,首先得先去了解它的结构,然后再具体分析。

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // 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
    
   ...此处省略代码...

}

从类的结构中,可以看到cachecache_t类型的,那么我们去cache_t里面看看。

2.1 cache_t

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; 
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; 
#if __LP64__
            uint16_t                   _flags;  
#endif
            uint16_t                   _occupied; 
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; 
    };
    
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    // _bucketsAndMaybeMask is a buckets_t pointer
    // _maybeMask is the buckets mask

    static constexpr uintptr_t bucketsMask = ~0ul;
    static_assert(!CONFIG_USE_PREOPT_CACHES, "preoptimized caches not supported");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    static constexpr uintptr_t maskShift = 48;
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << maskShift) - 1;
    
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#if CONFIG_USE_PREOPT_CACHES
    static constexpr uintptr_t preoptBucketsMarker = 1ul;
    static constexpr uintptr_t preoptBucketsMask = bucketsMask & ~preoptBucketsMarker;
#endif
 ...此处省略代码...
}

从底层源码我们很容易看出,cache_t的结构,我们也可以代码测试lldb查看

在这里插入图片描述 从控制台输出可以看到结构是一模摸一样样。查看cache_t的源码,我们还发现底层分成了3个架构来处理,其中真机的架构中maskbucket是写在一起,目的是为了优化,可以通过各自的掩码来获取相应的数据。

  1. CACHE_MASK_STORAGE_OUTLINED: 表示运行的环境是模拟器 或者 macOS系统
  2. CACHE_MASK_STORAGE_HIGH_16: 表示运行环境是 64位的真机
  3. CACHE_MASK_STORAGE_LOW_4 :表示运行环境是 非64位 的真机

cache_t的结构里面还发现了如下代码

    static bucket_t *emptyBuckets();
    static bucket_t *allocateBuckets(mask_t newCapacity);
    static bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
    void bad_cache(id receiver, SEL sel) __attribute__((noreturn, cold));

从这些代码中可以,知道是对bucket_t的进行了操作,那么这个bucket_t是个什么重要角色呢?

2.2 bucket_t

以下是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

    // Compute the ptrauth signing modifier from &_imp, newSel, and cls.
    uintptr_t modifierForSEL(bucket_t *base, SEL newSel, Class cls) const {
        return (uintptr_t)base ^ (uintptr_t)newSel ^ (uintptr_t)cls;
    }
  ...此处省略代码...
  
}  

从上面👆的bucket_t的结构体源码可以看出,bucket_t里面存储的是SEL IMP,同样分为两个版本,真机 和 非真机,区别在于SELIMP的顺序不一致。 到此大概知道了cache是和方法有关的了,这家伙就是方法缓存嘛😁。

由此可以画出一个简单的结构图,如下

cache_t结构图 那么到底是不是方法缓存呢?又是如何进行方法缓存的呢?我接着往下探索分析

2.2.1 buckets()

从源码中发现,有个buckets()方法可以获取bucket_t

(lldb) p $2.buckets()
(bucket_t *) $3 = 0x00000001003623c0
(lldb) p *$3
(bucket_t) $4 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = nil
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}
(lldb) 

好尴尬啊!什么都么有啊!_sel值为nil?不是方法缓存吗?方法哪里去了啊! 这是因为我们都没有调用方法,哪里来的缓存那!那就调用一个方法再看看

(lldb) p [p sayHello]
2021-06-25 15:37:47.401935+0800 JPBuild[18788:5401308] -[JPPerson sayHello]
(lldb) p/x pClass
(Class) $5 = 0x0000000100008688 JPPerson
(lldb) p (cache_t*)0x0000000100008698
(cache_t *) $6 = 0x0000000100008698
(lldb) p *$6
(cache_t) $7 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4301295648
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 7
        }
      }
      _flags = 32808
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0001802800000007
      }
    }
  }
}
(lldb) p *$7.buckets()
(bucket_t) $8 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = nil
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}
(lldb) 

2.2.2 sel()、imp()

什么鬼👻???纳尼?还是没有啊!但是我们发现了_maybeMask_flags_occupied是有值的。于是继续查看源码,发现了,bucket_t里面的sel()方法,这不就打印方法名的啊!

(lldb) p $7.buckets()[1]
(bucket_t) $10 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = nil
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}
(lldb) p $7.buckets()
(bucket_t *) $11 = 0x0000000100609020
(lldb) p *$11
(bucket_t) $12 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = nil
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}
(lldb) p $12.sel()
(SEL) $13 = (null)
(lldb)  p $7.buckets()[2]
(bucket_t) $14 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = nil
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}
(lldb)  p $7.buckets()[3]
(bucket_t) $15 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 49128
    }
  }
}
(lldb)  p $15.sel()
(SEL) $16 = "sayHello"
(lldb) 

可以看到输出了我们调用的方法 "sayHello"IMP也是可以输出的,使用下面这个方法

 inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;

输出结果如下

(lldb) p $15.imp(nil,pClass)
(IMP) $17 = 0x0000000100003960 (JPBuild`-[JPPerson sayHello])
(lldb) 

3. 脱离源码分析

在上面是在底层源码里面查看结构,并且结合LLDB调试来分析的,那么我们如果源码调式不了呢?改怎么办呢?那么接下来就通过,模仿源码结构,直接代码分析。

3.1 小规模取样,模仿源码结构

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct jp_bucket_t {
    SEL _sel;
    IMP _imp;
};
struct jp_cache_t {
    struct jp_bucket_t *_bukets; // 8
    mask_t    _maybeMask; // 4
    uint16_t  _flags;  // 2
    uint16_t  _occupied; // 2
};

struct jp_class_data_bits_t {
    uintptr_t bits;
};

// cache class
struct jp_objc_class {
    Class isa;//在源码中,objc_class的ISA属性是继承自objc_object的,
    //但在我们将其拷贝过来时,去掉了objc_class的继承关系,
    //需要将这个属性明确,否则打印的结果是有问题的
    Class superclass;
    struct jp_cache_t cache;             // formerly cache pointer and vtable
    struct jp_class_data_bits_t bits;
};

方法

@implementation LGPerson

- (void)say1{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say2{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say3{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say4{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say5{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say6{
    NSLog(@"LGPerson say : %s",__func__);
}
- (void)say7{
    NSLog(@"LGPerson say : %s",__func__);
}

+ (void)sayHappy{
    NSLog(@"LGPerson say : %s",__func__);
}
@end

3.2 代码测试

调用两个方法,打印看看,是否缓存了方法 bucket_t测试1 调用了两个方法,都打印出来了,那么我们多调用几个方法看看 bucket_t测试2 从以上两个测试打印的结果来看,_occupied_maybeMask的值有变化,方法调用的数量不同值会变大。那么_occupied_maybeMask这两个家伙又是什么呢?

请看下一篇博客分析

iOS底层探索之类的结构—cache分析(下)

更多内容持续更新

🌹 请动动你的小手,点个赞👍🌹

🌹 喜欢的可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我,哈哈😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹