在開發(fā)過程中,有時會有一個疑問:alloc&init
究竟做了什么?
alloc
是分配內(nèi)存,init
具體干了點啥還真不是很清楚渠退,下面通過看 objc 源碼
的方式,去看下 alloc&init
的底層實現(xiàn)脐彩。
來看下面這一段代碼:
NSObject *object = [NSObject alloc];
上面這段代碼其實就是開辟了一塊內(nèi)存空間碎乃,將這塊內(nèi)存空間的地址返回給了 object
,我們可以在objc
源碼中具體看下alloc
的實現(xiàn)惠奸,objc
源碼請自行查找梅誓。
alloc
流程圖如下:
在 NSObject.mm
文件中,我們看到了 alloc 的實現(xiàn):
1. + (id)alloc
+ (id)alloc {
return _objc_rootAlloc(self);
}
2. id_objc_rootAlloc(Class cls)
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
3.static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
// 有可用的編譯器優(yōu)化
#if __OBJC2__
// checkNil 為 false, !cls 也為 false佛南,所以 slowpath 為 false
if (slowpath(checkNil && !cls)) return nil;
// 判斷一個類是否有自定義的 +allocWithZone 實現(xiàn)梗掰,沒有則走到if里面的實現(xiàn)
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
// 沒有可用的編譯器優(yōu)化
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
這里繼續(xù)跟斷點,發(fā)現(xiàn)走到了_objc_rootAllocWithZone
方法里面嗅回。
slowpath & fastpath
這兩個都是 objc
源碼中定義的宏:
// x很可能為真 可以理解為真值判斷
#define fastpath(x) (__builtin_expect(bool(x), 1))
// x很可能為假 可以理解為假值判斷
#define slowpath(x) (__builtin_expect(bool(x), 0))
__builtin_expect
指令是由 gcc 引入的
目的:編譯器可以對代碼進行優(yōu)化及穗,減少指令跳轉(zhuǎn)帶來的性能下降,即性能優(yōu)化
作用:允許程序員將最有可能執(zhí)行的分支告訴編譯器
寫法:__builtin_expect(EXP, N)绵载。表示 EXP==N的概率很大埂陆。
fastpath
定義中 __builtin_expect((x),1)
表示 x 的值為真的可能性很大,即執(zhí)行 if 里面語句的機會更大
slowpath
定義中 __builtin_expect((x),0)
表示 x 的值為假的可能性很大娃豹,即執(zhí)行 else 里面語句的機會更大
4. id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
這里發(fā)現(xiàn) zone
參數(shù)沒用了猜惋,內(nèi)部接著調(diào)用_class_createInstanceFromZone
方法了。
5.static ALWAYS_INLINE id _class_createInstanceFromZone
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
// 1.申請的內(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 {
// 2. 開辟內(nèi)存
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
// 3. 將 cls 類與 obj 指針關(guān)聯(lián)
if (!zone && fast) {
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);
}
// 4. 返回開辟的內(nèi)存的地址培愁,也就是指向該內(nèi)存的指針
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
上面方法中具體干了 4 件事:
- 計算開辟空間大小
- 開辟內(nèi)存空間
- 將
cls
類與obj
指針關(guān)聯(lián) - 返回內(nèi)存地址的指針
該方法具體流程如下:
5.1 計算開辟空間大小
執(zhí)行流程如下圖所示:
5.1.1 instanceSize
跳轉(zhuǎn)至instanceSize
的源碼實現(xiàn):
size_t instanceSize(size_t extraBytes) const {
// 編譯器快速計算內(nèi)存大小
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
// 計算類中所有屬性的大小 + 額外字節(jié)數(shù) 0
size_t size = alignedInstanceSize() + extraBytes;
// 最小為 16
if (size < 16) size = 16;
return size;
}
我們可以看到:if (size < 16) size = 16;
,說明開辟的內(nèi)存空間最小是 16缓窜,下面定续,我們繼續(xù)看alignedInstanceSize ()
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
alignedInstanceSize
這個方法是返回字節(jié)對齊后的內(nèi)存地址大小谍咆,具體包含下面兩個方法:
unalignedInstanceSize()
是這個對象的一些成員變量占用的大小,也是未進行字節(jié)對齊之前的內(nèi)存大小私股,存放在data()->ro()->instanceSize
摹察,在 dyld
加載 mach-o
文件時,會加載整個類的結(jié)構(gòu)倡鲸,也就是在那時進行賦值的供嚎,這里直接取就行了:
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
我們新創(chuàng)建一個類:Person
,它沒有任何成員變量峭状,但是它的 unalignedInstanceSize = 8
個字節(jié)克滴,因為繼承于 NSObject
,有一個isa
指針优床,64 位
下一個指針占用8 個字節(jié)
劝赔。
word_align()
方法,進行字節(jié)對齊:
# define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
// 先對WORD_MASK進行非 ~ 運算
// 再和(x + WORD_MASK)進行 與 & 運算
return (x + WORD_MASK) & ~WORD_MASK;
}
這個方法主要就是進行字節(jié)對齊胆敞,向上取 8 的倍數(shù)着帽。
通過斷點調(diào)試,instanceSize
方法移层,會執(zhí)行到cache.fastInstanceSize
方法仍翰,快速計算內(nèi)存大小,我們?nèi)タ聪逻@個方法:
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
//Gcc的內(nèi)建函數(shù) __builtin_constant_p 用于判斷一個值是否為編譯時常數(shù)观话,如果參數(shù)EXP 的值是常數(shù)予借,函數(shù)返回 1,否則返回 0
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
//刪除由setFastInstanceSize添加的FAST_CACHE_ALLOC_DELTA16 8個字節(jié)
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
跳轉(zhuǎn)至align16()
的源碼實現(xiàn)匪燕,這個方法是16字節(jié)對齊算法
:
//16字節(jié)對齊算法
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
5.1.2 內(nèi)存對齊
內(nèi)存對齊原則:
數(shù)據(jù)成員對齊規(guī)則
:struct 或者 union 的數(shù)據(jù)成員蕾羊,第一個數(shù)據(jù)成員放在 offset 為 0 的地方,之后每個數(shù)據(jù)成員存儲的起始位置要從該成員大小或者成員的子成員大忻毖薄(只要該成員有子成員龟再,比如數(shù)據(jù)、結(jié)構(gòu)體等)的整數(shù)倍開始(例如int在32位機中是4字節(jié)尼变,則要從4的整數(shù)倍地址開始存儲)
數(shù)據(jù)成員為結(jié)構(gòu)體
:如果一個結(jié)構(gòu)里有某些結(jié)構(gòu)體成員利凑,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開始存儲(例如:struct a里面存有struct b,b里面有char嫌术、int哀澈、double等元素,則b應(yīng)該從8的整數(shù)倍開始存儲)
結(jié)構(gòu)體的整體對齊規(guī)則
:結(jié)構(gòu)體的總大小度气,即sizeof的結(jié)果割按,必須是其內(nèi)部做大成員的整數(shù)倍,不足的要補齊
為什么要 16 字節(jié)對齊磷籍?
內(nèi)存由一個個字節(jié)組成适荣,cpu 在存取數(shù)據(jù)時现柠,并不是以字節(jié)為單位進行存儲,而是以塊為單位存取弛矛,塊的大小為內(nèi)存存取粒度够吩,通過減少存取次數(shù)來降低 cpu 開銷
16字節(jié)對齊,由于一個對象中丈氓,isa 指針占 8 個字節(jié)周循,當(dāng)沒有其它屬性的時候,會預(yù)留 8 個字節(jié)万俗,即 16 字節(jié)對齊湾笛,如果不預(yù)留,相當(dāng)于這個對象的isa和其他對象的isa緊挨著该编,容易造成訪問混亂
16字節(jié)對齊后迄本,可以加快CPU讀取速度,同時使訪問更安全课竣,不會產(chǎn)生訪問混亂的情況
以 align(8)
為例嘉赎,解釋下 16 字節(jié)對齊
算法的過程:
- 首先將原始的內(nèi)存
8
與size_t(15)
相加,得到8 + 15 = 23
- 將
size_t(15)
即15
進行~(取反)
操作于樟,~(取反)
的規(guī)則是:1變?yōu)?公条,0變?yōu)?
? 最后將23
與15
的取反結(jié)果 進行&(與)
操作,最后的結(jié)果為16
迂曲,即內(nèi)存的大小是以16
的倍數(shù)增加的
5.1.3 calloc
: 申請內(nèi)存靶橱,返回內(nèi)存地址指針
通過instanceSize
計算的內(nèi)存大小,向內(nèi)存中申請 大小 為 size
的內(nèi)存路捧,并賦值給obj
关霸,因此 obj
是指向一塊內(nèi)存的指針
obj = (id)calloc(1, size);
這里我們可以通過斷點來印證上述的說法,在未執(zhí)行calloc
時杰扫,po obj
為nil
队寇,執(zhí)行后,再po obj
發(fā)現(xiàn)章姓,返回了一個16
進制的地址
- 這里
po obj
還是一個內(nèi)存地址佳遣,是因為還沒有與傳入的cls
進行關(guān)聯(lián) - 同時印證了
alloc
的根本作用就是開辟內(nèi)存
5.2 obj->initInstanceIsa
:類與 isa 關(guān)聯(lián)
主要過程就是初始化一個isa
指針,并將isa
指針指向申請的內(nèi)存地址凡伊,再將指針與cls
類進行關(guān)聯(lián)零渐。
在obj->initIsa(cls);
代碼前后,我們可以分別po obj
看下不同:
// 之前
(lldb) po obj
0x0000000101d21c20
// 之后
(lldb) po obj
<LGPerson: 0x101d21c20>
可以明顯的看到:obj
的isa
指向的是 LGPerson系忙。
6. 總結(jié)
- 通過對
alloc
源碼的分析诵盼,可以得知alloc
的主要目的就是開辟內(nèi)存,而且開辟的內(nèi)存需要使用16字節(jié)對齊
算法,現(xiàn)在開辟的內(nèi)存的大小基本上都是16
的整數(shù)倍 - 開辟內(nèi)存的核心步驟有3步:計算內(nèi)存大小 -- 申請內(nèi)存空間 -- 關(guān)聯(lián)cls和obj的isa指針
7. init分析
源碼如下:
// Replaced by CF (throws an NSException)
+ (id)init {
return (id)self;
}
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
可以看到拦耐,init 其實就是返回了self耕腾,可能是為了方便程序猿重寫 init 方法吧。
8. new 分析
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
看起來和上面一樣杀糯,不是嗎?
順便記錄下 lldb 指令:
- register read xxx
寄存器讀取 - x p
以 16 進制打印 p 的內(nèi)存占用 - x/4xg p
每4 位讀取 p 的內(nèi)存占用