alloc、init你弄懂50%了嗎?

轉載請注明出處:http://www.iosxxoo.com/2016/06/21/2016-06-21/

前言

這是一篇我記錄對alloc、init分析思考的筆記。如果讀者想看懂我的第二個思考,可能需要您至少了解內存的分段分頁管理,如果您對其一點都不知道,可以先看這篇軟文簡單了解一下。另外很重要的一點是,請先思考。

思考1.實例化對象為什么要alloc,init又是干嘛的?

很多人都知道,實例化一個對象應該這么寫:

MyClass* myObj = [MyClass alloc] init];

那么有沒有思考過為什么呢?其實我這么寫也是完全可以的:

MyClass *myObj = [MyClass alloc];
myObj = [myObj init];

我們來看看這干了啥。

alloc allocates a chunk of memory to hold the object, and returns the pointer.

就是說alloc分配了一坨 內存給對象,讓它不釋放,并且把地址返回給指針。

MyClass *myObj = [MyClass alloc];

那么這樣過后myobj為什么不能被使用呢?這是因為這片內存還沒有被正確的初始化。

舉個栗子,萬達要修房子,他們第一步一定是要先向政府搞到一塊地,第二步才能在這塊地上動工修樓。

這里操作系統就是政府,alloc就是去爭地,init就是在地上修房子。沒有調用init,房子都沒有修好,別人怎么買房進去住?所以我們需要用init來初始化這片內存:

-init{
     self=[super init]; // 1.
     if(self){          // 2.
         ....
     }
     return self;       // 3.
}

第一步需要初始化父類的信息,比如實例變量等等。可以理解成王思聰在修房子前要詢問他老爸的意見,他老爸說想娛樂會所,他沒有意見的話就會修成娛樂會所,他如果有意見,就可以悄悄的在第二步里面改為修成LOL俱樂部。第三步就不說了。

最后提醒一下,不要這樣寫:

 MyClass* myObj = [MyClass alloc];
 myObj=[myObj init];

因為你可能會忘記在第二行加init,并且代碼也會增長。

思考2.關于alloc的思考

在思考1中我們說了:alloc分配了一坨 內存給對象,讓它不釋放,并且把地址返回給指針。這里主要有兩個問題:

  • 調用alloc后內存是直接映射到堆還是只分配給了虛擬內存?
  • 這一坨內存到底是多大?

我們依次來展開。
可能有些讀者不明白第一個問題是什么意思,這里需要額外講一些關于內存的東西,其實這是iOS開發很重要的東西,不管是面試還是學習都可能會用到。


額外的東西

iOS里的內存是有分類的,它分成Clean Memory和Dirty Memory。顧名思義,Clean Memory是可以被操作系統回收的,Dirty Memory是不可被操作系統回收的。

  • Clean Memory:在閃存中有備份,能再次讀取重建。如:

    • Code(代碼段),framework,memory-mapped files
  • Dirty Memory:所有非Clean Memory,如:

    • 被分配了的堆空間,image cache

舉個栗子,在這樣的代碼中:

- (void)dirtyOrCleanMemory
{
    NSString *str1 = [NSString stringWithString:@"Welcome!"];   // 1.
    NSString *str2 = @"Welcome"; // 2.
    
    char *buf = malloc(100 * 1024 * 1024); // 3.分配100M內存給buf
    
    for (int i = 0; i < 3 * 1024 * 1024; ++i) {
        buf[i] = rand(); 
    }   // 4.buf的前3M內存被賦值
}

對每行分析:

1.Dirty Memory。
因為stringWithString:是在堆上分配內存的,如果我們不回收它的話,系統會一直占用這塊內存。

2.Clean Memory。
因為用這樣的方法創建的是一個常量字符串,常量字符串是放在只讀數據段的,如果這塊內存被釋放了,而我們又訪問它的時候,操作系統可以在只讀數據段中把值再讀取出來重建這塊內存。(ps:所以用這種方法創建的string是沒有引用計數的。)

接下來的知識就是引出思考問題1、2比較重要的點了:

3.Clean Memory。
這個時候buf指向的100M內存區域是Clean Memory的,因為操作系統是很懶的,只有當我們要用到這塊區域的時候才會映射到物理內存,沒有使用的時候只會分配一塊虛擬內存給buf。讀起來很繞口,上張圖:

可以看到虛擬內存和物理內存沒有映射關系,所以是Clean Memory的。

4.Dirty & Clean Memory混合。
前3M是Dirty Memory,后97M是Clean Memory。這句for語句執行完成后,buf的前3M內存被賦值,也就是buf的前3M被使用了,所以這個時候的映射關系是這樣的:


額外的東西Done.


回到主線

  • 調用alloc后內存是直接映射到堆(物理內存)還是只分配給了虛擬內存?
  • 一坨內存的一坨是多大?

這個時候我們的第一個問題讀者應該能明白了。那么我們怎么驗證alloc是直接映射到堆上還是只分配給虛擬內存呢?這個問題讓我想了好些天,最后xo哥想到了一劑良藥來驗證,那就是用instrument來推反。

使用instrument來證反

我們假設的論點是:對象收到alloc消息后只在虛擬內存分配空間。
這里需要一丁點代碼。

1.我們隨便新建個工程。
2.然后做個model類:

#import <Foundation/Foundation.h>

@interface XOModel : NSObject
{
    int a1;
    NSString *a2;
}

@end

3.在controller里給view加一個點擊事件:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    for (int i = 0; i < 100000; ++i) {
        XOModel *model = [XOModel alloc]; // 注意這句只有alloc
        [self.array addObject:model];
    }
}

4.打開instrument的alloction,運行觸發一下點擊事件,查看如下:



(圖解:Persistent bytes表示所有的這類東西在堆里的大小,Persistent表示所有的這類東西的個數.)

我們發現發現在Persistent bytes(堆)里實實在在地分配給了XOModel 3.05MB的空間。
我們再觸發一下點擊事件:


發現堆分配給了XOModel的大小空間變成了原來的兩倍6.10MB。

結論過渡:如果對象收到alloc消息只在虛擬內存分配空間,那么persistent bytes(堆)里是不會分配給XOModel大小的,也就是說這里的persistent bytes大小應該是0。所以問題1的結論如下:

結論:alloc不只分配在虛擬內存,同時會在物理內存建立映射。

對象的內存分配

最后剩下我們的最后一個問題:類對象收到alloc消息后,操作系統會分配出來的一坨內存是多大?

3.05M的大小是100000個XOModel對象的總和,那么一個XOModel的實例對象,操作系統會給他分配多大的空間呢?很簡單嘛,3.05M/100000就得到了,等等,難道你真準備這樣去算?好吧,其實一開始我真這樣想過,但是這肯定是算不出準確答案的,關鍵是你要去思考。

這里有兩種辦法,我采用的第二種辦法。

第一種驗證方法還是instrument

我們可以修改一下觸發的代碼,然后重新刷新instrument查看XOModel大小,具體操作同上,不重復了:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    for (int i = 0; i < 1; ++i) { // 修改處
        XOModel *model = [XOModel alloc]; // 注意這句只有alloc
        [self.array addObject:model];
    }
}

第二種驗證辦法借用runtime

我們可以借助runtime來查看一個類對象所需要的內存大小。值得一提的是我最開始用的方法是class_getInstanceSize,原型如下:

/** 
 * Returns the size of instances of a class.
 * 
 * @param cls A class object.
 * 
 * @return The size in bytes of instances of the class \e cls, or \c 0 if \e cls is \c Nil.
 */
OBJC_EXPORT size_t class_getInstanceSize(Class cls) 
     __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);

貌似是我們需要的函數,但是我發現這個方法有bug,返回的size和instruments的值不同,后來又發現有人遇到同樣的問題,所以摘抄了另一種方法,代碼如下:

#import <objc/runtime.h>
#import <malloc/malloc.h>
...
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    XOModel *model = [XOModel alloc];
    [self.array addObject:model];
    
    NSLog(@"Size of %@: %zd", NSStringFromClass([XOModel class]), malloc_size((__bridge const void *) model));
}

再貼下XOModel代碼:

#import <Foundation/Foundation.h>

@interface XOModel : NSObject
{
    int a1;
    NSString *a2;
}

@end

iPhone 6(或其他64位)的機子上運行,輸出如下:

AllocTest[38470:2551068] Size of XOModel: 32

“啊咧,我一個int,一個指針你分給我32個字節,操作系統你是腦子進屎了嗎?”

我們再修改一下XOModel代碼,不要實例變量:

#import <Foundation/Foundation.h>

@interface XOModel : NSObject
{
}

@end

輸出如下:

AllocTest[38630:2562602] Size of XOModel: 16

“我靠,我什么東西都沒有,操作系統你還要分給我16個字節,是不是傻?”

智慧的操作系統這樣做當然是有它自己的原因滴,這里我們需要知道三個東西:

  1. 任何類對象都有一個isa指針,需要分配內存。
  2. 32位機子上指針大小為4字節,64位機子為8字節。
  3. 字節對齊

第一點就不說了,不知道的話您多半也沒耐心看到現在了。
第二點貼個文檔圖,iOS7過后部分蘋果機就開始從32位操作系統轉到64位了,所以部分數據類型的大小也有變化,這里我們主要關注例子中的指針:


現在,基于這兩點對樣例分析。

  • 第一個樣例(類對象中一個int,一個NSString指針,一個isa指針),64位操作系統上應該是4+8+8=20,然而輸出是32,不對。
  • 第二個樣例(類對象中只有一個isa指針),64位操作系統上應該是8,然而輸出是16,還是不對。why?

這就是最后一個探究點 -- 字節對齊。

字節對齊

字節對齊我了解得也不是太多,簡單點講目的就是為了提高存取效率,概念就不展開了,可以看這個,我這里就直接講原理了。先貼一份蘋果的文檔:

When allocating any small blocks of memory, remember that the granularity for blocks allocated by the malloc library is 16 bytes. Thus, the smallest block of memory you can allocate is 16 bytes and any blocks larger than that are a multiple of 16. For example, if you call malloc and ask for 4 bytes, it returns a block whose size is 16 bytes; if you request 24 bytes, it returns a block whose size is 32 bytes. Because of this granularity, you should design your data structures carefully and try to make them multiples of 16 bytes whenever possible.

有點長,簡單的意思就是:

當我們分配一塊內存的時候,假設需要的內存小于16個字節,操作系統會直接分配16個字節;加入需要的內存大于16個字節,操作系統會分配a*16個字節。舉個栗子,如果你調用malloc并且需要4個字節,系統會給你一塊16個字節的內存塊;如果你調用malloc并且需要24個字節,系統會給你一塊32個字節的內存塊。

現在再看我們的栗子,就可以直接上圖了:
第一個例子不對齊應該是20字節,對齊就是32字節。


第二個例子不對其應該是8字節,對齊就是16字節:


ps:在32位機器上可能會有不一樣的結果,因為指針大小不同,但是32位的蘋果機也是16字節對齊的。

至此我們對alloc的探究就結束了。

結語

這次的探究算是比較徹底了,過程當中也學到了很多東西,在這個浮躁的社會,學貴在道,術次之,打好基礎、保持思考才不會磨滅掉你對它最初的興趣。

參考鏈接:

Done

作者: @biggergao
個人博客: Mr.碼了戈壁

2016年06月21日

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

推薦閱讀更多精彩內容