OC-再探-對象原理-【Alloc init】

[TOC]

前言

  • 本文主要圍繞以下幾點內容展開討論;

對象的分配一定會占用8個字節嗎?如果它是char類型或者int類型的,占不滿8字節怎么辦,系統會不會對內存分配進行優化?

屬性的字節對齊和對象的字節對齊有何不同?

isa是個啥?里面包含啥?

共同體、位域的原理?

或運算的原理;

OC內存優化

探究字節對齊

字節占位截圖

舉個例子:

  • int 4字節;
  • char 1字節;
  • nsstring 8字節;

思考由于字節對齊的原則(不滿足8字節就要補齊),如果一個對象中都是int這樣的屬性那是不是要開辟很多個8字節的內存空間,這樣是不是很浪費?

首先還是要搞清楚,什么是字節對齊和字節補齊!請看下面;

  • 該對象暴漏2個屬性,按照字節對齊的原則,int類型只占了4個字節,需要補齊為8個字節,所以該對象目前有三個屬性 isa、name、age ,所以內存為該對象開辟了24個字節;
字節對齊示例1
  • 該對象暴漏3個屬性,按照字節對齊的原則,int類型只占了4個字節,需要補齊為8個字節,但是這個對象的下個屬性是ch,ch本身占了1個字節,系統默認將這一個字節排列到了age的后面,節約的空間,所以該對象目前有四個屬性 isa、name、age、ch ,所以內存為該對象也是開辟了24個字節;
字節對齊示例2

通過輸出內存值的方式來驗證字節對齊和字節補齊的原理,請看下面;

  • C語言的結構體浪費內存空間,它是按照寫入結構體的順序依次去開辟內存空間的,也就造成了下圖的情況,同樣的結構體,一個開辟了24字節、另一個開辟了16字節
C語言的結構體
  • OC語言對象中的屬性并不會因為屬性排列的先后順序,而導致內存分配時造成的浪費,通過debug調試 在源碼size_t size = cls->instanceSize(extraBytes);出輸出兩個對象的size均為32字節;
OC語言的對象內存

探究class_getInstanceSize

  • 注意!:這里要注意一點,在oc中輸出類的size直接使用NSLog(@"%lu",sizeof(p));是不準確的,因為它不能把類中所有的元素都計算出來。舉個例子;
OC語言的對象內存
  • 疑問:那如何打印系統實際為我們類開辟的的內存空間是多大呢?請看下圖;

使用class_getInstanceSize接口直接獲取內存大小;

OC語言的對象內存
  • 疑問:那這樣打印的值就是最終系統為我們開辟的內存大小嗎?請看下圖;

當對象中所有元素(包括isa)所需要開辟的內存空間<16時,這時使用class_getInstanceSize輸出的內存大小是要小于16字節的;

OC語言的對象

上面已經說了系統為我們類開辟內存大小可以用class_getInstanceSize來輸出,那我們類最開始希望內存為我們開辟多少內存用什么來展示呢?請看下圖;

OC語言的對象

結論:class_getInstanceSize返回類對象至少需要多少空間malloc_size()返回的是實際分配的內存空間是不一致的!!!!

探究malloc_size

探究 malloc_size方法;

  • 1、找到malloc_size方法;

alloc_size的調用是在object-runtime_new.mm中進行的,如下圖;但是alloc_size的實現文件并沒暴漏出來,所以我們還是要去官網下載malloc_size的源碼;

OC語言的對象

OC語言的對象
malloc_size官網截圖
  • 3、debug源碼(1);

calloc-->malloc_zone_calloc-->ptr = zone->calloc(zone, num_items, size);

疑問?:我擦?calloc執行到最后還是calloc怎么遞歸了?

  • 3、debug源碼(2);

通過箭頭函數(屬性函數:屬性函數主要的目的是賦值,他的實現不在這里)找到這個函數實現的方法請看下圖;

malloc_size官網截圖
malloc_size官網截圖
  • 3、debug源碼(3);
malloc_size官網截圖
  • 4、debug源碼(4)-->16字節對齊!
malloc_size官網截圖16字節對齊
  • 重點!!!:這里我們再回顧下16字節對齊和8字節對齊的原理,16字節對齊就是 2^4-1,8字節對齊就是2^3-1
malloc_size官網截圖16字節對齊
malloc_size官網截圖16字節對齊
  • 5、debug源碼(5)-->16字節對齊!

這也就說明了為什么,傳進來的size為40字節的內存空間會被系統修改為48,因為按照16字節對齊的原則40需要補齊為48;

malloc_size官網截圖16字節對齊
  • 6、debug源碼(6)-->為啥參照對象就是8字節對齊、系統分配就是16字節對齊?
NSLog(@"class_getInstanceSize返回類對象至少需要多少空間為%lu",class_getInstanceSize([p class]));
NSLog(@"malloc_size()返回的是實際分配的內存空間%lu",malloc_size((__bridge const void *)(p)));

8字節對齊---------參考的是對象當中的屬性
16字節對齊---------參考的是整個對象

  • 7、debug源碼(7)-->這樣就會造成一個現象,有可能至少需要的內存空間和實際分配的內存空間是不相同的那為啥要多出來8位字節呢?

答案:主要是為了對象之間的系統安全,因為你滿足8字節對齊只能保證對象中屬性之間的安全性,只有保證16字節對齊才能保證對象之間的安全性;系統開辟對象內存是連續的,滿足16字節對齊原則,防止內存越界;(請看下圖)

malloc_size官網截圖16字節對齊

探究編譯器優化

思考?:那系統這么厲害是怎么優化代碼,讓運行的代碼達到優化的呢?

  • 7、debug源碼(7)-->從Debug切換到release后系統做了什么?(此篇文章只簡單介紹

編譯代碼-->斷點-->always show disassembly查看匯編源碼會發現Debug模式下要比release模式下的代碼多很多;這就說明在release模式下系統會將臃腫的代碼進行整合和優化;

編譯優化
編譯優化
編譯優化

探究obj->initInstanceIsa(ISA)

1、探究ISA成員

isa是啥?:

ISA就是一個聯合體!

聯合體是干嘛的?

在進行某些算法的C語言編程的時候,需要使幾種不同類型的變量存放到同一段內存單元中。也就是使用覆蓋技術,幾個變量互相覆蓋。這種幾個不同的變量共同占用一段內存的結構,在C語言中,被稱作“共用體”類型結構,簡稱共用體,也叫聯合體

舉個聯合體的例子!!!!!!!

注意:Union聯合體的成員之間共享內存空間。以isa_t為例,cls成員和bits成員雖然不同,但是兩者的值實際在任何時候都是一致的。例如,isa.class = [NSString class]指定了cls指向NSString類的內存地址,此時查看isa.bits會發現其值為NSString類的內存地址;反之,isa.bits = 0xFF,則isa.class的值變為255

isa里面有啥?

clsbits 其中 struct中的內容是為了解釋bits的8*8位;

union isa_t {
   isa_t() { }
   isa_t(uintptr_t value) : bits(value) { }

   Class cls;
   uintptr_t bits;
#if defined(ISA_BITFIELD)
   struct {
       ISA_BITFIELD;  // defined in isa.h
   };
#endif
};

那這個8字節的isa對象里面都放了啥?

OC語言的對象
2、位域,有以下特性

什么是位域?

開辟完的內存空間你指定那些屬性放在哪個指定的區域,這就叫位域!

為什么引入位域的概念?

因為要節約內存空間! 因為一個字節有8位,如果你只用一個字節就可以放下這個成員的話,我就沒必要開辟太大的內存給你用,所以就用位域的概念在每個位域放置不同的成員,然后通過移位的方式來達到取值的效果;

二進制重排oc內存優化的原理類似;

二進制重排傳送門

OC語言的對象
內存

聯合體和位域

3、聯合體union也成共用體,有以下特性
  1. union中可以定義多個成員, union的大小由最大成員的大小決定.
  2. union成員共享同一塊大小的內存,一次只能使用其中的一個成員.
  3. 對union某一個成員賦值, 會覆蓋其他成員的值.
  4. union的存放順序是所有成員都從低地址開始存放的.

使用聯合體的好處

  1. 多個成員共用一塊內存
  2. 可讀性強
  3. 使用位運算提高數據存儲的效率

壞處

  1. 也是共用同一塊內存, 有可能會浪費一定的空間
證明

先創建一個對象 LGTank

@interface LGTank(){
    // 聯合體
    union {
        char bits;
        // 位域
        struct {
            char front  : 1;
            char back   : 8;
            char left   : 1;
            char right  : 1;
        };
        
    } _direction;
}
1. union中可以定義多個成員, union的大小由最大成員的大小決定.
  1. struct中的值類型都為char時, 打印出來的大小為 1
成員

bits 大小為1
結構體大小也為1
所以聯合體大小為1

  1. 將struct其中一個成員變量類型改為 short, 打印出來的大小為 2
成員
bits 大小為1
結構體大小為2
所以聯合體大小為2
  1. 將bits類型改為 int, 打印出來的大小為 4

    成員

    bits 大小為4
    結構體大小為2
    所以聯合體大小為4

  2. 將struct其中一個成員變量類型改為 int, 打印出來的大小為 4

    成員

    bits 大小為4
    結構體大小為4
    所以聯合體大小為4

2. 聯合體結構的分析
  1. 變量 char bits

    這個使我們常見的定義變量的方式, 類型+變量名.

  2. 位域struct的分析

struct {
        char front  : 1;
        char back   : 1;
        char left   : 1;
        char right  : 1;
    };

假如我們不知道有聯合體和位域這個稱謂, 這里定義了一個結構體, 沒有聲明結構體類型, 也沒有創建結構體變量, 那放在這里有什么用呢?

  • 打印聯合體信息


    聯合體

    可以看出來存在兩個變量, 一個變量 bits, 一個匿名結構體變量, 值為(1, 0, 0, 0)

  • 注釋這段代碼, 發現程序還是能夠正常運行, 也還是能輸出bits的值, 跟有沒有注釋這段代碼前的結果一模一樣.


    聯合體

    但是這段代碼輸出的聯合體重的變量只有一個 bits

3. 或運算和&運算原理

或運算有1得1;

    0000 0000
    0000 0001
 或等于  
    0000 0001
  • &運算有0得0、11得1;
    0000 0000
    1111 0110
 或等于  
    0000 0000
4. 聯合體的賦值

對結構體的賦值一般都是通過位運算進行賦值

#define LGDirectionFrontMask    (1 << 0)
#define LGDirectionBackMask     (1 << 1)
#define LGDirectionLeftMask     (1 << 2)
#define LGDirectionRightMask    (1 << 3)
- (void)setFront:(BOOL)isFront {
    if (isFront) {
        _direction.bits |= LGDirectionFrontMask;
    } else {
        _direction.bits &= ~LGDirectionFrontMask;
    }
}

因為聯合體共用的是一塊內存, 所以不能直接對bits賦一個數值, 這樣會導致結果超出預期范圍之外.

需要通過對面罩Mask做位運算來進行賦值
需要設置front為YES,
比如 LGDirectionFrontMask 的二進制為 0x00000001

  1. 最后一位一定是1, 所以需要跟最后一位或運算
  2. 現在需要改變 front位的值, 而不影響到其他位的值, 所以其他為要跟00使用或運算

需要設置front為NO,

  1. 最后一位是0, 必須執行與運算
  2. 而執行運算的時候, 不影響其他位域的值, 則需要跟1進行或運算. 從而可以推斷出跟bits進行與運算的數是0x11111110, 剛好等于~LGDirectionFrontMask
5. 共用空間
共同體

再來看這張圖, 在設置屬性的值的地方, 明明沒有設置結構體中front的值, 可是輸出的結果中卻顯示front=1, 就是我們設置的bits=1

猜想: 是否結構體中的值表示這個聯合體的某一位的值?

#define LGDirectionFrontMask    0b0000011

嘗試將面罩從0b1設置為0b11, 打印出來的結果如下

共同體

果然是表示這個聯合體前兩位的一個值

再次驗證

#define LGDirectionFrontMask    0b0000101

猜測結果 應該是 front位和left位值為 1

共同體

輸出的結果和我們預期是完全一模一樣的.

所以, 我們可以知道這個結構體在聯合體里面的作用相當于一個注釋的功能, 就是告訴使用這個聯合體的人, 這個聯合體每一位對應存的是什么信息, 你要按照結構體中標注的位域空間來進行讀取.

驚喜二, 上述驗證除了解釋聯合體的特性之外, 還輔助我解釋了小端序這一特性

小端序

前面的筆記中說過小端模式的特點是: 是指數據的高字節保存在內存的高地址中,而數據的低字節保存在內存的低地址中。

當時一直不明白什么是數據的高字節/低字節, 什么又是內存的高地址/低地址

讓聯合體帶著我們來看

#define LGDirectionFrontMask    0b00000101

LGDirectionFrontMask這個常量, 左邊的是高字節, 這個很好理解, 不會理解的就當做十進制數的個十百千萬的順序來理解就行.

而內存的低地址/高地址怎么理解呢? 我們平時通過x/4gx obj來打印一個對象的屬性內存段地址時, 可以看到屬性的地址是依次增加的, 也就是說在前面的屬性的地址屬于低地址.

通過上面實例來分析: LGDirectionFrontMask最右邊的1, 表示這個字節的最低字節了, 而對應的聯合體union的內存地址中的 front 位--第一位, 也就是內存的最低地址, 從而驗證了數據的低字節保存的內存的低地址中

nice, 學習總是環環相扣, 當你對某個知識點理不清楚的時候, 不要鉆牛角尖, 去了解下一個知識點, 也許會柳暗花明又一村

聯合體和結構體的對比

  • 分別定義一個相同結構的聯合體和結構體
// 聯合體
union {
    char bits;
    // 位域
    struct {
        char front  : 1;
        char back   : 1;
        char left   : 1;
        char right  : 1;
    };

} _direction;

//結構體
struct {
    char bits;
    // 匿名結構體
    struct {
        char front  : 1;
        char back   : 1;
        char left   : 1;
        char right  : 1;
    };
} _word;
  • 使用相同的面罩給他們賦值
#define LGDirectionFrontMask    0b0000101
- (void)setFront:(BOOL)isFront {
    if (isFront) {
        _direction.bits |= LGDirectionFrontMask;
        _word.bits      |= LGDirectionFrontMask;
    } else {
        _direction.bits &= ~LGDirectionFrontMask;
        _word.bits      &= ~LGDirectionFrontMask;
    }
}
  • 查看_direction和_word
(lldb) p tank->_direction
((anonymous union)) $0 = {
  bits = '\x05'
   = (front = '\x01', back = '\0', left = '\x01', right = '\0')
}
(lldb) p tank->_word
((anonymous struct)) $1 = {
  bits = '\x05'
   = (front = '\0', back = '\0', left = '\0', right = '\0')
}

在聯合體中, 可以看到我們定義的那個匿名結構體中有值, 從右往左讀分別是0x0101, 而這個順序恰好是bits值(5)的二進制值, 這便解釋了位域這個詞的含義, 結構體所在內存的每一位的值.

而在結構體中, 可以看到屬性bits有值, 但是另一個屬性匿名結構體中的值并沒有任何變化, 說明這兩個屬性之間沒有任何關聯, 分別存在兩個不同的地方.

如何判斷ISA是cls還是 bits??
  • 首先上面說了clsbits都屬于聯合體中的成員;那ISA它是哪種類型取決于什么呢?

答案:取決于nonpointer是0還是1、如果是1代表是bits、如果是0則是cls;

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

總結

內存對齊的原則

內存對齊的原則
  • 原則一: 第一個數據成員放在offset為0的地方
  • 原則二: 以后每個成員的起始位置為該成員大小的整數倍
  • 原則三: 如果成員是結構體, 則這個成員的起始位置為結構體內部的最大成員的整數倍
  • 原則四: 結構體總大小, 也就是sizeof的結果, 必須為內部成員(包括成員為結構體的內部成員)最大成員大小的整數倍, 不足要補齊.

內存對齊的思考

  1. 空間換時間

    我們的內存按照8字節對齊, 計算機每次在讀數據的時候, 每次按照8字節的刻度讀取, 遠比逐個字節讀取的效率要高得多.

  2. 如何定義屬性保證結構體所占字節最小

    這個系統會自動編排, 不需要我們關心成員的排列方式. 系統是如何自動編排的, 有興趣的時候可以研究下.

  3. 內存優化

    內存對齊可以節約內存空間, 優化內存.
    isa指針的結構也是優化內存的一種設計, 將不同的信息存在isa的不同位域, 避免使用過多的屬性

  4. 二進制重排

    先將有意義的內存排列在一起, 優先進行加載, 對沒有用到的內存排列在其他地方等待加載, 以提高啟動速度.
    [圖片上傳失敗...(image-bf64c7-1586161219748)]

屬性的字節對齊和對象的字節對齊有何不同?

屬性字節對齊8字節對齊---------參考的是對象當中的屬性
對象字節對齊16字節對齊---------參考的是整個對象

isa是個啥?里面包含啥?

isa是一個聯合體;它里面包含了兩個屬性;clsbits,struct是對64位bits字節歸屬的解釋;

聯合體、位域的原理?他們搭配起來起到了什么作用?

聯合體、位域的原理

或運算的原理;

  • 或運算有1得1;
  • &運算有0得0、11得1;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容