一、對(duì)象的指針地址和內(nèi)存
先看下面的代碼:
HPerson * p1 = [HPerson alloc];
HPerson * p2 = [p1 init];
HPerson * p3 = [p1 init];
NSLog(@"%@-%p", p1, p1);
NSLog(@"%@-%p", p2, p2);
NSLog(@"%@-%p", p3, p3);
請(qǐng)問這3者的會(huì)有什么不同嗎韩脏?
執(zhí)行代碼就會(huì)發(fā)現(xiàn)p1该面,p2,p3
打印的內(nèi)存地址是完全一樣的:
所以在這個(gè)過程我們可以得出一個(gè)結(jié)論:
1奈附、alloc
讓對(duì)象有了內(nèi)存空間,有了指針指向煮剧。
2斥滤、init
后內(nèi)存沒有變化,證明init
沒有對(duì)指針做什么操作勉盅。
再來看下面的代碼:
HPerson * p1 = [HPerson alloc];
HPerson * p2 = [p1 init];
HPerson * p3 = [p1 init];
NSLog(@"%@-%p-%p", p1, p1, &p1);
NSLog(@"%@-%p-%p", p2, p2, &p2);
NSLog(@"%@-%p-%p", p3, p3, &p3);
執(zhí)行:
發(fā)現(xiàn)3個(gè)不同的指針地址指向了同一塊內(nèi)存空間佑颇,而且是3個(gè)連續(xù)的指針地址。
那么alloc
是怎么分配內(nèi)存空間的呢草娜?init
真的什么都沒有做嗎挑胸?
二、探索底層的三種方法
如果我們直接進(jìn)行代碼跳轉(zhuǎn):
就會(huì)進(jìn)入到NSObject
的alloc
方法:
發(fā)現(xiàn)找不到方法的實(shí)現(xiàn)宰闰!
為什么沒有實(shí)現(xiàn)呢茬贵?我們應(yīng)該如何去查看alloc
方法的實(shí)行呢?
方法一:斷點(diǎn)調(diào)試
我們先在alloc
處打上斷點(diǎn):
然后按住ctrl
點(diǎn)擊step into
:
即可看到议蟆,
alloc
方法調(diào)用的是objc_alloc
方法:
然后我們打上符號(hào)斷點(diǎn):
再點(diǎn)擊繼續(xù)執(zhí)行程序:
然后我們就會(huì)發(fā)現(xiàn)objc_alloc
來源于libobjc.A.dylib
闷沥,即objc
動(dòng)態(tài)庫(kù)底層方法:
方法二:利用匯編一步一步跟進(jìn)
在debug
選擇欄打開進(jìn)入?yún)R編:
依舊在alloc
處打上斷點(diǎn):
運(yùn)行程序就會(huì)進(jìn)入到匯編頁(yè)面:
在該頁(yè)面就會(huì)發(fā)現(xiàn)是調(diào)用了objc_alloc
方法,然后用符號(hào)斷點(diǎn)去查看該方法使用的哪個(gè)動(dòng)態(tài)庫(kù)咐容。
方法三:通過已知方法進(jìn)行符號(hào)斷點(diǎn)
當(dāng)程序停在我們的斷點(diǎn)處時(shí):
添加已知方法的符號(hào)斷點(diǎn):
然后進(jìn)入:
就會(huì)發(fā)現(xiàn)alloc
是來自libobjc.A.dylib
動(dòng)態(tài)庫(kù):
以上就是3中基本的探索底層的方式,還有反匯編蚂维、lldb戳粒、堆棧
等等。
三虫啥、匯編結(jié)合源碼調(diào)試分析
1蔚约、下載源碼
我們已經(jīng)定位到了alloc
是在libobjc
這個(gè)動(dòng)態(tài)庫(kù)里面,接下來就是進(jìn)入源碼進(jìn)行調(diào)試涂籽。
先在蘋果開源網(wǎng)站下載源碼苹祟,或者在蘋果的源代碼目錄進(jìn)行下載,源代碼目錄更加方便评雌。
以源代碼目錄為例:
進(jìn)入網(wǎng)站后搜索objc
:
找到objc4
树枫,點(diǎn)進(jìn)去:
最新的objc4-824.tar.gz有問題,無法下載景东,所以使用的是objc4-818.2.tar.gz砂轻。
2、查看源碼
打開下載的objc4-818.2斤吐,搜索alloc {
:
發(fā)現(xiàn)alloc
方法里面調(diào)用的是_objc_rootAlloc
方法搔涝。
再點(diǎn)_objc_rootAlloc
方法進(jìn)去:
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
發(fā)現(xiàn)_objc_rootAlloc
方法里面是callAlloc
方法厨喂。
再點(diǎn)callAlloc
方法進(jìn)去:
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
這里就是alloc
的核心方法。
__OBJC2__
指的是2.0版本庄呈,現(xiàn)在用的都是2.0版本蜕煌。
那么到了這里,是執(zhí)行_objc_rootAllocWithZone
呢诬留?還是執(zhí)行objc_msgSend
呢幌绍?
3、匯編調(diào)試
我們可以通過匯編來查看故响,回到最開始的代碼傀广,在alloc
處打上斷點(diǎn),于此再加上_objc_rootAlloc
的符號(hào)斷點(diǎn):
運(yùn)行:
運(yùn)行后確實(shí)進(jìn)入了_objc_rootAlloc
方法彩届,但是進(jìn)入?yún)s是HPerson
的父類NSObject
的_objc_rootAlloc
方法伪冰,我們應(yīng)該進(jìn)入的HPerson
的方法才對(duì)!
所以需要先取消_objc_rootAlloc
的斷點(diǎn)重新運(yùn)行樟蠕,當(dāng)斷到了HPerson
的alloc
方法時(shí)贮聂,再加上_objc_rootAlloc
的斷點(diǎn):
斷住后,發(fā)現(xiàn)先執(zhí)行_objc_rootAllocWithZone
方法寨辩,再執(zhí)行objc_msgSend
方法吓懈。
4、源碼調(diào)試
先按照iOS_objc4-756.2 最新源碼編譯調(diào)試配置下載下來的objc4-818.2靡狞。
在objc
源碼里面創(chuàng)建HObjectBuild
這個(gè)targets
耻警,并創(chuàng)建HPerson
這個(gè)類:
在main.m
里面打上斷點(diǎn),并運(yùn)行程序:
進(jìn)入上面查看到的_objc_rootAllocWithZone
方法里:
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);
}
再進(jìn)入到_class_createInstanceFromZone
方法里:
/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
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;
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
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);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
在_class_createInstanceFromZone
方法內(nèi)甸怕,發(fā)現(xiàn)返回的是obj
這個(gè)對(duì)象甘穿,所以我們?cè)?code>obj這里打上斷點(diǎn):
繼續(xù)執(zhí)行程序:
發(fā)現(xiàn)obj
已經(jīng)有地址了,因?yàn)楫?dāng)前內(nèi)存并未使用過梢杭,是臟內(nèi)存地址温兼!
點(diǎn)擊step over
,到calloc
方法武契,內(nèi)存地址沒有發(fā)生變化:
在點(diǎn)擊step over
募判,執(zhí)行完calloc
方法后,發(fā)現(xiàn)obj
的內(nèi)存地址不一樣了:
說明在calloc
方法中對(duì)obj
進(jìn)行了內(nèi)存地址賦值咒唆。
在打印中發(fā)現(xiàn)obj
對(duì)象的類型是id
届垫,這是因?yàn)檫@個(gè)地址還沒有綁定到我們的HPerson
這類里面去,關(guān)聯(lián)類的是isa
钧排!
繼續(xù)執(zhí)行敦腔,過了initInstanceIsa
方法后再打印發(fā)現(xiàn)obj
已經(jīng)關(guān)聯(lián)了HPerson
類:
所以在initInstanceIsa
方法內(nèi)讓isa
關(guān)聯(lián)了HPerson
以及c++
的方法和函數(shù)!
接下來就是返回obj
恨溜,所有的alloc
方法流程已經(jīng)走完了符衔!
四找前、字節(jié)對(duì)齊
重新運(yùn)行,在_class_createInstanceFromZone
方法的
size = cls->instanceSize(extraBytes);
打上斷點(diǎn):
發(fā)現(xiàn)需要額外增加的字節(jié)為
0
判族。
然后進(jìn)入到instanceSize
方法:
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
在return
處打上斷點(diǎn)躺盛,繼續(xù):
發(fā)現(xiàn)進(jìn)入到第一個(gè)return
,說明有緩存
形帮。
然后進(jìn)入fastInstanceSize
方法:
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
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
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
在return
處打上斷點(diǎn)槽惫,繼續(xù):
走的是第二個(gè)
return
,而且size
為16
辩撑。
那么這個(gè)數(shù)據(jù)怎么來的呢界斜?
如果沒有緩存的話,就會(huì)走alignedInstanceSize
方法合冀,然后返回字節(jié)對(duì)齊的內(nèi)存大懈鬓薄:
那么,一個(gè)對(duì)象的內(nèi)存大小由什么來確定呢君躺?
只有成員變量
決定峭判!
我們進(jìn)入unalignedInstanceSize
方法:
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
發(fā)現(xiàn)大小是有instanceSize
來決定的,即實(shí)例變量
的大凶亟小林螃!
depending on class's ivars.-->取決于類的ivars。
并且是編譯完成的干凈內(nèi)存的大邪称疗认!
所以是依賴于成員變量
的大小砌滞!
當(dāng)前的HPerson
對(duì)象沒有成員變量
侮邀,所以這里返回的大小是8
:
為什么是8
呢?因?yàn)?code>NSObject有一個(gè)成員變量isa
:
為什么isa
的大小是8
呢贝润?
因?yàn)?code>Class是一個(gè)結(jié)構(gòu)體
!isa
是一個(gè)結(jié)構(gòu)體指針
铝宵,所以是8
字節(jié)打掘!
我們可以在源碼里搜索objc_class
:
發(fā)現(xiàn)Class
是一個(gè)結(jié)構(gòu)體指針
類型!
而且objc_class
繼承于objc_object
鹏秋,即萬物皆對(duì)象尊蚁!即類也是一個(gè)對(duì)象!
返回大小8
字節(jié)后侣夷,如果小于16
字節(jié)則等于16
:
所以得出16
字節(jié)横朋!
如果大于16
呢?
就會(huì)進(jìn)行字節(jié)對(duì)齊
百拓!
進(jìn)入到alignedInstanceSize
方法:
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
里面有word_align
方法:
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
這個(gè)就是字節(jié)對(duì)齊
算法琴锭!
點(diǎn)進(jìn)WORD_MASK
:
#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
發(fā)現(xiàn)WORD_MASK
等于7
晰甚!
按照(x + WORD_MASK) & ~WORD_MASK
這個(gè)算法,x
為8
:
(x + WORD_MASK) & ~WORD_MASK
= (8 + 7) & ~7
= 15 & ~7
轉(zhuǎn)為二進(jìn)制:
15 為:0000 1111
7 為:0000 0111
~7 為:1111 1000
所以:
15 & ~7
= 0000 1111 & 1111 1000
= 0000 1000
轉(zhuǎn)為十進(jìn)制為:8
但是為9
的時(shí)候則為16
决帖!
所以這是一個(gè)以8
字節(jié)對(duì)齊厕九,向上取8的整數(shù)的方法!
為什么是8
的倍數(shù)呢地回?
因?yàn)樽畲缶褪?code>指針扁远,8
字節(jié)!同時(shí)也是為了空間換時(shí)間刻像,方便內(nèi)存讀瘸┞颉!
五细睡、對(duì)象的內(nèi)存空間
先給HPerson
添加對(duì)象:
打上斷點(diǎn)谷羞,運(yùn)行,然后在lldb
中輸入x p
纹冤,顯示對(duì)象p
的內(nèi)存分布:
0x108f079c0
是對(duì)象p
的內(nèi)存首地址洒宝,接下來就是對(duì)象p
的內(nèi)存。
iOS端為小端模式
萌京,所有需要倒著讀妊愀琛:
內(nèi)存打印出來是什么呢?
是isa
知残!
為什么沒有打印出isa
呢靠瞎?
因?yàn)橐?code>&ISA_MASK:
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# else
# define ISA_MASK 0x0000000ffffffff8ULL
# endif
這里使用的是模擬器,所以&0x0000000ffffffff8
:
現(xiàn)在就正確的打印出
isa
了求妹!
后面的0
則為對(duì)象的屬性的存儲(chǔ)空間乏盐!
進(jìn)入debug
查看一下內(nèi)存:
輸入內(nèi)存首地址:
發(fā)現(xiàn)即使沒有給屬性賦值,依舊會(huì)開辟內(nèi)存制恍!
給屬性賦值后再運(yùn)行:
x/5gx
為格式化輸出父能!
如果把height
改為BOOL
類型:
就會(huì)發(fā)現(xiàn)age
和height
放在了一起!
這是蘋果的底層對(duì)內(nèi)存進(jìn)行了優(yōu)化净神,即內(nèi)存對(duì)齊
何吝!