alloc、init你弄懂50%了嗎?
前言
這是一篇我記錄對(duì)alloc、init分析思考的筆記。如果讀者想看懂我的第二個(gè)思考,可能需要您至少了解內(nèi)存的分段分頁(yè)管理,如果您對(duì)其一點(diǎn)都不知道,可以先看這篇軟文簡(jiǎn)單了解一下。另外很重要的一點(diǎn)是,請(qǐng)先思考。
思考1.對(duì)象為什么要alloc,init又是干嘛的?
很多人都知道,初始化一個(gè)對(duì)象應(yīng)該這么寫(xiě):
- MyClass* myObj = [MyClass alloc] init];
那么有沒(méi)有思考過(guò)為什么呢?其實(shí)我這么寫(xiě)也是完全可以的:
- MyClass *myObj = [MyClass alloc];
- myObj = [myObj init];
我們來(lái)看看這干了啥。
- alloc allocates a chunk of memory to hold the object, and returns the pointer.
就是說(shuō)alloc分配了一坨 內(nèi)存給對(duì)象,讓它不釋放,并且把地址返回給指針。
- MyClass *myObj = [MyClass alloc];
那么這樣過(guò)后myobj為什么不能被使用呢?這是因?yàn)檫@片內(nèi)存還沒(méi)有被正確的初始化。
舉個(gè)栗子,萬(wàn)達(dá)要修房子,他們第一步一定是要先向政府搞到一塊地,第二步才能在這塊地上動(dòng)工修樓。
這里操作系統(tǒng)就是政府,alloc就是去爭(zhēng)地,init就是在地上修房子。沒(méi)有調(diào)用init,房子都沒(méi)有修好,別人怎么買(mǎi)房進(jìn)去住?所以我們需要用init來(lái)初始化這片內(nèi)存:
- -init{
- self=[super init]; // 1.
- if(self){ // 2.
- ....
- }
- return self; // 3.
- }
第一步需要初始化父類(lèi)的信息,比如實(shí)例變量等等。可以理解成王思聰在修房子前要詢(xún)問(wèn)他老爸的意見(jiàn),他老爸說(shuō)想娛樂(lè)會(huì)所,他沒(méi)有意見(jiàn)的話就會(huì)修成娛樂(lè)會(huì)所,他如果有意見(jiàn),就可以悄悄的在第二步里面改為修成LOL俱樂(lè)部。第三步就不說(shuō)了。
最后提醒一下,不要這樣寫(xiě):
- MyClass* myObj = [MyClass alloc];
- myObj=[myObj init];
因?yàn)槟憧赡軙?huì)忘記在第二行加init,并且代碼也會(huì)增長(zhǎng)。
思考2.關(guān)于alloc的思考
在思考1中我們說(shuō)了:alloc分配了一坨 內(nèi)存給對(duì)象,讓它不釋放,并且把地址返回給指針。這里主要有兩個(gè)問(wèn)題:
- 調(diào)用alloc后內(nèi)存是直接映射到堆還是只分配給了虛擬內(nèi)存?
- 這一坨內(nèi)存到底是多大?
我們依次來(lái)展開(kāi)。
可能有些讀者不明白第一個(gè)問(wèn)題是什么意思,這里需要額外講一些關(guān)于內(nèi)存的東西,其實(shí)這是iOS開(kāi)發(fā)很重要的東西,不管是面試還是學(xué)習(xí)都可能會(huì)用到。
額外的東西
iOS里的內(nèi)存是有分類(lèi)的,它分成Clean Memory和Dirty Memory。顧名思義,Clean Memory是可以被操作系統(tǒng)回收的,Dirty Memory是不可被操作系統(tǒng)回收的。
- Clean Memory:在閃存中有備份,能再次讀取重建。如:
Code(代碼段),framework,memory-mapped files
- Dirty Memory:所有非Clean Memory,如:
被分配了的堆空間,image cache
舉個(gè)栗子,在這樣的代碼中:
- - (void)dirtyOrCleanMemory
- {
- NSString *str1 = [NSString stringWithString:@"Welcome!"]; // 1.
- NSString *str2 = @"Welcome"; // 2.
- char *buf = malloc(100 * 1024 * 1024); // 3.分配100M內(nèi)存給buf
- for (int i = 0; i < 3 * 1024 * 1024; ++i) {
- buf[i] = rand();
- } // 4.buf的前3M內(nèi)存被賦值
- }
對(duì)每行分析:
1.Dirty Memory。
因?yàn)閟tringWithString:是在堆上分配內(nèi)存的,如果我們不回收它的話,系統(tǒng)會(huì)一直占用這塊內(nèi)存。
2.Clean Memory。
因?yàn)橛眠@樣的方法創(chuàng)建的是一個(gè)常量字符串,常量字符串是放在只讀數(shù)據(jù)段的,如果這塊內(nèi)存被釋放了,而我們又訪問(wèn)它的時(shí)候,操作系統(tǒng)可以在只讀數(shù)據(jù)段中把值再讀取出來(lái)重建這塊內(nèi)存。(ps:所以用這種方法創(chuàng)建的string是沒(méi)有引用計(jì)數(shù)的。)
接下來(lái)的知識(shí)就是引出思考問(wèn)題1、2比較重要的點(diǎn)了:
3.Clean Memory。
這個(gè)時(shí)候buf指向的100M內(nèi)存區(qū)域是Clean Memory的,因?yàn)椴僮飨到y(tǒng)是很懶的,只有當(dāng)我們要用到這塊區(qū)域的時(shí)候才會(huì)映射到物理內(nèi)存,沒(méi)有使用的時(shí)候只會(huì)分配一塊虛擬內(nèi)存給buf。讀起來(lái)很繞口,上張圖:
可以看到虛擬內(nèi)存和物理內(nèi)存沒(méi)有映射關(guān)系,所以是Clean Memory的。
4.Dirty & Clean Memory混合。
前3M是Dirty Memory,后97M是Clean Memory。這句for語(yǔ)句執(zhí)行完成后,buf的前3M內(nèi)存被賦值,也就是buf的前3M被使用了,所以這個(gè)時(shí)候的映射關(guān)系是這樣的:
額外的東西Done.
回到主線
調(diào)用alloc后內(nèi)存是直接映射到堆(物理內(nèi)存)還是只分配給了虛擬內(nèi)存?
一坨內(nèi)存的一坨是多大?
這個(gè)時(shí)候我們的第一個(gè)問(wèn)題讀者應(yīng)該能明白了。那么我們?cè)趺打?yàn)證alloc是直接映射到堆上還是只分配給虛擬內(nèi)存呢?這個(gè)問(wèn)題讓我想了好些天,最后xo哥想到了一劑良藥來(lái)驗(yàn)證,那就是用instrument來(lái)推反。
使用instrument來(lái)證反
我們假設(shè)的論點(diǎn)是:對(duì)象收到alloc消息后只在虛擬內(nèi)存分配空間。
這里需要一丁點(diǎn)代碼。
1.我們隨便新建個(gè)工程。
2.然后做個(gè)model類(lèi):
- #import <Foundation/Foundation.h>
- @interface XOModel : NSObject
- {
- int a1;
- NSString *a2;
- }
- @end
3.在controller里給view加一個(gè)點(diǎn)擊事件:
- - (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.打開(kāi)instrument的alloction,運(yùn)行觸發(fā)一下點(diǎn)擊事件,查看如下:
(圖解:Persistent bytes表示所有的這類(lèi)東西在堆里的大小,Persistent表示所有的這類(lèi)東西的個(gè)數(shù).)
我們發(fā)現(xiàn)發(fā)現(xiàn)在Persistent bytes(堆)里實(shí)實(shí)在在地分配給了XOModel 3.05MB的空間。
我們?cè)儆|發(fā)一下點(diǎn)擊事件:
發(fā)現(xiàn)堆分配給了XOModel的大小空間變成了原來(lái)的兩倍6.10MB。
結(jié)論過(guò)渡:如果對(duì)象收到alloc消息只在虛擬內(nèi)存分配空間,那么persistent bytes(堆)里是不會(huì)分配給XOModel大小的,也就是說(shuō)這里的persistent bytes大小應(yīng)該是0。所以問(wèn)題1的結(jié)論如下:
結(jié)論:alloc不只分配在虛擬內(nèi)存,同時(shí)會(huì)在物理內(nèi)存建立映射。
對(duì)象的內(nèi)存分配
最后剩下我們的最后一個(gè)問(wèn)題:類(lèi)對(duì)象收到alloc消息后,操作系統(tǒng)會(huì)分配出來(lái)的一坨內(nèi)存是多大?
3.05M的大小是100000個(gè)XOModel對(duì)象的總和,那么一個(gè)XOModel的實(shí)例對(duì)象,操作系統(tǒng)會(huì)給他分配多大的空間呢?很簡(jiǎn)單嘛,3.05M/100000就得到了,等等,難道你真準(zhǔn)備這樣去算?好吧,其實(shí)一開(kāi)始我真這樣想過(guò),但是這肯定是算不出準(zhǔn)確答案的,關(guān)鍵是你要去思考。
這里有兩種辦法,我采用的第二種辦法。
第一種驗(yàn)證方法還是instrument
我們可以修改一下觸發(fā)的代碼,然后重新刷新instrument查看XOModel大小,具體操作同上,不重復(fù)了:
- - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
- for (int i = 0; i < 1; ++i) { // 修改處
- XOModel *model = [XOModel alloc]; // 注意這句只有alloc
- [self.array addObject:model];
- }
- }
第二種驗(yàn)證辦法借用runtime
我們可以借助runtime來(lái)查看一個(gè)類(lèi)對(duì)象所需要的內(nèi)存大小。值得一提的是我最開(kāi)始用的方法是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);
貌似是我們需要的函數(shù),但是我發(fā)現(xiàn)這個(gè)方法有bug,返回的size和instruments的值不同,后來(lái)又發(fā)現(xiàn)有人遇到同樣的問(wèn)題,所以摘抄了另一種方法,代碼如下:
- #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 %<a href="http://www.jobbole.com/members/q697158886">@:</a> %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位)的機(jī)子上運(yùn)行,輸出如下:
- AllocTest[38470:2551068] Size of XOModel: 32
“啊咧,我一個(gè)int,一個(gè)指針你分給我32個(gè)字節(jié),操作系統(tǒng)你是腦子進(jìn)屎了嗎?”
我們?cè)傩薷囊幌耎OModel代碼,不要實(shí)例變量:
- #import <Foundation/Foundation.h>
- @interface XOModel : NSObject
- {
- }
- @end
輸出如下:
- AllocTest[38630:2562602] Size of XOModel: 16
“我靠,我什么東西都沒(méi)有,操作系統(tǒng)你還要分給我16個(gè)字節(jié),是不是傻?”
智慧的操作系統(tǒng)這樣做當(dāng)然是有它自己的原因滴,這里我們需要知道三個(gè)東西:
- 任何類(lèi)對(duì)象都有一個(gè)isa指針,需要分配內(nèi)存。
- 32位機(jī)子上指針大小為4字節(jié),64位機(jī)子為8字節(jié)。
- 字節(jié)對(duì)齊。
第一點(diǎn)就不說(shuō)了,不知道的話您多半也沒(méi)耐心看到現(xiàn)在了。
第二點(diǎn)貼個(gè)文檔圖,iOS7過(guò)后部分蘋(píng)果機(jī)就開(kāi)始從32位操作系統(tǒng)轉(zhuǎn)到64位了,所以部分?jǐn)?shù)據(jù)類(lèi)型的大小也有變化,這里我們主要關(guān)注例子中的指針:
現(xiàn)在,基于這兩點(diǎn)對(duì)樣例分析。
- 第一個(gè)樣例(類(lèi)對(duì)象中一個(gè)int,一個(gè)NSString指針,一個(gè)isa指針),64位操作系統(tǒng)上應(yīng)該是4+8+8=20,然而輸出是32,不對(duì)。
- 第二個(gè)樣例(類(lèi)對(duì)象中只有一個(gè)isa指針),64位操作系統(tǒng)上應(yīng)該是8,然而輸出是16,還是不對(duì)。why?
字節(jié)對(duì)齊
字節(jié)對(duì)齊我了解得也不是太多,簡(jiǎn)單點(diǎn)講目的就是為了提高存取效率,概念就不展開(kāi)了,可以看這個(gè),我這里就直接講原理了。先貼一份蘋(píng)果的文檔:
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.
有點(diǎn)長(zhǎng),簡(jiǎn)單的意思就是:
當(dāng)我們分配一塊內(nèi)存的時(shí)候,假設(shè)需要的內(nèi)存小于16個(gè)字節(jié),操作系統(tǒng)會(huì)直接分配16個(gè)字節(jié);加入需要的內(nèi)存大于16個(gè)字節(jié),操作系統(tǒng)會(huì)分配a*16個(gè)字節(jié)。舉個(gè)栗子,如果你調(diào)用malloc并且需要4個(gè)字節(jié),系統(tǒng)會(huì)給你一塊16個(gè)字節(jié)的內(nèi)存塊;如果你調(diào)用malloc并且需要24個(gè)字節(jié),系統(tǒng)會(huì)給你一塊32個(gè)字節(jié)的內(nèi)存塊。
現(xiàn)在再看我們的栗子,就可以直接上圖了:
第一個(gè)例子不對(duì)齊應(yīng)該是20字節(jié),對(duì)齊就是32字節(jié)。
第二個(gè)例子不對(duì)其應(yīng)該是8字節(jié),對(duì)齊就是16字節(jié):
ps:在32位機(jī)器上可能會(huì)有不一樣的結(jié)果,因?yàn)橹羔槾笮〔煌?2位的蘋(píng)果機(jī)也是16字節(jié)對(duì)齊的。
至此我們對(duì)alloc的探究就結(jié)束了。
結(jié)語(yǔ)
這次的探究算是比較徹底了,過(guò)程當(dāng)中也學(xué)到了很多東西,在這個(gè)浮躁的社會(huì),學(xué)貴在道,術(shù)次之,打好基礎(chǔ)、保持思考才不會(huì)磨滅掉你對(duì)它最初的興趣。
參考鏈接:(由于微信不允許發(fā)鏈接,可點(diǎn)擊原文進(jìn)行查看)
- iOS內(nèi)存管理及優(yōu)化-騰訊莊延軍
- Checking the size of an object in Objective-C – Stack Overflow
- Does class_getInstanceSize have a known bug about returning incorrect sizes? – Stack Overflow
- Memory Usage Performance Guidelines – 蘋(píng)果文檔
- 字節(jié)對(duì)齊-百度百科
Done