iOS面試題—內(nèi)存管理、自動(dòng)釋放池與循環(huán)引用

  • 內(nèi)存布局
  • 內(nèi)存管理方案
  • MRC(手動(dòng)引用計(jì)數(shù))和ARC(自動(dòng)引用計(jì)數(shù))
  • 循環(huán)引用

一、內(nèi)存布局

  • 棧(stack):方法調(diào)用,局部變量等,是連續(xù)的,高地址往低地址擴(kuò)展
  • 堆(heap):通過(guò)alloc等分配的對(duì)象,是離散的,低地址往高地址擴(kuò)展,需要我們手動(dòng)控制
  • 未初始化數(shù)據(jù)(bss):未初始化的全局變量等
  • 已初始化數(shù)據(jù)(data):已初始化的全局變量等
  • 代碼段(text):程序代碼
2、64bit和32bit下 long 和char*所占字節(jié)是不同的
char:1字節(jié)(ASCII 2^8= 256個(gè)字符)
char*(即指針變量): 4個(gè)字節(jié)(32位的尋址空間是2^32
,即32個(gè)bit,也就是4個(gè)字節(jié)。同理64位編譯器為8個(gè)字節(jié))
short int : 2個(gè)字節(jié) 范圍 -2^16~> 2^16 即 -32768~>32767
int: 4個(gè)字節(jié) 范圍 -2147483648~>2147483647
unsigned int : 4個(gè)字節(jié)
long: 4個(gè)字節(jié) 范圍 和int一樣 64位下8個(gè)字節(jié),范圍 -9223372036854775808~9223372036854775807
long long: 8個(gè)字節(jié) 范圍 -9223372036854775808~9223372036854775807
unsigned long long: 8個(gè)字節(jié) 最大值:1844674407370955161
float: 4個(gè)字節(jié)
double: 8個(gè)字節(jié)。
3、static、const和sizeof關(guān)鍵字
static關(guān)鍵字

答:Static的用途主要有兩個(gè),一是用于修飾存儲(chǔ)類型使之成為靜態(tài)存儲(chǔ)類型,二是用于修飾鏈接屬性使之成為內(nèi)部鏈接屬性。

  • 1、靜態(tài)存儲(chǔ)類型:
    在函數(shù)內(nèi)定義的靜態(tài)局部變量,該變量存在內(nèi)存的靜態(tài)區(qū),所以即使該函數(shù)運(yùn)行結(jié)束,靜態(tài)變量的值不會(huì)被銷毀,函數(shù)下次運(yùn)行時(shí)能仍用到這個(gè)值。
    在函數(shù)外定義的靜態(tài)變量——靜態(tài)全局變量,該變量的作用域只能在定義該變量的文件中,不能被其他文件通過(guò)extern引用。
  • 2、內(nèi)部鏈接屬性
    靜態(tài)函數(shù)只能在聲明它的源文件中使用。
const關(guān)鍵字
  • 1、聲明常變量,使得指定的變量不能被修改。
const int a = 5;/*a的值一直為5,不能被改變*/
const int b; b = 10;/*b的值被賦值為10后,不能被改變*/
const int *ptr; /*ptr為指向整型常量的指針,ptr的值可以修改,但不能修改其所指向的值*/
int *const ptr;/*ptr為指向整型的常量指針,ptr的值不能修改,但可以修改其所指向的值*/
const int *const ptr;/*ptr為指向整型常量的常量指針,ptr及其指向的值都不能修改*/
  • 2、修飾函數(shù)形參,使得形參在函數(shù)內(nèi)不能被修改,表示輸入?yún)?shù)。如下:
int fun(const int a);或int fun(const char *str);
  • 3、修飾函數(shù)返回值,使得函數(shù)的返回值不能被修改。
const char *getstr(void);使用:const *str= getstr();
const int getint(void);  使用:const int a =getint();
sizeof關(guān)鍵字

sizeof是在編譯階段處理,且不能被編譯為機(jī)器碼。sizeof的結(jié)果等于對(duì)象或類型所占的內(nèi)存字節(jié)數(shù)。sizeof的返回值類型為size_t。

  • 變量:int a; sizeof(a)為4;
  • 指針:int *p; sizeof(p)為4;
  • 數(shù)組:int b[10]; sizeof(b)為數(shù)組的大小,4*10;int c[0]; sizeof(c)等于0
  • 結(jié)構(gòu)體:struct (int a; char ch;)s1; sizeof(s1)為8 與結(jié)構(gòu)體字節(jié)對(duì)齊有關(guān)。
    對(duì)結(jié)構(gòu)體求sizeof時(shí),有兩個(gè)原則:
 (1)展開(kāi)后的結(jié)構(gòu)體的第一個(gè)成員的偏移量應(yīng)當(dāng)是被展開(kāi)的結(jié)構(gòu)體中最大的成員的整數(shù)倍。
 (2)結(jié)構(gòu)體大小必須是所有成員大小的整數(shù)倍,這里所有成員計(jì)算的是展開(kāi)后的成員,而不是將嵌套的結(jié)構(gòu)體當(dāng)做一個(gè)整體。
  • 注意:不能對(duì)結(jié)構(gòu)體中的位域成員使用sizeof
    sizeof(void)等于1
    sizeof(void *)等于4

二、內(nèi)存管理方案

  • taggedPointer :存儲(chǔ)小對(duì)象如NSNumber。深入理解Tagged Pointer
  • NONPOINTER_ISA(非指針型的isa):在64位架構(gòu)下,isa指針是占64比特位的,實(shí)際上只有30多位就已經(jīng)夠用了,為了提高利用率,剩余的比特位存儲(chǔ)了內(nèi)存管理的相關(guān)數(shù)據(jù)內(nèi)容。
  • 散列表:復(fù)雜的數(shù)據(jù)結(jié)構(gòu),包括了引用計(jì)數(shù)表和弱引用表
    通過(guò)SideTables()結(jié)構(gòu)來(lái)實(shí)現(xiàn)的,SideTables()結(jié)構(gòu)下,有很多SideTable的數(shù)據(jù)結(jié)構(gòu)。
    而sideTable當(dāng)中包含了自旋鎖,引用計(jì)數(shù)表,弱引用表。
    SideTables()實(shí)際上是一個(gè)哈希表,通過(guò)對(duì)象的地址來(lái)計(jì)算該對(duì)象的引用計(jì)數(shù)在哪個(gè)sideTable中。
自旋鎖:
  • 自旋鎖是“忙等”的鎖。
  • 適用于輕量訪問(wèn)。
    引用計(jì)數(shù)表和弱引用表實(shí)際是一個(gè)哈希表,來(lái)提高查找效率。

三、MRC(手動(dòng)引用計(jì)數(shù))和ARC(自動(dòng)引用計(jì)數(shù))

1、MRC:alloc,retain,release,retainCount,autorelease,dealloc
2、ARC:

  • ARC是LLVM和Runtime協(xié)作的結(jié)果
  • ARC禁止手動(dòng)調(diào)用retain,release,retainCount,autorelease關(guān)鍵字
  • ARC新增weak,strong關(guān)鍵字

3、引用計(jì)數(shù)管理:

  • alloc: 經(jīng)過(guò)一系列函數(shù)調(diào)用,最終調(diào)用了calloc函數(shù),這里并沒(méi)有設(shè)置引用計(jì)數(shù)為1
  • retain: 經(jīng)過(guò)兩次哈希查找,找到其對(duì)應(yīng)引用計(jì)數(shù)值,然后將引用計(jì)數(shù)加1(實(shí)際是加偏移量)
  • release:和retain相反,經(jīng)過(guò)兩次哈希查找,找到其對(duì)應(yīng)引用計(jì)數(shù)值,然后將引用計(jì)數(shù)減1
  • dealloc:


    圖片.png

4、弱引用管理:

  • 添加weak變量:通過(guò)哈希算法位置查找添加。如果查找對(duì)應(yīng)位置中已經(jīng)有了當(dāng)前對(duì)象所對(duì)應(yīng)的弱引用數(shù)組,就把新的弱引用變量添加到數(shù)組當(dāng)中;如果沒(méi)有,就創(chuàng)建一個(gè)弱引用數(shù)組,并將該弱引用變量添加到該數(shù)組中。
  • 當(dāng)一個(gè)被weak修飾的對(duì)象被釋放后,weak對(duì)象怎么處理的?
    清除weak變量,同時(shí)設(shè)置指向?yàn)閚il。當(dāng)對(duì)象被dealloc釋放后,在dealloc的內(nèi)部實(shí)現(xiàn)中,會(huì)調(diào)用弱引用清除的相關(guān)函數(shù),會(huì)根據(jù)當(dāng)前對(duì)象指針查找弱引用表,找到當(dāng)前對(duì)象所對(duì)應(yīng)的弱引用數(shù)組,將數(shù)組中的所有弱引用指針都置為nil。

5、自動(dòng)釋放池:

在當(dāng)次runloop將要結(jié)束的時(shí)候調(diào)用objc_autoreleasePoolPop,并push進(jìn)來(lái)一個(gè)新的AutoreleasePool
AutoreleasePoolPage是以棧為結(jié)點(diǎn)通過(guò)雙向鏈表的形式組合而成,是和線程一一對(duì)應(yīng)的。
內(nèi)部屬性有parent,child對(duì)應(yīng)前后兩個(gè)結(jié)點(diǎn),thread對(duì)應(yīng)線程 ,next指針指向棧中下一個(gè)可填充的位置。

  • AutoreleasePool實(shí)現(xiàn)原理?
    編譯器會(huì)將 @autoreleasepool {} 改寫為:
void * ctx = objc_autoreleasePoolPush;
    {}
objc_autoreleasePoolPop(ctx);
  • nobjc_autoreleasePoolPush:

    把當(dāng)前next位置置為nil,即哨兵對(duì)象,然后next指針指向下一個(gè)可入棧位置,
    AutoreleasePool的多層嵌套,即每次objc_autoreleasePoolPush,實(shí)際上是不斷地向棧中插入哨兵對(duì)象。

  • objc_autoreleasePoolPop:
    根據(jù)傳入的哨兵對(duì)象找到對(duì)應(yīng)位置。
    給上次push操作之后添加的對(duì)象依次發(fā)送release消息。
    回退next指針到正確的位置。

四、循環(huán)引用

循環(huán)引用的實(shí)質(zhì):多個(gè)對(duì)象相互之間有強(qiáng)引用,不能釋放讓系統(tǒng)回收。
如何解決循環(huán)引用?

1、避免產(chǎn)生循環(huán)引用,通常是將 strong 引用改為 weak 引用。
比如在修飾屬性時(shí)用weak
在block內(nèi)調(diào)用對(duì)象方法時(shí),使用其弱引用,這里可以使用兩個(gè)宏

#define WS(weakSelf)            __weak __typeof(&*self)weakSelf = self; // 弱引用
#define ST(strongSelf)          __strong __typeof(&*self)strongSelf = weakSelf; //使用這個(gè)要先聲明weakSelf

還可以使用__block來(lái)修飾變量
在MRC下,__block不會(huì)增加其引用計(jì)數(shù),避免了循環(huán)引用
在ARC下,__block修飾對(duì)象會(huì)被強(qiáng)引用,無(wú)法避免循環(huán)引用,需要手動(dòng)解除。

2、在合適時(shí)機(jī)去手動(dòng)斷開(kāi)循環(huán)引用。
通常我們使用第一種。
循環(huán)引用場(chǎng)景:

  • 自循環(huán)引用
    對(duì)象強(qiáng)持有的屬性同時(shí)持有該對(duì)象

  • 相互循環(huán)引用


  • 多循環(huán)引用


1、代理(delegate)循環(huán)引用屬于相互循環(huán)引用

delegate 是iOS中開(kāi)發(fā)中比較常遇到的循環(huán)引用,一般在聲明delegate的時(shí)候都要使用弱引用 weak,或者assign,當(dāng)然怎么選擇使用assign還是weak,MRC的話只能用assign,在ARC的情況下最好使用weak,因?yàn)閣eak修飾的變量在釋放后自動(dòng)指向nil,防止野指針存在

2、NSTimer循環(huán)引用屬于相互循環(huán)使用

在控制器內(nèi),創(chuàng)建NSTimer作為其屬性,由于定時(shí)器創(chuàng)建后也會(huì)強(qiáng)引用該控制器對(duì)象,那么該對(duì)象和定時(shí)器就相互循環(huán)引用了。
如何解決呢?
這里我們可以使用手動(dòng)斷開(kāi)循環(huán)引用:
如果是不重復(fù)定時(shí)器,在回調(diào)方法里將定時(shí)器invalidate并置為nil即可。
如果是重復(fù)定時(shí)器,在合適的位置將其invalidate并置為nil即可

3、block循環(huán)引用
一個(gè)簡(jiǎn)單的例子:
@property (copy, nonatomic) dispatch_block_t myBlock;
@property (copy, nonatomic) NSString *blockString;

- (void)testBlock {
    self.myBlock = ^() {
        NSLog(@"%@",self.blockString);
    };
}

由于block會(huì)對(duì)block中的對(duì)象進(jìn)行持有操作,就相當(dāng)于持有了其中的對(duì)象,而如果此時(shí)block中的對(duì)象又持有了該block,則會(huì)造成循環(huán)引用。
解決方案就是使用__weak修飾self即可

__weak typeof(self) weakSelf = self;
self.myBlock = ^() {
        NSLog(@"%@",weakSelf.blockString);
 };
  • 并不是所有block都會(huì)造成循環(huán)引用。
    只有被強(qiáng)引用了的block才會(huì)產(chǎn)生循環(huán)引用
    而比如dispatch_async(dispatch_get_main_queue(), ^{}),[UIView animateWithDuration:1 animations:^{}]這些系統(tǒng)方法等
    或者block并不是其屬性而是臨時(shí)變量,即棧block
[self testWithBlock:^{
    NSLog(@"%@",self);
}];

- (void)testWithBlock:(dispatch_block_t)block {
    block();
}
  • 還有一種場(chǎng)景,在block執(zhí)行開(kāi)始時(shí)self對(duì)象還未被釋放,而執(zhí)行過(guò)程中,self被釋放了,由于是用weak修飾的,那么weakSelf也被釋放了,此時(shí)在block里訪問(wèn)weakSelf時(shí),就可能會(huì)發(fā)生錯(cuò)誤(向nil對(duì)象發(fā)消息并不會(huì)崩潰,但也沒(méi)任何效果)。
    對(duì)于這種場(chǎng)景,應(yīng)該在block中對(duì) 對(duì)象使用__strong修飾,使得在block期間對(duì) 對(duì)象持有,block執(zhí)行結(jié)束后,解除其持有。
__weak typeof(self) weakSelf = self;
self.myBlock = ^() {
        __strong __typeof(self) strongSelf = weakSelf;
        [strongSelf test];
 };
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容