iOS 底層探究之 alloc
我們通過幾個問題來探究下一個iOS如何獲取到一個對象:
alloc和init的區(qū)別? alloc方法做了哪些事情?
alloc 和 init的區(qū)別
從字面意思上,我們可以知道alloc是用來分配內(nèi)存,init是用來初始化數(shù)據(jù)。下面我們通過代碼來驗證一下:
NSObject *obj1 = [NSObject alloc];
NSObject *obj2 = [obj1 init];
NSObject *obj3 = [obj1 init];
NSObject *obj4 = [NSObject alloc];
NSLog(@"obj1: %@, %p, %p", obj1, obj1, &obj1);
NSLog(@"obj2: %@, %p, %p", obj2, obj2, &obj2);
NSLog(@"obj3: %@, %p, %p", obj3, obj3, &obj3);
NSLog(@"obj4: %@, %p, %p", obj4, obj4, &obj4);
obj1: <NSObject: 0x6000000fc580>, 0x6000000fc580, 0x7ffee64db358
obj2: <NSObject: 0x6000000fc580>, 0x6000000fc580, 0x7ffee64db350
obj3: <NSObject: 0x6000000fc580>, 0x6000000fc580, 0x7ffee64db348
obj4: <NSObject: 0x6000000fc6a0>, 0x6000000fc6a0, 0x7ffee64db340
分析NSObject對象的打印:
obj1、obj2、obj3 的內(nèi)存地址是一樣 0x6000000fc580,和obj4 0x6000000fc6a0,說明init不會分配,調(diào)用alloc時才分配了棧地址, obj1、obj2、obj3、obj4 變量的指針地址都不一樣,而且是連續(xù),依次變小的,因為指針地址分配在棧區(qū),棧區(qū)分配內(nèi)存是連續(xù)的。 棧區(qū)和堆區(qū)的內(nèi)存分配圖解:

總結(jié):
alloc才會分配內(nèi)存地址,init用于初始化數(shù)據(jù)。
變量指針地址分配在棧區(qū),而且是嚴格根據(jù)變量聲明順序連續(xù)分配內(nèi)存地址,從高到低分配。
NSObject對象的內(nèi)容一般存儲在堆區(qū),從低到高分配,因為堆空間分配是找到一塊可用且大于需要分配內(nèi)存大小的地址,有可能后分配的內(nèi)存地址可能更小。
alloc方法做了哪些事情
從我對alloc的調(diào)用棧和實現(xiàn)邏輯,得到以下結(jié)論:
分配對象所需的內(nèi)存,并做了內(nèi)存對齊工作 將對象和所屬類型通過isa屬性綁定起來
準備工作
下載可編譯的objc4源碼[1],可以直接使用,不需要配置。如果斷點不生效,我的解決方案是將target -> build phases -> compile sources -> 將要斷點的文件移到最前面就生效了。
alloc調(diào)用鏈
NSObject調(diào)用alloc
調(diào)用objc_alloc
callAlloc(cls, true, false)
NSObject 通過objc_msgSend調(diào)用 +alloc
_objc_rootAlloc
callAlloc(cls, false, true)
_objc_rootAllocWithZone
_class_createInstanceFromZone(): 內(nèi)部實現(xiàn)內(nèi)存分配和綁定類型
instanceSize(): 計算obj所需要的內(nèi)存及實現(xiàn)內(nèi)存對齊 calloc(): 分配內(nèi)存,得到一個對象 initInstanceIsa(): 綁定類型 alloc調(diào)用流程圖:

分配內(nèi)存,并實現(xiàn)內(nèi)存對齊
instanceSize()方法提供了兩種計算內(nèi)存的方法,第一個分支走hasFastInstanceSize(), 第二個分支走alignedInstanceSize()
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;
}
2.判斷是否可以快速計算實例化內(nèi)存大小。__builtin_constant_p()函數(shù)表示如果為常數(shù)返回1,如果是變量是返回0。而且在_class_createInstanceFromZone(cls, 0, nil, OBJECT_CONSTRUCT_CALL_BADALLOC)調(diào)用時extra傳入的就是0,所以if分支為真,應(yīng)該調(diào)用 _flags & FAST_CACHE_ALLOC_MASK16。但是在實際運行中,發(fā)現(xiàn)走的是 _flags & FAST_CACHE_ALLOC_MASK。我通過 po __builtin_constant_p(extra) == 0發(fā)現(xiàn)是true,因為無法看到__builtin_constant_p的實現(xiàn),這里也就不深究了。最后結(jié)果返回的YES,所以下一步調(diào)用 fastInstanceSize().
bool hasFastInstanceSize(size_t extra) const
{
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
}
return _flags & FAST_CACHE_ALLOC_MASK;
}
3.調(diào)用fastInstanceSize函數(shù),這里才是實現(xiàn)內(nèi)存對齊的地方。因為 po __builtin_constant_p(extra) == 0 所以走else分支,調(diào)用align16()實現(xiàn)內(nèi)存對齊。
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);
}
}
4.align16()中對對象所需的做(x + size_t(15)) & ~size_t(15),目的很簡單,即對16取余,當有余數(shù)是,取出這部分加上16. 比如: size_t(15)是01111,取反后是10000, 如果超過16的話,前面補1。33 二進制是100001, &10000得到100000即32。
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
5.以上在objc4實際運行的調(diào)用鏈,總結(jié)可得: iOS通過alloc分配內(nèi)存,且做了內(nèi)存對齊,對齊的字節(jié)數(shù)是16.實際上我們得對象的結(jié)尾數(shù)字不是0就是8,就是這個原因。
6.instanceSize()方法的else分支走alignedInstanceSize()方法,最終調(diào)用word_align(),同4中分析可知對齊字節(jié)是8。
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
# define WORD_MASK 7UL // 64位下
總結(jié): alloc最終通過_class_createInstanceFromZone()方法調(diào)用instanceSize()計算對象所需的內(nèi)存,在64位下進行16對齊,然后通過calloc()分配內(nèi)存。
綁定類型
alloc最終_class_createInstanceFromZone()方法initInstanceIsa()實現(xiàn)類型綁定。
inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
然后調(diào)用objc_object::initIsa()方法,在64位機器下,isa都進行了優(yōu)化(nonpointer == 1),所以走else分支, 通過setClass()將obj和Class綁定起來
inline void objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
newisa.has_cxx_dtor = hasCxxDtor;
newisa.setClass(cls, this);
newisa.extra_rc = 1;
}
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
總結(jié)
綜上的現(xiàn)象,我們可知alloc()方法實現(xiàn)了對象的內(nèi)存分配,內(nèi)存對齊,將對象和類型綁定三個功能。
內(nèi)存對齊實際案例
Apple在64位下,對象內(nèi)存對齊是16,結(jié)構(gòu)體是8。
內(nèi)存分配時,會根據(jù)屬性或成員變量的類型length, 屬性或成員的起始內(nèi)存必須是該類型length的整數(shù)倍。
驗證64位下內(nèi)存對齊是16
在內(nèi)存分配時,最終調(diào)用objc-runtime-new.h _class_createInstanceFromZone()方法中 調(diào)用順序是: _class_createInstanceFromZone() -> instanceSize() -> cache.fastInstanceSize() -> align16() 最終調(diào)用的是align16()方法, 對分配的內(nèi)存x做內(nèi)存對其, 對其規(guī)則(x + size_t(15)) & ~size_t(15) ~size_t(15): size_t(15)是01111,取反后是10000, 如果超過16的話,前面補1
(x + size_t(15)) 這是為了實現(xiàn)分配的內(nèi)存不小于實際需要的,向上加一個16(計算機從0開始)
(x + size_t(15)) & ~size_t(15) 在2的部分上去除余數(shù),
比如13 + 15 = 28, 最后得到16, 28 二進制是11100, &10000 得到10000即16
18 + 15 = 33 最后得到32, 33 二進制是100001, &10000得到100000即32

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;
}
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);
}
}
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
對象內(nèi)存分析
@interface LKXObjectDemo1 : NSObject {
// isa // 8
int age; // 4
double hegiht; // 8
char chr; // 1
double weight; // 8
}
@end
@interface LKXObjectDemo2 : NSObject {
// isa // 8
char chr; // 1
int age; // 4
double weight; // 8
double hegiht; // 8
}
@end
@interface LKXObjectDemo3 : NSObject {
@public
// isa // 8
char chr; // 1
int age; // 4
int idx; // 4
double weight; // 8
double hegiht; // 8
}
@end
LKXObjectDemo1 分配內(nèi)存48字節(jié),使用內(nèi)存40字節(jié),假如起始位置是0x10020000
isa 占用內(nèi)存8字節(jié),起始位置是0x10020000,結(jié)束位置是0x10020007
int age 占用內(nèi)存4字節(jié),起始位置是0x10020008,結(jié)束位置是0x1002000B
double hegiht 占用內(nèi)存8字節(jié),起始位置也要是8的倍數(shù),所以起始位置是0x10020010,結(jié)束位置是0x10020018
char chr 占用內(nèi)存1字節(jié),起始位置是0x10020018,結(jié)束位置是0x10020018
double weight占用內(nèi)存8字節(jié),起始位置也要是8的倍數(shù),所以起始位置是0x10020020,結(jié)束位置是0x10020027
0x27是40,因為對象內(nèi)存對其是16,所以分配內(nèi)存48
LKXObjectDemo2 分配內(nèi)存32字節(jié),使用內(nèi)存32字節(jié),假如起始位置是0x10020000
isa 占用內(nèi)存8字節(jié),起始位置是0x10020000,結(jié)束位置是0x10020007
char chr 占用內(nèi)存1字節(jié),起始位置是0x10020008,結(jié)束位置是0x10020008
int age 占用內(nèi)存4字節(jié),起始位置也要是4的倍數(shù),起始位置是0x1002000B,結(jié)束位置是0x1002000F
double weight 占用內(nèi)存8字節(jié),起始位置是0x10020010,結(jié)束位置是0x10020017
double hegiht 占用內(nèi)存8字節(jié),起始位置是0x10020018,結(jié)束位置是0x1002001F
0x1F是32, 所以占用32字節(jié)
LKXObjectDemo3 分配內(nèi)存48字節(jié),使用內(nèi)存40字節(jié),假如起始位置是0x10020000
isa 占用內(nèi)存8字節(jié),起始位置是0x10020000,結(jié)束位置是0x10020007
char chr 占用內(nèi)存1字節(jié),起始位置是0x10020008,結(jié)束位置是0x10020008
int age 占用內(nèi)存4字節(jié),起始位置也要是4的倍數(shù),起始位置是0x1002000B,結(jié)束位置是0x1002000F
int idx 占用內(nèi)存4字節(jié),起始位置是0x10020010,結(jié)束位置是0x10020013
double weight 占用內(nèi)存8字節(jié),起始位置也要是8的倍數(shù),起始位置是0x10020018,結(jié)束位置是0x1002001F
double hegiht 占用內(nèi)存8字節(jié),起始位置是0x10020020,結(jié)束位置是0x10020027
0x27是40,因為對象內(nèi)存對其是16,所以分配內(nèi)存48
demo3成員變量分析,從輸出可以看出
demo3(0x101b0b840)的內(nèi)存地址和chr(0x101b0b848)相差8個字節(jié), 這個8個字節(jié)就是isa的地址, demo3指向的內(nèi)存是 0x011d8001000085f9,LKXObjectDemo3 class的內(nèi)存地址是 0x00000001000085f8,剛好是后9位相同,這說明isa指向類類型內(nèi)存地址
從chr(0x101b0b848)、chr2(0x101b0b849)相隔1字節(jié),而且指向的內(nèi)存0x0000000a00003363可以看出,3的ASCII碼是33,c的的ASCII碼是63
從chr(0x101b0b848)、 age(0x101b0b84c)、idx(0x101b0b850)的內(nèi)存地址是相鄰的,而且相隔4字節(jié),說明成員屬性分配內(nèi)存必須是其類型長度的整數(shù)倍,因為int類型長度是4。因為char類型長度是1,所以沒有影響。
weight(0x101c042c8)和height(0x101c042d0)各占8字節(jié)
demo3->chr = 'c';
demo3->age = 10;
demo3->idx = 1;
demo3->weight = 120;
demo3->hegiht = 170;
NSLog(@"chr: %p, age: %p, idx: %p, weight: %p, height: %p", &(demo3->chr), &(demo3->age), &(demo3->idx),
&(demo3->weight), &(demo3->hegiht));
demo3: 0x101b0b840
chr: 0x101b0b848, chr2: 0x101b0b849,
age: 0x101b0b84c, idx: 0x101b0b850,
weight: 0x101b0b858, height: 0x101b0b860
0x101b0b840: 0x011d8001000085f9 0x0000000a00003363
0x101b0b850: 0x0000000000000001 0x405e000000000000
0x101b0b860: 0x4065400000000000 0x0000000000000000
0x101b0b870: 0x0000000000000000 0x0000000000000000
p [LKXObjectDemo3 class]
(Class) $1 = 0x00000001000085f8
struct 內(nèi)存分析
struct StructDemo1 {
char ch; // 1
double height; // 8
float weight; // 4
char *name; // 8
int age; // 4
} StructDemo1;
struct StructDemo2 {
char ch; // 1
int age; // 4
char *name; // 8
double height; // 8
float weight; // 8
} StructDemo2;
struct StructDemo3 {
struct StructDemo1 s1; // 40
struct StructDemo2 s2; // 32
float weight; // 4
char chr; // 1
int index; // 4
double height; // 8
} StructDemo3;
StructDemo1內(nèi)存是大小是40字節(jié), 因為每個屬性都必須是其類型length,假如起始位置是0x10020000
char ch 占用1字節(jié),那么ch的起始位置是0x10020000, 結(jié)束位置是0x10020000
double height 占用8字節(jié),起始位置也要是8的倍數(shù),那么height的起始位置是0x10020008, 結(jié)束位置是 0x1002000F
float weight 占用4字節(jié),weight的起始位置是0x10020010, 結(jié)束位置是 0x10020014
char *name 占用8字節(jié),name的起始位置是0x10020018, 結(jié)束位置是 0x1002001F
int age 占用4字節(jié),age的起始位置是0x10020020, 結(jié)束位置是 0x10020023
0x23是36,因為struct內(nèi)存對其是8字節(jié),所以最終分配了40字節(jié)
StructDemo2內(nèi)存是大小是32字節(jié),假如起始位置是0x10020000
char ch 占用1字節(jié),那么ch的起始位置是0x10020000, 結(jié)束位置是0x10020000
int age 占用4字節(jié),起始位置也要是4的倍數(shù), age的起始位置是0x10020004, 結(jié)束位置是 0x10020007
char *name 占用8字節(jié),name的起始位置是0x10020008, 結(jié)束位置是 0x1002000F
double height 占用8字節(jié),那么height的起始位置是0x10020010, 結(jié)束位置是 0x10020017
float weight 占用4字節(jié),weight的起始位置是0x10020018, 結(jié)束位置是 0x1002001B
0x1B是28,因為struct內(nèi)存對其是8字節(jié),所以最終分配了32字節(jié)
StructDemo1內(nèi)存是大小是96字節(jié),假如起始位置是0x10020000
struct StructDemo1 s1 占用40字節(jié), s1起始位置是0x10020000,結(jié)束位置0x10020027
struct StructDemo2 s2 占用32字節(jié), s1起始位置是0x10020028,結(jié)束位置0x10020047
float weight 占用4字節(jié),weight的起始位置是0x10020048, 結(jié)束位置是 0x1002004B
char chr 占用1字節(jié),那么chr的起始位置是0x1002004C, 結(jié)束位置是0x1002004C
int index 占用4字節(jié),起始位置也要是4的倍數(shù), index的起始位置是0x10020050, 結(jié)束位置是 0x10020053
double height 占用8字節(jié),那么height的起始位置是0x10020058, 結(jié)束位置是 0x1002005F
0x5F是96,剛好使用了96字節(jié)
補充
為什么要內(nèi)存對齊?
平臺移植問題: 不同的硬件平臺訪問地址是有其規(guī)則,不是所有硬件都可以任意訪問所有位置。 性能問題: 數(shù)據(jù)結(jié)構(gòu)(特別是棧)應(yīng)該盡可能在自然邊界上對其。因為訪問未對齊的內(nèi)存,處理器需要做兩次內(nèi)存訪問;而對齊的內(nèi)存訪問僅需要一次。
參考文章
OC底層原理初探之對象的本質(zhì)(一)alloc探索上[2]
OC底層原理初探之a(chǎn)lloc的探索上[3]
參考資料
可編譯的objc4源碼: https://github.com/LGCooci/KCCbjc4_debug
[2]OC底層原理初探之對象的本質(zhì)(一)alloc探索上: https://juejin.cn/post/6970693675416289287
[3]OC底層原理初探之a(chǎn)lloc的探索上: https://juejin.cn/post/7056039064092278820
來源:一吱老菜鳥
https://juejin.cn/post/7074864288371834910
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取