isa 走點陣圖 在講 OC->Class 底層類結構之前,先看下下麵這張圖: 通過isa走點陣圖 得出的結論是: 1,類,父類,元類都包含了 isa, superclass 2,對象isa指向類對象,類對象的isa指向了元類,元類的 isa 指向了根元類,根元類 isa 指向自己 3,類的 super ...
isa 走點陣圖
在講 OC->Class
底層類結構之前,先看下下麵這張圖:
通過isa走點陣圖 得出的結論是:
1,類,父類,元類都包含了 isa
, superclass
2,對象isa指向類對象,類對象的isa指向了元類,元類的 isa
指向了根元類,根元類 isa
指向自己
3,類的 superclass
指向父類,父類的 superclass
指向的根類,根類的superclass
指向的nil
4,元類的 superclass
指向父元類,父元類 superclass
指向的根元類,根元類 superclass
指向根類,根類 superclass
指向nil
這下又複習了 isa
,superclass
走位;那麼問題這些類,類對象,元類對象當中的在底層展現的數據結構是怎樣呢,這是我需要探索的,於是把源碼貼出來展開分析下:
struct objc_class
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 *data() const {
return bits.data();
}
const class_ro_t *safe_ro() const {
return bits.safe_ro();
}
}
從源碼沒見 isa
屬性,其實它繼承了objc_object
,而 objc_object
中有個isa
,在運行時類圖生成中會產生一個isa
指向objc_object
這個類圖,而 superclass
指向它的父類;根據上面 isa
, superclass
走點陣圖就知道它的指向關係。
cache_t & class_data_bits_t
cache
方法緩存,這個作用將常調用的方法緩存下來;便於下次直接查找調用,提高查找效率。
它的結構:
struct cache_t {
struct bucket_t *buckets() const;//存儲方法的散列表
mask_t mask() const;//散列表緩存長度
mask_t occupied() const;//已緩存方法個數
}
struct class_data_bits_t {
class_rw_t* data() const;//類信息
}
bits
存儲具體類信息,它需要&FAST_DATA_MASK來計算得到類心所有信息,源碼如下:
FAST_DATA_MASK 掩碼值
bool has_rw_pointer() const {
#if FAST_IS_RW_POINTER
return (bool)(bits & FAST_IS_RW_POINTER);
#else
class_rw_t *maybe_rw = (class_rw_t *)(bits & FAST_DATA_MASK);
return maybe_rw && (bool)(maybe_rw->flags & RW_REALIZED);
#endif
}
通過源碼確實需要這種方式計算能得到類的存儲信息;那為什麼要用這種方式去處理呢。
比如說我要得到存儲在 class_rw_t
類信息信息我只要通過 FAST_DATA_MASK
掩碼值就能得到它的地址信息,通過地址信息就能從記憶體中拿到所有類的存儲信息。
那這樣我的FAST_DATA_MASK
掩碼值不一樣,我通過&
計算,得到的數據信息也就不一樣,不得不說蘋果工程師想的周到,而且這種方式不僅isa也是這樣,很多地方都用這種方式取值,大大提高訪問速度,數據提取效率。
class_rw_t ,class_ro_t,class_rw_ext_t
struct class_rw_t {
const class_ro_t *ro() const ;
const method_array_t methods() const ;//如果是類對象:放對象方法,元類:元類對象方法
const property_array_t properties() const;
const protocol_array_t protocols() const;
class_rw_ext_t *ext() const;
}
struct class_rw_ext_t {
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
uint32_t version;
}
可以看出類的信息具體就存儲在class_rw_t
,class_ro_t
,class_rw_ext_t
中,
剖析下class_rw_t
先看看method_array_t
,property_array_t
,protocol_array_t
源碼結構
class property_array_t :
public list_array_tt<property_t, property_list_t, RawPtr>
{
typedef list_array_tt<property_t, property_list_t, RawPtr> Super;
public:
property_array_t() : Super() { }
property_array_t(property_list_t *l) : Super(l) { }
};
class protocol_array_t :
public list_array_tt<protocol_ref_t, protocol_list_t, RawPtr>
{
typedef list_array_tt<protocol_ref_t, protocol_list_t, RawPtr> Super;
public:
protocol_array_t() : Super() { }
protocol_array_t(protocol_list_t *l) : Super(l) { }
};
看完之後,他們都繼承list_array_tt
,那麼 list_array_tt
是什麼鬼,它數據結構是怎樣的,這下在取找下它。源碼如下:
template <typename Element, typename List, template<typename> class Ptr>
class list_array_tt {
protected:
template <bool authenticated>
class iteratorImpl {
const Ptr<List> *lists;
const Ptr<List> *listsEnd;
}
using iterator = iteratorImpl<false>;
using signedIterator = iteratorImpl<true>;
public:
list_array_tt() : list(nullptr) { }
list_array_tt(List *l) : list(l) { }
list_array_tt(const list_array_tt &other) {
*this = other;
}
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
array_t *newArray =(array_t*)malloc(array_t::byteSize(newCount));
newArray->count = newCount;
array()->count = newCount;
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
for (unsigned i = 0; i < addedCount; i++)
newArray->lists[i] = addedLists[i];
free(array());
setArray(newArray);
validate();
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
validate();
}
else {
// 1 list -> many lists
Ptr<List> oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
for (unsigned i = 0; i < addedCount; i++)
array()->lists[i] = addedLists[i];
validate();
}
}
}
我把主要地方拿去出來,可以看到 attachLists
它的目的是將一個或多個列表(List
類型)附加到某個 list_array_tt
對象中。這個對象可以包含零個、一個或多個列表,這些列表可以是單個指針,也可以是指針數組。函數的輸入參數是一個指向 List
指針數組的指針 addedLists
和一個無符號整數 addedCount
,表示要添加的列表數量。
由此我推斷它是一個數組,而且是一個二維數組存儲的,所有由此得出 class_rw_t
中methods
,properties
,protocols
這幾個屬性利用二維數組取存儲類的方法,協議等信息,而且是可讀可寫的屬性。
那它設計這種二維數組有什麼好處呢?當然有好處,它可以動態的給數組裡面增加刪除方法,很方便我們分類方法的編寫完進行存儲。
那搞清楚了 class_rw_t
幾個重要數據存儲信息,那 class_rw_t
它的作用是乾什麼的呢;
從class_rw_t
結構體定義來看;它是在應用運行時,將OC類,分類的信息直接寫入到class_rw_t
結構的數據結構中,在類的方法,協議進行調用時,從裡面去讀取,然後常調用的方法,又存儲在cache_t這個結構體中,可想而知,蘋果對OC類的處理,煞費苦心。
struct class_ro_t
在 class_rw_t
結構體中有個 class_ro_t
結構體,在探索下這個東西做什麼的,它的源碼如下:
struct class_ro_t {
WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
property_list_t *baseProperties;
}
先說說 ivars
這個屬性修飾的結構體源碼如下:
struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
bool containsIvar(Ivar ivar) const {
return (ivar >= (Ivar)&*begin() && ivar < (Ivar)&*end());
}
};
這個貌似只有一個繼承 entsize_list_tt
,那在探索下源碼:
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
struct iteratorImpl {
uint32_t entsize;
uint32_t index; // keeping track of this saves a divide in operator-
using ElementPtr = std::conditional_t<authenticated, Element * __ptrauth(ptrauth_key_process_dependent_data, 1, 0xdead), Element *>;
ElementPtr element;
typedef std::random_access_iterator_tag iterator_category;
typedef Element value_type;
typedef ptrdiff_t difference_type;
typedef Element* pointer;
typedef Element& reference;
iteratorImpl() { }
iteratorImpl(const List& list, uint32_t start = 0)
: entsize(list.entsize())
, index(start)
, element(&list.getOrEnd(start))
{ }
}
}
可以看出這段代碼定義了一個結構體 entsize_list_tt
,它內部包含一個嵌套的結構體 iteratorImpl
,用於實現一個迭代器。遍歷容器(如列表、數組等)的對象。
到此可以得出ivars
是一個 ivar_list_t
數組,它存儲了類的屬性變數信息,那protocol_list_t
結構體內部也是數組形式構建的。
baseProtocols
,baseProperties
這兩個屬性對類的存儲信息只能讀取,不能寫入。
所以總結的是:從 class_ro_t
結構體定義來看,它存儲類的變數,方法,協議信息,而且這個結構體屬於類的只讀信息,它包含了類的初始信息。
class_rw_ext_t
這個結構體不在過多敘述,簡單來說它是基於 class_rw_t
之後為了更好管理oc類的高級特性,比如關聯屬性等,衍生出來的一個結構體,包括:method_array_t
,property_arrat_t
,protocol_array_t
等定義屬性類型
到這裡類結構及存儲所關聯的信息都在這裡了;來一張他們關聯的結構思維圖:
總結:一開始編譯時,程式將類的初始信息放在 class_ro_t
中,當程式運行時,將類的信息合併在一起的時候,它會將 class_ro_t
類的信息合併到 class_rw_t
結構體中去。
struct method_t
為什麼要說method_t,因為它不僅在 class_ro_t
有使用,在OC底層其他地方也有使用;比如如下源碼:
void method_exchangeImplementations(Method m1Signed, Method m2Signed)
{
if (!m1Signed || !m2Signed) return;
method_t *m1 = _method_auth(m1Signed);
method_t *m2 = _method_auth(m2Signed);
mutex_locker_t lock(runtimeLock);
IMP imp1 = m1->imp(false);
IMP imp2 = m2->imp(false);
SEL sel1 = m1->name();
SEL sel2 = m2->name();
m1->setImp(imp2);
m2->setImp(imp1);
// RR/AWZ updates are slow because class is unknown
// Cache updates are slow because class is unknown
// fixme build list of classes whose Methods are known externally?
flushCaches(nil, __func__, [sel1, sel2, imp1, imp2](Class c){
return c->cache.shouldFlush(sel1, imp1) || c->cache.shouldFlush(sel2, imp2);
});
adjustCustomFlagsForMethodChange(nil, m1);
adjustCustomFlagsForMethodChange(nil, m2);
}
static IMP
_method_setImplementation(Class cls, method_t *m, IMP imp)
{
lockdebug::assert_locked(&runtimeLock);
if (!m) return nil;
if (!imp) return nil;
IMP old = m->imp(false);
SEL sel = m->name();
m->setImp(imp);
// Cache updates are slow if cls is nil (i.e. unknown)
// RR/AWZ updates are slow if cls is nil (i.e. unknown)
// fixme build list of classes whose Methods are known externally?
flushCaches(cls, __func__, [sel, old](Class c){
return c->cache.shouldFlush(sel, old);
});
adjustCustomFlagsForMethodChange(cls, m);
return old;
}
方法交換,實現中底層都有用到,我們探索下,先看看 method_t
源碼:
struct method_t {
// The representation of a "big" method. This is the traditional
// representation of three pointers storing the selector, types
// and implementation.
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
// A "big" method, but name is signed. Used for method lists created at runtime.
struct bigSigned {
SEL __ptrauth_objc_sel name;
const char * ptrauth_method_list_types types;
MethodListIMP imp;
};
// ***HACK: This is a TEMPORARY HACK FOR EXCLAVEKIT. It MUST go away.
// rdar://96885136 (Disallow insecure un-signed big method lists for ExclaveKit)
#if TARGET_OS_EXCLAVEKIT
struct bigStripped {
SEL name;
const char *types;
MethodListIMP imp;
};
#endif
}
可以看到這結構體中掐套了多個結構體;在把它簡化下:
struct method_t {
SEL name;//方法名
const char *types;//包含函數具有參數編碼的字元串類型的返回值
MethodListIMP imp;//函數指針(指向函數地址的指針)
}
SEL
:函數名,沒特別的意義;
特點:
1,使用@selector()
,sel_registerName()
獲得
2,使用sel_getName()
,NSStringFromSelector()
轉成字元串
3,不同類中相同名字方法,對應的方法選擇器是相同或相等的
底層代碼結構:
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
types
:包含了函數返回值、參數編碼的字元串
可以看到types在值:v16@0:8 ,可以看出name,types,IMP其實都在class_ro_t結構體中,這樣確實證明瞭之前說的;class_ro_t結構體在運行時存儲著類的初始狀態數據。
v16@0:8說明下:
v:方法返回類型,這裡說void,
16:第一個參數,
@:id類型第二個參數,
0:第三個參數
: :selector類型
8:第四個參數
那這種types參數又是什麼鬼東西,查下了資料這叫:Type Encoding(類型編碼)
怎麼證明瞭,使用如下代碼:
蘋果官網types encoding表格:
IMP
其實就是指向函數的指針,感覺這個就沒有必要講了。
struct cache_t
cache_t
用於 class
的方法緩存,對class
常調用的方法緩存下來,提高查詢效率,這個上之前都已經說過;接下來看看 bucket_t
。
struct bucket_t
struct bucket_t {
cache_key_t _key;//函數名
IMP _imp;//函數記憶體地址
}
這種散列表的模型,其實在底層用一個數組展現:
其實它的內部就是一個一維數組,那可能問了,數組難道它是迴圈查找嗎,其實不然;在它元素超找時,它是拿到你的 函數名 & mask
,而這個 mask
就是 cache_t
結構體中的 mask
值;計算得到函數在 散列表
存儲的索引值,在通過索引拿到函數地址,進行執行。
接下來看個事例:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu=[Student new];
[stu test];
[stu test];
[stu test];
[stu test];
}
return 0;
}
如上方法:當首次調用它會去類對象中查找,在方法執行時,他會放入cache_t
緩存中,當第二次,第三次,第四次時,它就去緩存中查找。
當方法執行後;我們看到 _mask
是:3,這個3代表了我類中定義了三個函數;而——_occupied
是一個隨意的值;它其實代表了換存方法的個數。
那如何知道方法有緩存了,再繼續往下執行:
這時候執行完 test02, _mask
的值從 3
變成了 7
,說明散列表 bucket_t
做了擴容操作。在這裡bucket_t
元素需要 _mask
個元素,所以最終 bucket_t
從原有的3個元素進行了 2倍
擴容。
在看下方法是否進行緩存:
可以看見當執行完 [stu test02] 時,數據做了擴容,並且擴容的數據使用(null)
進行填充。
在看個事例:
在執行 [stu test] 之前;其實bucket_t
就3個元素,並且存入了 init
方法;
當執行完 [stu test] 之後;就存入 test
方法。
但是註意的地方:它在擴容時對之前的緩存進行清除。
通過查看源碼,我們知道了它如何進行清除操作,
當執行完 [stu test02]; ,[stu test03]; 之後,它先將緩存清空;這時候 init
, test
方法被清空,bucket_t
擴容完在存儲:test02
,test03
方法。
那問題又來了,它是如何快速定位到方法的,然後執行的?接下來看看代碼:
可以清楚看見,當我使用 @selector(test03)&stu_cache._mask
就可以得到下標,然後再從 bucket_t
拿到方法。
到這裡 class結構,類的方法緩存到此結束了,從上面也可以思考下:如果自己去實現散列表數組
,是不是思路就跟清晰了。