在iOS開發(fā)的過程中昧辽,我們最熟悉的就是對象瘪校,經(jīng)常會(huì)使用到的一個(gè)函數(shù):
alloc
,那這個(gè)函數(shù)的底層到底做了什么呢 九孩?我們一起一探究竟先馆。
開始探索前,先看一下探索過程中可能用到的一些指令躺彬!
一煤墙、常用指令
1. po: 為 print object 的縮寫,顯示對象的文本描述
2. bt: 打印函數(shù)的堆棧
3. register read 讀取寄存器
4. x/nuf
n表示要顯示的內(nèi)存單元的個(gè)數(shù)
u表示一個(gè)地址單元的長度:
取值范圍:
b 單字節(jié)
h 表示雙字節(jié)
w 表示四字節(jié)
g 表示八字節(jié)
f表示顯示方式:
取值范圍:
x 按十六進(jìn)制格式
d 按十進(jìn)制格式
u 按十進(jìn)制格式顯示無符號
o 按八進(jìn)制格式
t 按二進(jìn)制格式
a 按十六進(jìn)制格式
i 指令地址格式
c 按字符格式
f 按浮點(diǎn)數(shù)格式
持續(xù)更新中...
二宪拥、alloc做了什么仿野?
通過以下代碼我們可以知道alloc是向系統(tǒng)申請內(nèi)存空間
JLPerson *p1 = [JLPerson alloc];
JLPerson *p2 = [p1 init];
JLPerson *p3 = [p1 init];
NSLog(@"%@-%p-%p",p1,p1,&p1);
NSLog(@"%@-%p-%p",p2,p2,&p2);
NSLog(@"%@-%p-%p",p3,p3,&p3);
-----------------------------------------------------------
<JLPerson: 0x6000032c8020>-0x6000032c8020-0x7ffeef26e1a8
<JLPerson: 0x6000032c8020>-0x6000032c8020-0x7ffeef26e1a0
<JLPerson: 0x6000032c8020>-0x6000032c8020-0x7ffeef26e198
從上面的代碼中可以看出p1、p2她君、p3的指針地址之間是相差8個(gè)字節(jié)脚作,并且地址是連續(xù)
的,這就符合棧內(nèi)存的分配原則
。
根據(jù)代碼的演示我們可以得到以下的圖示球涛。
總結(jié):指針地址是在
棧內(nèi)存
劣针,申請的內(nèi)存空間在堆內(nèi)存
。
三亿扁、alloc底層是怎么調(diào)用捺典?
我們已經(jīng)知道了alloc
是申請內(nèi)存空間,那么它是怎么申請內(nèi)存的呢从祝?申請多少內(nèi)存空間襟己?內(nèi)存的大小怎么計(jì)算?
帶著這些問題往下探索牍陌。
三種探索底層的方式:
- 下符號斷點(diǎn)的形式直接跟流程: Symbolic Breakpoint
- 按住control -> step into
- 匯編查看跟流程:Debug -> Debug workflow -> Always show Disassembly
通過上面三種方式我們知道了alloc
底層是屬于libobjc
庫稀蟋,我們將源碼下載編譯跑起來。
蘋果開源源碼匯總: https://opensource.apple.com
這個(gè)地址?的更直接: https://opensource.apple.com/tarballs/
-
發(fā)現(xiàn)問題
首先我們對下載的源碼進(jìn)行編譯呐赡,對alloc函數(shù)
進(jìn)行斷點(diǎn)跟蹤(也可以使用符號斷點(diǎn)或者匯編的方式進(jìn)行),按照正常的思維流程應(yīng)該是響應(yīng)alloc函數(shù)的底層調(diào)用
骏融,但是真正的調(diào)試卻是走了objc_alloc
链嘀,這是為什么呢?
-
探索問題
-
通過對源碼進(jìn)行全局搜索
objc_alloc
档玻,對結(jié)果一個(gè)個(gè)解讀我們可以從中發(fā)現(xiàn)一個(gè)函數(shù)fixupMessageRef
怀泊,里面有一個(gè)if (msg->sel == @selector(alloc))
判斷,滿足條件就是msg
指向的imp
替換成objc_alloc
误趴。
objc_alloc.png 既然找到了
fixupMessageRef
霹琼,那么我順著這條思路找一找fixupMessageRef
是什么時(shí)候調(diào)用的呢?
通過逆向的查找我們可以得出以下的一個(gè)調(diào)用流程:
fixupMessageRef
<--_read_images
<--map_images_nolock
<--map_images
<--_dyld_objc_notify_register
<--_objc_init
把這些函數(shù)全部打上斷點(diǎn)凉当,運(yùn)行程序枣申,看是否如我們所想的那樣進(jìn)行了IMP的替換
;運(yùn)行后發(fā)現(xiàn)還是會(huì)走objc_alloc方法看杭,但是并沒有走fixupMessageRef
方法進(jìn)行替換忠藤。為什么會(huì)提供一個(gè)不被執(zhí)行的修復(fù)函數(shù)呢?難道是因?yàn)樵诰幾g的過程中就有可能發(fā)生問題楼雹,然后做一個(gè)容錯(cuò)的處理嗎模孩?找到
LLVM
的源碼,通過解讀LLVM的源碼可以得出alloc贮缅、release榨咐、autoRelease
等一些方法在編譯的過程中LLVM
會(huì)對這些函數(shù)進(jìn)行Hook
攔截
我們已經(jīng)知道了為什么要走objc_alloc
方法了,那對于alloc
主線的流程通過斷點(diǎn)方式跟下來就可以了谴供。
-
alloc調(diào)用流程圖:
- alloc的主線流程圖我們已經(jīng)比較清晰了块茁,接下來我們重點(diǎn)看一下
_class_createInstanceFromZone
這個(gè)函數(shù)的實(shí)現(xiàn)。
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
...
size_t size;
# 計(jì)算當(dāng)前類需要開辟的內(nèi)存空間大小
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
# 申請內(nèi)存空間
obj = (id)calloc(1, size);
}
...
if (!zone && fast) {
# 將類cls和obj指針進(jìn)行關(guān)聯(lián)
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
- 我們重點(diǎn)看一下
instanceSize
這個(gè)函數(shù)憔鬼,主要用來計(jì)算當(dāng)前類需要開辟的內(nèi)存空間大小龟劲。
看一下函數(shù)的整個(gè)流程圖:
字節(jié)對齊算法:
問題1:為什么alloc第一次會(huì)進(jìn)objc_alloc胃夏,然后才會(huì)進(jìn)去_objc_rootAlloc ?
(LLVM底層對objc_alloc進(jìn)行攔截)
擴(kuò)展:
- init 初始化,使用
工廠模式
昌跌,可以對其進(jìn)行重寫
仰禀,用來擴(kuò)展 - new 底層是
alloc
和init
的組合,直接使用new
相對于使用alloc init
擴(kuò)展性更差了
四蚕愤、內(nèi)存對齊原則
前言:
1.屬性和成員變量會(huì)影響內(nèi)存大小答恶,方法不影響內(nèi)存大小
2.oc對象開辟內(nèi)存空間大小是以16字節(jié)對齊
,對象的成員變量的字節(jié)是以8字節(jié)對齊
各類型字節(jié)大衅加铡:
問題1:為什么需要字節(jié)對齊悬嗓?
- 通常內(nèi)存是由字節(jié)組成,cpu在存取數(shù)據(jù)時(shí)裕坊,是以
塊
為單位存取包竹,塊
的大小決定了內(nèi)存存取的力度。頻繁的存取未對齊的數(shù)據(jù)籍凝,會(huì)降低cpu的性能周瞎,所以可以通過內(nèi)存對齊
的方式來減少存取次數(shù)
,從而達(dá)到降低cpu的開銷
,以空間
來換取時(shí)間
饵蒂。
問題2:為什么oc對象開辟內(nèi)存空間是以16字節(jié)對齊声诸?
- 由于在一個(gè)對象中,第一個(gè)屬性
isa
占8字節(jié)
退盯,一個(gè)對象中肯定還會(huì)包含其他的屬性
和成員變量
彼乌,系統(tǒng)會(huì)預(yù)留8字節(jié)
,即16字節(jié)對齊
渊迁,而如果是8字節(jié)對齊
的話慰照,該對象的isa
和下一個(gè)對象的isa
緊挨著,訪問時(shí)容易造成訪問混亂琉朽。 - 16字節(jié)對齊焚挠,可以加快
cpu讀取速度
,也可以使訪問更加安全
漓骚。
下面我們看一下結(jié)構(gòu)體的內(nèi)存對齊
struct LGStruct1 {
double a; // 8 [0 7]
char b; // 1 [8]
int c; // 4 [12 13 14 15] (9 10 11空3個(gè)字節(jié) 12是4的倍數(shù))
short d; // 2 [16 17]
}struct1;
# 根據(jù)字節(jié)對齊是8字節(jié)原則 8的倍數(shù)最后為 24
struct LGStruct2 {
double a; // 8 [0 7]
int b; // 4 [8 9 10 11] (8是4的倍數(shù))
char c; // 1 [12]
short d; // 2 [14 15] (13 空1個(gè)字節(jié) 14是2的倍數(shù))
}struct2;
# 根據(jù)字節(jié)對齊是8字節(jié)原則 8的倍數(shù)最后為 16
從上述代碼中可以看出蝌衔,結(jié)構(gòu)體的屬性都一樣,屬性的順序不一樣蝌蹂,內(nèi)存大小也不一樣噩斟。
上面是單個(gè)結(jié)構(gòu)體的內(nèi)存對齊,如果結(jié)構(gòu)體嵌套又是怎樣的呢孤个?
struct LGStruct1 {
double a; // 8
char b; // 1
int c; //4
short d; // 2
}struct1;
struct LGStruct3 {
double a; //8 [0 --> 7]
int b; //4 [8 9 10 11 ]
char c; //1 [12]
short d; //2 [14 15]
int e; //4 [16 17 18 19]
struct LGStruct1 {
double a; // 8 [24 --> 31]
char b; // 1 [32]
int c; //4 [36 37 38 39]
short d; // 2 [40 41]
}str;
}struct3;
# 將struct3進(jìn)行展開, 根據(jù)8字節(jié)內(nèi)存對齊原則剃允,最終輸出為 48
總結(jié):
一般結(jié)構(gòu)體大小
- 1.結(jié)構(gòu)體成員的偏移量必須是成員大小的整數(shù)倍
- 2.結(jié)構(gòu)體大小是最大元素的倍數(shù)(最大元素字節(jié)對齊)
嵌套結(jié)構(gòu)體大小
- 1.展開后的結(jié)構(gòu)體的第一個(gè)成員的偏移量應(yīng)當(dāng)是被展開的結(jié)構(gòu)體中最大的成員變量的整數(shù)倍
- 2.結(jié)構(gòu)體大小必須是所有成員中最大元素的整數(shù)倍(8字節(jié)對齊)
如果以上內(nèi)容有錯(cuò)誤的地方,還請各位大佬指點(diǎn)!
持續(xù)更新和修復(fù)中...