iOS 底層探究之 alloc

我們通過幾個問題來探究下一個iOS如何獲取到一個對象:

  1. alloc和init的區別?
  2. 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對象的打印:

  1. obj1、obj2、obj3 的內存地址是一樣 0x6000000fc580,和obj4 0x6000000fc6a0,說明init不會分配,調用alloc時才分配了棧地址,
  2. obj1、obj2、obj3、obj4 變量的指針地址都不一樣,而且是連續,依次變小的,因為指針地址分配在棧區,棧區分配內存是連續的。
  3. 棧區和堆區的內存分配圖解:
image.png

總結:

  1. alloc才會分配內存地址,init用于初始化數據。

  2. 變量指針地址分配在棧區,而且是嚴格根據變量聲明順序連續分配內存地址,從高到低分配。

  3. NSObject對象的內容一般存儲在堆區,從低到高分配,因為堆空間分配是找到一塊可用且大于需要分配內存大小的地址,有可能后分配的內存地址可能更小。

alloc方法做了哪些事情

從我對alloc的調用棧和實現邏輯,得到以下結論:

  1. 分配對象所需的內存,并做了內存對齊工作
  2. 將對象和所屬類型通過isa屬性綁定起來

準備工作

下載可編譯的objc4源碼[1],可以直接使用,不需要配置。如果斷點不生效,我的解決方案是將target -> build phases -> compile sources -> 將要斷點的文件移到最前面就生效了。

alloc調用鏈

  1. NSObject調用alloc

  2. 調用objc_alloc

  3. callAlloc(cls, true, false)

  4. NSObject 通過objc_msgSend調用 +alloc

  5. _objc_rootAlloc

  6. callAlloc(cls, false, true)

  7. _objc_rootAllocWithZone

  8. _class_createInstanceFromZone(): 內部實現內存分配和綁定類型
    (1). instanceSize(): 計算obj所需要的內存及實現內存對齊
    (2). calloc(): 分配內存,得到一個對象
    (3). initInstanceIsa(): 綁定類型

  9. alloc調用流程圖:

image.png

分配內存,并實現內存對齊

  1. 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()分配內存。

綁定類型

  1. alloc最終_class_createInstanceFromZone()方法initInstanceIsa()實現類型綁定。
inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}
  1. 然后調用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()方法實現了對象的內存分配,內存對齊,將對象和類型綁定三個功能。

內存對齊實際案例

  1. Apple在64位下,對象內存對齊是16,結構體是8。

  2. 內存分配時,會根據屬性或成員變量的類型length, 屬性或成員的起始內存必須是該類型length的整數倍。

驗證64位下內存對齊是16

  1. 在內存分配時,最終調用objc-runtime-new.h _class_createInstanceFromZone()方法中
  2. 調用順序是:_class_createInstanceFromZone() -> instanceSize() -> cache.fastInstanceSize() -> align16()
  3. 最終調用的是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
  1. 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

  1. 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字節

  1. 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

  1. 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;
  1. 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字節

  1. 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字節

  1. 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字節

補充

為什么要內存對齊?

  1. 平臺移植問題: 不同的硬件平臺訪問地址是有其規則,不是所有硬件都可以任意訪問所有位置。
  2. 性能問題: 數據結構(特別是棧)應該盡可能在自然邊界上對其。因為訪問未對齊的內存,處理器需要做兩次內存訪問;而對齊的內存訪問僅需要一次。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370