iOS底層原理探索篇 主要是圍繞底層進行
源碼分析
-LLDB調試
-源碼斷點
-彙編調試
,讓本身之後回顧複習的😀😀數組目錄以下:緩存
iOS底層原理探索 — 開篇bash
iOS底層原理探索 — alloc&init探索iphone
經過前面篇章的探索,咱們已成功的從對象過渡到類了.但在探索類以前,還須要補充一下咱們在前面篇章中沒有細講的一些小細節.post
咱們在iOS底層原理探索 — alloc&init探索一文中留下了一個細節沒有細說,就是在分析alloc
源碼分析流程的時候,在調用callAlloc
方法時,咱們只是簡單的說了:此方法內部有一系列的判斷條件,其中因爲方法canAllocFast()
的內部調用了bits.canAllocFast()
,其返回值爲固定值false
,因此能夠肯定以後建立對象只會走class_createInstance
方法.即:callAlloc
中if (fastpath(cls->canAllocFast()))
方法不走直接走的else
後面的代碼.那麼爲何會這樣呢?來看源碼:測試
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__ //這個表示object-c 2.0 版本纔有的功能
/*
這裏的hasDefaultAWZ()方法是用來判斷當前class是否有默認的allocWithZone。
if (fastpath(!cls->ISA()->hasCustomAWZ())):
意思就是若是該類實現了allocWithZone方法,那麼就不會走if裏的邏輯,直接走如下邏輯
if (allocWithZone) return [cls allocWithZone:nil];
*/
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);//initInstanceIsa 裏面是初始化 isa 指針的操做。
return obj;
}
else {
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
複製代碼
fastpath(!cls->ISA()->hasCustomAWZ())
的決定條件就是你是否有重寫allocWithZone
的方法:即if (fastpath(!cls->ISA()->hasCustomAWZ())):
意思就是若是該類實現了allocWithZone方法,那麼就不會走if裏的邏輯,直接走if (allocWithZone) return [cls allocWithZone:nil]
;fastpath(cls->canAllocFast())
就是關於宏定義的設置:咱們沿着源碼點進去能夠看到:bool canAllocFast() {
assert(!isFuture());
return bits.canAllocFast();
}
複製代碼
順着bits.canAllocFast();
點進去能夠看到:#if FAST_ALLOC
size_t fastInstanceSize()
{
assert(bits & FAST_ALLOC);
return (bits >> FAST_SHIFTED_SIZE_SHIFT) * 16;
}
void setFastInstanceSize(size_t newSize)
{
// Set during realization or construction only. No locking needed.
assert(data()->flags & RW_REALIZING);
// Round up to 16-byte boundary, then divide to get 16-byte units
newSize = ((newSize + 15) & ~15) / 16;
uintptr_t newBits = newSize << FAST_SHIFTED_SIZE_SHIFT;
if ((newBits >> FAST_SHIFTED_SIZE_SHIFT) == newSize) {
int shift = WORD_BITS - FAST_SHIFTED_SIZE_SHIFT;
uintptr_t oldBits = (bits << shift) >> shift;
if ((oldBits & FAST_ALLOC_MASK) == FAST_ALLOC_VALUE) {
newBits |= FAST_ALLOC;
}
bits = oldBits | newBits;
}
}
bool canAllocFast() {
return bits & FAST_ALLOC;
}
#else // 通常都會走這裏
size_t fastInstanceSize() {
abort();
}
void setFastInstanceSize(size_t) {
// nothing
}
// 通常流程都會走這個false的返回
bool canAllocFast() {
return false;
}
#endif
複製代碼
通常都會走#else
後面的代碼,也就是bool canAllocFast(){return false}
.爲何會這樣呢?,這就要去看條件控制:#if FAST_ALLOC
這個宏定義的走向了. 在全局搜索宏定義FAST_ALLOC
,發現#define FAST_ALLOC (1UL<<2)
而這個宏定義外面還加了一層條件判斷:#if !__LP64__
...
#elif 1
...
#else
...
#define FAST_ALLOC (1UL<<2)
#endif
複製代碼
由於咱們的環境都是在64
位環境下,因此能夠判斷上面的判斷只會走#elif 1
裏面的代碼,而#define FAST_ALLOC
的定義是在#else
裏面,即FAST_ALLOC
永遠都不會define
了.即只會走bool canAllocFast(){return false}
,進而就有callAlloc
中if (fastpath(cls->canAllocFast()))
方法不走,直接走的else{}
裏面的代碼.即走的下面紅框裏面的代碼 優化
咱們在iOS底層原理探索 一 isa原理與對象的本質一文中有分析到,isa
的結構實際上是一個聯合體,而聯合體有一大特性,就是其內部屬性是共享同一片內存的,也就是說屬性之間都是互斥的.
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
複製代碼
所以也就能解釋初始化isa
的時候,一個分支是賦值cls
屬性,一個分支是賦值bits
屬性了.
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
assert(!isTaggedPointer());
if (!nonpointer) {
isa.cls = cls;
} else {
assert(!DisableNonpointerIsa);
assert(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
assert(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
isa = newisa;
}
}
複製代碼
咱們在探索類和元類的時候,對於其建立時機還不是很清楚,這裏咱們先拋出結論:類和元類是在編譯期建立的,即在進行alloc
操做以前,類和元類就已經被編譯器建立出來了. 那麼如何來證實呢,咱們有兩種方式來證實:
LLDB
指令打印類和元類指針main
函數開始以前打上斷點,也就沒有來到
TCJPerson *obj = [TCJPerson alloc];
,可是咱們經過
LLDB
能打印出
TCJPerson
的類和元類.這就證實了,類和元類的建立時機是在編譯期.
MachoView
軟件輔助證實:MachoView 密碼:kx8c 編譯項目後,使用MachoView
打開程序二進制可執行文件查看:
經過上面兩種方式證實了:類和元類的建立時機是在編譯期.
a
和
b
都是被賦值爲10,可是
a
和
b
內存地址是不同的,這種方式被稱爲
值拷貝
.
obj1
和
obj2
對象不光自身內存地址不同,連指向的對象的內存地址也不同,這種方式被稱爲
指針拷貝
或
引用拷貝
.
咱們能夠用一幅圖來總結上面的兩個例子:
&a
和&a[0]
的地址是相同的.即首地址就表明數組的第一個元素的地址.0x7ffeefbff400
和第二個元素地址0x7ffeefbff404
相差4個字節,也就是int
的所佔的4字節.d
、d+1
、d+2
這個地方的指針相加就是偏移地址.地址加1就是偏移,偏移一個位數所在元素的大小.OC
中的類其實也是一種對象,怎麼來證實呢,很簡單,咱們只須要用clang
命令重寫咱們的OC代碼將其轉化爲C++
代碼看其底層便可.
TCJPerson
對象,並獲取到TCJPerson
的類,而後利用LLDB
指令查看NSObject
,繼續輸出第三個發現輸出不了.Class superclass
,它表明的是繼承關係,也即證實了TCJPerson
是繼承自NSObject
的.OC
代碼轉化爲C++
代碼幫助分析原文件main.c
:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface TCJPerson : NSObject
@end
@implementation TCJPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
TCJPerson *obj = [TCJPerson alloc];
Class objClass = object_getClass(obj);
NSLog(@"%@ - %p", obj, objClass); //0x00007ffffffffff8ULL
}
return 0;
}
複製代碼
在終端執行clang
指令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
複製代碼
便可將OC
原文件main.c
轉化爲C++
文件mian.cpp
文件後可看到:
至此,咱們能夠得出一個結論,Class
類型在底層是一個結構體類型的指針,這個結構體類型爲objc_classs
. 咱們再在libObjc
的源碼中能夠找到objc_classs
的詳細定義:
objc_classs
的定義,咱們能夠知道,
objc_classs
是繼承於
objc_object
的.這就證實了
萬物皆對象,也從本質上說明
類是一種對象,而且第一個屬性是從
objc_object
上繼承而來的
isa
. 除了
isa
,類還包含了
superclass
父類:表達繼承關係;
cache
:方法緩存重要結構體;
bits
:存儲數據的結構體.
至此咱們能夠總結得出:
類是一種對象,而且幫咱們定義了一些屬性和方法.
又OC
是對C
的底層封裝,進而有下面的關係:
C | OC |
---|---|
objc_object | NSObject |
objc_class | NSObject(Class) |
最後咱們知道Class
的基本結構類型爲:
isa
是
Class
?
isa
是能夠由Class
接收的.isa
是用來返回類的,後面是經過nonpointer
區分純淨isa
和優化的isa
.return (Class)(isa.bits & ISA_MASK)
,進行了Class類型強轉.
OC
中的類都會有屬性及成員變量,那麼它們到底是怎麼存在於類裏面的呢?
這裏咱們須要對類的內存結構有一個比較清晰的認識:
類的內存結構 | 大小(字節) |
---|---|
isa | 8 |
superclass | 8 |
cache | 16 |
前面兩個的大小很好理解,由於isa
和superclass
都是結構體指針,而在arm64
環境下,一個結構體指針的內存佔用大小爲8字節.而第三個屬性cache
則須要咱們進行抽絲剝繭了. 來看源碼:
cache
屬性實際上是
cache_t
類型的結構體,其內部有一個
8
字節的結構體指針,有2個各爲4字節的
mask_t
.因此加起來就是16個字節,也就是說前三個屬性總共的內存偏移量爲 8 + 8 + 16 = 32 個字節,32 是 10 進制的表示,在 16 進制下就是 20.
利用LLDB
命令來探索類結構的第四個屬性bits
.
bits
的指針地址,就須要進行指針偏移,這裏進行一下16進制下的地址偏移計算:
0x100001200 + 0x20 = 0x100001220
複製代碼
咱們繼續打印這個地址有:
經過輸出結果,得知bits
並非一個對象,而是一個結構體,這裏須要進行強轉一下:
又由
objc_class
源碼可知,其內部有
data()
方法:
因此接着調用
data()
方法拿到
class_rw_t
:
接着咱們繼續查看
libObjc
中關於
class_rw_t
的源碼:得知
class_rw_t
也是一個結構體.
由源碼推測出相關的屬性應該存放在
properties
裏面,咱們在打印一下:
接着打印
properties
:
咦,竟然爲空.爲何會這樣呢?由於這裏咱們漏掉了一個重要的線索就是
const class_ro_t *ro;
.咱們來到其源碼:
能夠看到
ro
的類型是
class_ro_t
結構體,它包含了
baseMethodList
、
baseProtocols
、
ivars
、
baseProperties
等屬性.咱們剛纔在
class_rw_t
中沒有找到咱們聲明在
TCJPerson
類中的實例變量
titleStr
和屬性
helloName
,那麼但願就在
class_ro_t
身上了,咱們接着打印看看它的內容:
經過打印結果,咱們猜想,屬性應該存在
baseProperties
裏面,咱們接着打印看看:
嗯哼,還有誰?咱們的屬性
helloName
被找到了,就存放在
class_ro_t
的
baseProperites
裏面.咦,怎麼沒有看到咱們的實例變量
titleStr
?咱們從
$10
的
count
爲 1 能夠得知確定不在
baseProperites
裏面根.據名稱咱們猜想應該是在
$8
的
ivars
裏面.那咱們接着打印:
嗯哼,實例變量
titleStr
也找到了,那爲何這裏的
count
是2呢?咱們接着打印第二個元素看看:
結果爲
_helloName
.這一結果證明了編譯器會幫助咱們給屬性
helloName
生成一個帶下劃線前綴的實例變量
_helloName
. 至此,咱們能夠處處一下結論:
class_rw_t
是能夠在運行時來拓展類的一些屬性、方法和協議等內容.class_ro_t
是在編譯時就已經肯定了的,存儲的是類的成員變量、屬性、方法和協議等內容.研究完了類的屬性是怎麼存儲的,咱們再來看看類的方法又是怎麼存儲的. 在TCJPerson
類裏面增長一個readBook
的實例方法和一個writeBook
的類方法.
class_ro_t
中的
baseMethodList
的內容:
嗯哼,
readBook
方法被找出來了,這說明
baseMethodList
就是存儲實例方法的地方.咱們接着打印剩下的內容:
能夠看到
baseMethodList
中除了咱們的實例方法
readBook
外,還有屬性
helloName
的
getter
和
setter
方法以及一個
C++
析構方法.而咱們的類方法
writeBook
並無被打印出來.那麼類方法存儲在哪呢?
咱們上面已經獲得了屬性,實例方法是怎麼樣存儲的了,可是還留下了一個疑問點,就是類方法是怎麼存儲的,接下來咱們用 Runtime
的 API
來實際測試一下.
testInstanceMethod_classToMetaclass
方法測試的是分別從類和元類去獲取實例方法、類方法的結果.由打印結果咱們能夠知道:
readBook
是實例方法,存儲於類對象的內存中,不存在於元類對象中.而 writeBook
是類方法,存儲於元類對象的內存中,不存在於類對象中.readBook
是類對象的實例方法,跟元類不要緊;writeBook
是元類對象的實例方法,因此存在元類中. 咱們再測試另外的一個方法:
從結果咱們能夠看出,對於類對象來講,經過 class_getClassMethod
獲取writeBook
是有值的,而獲取 readBook
是沒有值的;對於元類對象來講,經過 class_getClassMethod
獲取writeBook
也是有值的,而獲取 readBook
是沒有值的.這裏第一點很好理解,可是第二點會有點讓人糊塗,不是說類方法在元類中是體現爲對象方法的嗎?怎麼經過 class_getClassMethod
從元類中也能拿到 writeBook
,咱們進入到 class_getClassMethod
方法內部能夠解開這個疑惑:
class_getClassMethod
方法底層其實調用的是
class_getInstanceMethod
,而
cls->getMeta()
方法底層的判斷邏輯是若是已是元類就返回,若是不是就返回類的
isa
.這也就解釋了上面的
writeBook
爲何會出如今最後的打印中了. 除了上面的這種方式,咱們還能夠經過
isa
的方式來驗證類方法存放在元類中.
經過 isa
在類對象中找到元類.
打印元類的 baseMethodsList
. 咱們也來驗證一下: 首先咱們獲取objClass
的內存段:
& ISA_MASK
拿到其元類,而且打印其內存段:
接着按照上面類的屬性存儲探索的思路,進行指針偏移,獲取bits
屬性:這裏進行一下16進制下的地址偏移計算: 0x100001280 + 0x20 = 0x1000012a0
複製代碼
查找步驟都在圖中標明瞭.這也驗證了類方法存放在元類中. LLDB
來打印類和元類的指針,或者用 MachOView
軟件查看二進制可執行文件class_ro_t
結構中存儲了編譯時肯定的屬性、成員變量、方法和協議等內容,而且對於屬性helloName
:底層編譯會生成相應的setter
、getter
方法,且幫咱們轉化爲_helloName
,對於成員變量titleStr
:底層編譯不會生成相應的setter
、getter
方法,且沒有轉化爲_titleStr
在這一章中咱們完成了對 iOS
中類的結構的探索,下一章咱們將對類的緩存進行探索,敬請期待~