我們通過幾個問題來探究下一個iOS如何獲取到一個對象:
- alloc和init的區別?
- alloc方法做了哪些事情?
alloc 和 init的區別
從字面意思上,我們可以知道alloc是用來分配內存,init是用來初始化數據。下面我們通過代碼來驗證一下:
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
的內存地址是一樣0x6000000fc580
,和obj4 0x6000000fc6a0
,說明init不會分配,調用alloc時才分配了棧地址, -
obj1、obj2、obj3、obj4
變量的指針地址都不一樣,而且是連續,依次變小的,因為指針地址分配在棧區,棧區分配內存是連續的。 - 棧區和堆區的內存分配圖解:
總結:
alloc
才會分配內存地址,init
用于初始化數據。變量指針地址分配在棧區,而且是嚴格根據變量聲明順序連續分配內存地址,從高到低分配。
NSObject
對象的內容一般存儲在堆區,從低到高分配,因為堆空間分配是找到一塊可用且大于需要分配內存大小的地址,有可能后分配的內存地址可能更小。
alloc方法做了哪些事情
從我對alloc
的調用棧和實現邏輯,得到以下結論:
- 分配對象所需的內存,并做了內存對齊工作
- 將對象和所屬類型通過isa屬性綁定起來
準備工作
下載可編譯的objc4源碼[1],可以直接使用,不需要配置。如果斷點不生效,我的解決方案是將target -> build phases -> compile sources -> 將要斷點的文件移到最前面就生效了。
alloc調用鏈
NSObject
調用alloc
調用
objc_alloc
callAlloc(cls, true, false)
NSObject
通過objc_msgSend
調用+alloc
_objc_rootAlloc
callAlloc(cls, false, true)
_objc_rootAllocWithZone
_class_createInstanceFromZone()
: 內部實現內存分配和綁定類型
(1).instanceSize()
: 計算obj所需要的內存及實現內存對齊
(2).calloc()
: 分配內存,得到一個對象
(3).initInstanceIsa()
: 綁定類型alloc調用流程圖:
分配內存,并實現內存對齊
-
instanceSize()
方法提供了兩種計算內存的方法,第一個分支走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.判斷是否可以快速計算實例化內存大小。__builtin_constant_p()
函數表示如果為常數返回1,如果是變量是返回0。而且在_class_createInstanceFromZone(cls, 0, nil, OBJECT_CONSTRUCT_CALL_BADALLOC)
調用時extra
傳入的就是0,所以if分支為真,應該調用 _flags & FAST_CACHE_ALLOC_MASK16
。但是在實際運行中,發現走的是_flags & FAST_CACHE_ALLOC_MASK
。我通過 po __builtin_constant_p(extra) == 0
發現是true
,因為無法看到__builtin_constant_p
的實現,這里也就不深究了。最后結果返回的YES
,所以下一步調用 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.調用fastInstanceSize
函數,這里才是實現內存對齊的地方。因為 po __builtin_constant_p(extra) == 0
所以走else分支,調用align16()
實現內存對齊。
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取余,當有余數是,取出這部分加上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實際運行的調用鏈,總結可得: iOS通過alloc分配內存,且做了內存對齊,對齊的字節數是16.實際上我們得對象的結尾數字不是0就是8,就是這個原因。
6.instanceSize()
方法的else分支走alignedInstanceSize()
方法,最終調用word_align()
,同4中分析可知對齊字節是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位下
總結: alloc
最終通過_class_createInstanceFromZone()
方法調用instanceSize()
計算對象所需的內存,在64位下進行16對齊,然后通過calloc()
分配內存。
綁定類型
-
alloc
最終_class_createInstanceFromZone()
方法initInstanceIsa()
實現類型綁定。
inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
- 然后調用
objc_object::initIsa()
方法,在64位機器下,isa都進行了優化(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;
}
總結
綜上的現象,我們可知alloc()
方法實現了對象的內存分配,內存對齊,將對象和類型綁定三個功能。
內存對齊實際案例
Apple在64位下,對象內存對齊是16,結構體是8。
內存分配時,會根據屬性或成員變量的類型length, 屬性或成員的起始內存必須是該類型length的整數倍。
驗證64位下內存對齊是16
- 在內存分配時,最終調用
objc-runtime-new.h
_class_createInstanceFromZone()
方法中 - 調用順序是:
_class_createInstanceFromZone() -> instanceSize() -> cache.fastInstanceSize() -> align16()
- 最終調用的是align16()方法, 對分配的內存x做內存對其, 對其規則
(x + size_t(15)) & ~size_t(15)
a. ~size_t(15): size_t(15)
是01111,取反后是10000, 如果超過16的話,前面補1
b. (x + size_t(15))
這是為了實現分配的內存不小于實際需要的,向上加一個16(計算機從0開始)
c. (x + size_t(15)) & ~size_t(15)
在2的部分上去除余數,
d. 比如13 + 15 = 28, 最后得到16, 28 二進制是11100, &10000 得到10000即16
e. 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);
}
對象內存分析
@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
分配內存48字節,使用內存40字節,假如起始位置是0x10020000
a. isa
占用內存8字節,起始位置是0x10020000,結束位置是0x10020007
b. int age
占用內存4字節,起始位置是0x10020008,結束位置是0x1002000B
c. double hegiht
占用內存8字節,起始位置也要是8的倍數,所以起始位置是0x10020010,結束位置是0x10020018
d. char chr
占用內存1字節,起始位置是0x10020018,結束位置是0x10020018
e. double weight
占用內存8字節,起始位置也要是8的倍數,所以起始位置是0x10020020,結束位置是0x10020027
f. 0x27是40,因為對象內存對其是16,所以分配內存48
-
LKXObjectDemo2
分配內存32字節,使用內存32字節,假如起始位置是0x10020000
a. isa
占用內存8字節,起始位置是0x10020000,結束位置是0x10020007
b. char chr
占用內存1字節,起始位置是0x10020008,結束位置是0x10020008
c. int age
占用內存4字節,起始位置也要是4的倍數,起始位置是0x1002000B,結束位置是0x1002000F
d. double weight
占用內存8字節,起始位置是0x10020010,結束位置是0x10020017
e. double hegiht
占用內存8字節,起始位置是0x10020018,結束位置是0x1002001F
f. 0x1F
是32, 所以占用32字節
-
LKXObjectDemo3
分配內存48字節,使用內存40字節,假如起始位置是0x10020000
a. isa
占用內存8字節,起始位置是0x10020000,結束位置是0x10020007
b. char chr
占用內存1字節,起始位置是0x10020008,結束位置是0x10020008
c. int age
占用內存4字節,起始位置也要是4的倍數,起始位置是0x1002000B,結束位置是0x1002000F
d. int idx
占用內存4字節,起始位置是0x10020010,結束位置是0x10020013
e. double weight
占用內存8字節,起始位置也要是8的倍數,起始位置是0x10020018,結束位置是0x1002001F
f. double hegiht
占用內存8字節,起始位置是0x10020020,結束位置是0x10020027
g. 0x27是40,因為對象內存對其是16,所以分配內存48
-
demo3
成員變量分析,從輸出可以看出
a. demo3(0x101b0b840)
的內存地址和chr(0x101b0b848)相差8個字節, 這個8個字節就是isa的地址, demo3指向的內存是 0x011d8001000085f9,LKXObjectDemo3 class的內存地址是 0x00000001000085f8,剛好是后9位相同,這說明isa指向類類型內存地址
b. 從chr(0x101b0b848)、chr2(0x101b0b849)相隔1字節,而且指向的內存0x0000000a00003363可以看出,3的ASCII碼是33,c的的ASCII碼是63
c. 從chr(0x101b0b848)
、 age(0x101b0b84c)
、idx(0x101b0b850)
的內存地址是相鄰的,而且相隔4字節,說明成員屬性分配內存必須是其類型長度的整數倍,因為int類型長度是4。因為char類型長度是1,所以沒有影響。
d. weight(0x101c042c8)
和height(0x101c042d0)
各占8字節
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 內存分析
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
內存是大小是40字節, 因為每個屬性都必須是其類型length,假如起始位置是0x10020000
a. char ch
占用1字節,那么ch的起始位置是0x10020000, 結束位置是0x10020000
b. double height
占用8字節,起始位置也要是8的倍數,那么height的起始位置是0x10020008, 結束位置是 0x1002000F
c. float weight
占用4字節,weight的起始位置是0x10020010, 結束位置是 0x10020014
d. char *name
占用8字節,name的起始位置是0x10020018, 結束位置是 0x1002001F
e. int age
占用4字節,age的起始位置是0x10020020, 結束位置是 0x10020023
f. 0x23
是36,因為struct內存對其是8字節,所以最終分配了40字節
-
StructDemo2
內存是大小是32字節,假如起始位置是0x10020000
a. char ch
占用1字節,那么ch的起始位置是0x10020000, 結束位置是0x10020000
b. int age
占用4字節,起始位置也要是4的倍數, age的起始位置是0x10020004, 結束位置是 0x10020007
c. char *name
占用8字節,name
的起始位置是0x10020008, 結束位置是 0x1002000F
d. double height
占用8字節,那么height
的起始位置是0x10020010, 結束位置是 0x10020017
e. float weight
占用4字節,weight
的起始位置是0x10020018, 結束位置是 0x1002001B
f. 0x1B
是28,因為struct
內存對其是8字節,所以最終分配了32字節
-
StructDemo1
內存是大小是96字節,假如起始位置是0x10020000
a. struct StructDemo1 s1
占用40字節, s1起始位置是0x10020000,結束位置0x10020027
b. struct StructDemo2 s2
占用32字節, s1起始位置是0x10020028,結束位置0x10020047
c. float weight
占用4字節,weight的起始位置是0x10020048, 結束位置是 0x1002004B
d. char chr
占用1字節,那么chr的起始位置是0x1002004C, 結束位置是0x1002004C
e. int index
占用4字節,起始位置也要是4的倍數, index的起始位置是0x10020050, 結束位置是 0x10020053
f. double height
占用8字節,那么height的起始位置是0x10020058, 結束位置是 0x1002005F
g. 0x5F
是96,剛好使用了96字節
補充
為什么要內存對齊?
- 平臺移植問題: 不同的硬件平臺訪問地址是有其規則,不是所有硬件都可以任意訪問所有位置。
- 性能問題: 數據結構(特別是棧)應該盡可能在自然邊界上對其。因為訪問未對齊的內存,處理器需要做兩次內存訪問;而對齊的內存訪問僅需要一次。