iOS OC底層探索
注: 本文使用的環(huán)境是objc4-779.1 Xcode 11.5 (11E608c)
1. 類
根據(jù)前面幾篇文章的分析,我們知道Objective -C的對(duì)象通過isa與類關(guān)聯(lián)起來,那么到底什么是類呢?下面我們來探索一下。
我們知道Objective-C的基類是NSObject,日常開發(fā)中我們我們使用到的類基本都是用NSObject派生來的,那么在編譯后,他到底是什么樣子呢?
- 我的另一篇文章:通過Clang 看OC對(duì)象的本質(zhì)
在這篇文章中我們說道Class在底層是一個(gè)objc_class那么它到底是如何實(shí)現(xiàn)的呢?我們來到objc源碼中一探究竟。我們知道objc_class是一個(gè)結(jié)構(gòu)體我們搜索struct objc_class,我們發(fā)現(xiàn)會(huì)有很多結(jié)果,那么我們到底去分析那個(gè)版本呢,我們應(yīng)該知道runtime有old和new兩個(gè)版本,那么新版本當(dāng)然作為我們的首選,所有我們打開objc-runtime-new.h進(jìn)行一探究竟。
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();
}
}
在這里我們?cè)俅慰吹搅?code>objc_object所以說在面向?qū)ο罄锩嬲娴氖侨f物皆對(duì)象。OC中的NSObject就是對(duì)底層objc_object的封裝。
| C | Objective-C |
|---|---|
| objc_object | NSObject |
| objc_class | Nsobjcet(Class) |
2. 類中包含的內(nèi)容
通過objc_class的源碼我們知道類中包含:
-
ISA//isa指針 ,繼承自objc_object -
superclass// 父類指針 -
cache// cache_t類型的結(jié)構(gòu)體 -
bits// class_data_bits_t結(jié)構(gòu)體
特別提醒
isa 在源碼中是以注釋的形式體現(xiàn)出來的,并不是沒有寫,而是繼承自objc_object
struct objc_object {
private:
isa_t isa;
}
2.1 ISA指針
在以前的文章中我們已經(jīng)詳細(xì)的介紹了isa,在對(duì)象初始化的時(shí)候通過isa使對(duì)象和類關(guān)聯(lián)起來,那么類里面為什么還會(huì)有isa呢,通過我們的isa走位分析那篇文章我就可以知道,類里面的isa指向元類。元類與類同樣通過isa進(jìn)行了關(guān)聯(lián)。
2.2 superclass
顧名思義,superclass就是指向父類,繼承自哪個(gè)父類,一般來說根父類基本都是NSObject,根元類的父類也是NSObject。
2.3 cache
顧明思議,cach是緩存的意思,肯定存儲(chǔ)的是類中的一些緩存。cache是一個(gè)cache_t類型的結(jié)構(gòu)體。在objc-runtime-new.h中查看cache_t源碼如下:
主要有bucket_t的結(jié)構(gòu)體指針,mask_t的mask,uint16_t的_flags和 _occupied
| 類型 | 占用空間 |
|---|---|
| bucket_t* | 8字節(jié) |
| mask_t(uint32_t) | 4字節(jié) |
| uint16_t | 2 字節(jié) |
總計(jì)是8+4+2+2=16字節(jié)
cache_t 源碼實(shí)現(xiàn):
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 的實(shí)現(xiàn),我們發(fā)現(xiàn)主要有
- _buckets bucket_t類型的結(jié)構(gòu)體指針
- _mask mask_t類型的結(jié)構(gòu)體
- _flags
- _occupied
bucket_t 源碼實(shí)現(xiàn):
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 源碼實(shí)現(xiàn):
#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 源碼實(shí)現(xiàn):
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源碼我們大概就能夠知道它是存儲(chǔ)方法的,因?yàn)榉椒ǖ谋举|(zhì)就是SEL和IMP,這個(gè)有method_t源碼也可以證實(shí),所以cache的主要作用就是存儲(chǔ)我們的方法的,下面我們通過lldb來進(jìn)行驗(yàn)證一下:
-
bucket_t分析:
首先我們?cè)趏bjc源碼中實(shí)現(xiàn)一個(gè)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處斷點(diǎn)進(jìn)行l(wèi)ldb查看,結(jié)果如下:
特別提醒!
類的isa占用8字節(jié),superclass占用8字節(jié),所以我們對(duì)cache的分析由首地址加16進(jìn)行分析,16字節(jié)在16進(jìn)制中就是加10。
根據(jù)圖一我們可以看到在沒有執(zhí)行方法前我們的cache_t的buckets()里面是取不出數(shù)據(jù)的直接是個(gè)null,當(dāng)我們執(zhí)行完sayHello方法后再buckets里面取出的數(shù)據(jù),并通過打印sel獲取到了一個(gè)叫sayHello的SEL,在后面的兩個(gè)圖里面我們分別執(zhí)行了sayCode和sayNB方法后也分別獲取到了sayCode和sayNB的SEL,當(dāng)我們?cè)浇绔@取的時(shí)候就是空了,所以我們通過lldb分析可知,cahce主要是緩存方法的。那么為什么沒有找到alloc和class方法呢?因?yàn)樗鼈兪穷惙椒ǎ瑫?huì)存儲(chǔ)在元類里面,在本文的后續(xù)過程中我們會(huì)進(jìn)一步分析。
- 補(bǔ)充init
在我們沒有執(zhí)行任何自定義方法的時(shí)候,我們會(huì)發(fā)現(xiàn)cache里面有了一個(gè)數(shù)據(jù),通過lldb打印我們看到其實(shí)是init,因?yàn)閕nit是個(gè)實(shí)例方法,所以當(dāng)我們調(diào)用了init后也可以在cache里面找到init方法。
- _ocuupied
lldb分析:
當(dāng)我們沒有執(zhí)行任何方法的時(shí)候,我們通過lldb打印cache,我們發(fā)現(xiàn)_ocuupied的值為0,_mask的值為0。
當(dāng)我們執(zhí)行了一個(gè)方法后再次通過lldb打印cache,我們發(fā)現(xiàn)_ocuupied的值為1_msak的值為3,那么_ocuupied是不是記錄了我們緩存方法的個(gè)數(shù)呢?
當(dāng)我們執(zhí)行了兩個(gè)方法后再次通過lldb打印cache,我們發(fā)現(xiàn)_ocuupied的值為2_mask的值為3,這個(gè)時(shí)候我們肯定會(huì)覺得_ocuupied大概率是記錄了我們緩存方法的個(gè)數(shù),下面我們繼續(xù)進(jìn)行探索。
當(dāng)我們執(zhí)行了三個(gè)方法后再次通過lldb打印cache,我們發(fā)現(xiàn)_ocuupied的值為3_mask的值為3,這個(gè)時(shí)候我們基本確定_ocuupied記錄了我們緩存方法的個(gè)數(shù),下面我們繼續(xù)進(jìn)行探索。
當(dāng)我們執(zhí)行了四個(gè)方法后再次通過lldb打印cache,我們發(fā)現(xiàn)_ocuupied的值為1_mask的值為7,這個(gè)時(shí)候按照我們的猜想_ocuupied的值應(yīng)該為4,但是他卻成了1,那么到底是什么原因?qū)е铝诉@個(gè)情況呢?雖然_ocuupied的值變成了1但是_mask的值也變了,并且為7,剛剛一直是3的mask現(xiàn)在變成可7,而我們的ocuupied剛剛也是三,好像這個(gè)3記錄在了mask里面,ocuupied重新開始計(jì)數(shù)一樣,mask開始為3,當(dāng)ocuupied為3后mask就滿了,將mask進(jìn)行擴(kuò)容后,繼續(xù)重新對(duì)ocuupied進(jìn)行計(jì)數(shù)。這個(gè)很像哈希表這種數(shù)據(jù)結(jié)構(gòu),并且為了解決哈希沖突,使用的是開放尋址法,而開放尋址法必然要在合適的時(shí)機(jī)進(jìn)行擴(kuò)容,這個(gè)時(shí)機(jī)應(yīng)該是表快滿的時(shí)候。為了驗(yàn)證我們的猜想,還是查看cache_t的源碼進(jìn)行分析吧。
在源碼中我們發(fā)現(xiàn)了mask和occupied兩個(gè)函數(shù)。
mask_t mask();
mask_t occupied();
跳轉(zhuǎn)進(jìn)occupied函數(shù)后,源碼如下,緊隨其后的還有incrementOccupied()函數(shù)。
mask_t cache_t::occupied()
{
return _occupied;
}
void cache_t::incrementOccupied()
{
_occupied++;
}
根據(jù)上面的源碼,我們進(jìn)行全局搜索,查找調(diào)用occupied和 incrementOccupied()地方,發(fā)現(xiàn)occupied的調(diào)用有三處incrementOccupied()的調(diào)用有一處,但他們兩個(gè)都同事出現(xiàn)在了一個(gè)insert函數(shù)中,看到這個(gè)函數(shù)后我們的第一想法就是,這個(gè)函數(shù)是cache緩存的核心函數(shù),下面我們做進(jìn)一步的驗(yàn)證,再分析一下mask函數(shù)。mask()函數(shù)的實(shí)現(xiàn)如下:主要就是返回_mask的值。
mask_t cache_t::mask()
{
return _mask.load(memory_order::memory_order_relaxed);
}
下面我們就搜索一下mask(),發(fā)現(xiàn)共有三處調(diào)用,有兩處在同一函數(shù)內(nèi),有一處是返回值,所以我們重點(diǎn)分析兩處在一起的那個(gè)函數(shù),函數(shù)實(shí)現(xiàn)如下:
unsigned cache_t::capacity()
{
return mask() ? mask()+1 : 0;
}
既然沒找到mask直接在cahce中的調(diào)用與影響,那么我們可以繼續(xù)搜索一下capacity()函數(shù),這里的mask()被間接調(diào)用的可能性很大,通過搜索capacity()函數(shù)后發(fā)現(xiàn)共有四處調(diào)用,其中一處就在insert函數(shù)內(nèi),這時(shí)我們上面的猜想又得到了一些可能性。下面我們直接上insert函數(shù)的源碼作進(jìn)一步的分析吧。
insert函數(shù)源碼:
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);
}
- 排除前面的加鎖和斷言
- 首先獲取了
occupied的值在加1,在獲取mask的值,放在變量capacity內(nèi)
- 首先獲取了
- 先判斷cache是否為空,如果為空則初始化一個(gè)值
INIT_CACHE_SIZE,這里的初始化值為4,源碼放在后面,就是1左移2位,二進(jìn)制為 100, 10進(jìn)制為4,然后調(diào)用reallocate函數(shù)開辟空間。
- 先判斷cache是否為空,如果為空則初始化一個(gè)值
- 如果不大于四分之三則不作處理,(這里應(yīng)該是個(gè)擴(kuò)容算法,后面則進(jìn)一步驗(yàn)證了)
- 其他情況,也就是大于四分之三后,則對(duì)
capacity進(jìn)行擴(kuò)容,擴(kuò)容為當(dāng)前值的兩倍,并且如果擴(kuò)容后的值大于最大值MAX_CACHE_SIZE,也就是1左移16位,1 0000 0000 0000 0000, 對(duì)應(yīng)的10進(jìn)制的值是65536。則不再進(jìn)行擴(kuò)容。擴(kuò)容完畢后調(diào)用reallocate函數(shù)開辟空間。
- 其他情況,也就是大于四分之三后,則對(duì)
- 執(zhí)行完上述操作后,獲取
bucket_t和mask,并通過cache_hash函數(shù)計(jì)算出一個(gè)begin(應(yīng)該是緩存新調(diào)用方法的位置下標(biāo)),把begin的值賦值給變量i
- 執(zhí)行完上述操作后,獲取
- 通過一個(gè)
do while循環(huán),判斷計(jì)算出的位置是否為空,不為空則occupied自增,通過set方法將改類的方法進(jìn)行緩存到上面初始化的bucket里面,如果不為空則判斷bucket內(nèi)的sel是否等于要緩存的sel,直到通過cache_next計(jì)算出下一個(gè)位置不等于begin。
- 通過一個(gè)
上面提到的源碼
INIT_CACHE_SIZE 和 MAX_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_hash 和 cache_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);
}
通過對(duì)核心方法insert的分析我們大概知道了cache的基本原理與實(shí)現(xiàn),下面我們總結(jié)一下:
1.當(dāng)方法調(diào)用的時(shí)候會(huì)進(jìn)行緩存
2.緩存時(shí)需要判斷緩存是否為空,空則初始化空間,不空則判斷是否到達(dá)擴(kuò)容臨界點(diǎn),到了則擴(kuò)容,不到則直接緩存
3.緩存時(shí)則通過哈希計(jì)算緩存位置進(jìn)行存儲(chǔ)
我們知道在調(diào)用方法的時(shí)候會(huì)觸發(fā)方法的緩存,那么這倒地是怎樣一個(gè)調(diào)用堆棧呢,我么通過搜索insert(進(jìn)行查看,我們發(fā)現(xiàn)在cache_fill函數(shù)內(nèi)調(diào)用了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源碼我們發(fā)現(xiàn)主要就是獲取cache然后調(diào)用insert函數(shù)緩存方法,當(dāng)我們想進(jìn)一步通過搜索cache_fill進(jìn)行查找調(diào)用關(guān)系時(shí),卻發(fā)現(xiàn)并沒有相應(yīng)的源碼了,但是在``文件的注釋中發(fā)下了一些東西:其中cache_fill、cache_expand、cache_create、bcopy、flush_caches、cache_flush、cache_collect_free、等,這些在新的objc-cache.mm中并沒有什么蹤跡,但是在objc-cache-old.mm中卻能看見其蹤影,其實(shí)在 objc4-756中這些實(shí)現(xiàn)還是在的,應(yīng)該是蘋果通過一些優(yōu)化或者代碼的整合成了現(xiàn)在的樣子,雖然代碼在修改,但是原理基本是不變的,都是為了緩存方法。也都是通過擴(kuò)容和哈希去實(shí)現(xiàn)的。
/***********************************************************************
* 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處擴(kuò)容?
因?yàn)榇颂幘彺媸褂玫氖枪_@種數(shù)據(jù)結(jié)構(gòu),哈希中有一個(gè)叫做裝載因子的概念,表示空位的大小,在3/4處擴(kuò)容則說明裝載因子是1/4,裝載因子越大說明可能產(chǎn)生的沖突越多,這里取1/4應(yīng)該是蘋果評(píng)估的一個(gè)合理的數(shù)值。
方法緩存是有序的嗎?
因?yàn)橛昧斯#钥隙o序,這里也稍微做了一些驗(yàn)證,簡單說說吧,就不上圖已進(jìn)行說了,驗(yàn)證的對(duì)錯(cuò)也不太敢保證,只是自己的一些想法。其實(shí)在擴(kuò)容的時(shí)候原來緩存是清除了的,開辟了新的緩存來保存,在objc4-756中我記得是拷貝到新的緩存里,但是在objc4-779.1中我發(fā)現(xiàn)原來調(diào)用的方法已經(jīng)不再緩存內(nèi)了,就是通過上面的lldb驗(yàn)證的,而且第一次擴(kuò)容后的第一方法會(huì)存儲(chǔ)在最后一個(gè)位置,而不是擴(kuò)容后的第一個(gè)位置,驗(yàn)證了幾次都是這樣,然后也沒仔細(xì)探究了。我的想法是:
- 擴(kuò)容說明方法夠多,如果都調(diào)用則需要這么多空間進(jìn)行緩存
- 但是既然用到了擴(kuò)容則說明有些方法沒有頻繁調(diào)用,則觸發(fā)的緩存
- 擴(kuò)容前緩存的方法再次被調(diào)用的概率不高了,所以就沒有拷貝到新的緩存內(nèi),如果再次調(diào)用應(yīng)該會(huì)存儲(chǔ)到緩存內(nèi)
總結(jié)
- cache_t就是緩存我們OC方法的,每調(diào)用一個(gè)OC方法他就會(huì)將該方法緩存;
- 緩存的開辟從4個(gè)開始,到了3/4就開始擴(kuò)容2倍,直到65536
- 緩存內(nèi)主要存儲(chǔ)sel和imp
2.4 bits
bits是一個(gè)class_data_bits_t的結(jié)構(gòu)體,在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
放眼望去,還是那個(gè)data()最顯眼,下面我們就來研究一下它。首先讓我們來看看class_rw_t的源碼:里面除了flags、version、witness這些,主要還有個(gè)ro以及methods、properties、protocols,這個(gè)ro是一個(gè)class_ro_t類型的結(jié)構(gòu)體指針,其他看樣子是個(gè)數(shù)組,methods應(yīng)該是存儲(chǔ)方法的,properties應(yīng)該是存儲(chǔ)屬性的,protocols應(yīng)該是存儲(chǔ)協(xié)議的。下面我們來進(jìn)行驗(yàn)證。
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));
}
};
工程還是當(dāng)前的工程,分析方法依舊使用lldb,斷點(diǎn)打在獲取pClass之后。首先說明一下,要想獲取到class_data_bits_t的首地址,就要先獲取到類的首地址,然后向下偏移32個(gè)字節(jié),為什么呢?因?yàn)轭惖?isa 指針占用8字節(jié),superClass占用8字節(jié),cache通過我們的分析占用16字節(jié)所以拿到首地址后加32就是我們的bits的首地址。首先我們先打印一下data()的內(nèi)容

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

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

暫時(shí)不探索protocols
通過上面的探索,感覺確實(shí)是這樣存儲(chǔ)的,但是沒有找到類方法sayHappy,我們的成員變量hobby也沒有出現(xiàn)在其中。這個(gè)時(shí)候突然想起,剛才我們?cè)?code>class_rw_t中還發(fā)現(xiàn)了一個(gè)class_ro_t類型的ro,那么我們?cè)谔剿饕幌逻@個(gè)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;
}
}
};
這個(gè)源碼跟剛才的class_rw_t有很多類似的地方,也有方法、屬性和協(xié)議相關(guān)的東西,那么是不是這個(gè)ro也存儲(chǔ)了一些方法和屬性相關(guān)的東西呢?下面我們繼續(xù)通過lldb去查看。

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

通過上面的圖片我們看到ro內(nèi)部存儲(chǔ)的方法月rw一樣,也沒有類方法,那么他為什么要存儲(chǔ)兩份呢?這個(gè)只能通過我們后續(xù)的探索進(jìn)行考證了。那么類方法倒地存儲(chǔ)在了哪里呢?其實(shí)山重水復(fù)疑無路,柳暗花明又一村啊,我們還有元類沒探索呢,實(shí)例方法存儲(chǔ)在類中,類方法是不是存儲(chǔ)在元類中呢?
我們通過上面的方法lldb去元類里面看看
1.首先打印類的地址
2.然后取出類的isa,類的isa指向元類
3.&上isa_mask,就是元類
4.查看元類的bits中的rw和ro

果然我們?cè)谠愔姓业搅宋覀兊?code>sayHappy方法,這回就可以盡情happy了。其實(shí)實(shí)例方法都是由對(duì)象調(diào)用的,類方法由類調(diào)用,實(shí)例方法存儲(chǔ)在類中,類方法存儲(chǔ)在元類中也就不難理解了。
這回我們就找到了實(shí)例方法和類方法的存儲(chǔ)位置,下面繼續(xù)在ro里面探索一下屬性,以及我們還未找到的成員變量hobby。

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

結(jié)論:
果不其然,我們的成員變量hobby就存在于這里,并且我們還發(fā)現(xiàn)了_nickname,所以成員變量和屬性自動(dòng)生成的帶下劃線的成員變量都存儲(chǔ)在ivars里面。
至此我們的探索已經(jīng)差不多了,下面我們通過代碼來驗(yàn)證一下我們的上面的探索:
首先我們打印一下ivars和properties
實(shí)現(xiàn)代碼:
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];
//獲取實(shí)例變量名
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);
}
方法調(diào)用:
Class pClass = [LGPerson class];
testObjc_copyIvar_copyProperies(pClass);
打印結(jié)果:

實(shí)例方法打印:
實(shí)現(xiàn)代碼:
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);
}
打印結(jié)果:

判斷該類是否包含該實(shí)例方法:
實(shí)現(xiàn)代碼:
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__);
}
打印結(jié)果:

這里我們用到了
LGPerson的實(shí)例方法sayHello和其類方法sayHappy,在獲取實(shí)例方法的時(shí)候,在類中獲取到了實(shí)例方法sayHello,在元類在獲取到了sayHappy,說明類方法也是以實(shí)例方法的形式存儲(chǔ)在元類中。
下面我們?cè)诳纯搭愔惺欠癜惙椒ǎ?/strong>
實(shí)現(xiàn)代碼:
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__);
}
打印結(jié)果:

由于sayHello不是類方法,所前兩個(gè)打印是0x0,但是后面的就有些出乎我們的意料了,在上面我們通過lldb查看時(shí),并沒有在類中找到類方法,下載打印是居然是有的,那么這到底是為什么呢?我們查看了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();
}
源碼一看,一目了然,獲取類方法的實(shí)質(zhì)就是去元類里面查找元類的實(shí)例方法,上面我們也提到了,類方法本來就是已實(shí)例方法的形式存儲(chǔ)在元類中,并且在獲取元類的時(shí)候做了判斷,如果是元類就直接返回Class,如果不是就返回類的isa,其實(shí)類的isa就是元類。所以為什么打印結(jié)果是剛才的樣子也就清楚了。
全篇總結(jié)
Objective-C類有四個(gè)屬性
-
isa指向元類; -
superClass指向父類; -
cache緩存調(diào)用過的方法,并通過哈希這種數(shù)據(jù)結(jié)構(gòu)進(jìn)行擴(kuò)容,從4到65536,其中的mask作為一個(gè)掩碼,用作哈希計(jì)算時(shí)的鹽,避免哈希沖突,一直是減一的狀態(tài),所以一直不會(huì)滿,保證哈希安全,也用作記錄緩存大小,mask一直是緩存大小減1,所以獲取到mask加上1就是緩存的大小。occupied作為開辟新空間(新緩存方法)的計(jì)數(shù),以及判斷是否到了臨界點(diǎn)3/4處需要擴(kuò)容的重要條件; -
bits其中有rw存儲(chǔ)了類的實(shí)例方法(methods)和屬性(properties),rw中有個(gè)ro存儲(chǔ)了類的實(shí)例方法(baseMethodList)、屬性(baseProperties)和成員變量(ivars); - 類的類方法以實(shí)例方法的形式存儲(chǔ)在元類中。