iOS OC底層探索
注: 本文使用的環境是objc4-779.1 Xcode 11.5 (11E608c)
1. 類
根據前面幾篇文章的分析,我們知道Objective -C的對象通過isa與類關聯起來,那么到底什么是類呢?下面我們來探索一下。
我們知道Objective-C的基類是NSObject,日常開發中我們我們使用到的類基本都是用NSObject派生來的,那么在編譯后,他到底是什么樣子呢?
- 我的另一篇文章:通過Clang 看OC對象的本質
在這篇文章中我們說道Class在底層是一個objc_class
那么它到底是如何實現的呢?我們來到objc源碼中一探究竟。我們知道objc_class
是一個結構體我們搜索struct objc_class
,我們發現會有很多結果,那么我們到底去分析那個版本呢,我們應該知道runtime有old和new兩個版本,那么新版本當然作為我們的首選,所有我們打開objc-runtime-new.h進行一探究竟。
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_t
的mask
,uint16_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查看,結果如下:
特別提醒!
類的isa占用8字節,superclass占用8字節,所以我們對cache的分析由首地址加16進行分析,16字節在16進制中就是加10。
根據圖一我們可以看到在沒有執行方法前我們的cache_t
的buckets()
里面是取不出數據的直接是個null,當我們執行完sayHello
方法后再buckets里面取出的數據,并通過打印sel獲取到了一個叫sayHello
的SEL,在后面的兩個圖里面我們分別執行了sayCode
和sayNB
方法后也分別獲取到了sayCode
和sayNB
的SEL,當我們越界獲取的時候就是空了,所以我們通過lldb
分析可知,cahce主要是緩存方法的。那么為什么沒有找到alloc
和class
方法呢?因為它們是類方法,會存儲在元類里面,在本文的后續過程中我們會進一步分析。
- 補充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
就滿了,將mas
k進行擴容后,繼續重新對ocuupied
進行計數。這個很像哈希表這種數據結構,并且為了解決哈希沖突,使用的是開放尋址法,而開放尋址法必然要在合適的時機進行擴容,這個時機應該是表快滿的時候。為了驗證我們的猜想,還是查看cache_t
的源碼進行分析吧。
在源碼中我們發現了mask
和occupied
兩個函數。
mask_t mask();
mask_t occupied();
跳轉進occupied
函數后,源碼如下,緊隨其后的還有incrementOccupied()
函數。
mask_t cache_t::occupied()
{
return _occupied;
}
void cache_t::incrementOccupied()
{
_occupied++;
}
根據上面的源碼,我們進行全局搜索,查找調用occupied
和 incrementOccupied()
地方,發現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);
}
- 排除前面的加鎖和斷言
- 首先獲取了
occupied
的值在加1,在獲取mask
的值,放在變量capacity內
- 首先獲取了
- 先判斷cache是否為空,如果為空則初始化一個值
INIT_CACHE_SIZE
,這里的初始化值為4,源碼放在后面,就是1左移2位,二進制為 100, 10進制為4,然后調用reallocate
函數開辟空間。
- 先判斷cache是否為空,如果為空則初始化一個值
- 如果不大于四分之三則不作處理,(這里應該是個擴容算法,后面則進一步驗證了)
- 其他情況,也就是大于四分之三后,則對
capacity
進行擴容,擴容為當前值的兩倍,并且如果擴容后的值大于最大值MAX_CACHE_SIZE
,也就是1左移16位,1 0000 0000 0000 0000, 對應的10進制的值是65536。則不再進行擴容。擴容完畢后調用reallocate
函數開辟空間。
- 其他情況,也就是大于四分之三后,則對
- 執行完上述操作后,獲取
bucket_t
和mask
,并通過cache_hash
函數計算出一個begin
(應該是緩存新調用方法的位置下標),把begin
的值賦值給變量i
- 執行完上述操作后,獲取
- 通過一個
do while
循環,判斷計算出的位置是否為空,不為空則occupied
自增,通過set
方法將改類的方法進行緩存到上面初始化的bucket
里面,如果不為空則判斷bucket
內的sel
是否等于要緩存的sel,直到通過cache_next
計算出下一個位置不等于begin
。
- 通過一個
上面提到的源碼
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);
}
通過對核心方法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_fill
、cache_expand
、cache_create
、bcopy
、flush_caches
、cache_flush
、cache_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
的源碼:里面除了flags
、version
、witness
這些,主要還有個ro
以及methods
、properties
、protocols
,這個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()
的內容
打印完我們看到了源碼中的很多東西都打印出來了,迫不及待的我們趕緊打印一下methods
看看,看到打印出來的是個method_array_t
的類型,里面還有個list
我們不妨看看這個list
里面是否存儲的就是我們的方法,打印list
后得到的是一個method_list_t
類型的指針,既然是list
指針,那么首地址應該是第一元素吧,我們通過p *
去打印,發現確實打印出了我們的sayHello
方法,我么繼續打印,分別打印出了sayCode
、sayNB
、sayMaster
以及一些C++的方法,還有屬性的setter和get方法,那么我們的方法原理是什么呢,為什么要把方法存儲在這里呢,我們還不知道,在后面的探索中我們會繼續研究這些。
下面我們看看屬性 properties
,按照上述步驟打印,第一個就是我們的nickname
,后面就沒有了,那么我們測成員變量hobby
去哪了呢?
暫時不探索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
去查看。
通過lldb
打印我們看見與其源碼內的內容一樣,下面我們來探索一下方法baseMethodList
通過上面的圖片我們看到ro
內部存儲的方法月rw
一樣,也沒有類方法,那么他為什么要存儲兩份呢?這個只能通過我們后續的探索進行考證了。那么類方法倒地存儲在了哪里呢?其實山重水復疑無路,柳暗花明又一村啊,我們還有元類沒探索呢,實例方法存儲在類中,類方法是不是存儲在元類中呢?
我們通過上面的方法lldb
去元類里面看看
1.首先打印類的地址
2.然后取出類的isa
,類的isa
指向元類
3.&上isa_mask
,就是元類
4.查看元類的bits
中的rw
和ro
果然我們在元類中找到了我們的sayHappy
方法,這回就可以盡情happy了。其實實例方法都是由對象調用的,類方法由類調用,實例方法存儲在類中,類方法存儲在元類中也就不難理解了。
這回我們就找到了實例方法和類方法的存儲位置,下面繼續在ro
里面探索一下屬性,以及我們還未找到的成員變量hobby
。
通過查看存儲在ro
里面的baseProperties
其內容跟rw
也是一樣的,依舊沒有我們的hobby
,這個和類方法的思路不太一樣,成員變量也是類里面的,我們暫不考慮去元類里面找成員變量。這時候我們發現還有個ivars
可能存在我們想要的東西,下面我們查看一下ivars
:
結論:
果不其然,我們的成員變量hobby
就存在于這里,并且我們還發現了_nickname
,所以成員變量和屬性自動生成的帶下劃線的成員變量都存儲在ivars
里面。
至此我們的探索已經差不多了,下面我們通過代碼來驗證一下我們的上面的探索:
首先我們打印一下ivars
和properties
實現代碼:
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);
打印結果:
實例方法打印:
實現代碼:
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);
}
打印結果:
判斷該類是否包含該實例方法:
實現代碼:
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__);
}
打印結果:
這里我們用到了
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__);
}
打印結果:
由于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
類有四個屬性
-
isa
指向元類; -
superClass
指向父類; -
cache
緩存調用過的方法,并通過哈希這種數據結構進行擴容,從4到65536,其中的mask
作為一個掩碼,用作哈希計算時的鹽,避免哈希沖突,一直是減一的狀態,所以一直不會滿,保證哈希安全,也用作記錄緩存大小,mask一直是緩存大小減1,所以獲取到mask
加上1就是緩存的大小。occupied
作為開辟新空間(新緩存方法)的計數,以及判斷是否到了臨界點3/4處需要擴容的重要條件; -
bits
其中有rw
存儲了類的實例方法(methods)和屬性(properties),rw
中有個ro
存儲了類的實例方法(baseMethodList)、屬性(baseProperties)和成員變量(ivars); - 類的類方法以實例方法的形式存儲在元類中。