大廠常問iOS面試題--內存管理篇

1.什麼情況使用weak關鍵字,相比assign有什麼不同?

  • 什麼情況使用 weak 關鍵字?

    在 ARC 中,在有可能出現循環引用的時候,往往要通過讓其中一端使用 weak 來解決,比如: delegate 代理屬性

    自身已經對它進行一次強引用,沒有必要再強引用一次,此時也會使用 weak,自定義 IBOutlet 控件屬性一般也使用 weak;當然,也可以使用strong。在下文也有論述:《IBOutlet連出來的視圖屬性爲什麼可以被設置成weak?》

  • 不同點:

    weak 此特質表明該屬性定義了一種“非擁有關係” (nonowning relationship)。爲這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign類似, 然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。 而 assign 的“設置方法”只會執行鍼對“純量類型” (scalar type,例如 CGFloat 或 NSlnteger 等)的簡單賦值操作。

    assign 可以用非 OC 對象,而 weak 必須用於 OC 對象

2.如何讓自己的類用copy修飾符?如何重寫帶copy關鍵字的setter?

  • 若想令自己所寫的對象具有拷貝功能,則需實現 NSCopying 協議。如果自定義的對象分爲可變版本與不可變版本,那麼就要同時實現 NSCopying 與 NSMutableCopying 協議。

    具體步驟:

    需聲明該類遵從 NSCopying 協議

    實現 NSCopying 協議。該協議只有一個方法:

    - (id)copyWithZone:(NSZone *)zone;
    
    

    注意:一提到讓自己的類用 copy 修飾符,我們總是想覆寫copy方法,其實真正需要實現的卻是 “copyWithZone” 方法。

  • 重寫帶 copy 關鍵字的 setter,例如:

    - (void)setName:(NSString *)name {
        //[_name release];
        _name = [name copy];
    }
    
    

3.深拷貝與淺拷貝

淺拷貝只是對指針的拷貝,拷貝後兩個指針指向同一個內存空間,深拷貝不但對指針進行拷貝,而且對指針指向的內容進行拷貝,經深拷貝後的指針是指向兩個不同地址的指針。

當對象中存在指針成員時,除了在複製對象時需要考慮自定義拷貝構造函數,還應該考慮以下兩種情形:

  • 當函數的參數爲對象時,實參傳遞給形參的實際上是實參的一個拷貝對象,系統自動通過拷貝構造函數實現;

  • 當函數的返回值爲一個對象時,該對象實際上是函數內對象的一個拷貝,用於返回函數調用處。

copy方法:如果是非可擴展類對象,則是淺拷貝。如果是可擴展類對象,則是深拷貝。

mutableCopy方法:無論是可擴展類對象還是不可擴展類對象,都是深拷貝。

4.@property的本質是什麼?ivar、getter、setter是如何生成並添加到這個類中的

  • @property 的本質是實例變量(ivar)+存取方法(access method = getter + setter),即 @property = ivar + getter + setter;

    “屬性” (property)作爲 Objective-C 的一項特性,主要的作用就在於封裝對象中的數據。 Objective-C 對象通常會把其所需要的數據保存爲各種實例變量。實例變量一般通過“存取方法”(access method)來訪問。其中,“獲取方法” (getter)用於讀取變量值,而“設置方法” (setter)用於寫入變量值。

  • ivar、getter、setter 是自動合成這個類中的

    完成屬性定義後,編譯器會自動編寫訪問這些屬性所需的方法,此過程叫做“自動合成”(autosynthesis)。需要強調的是,這個過程由編譯 器在編譯期執行,所以編輯器裏看不到這些“合成方法”(synthesized method)的源代碼。除了生成方法代碼 getter、setter 之外,編譯器還要自動向類中添加適當類型的實例變量,並且在屬性名前面加下劃線,以此作爲實例變量的名字。在前例中,會生成兩個實例變量,其名稱分別爲 _firstName 與 _lastName。也可以在類的實現代碼裏通過 @synthesize 語法來指定實例變量的名字.

5.@protocol和category中如何使用@property

  • 在 protocol 中使用 property 只會生成 setter 和 getter 方法聲明,我們使用屬性的目的,是希望遵守我協議的對象能實現該屬性

  • category 使用 @property 也是隻會生成 setter 和 getter 方法的聲明,如果我們真的需要給 category 增加屬性的實現,需要藉助於運行時的兩個函數:objc_setAssociatedObject和objc_getAssociatedObject

6.簡要說一下@autoreleasePool的數據結構??

簡單說是雙向鏈表,每張鏈表頭尾相接,有 parent、child指針

每創建一個池子,會在首部創建一個 哨兵 對象,作爲標記

最外層池子的頂端會有一個next指針。當鏈表容量滿了,就會在鏈表的頂端,並指向下一張表。

7.BAD_ACCESS在什麼情況下出現?

訪問了懸垂指針,比如對一個已經釋放的對象執行了release、訪問已經釋放對象的成員變量或者發消息。 死循環

8.使用CADisplayLink、NSTimer有什麼注意點?

CADisplayLink、NSTimer會造成循環引用,可以使用YYWeakProxy或者爲CADisplayLink、NSTimer添加block方法解決循環引用

9.iOS內存分區情況

  • 棧區(Stack)

    由編譯器自動分配釋放,存放函數的參數,局部變量的值等

    棧是向低地址擴展的數據結構,是一塊連續的內存區域

  • 堆區(Heap)

    由程序員分配釋放

    是向高地址擴展的數據結構,是不連續的內存區域

  • 全局區

    全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域

    程序結束後由系統釋放

  • 常量區

    常量字符串就是放在這裏的

    程序結束後由系統釋放

  • 代碼區

    存放函數體的二進制代碼

  • 注:

    • 在 iOS 中,堆區的內存是應用程序共享的,堆中的內存分配是系統負責的

    • 系統使用一個鏈表來維護所有已經分配的內存空間(系統僅僅記錄,並不管理具體的內容)

    • 變量使用結束後,需要釋放內存,OC 中是判斷引用計數是否爲 0,如果是就說明沒有任何變量使用該空間,那麼系統將其回收

    • 當一個 app 啓動後,代碼區、常量區、全局區大小就已經固定,因此指向這些區的指針不會產生崩潰性的錯誤。而堆區和棧區是時時刻刻變化的(堆的創建銷燬,棧的彈入彈出),所以當使用一個指針指向這個區裏面的內存時,一定要注意內存是否已經被釋放,否則會產生程序崩潰(也即是野指針報錯)

10.iOS內存管理方式

  • Tagged Pointer(小對象)

    Tagged Pointer 專門用來存儲小的對象,例如 NSNumber 和 NSDate

    Tagged Pointer 指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披着對象皮的普通變量而已。所以,它的內存並不存儲在堆中,也不需要 malloc 和 free

    在內存讀取上有着 3 倍的效率,創建時比以前快 106 倍

    objc_msgSend 能識別 Tagged Pointer,比如 NSNumber 的 intValue 方法,直接從指針提取數據

    使用 Tagged Pointer 後,指針內存儲的數據變成了 Tag + Data,也就是將數據直接存儲在了指針中

  • NONPOINTER_ISA (指針中存放與該對象內存相關的信息) 蘋果將 isa 設計成了聯合體,在 isa 中存儲了與該對象相關的一些內存的信息,原因也如上面所說,並不需要 64 個二進制位全部都用來存儲指針。

    isa 的結構:

    // x86_64 架構
    struct {
        uintptr_t nonpointer        : 1;  // 0:普通指針,1:優化過,使用位域存儲更多信息
        uintptr_t has_assoc         : 1;  // 對象是否含有或曾經含有關聯引用
        uintptr_t has_cxx_dtor      : 1;  // 表示是否有C++析構函數或OC的dealloc
        uintptr_t shiftcls          : 44; // 存放着 Class、Meta-Class 對象的內存地址信息
        uintptr_t magic             : 6;  // 用於在調試時分辨對象是否未完成初始化
        uintptr_t weakly_referenced : 1;  // 是否被弱引用指向
        uintptr_t deallocating      : 1;  // 對象是否正在釋放
        uintptr_t has_sidetable_rc  : 1;  // 是否需要使用 sidetable 來存儲引用計數
        uintptr_t extra_rc          : 8;  // 引用計數能夠用 8 個二進制位存儲時,直接存儲在這裏
    };
    
    // arm64 架構
    struct {
        uintptr_t nonpointer        : 1;  // 0:普通指針,1:優化過,使用位域存儲更多信息
        uintptr_t has_assoc         : 1;  // 對象是否含有或曾經含有關聯引用
        uintptr_t has_cxx_dtor      : 1;  // 表示是否有C++析構函數或OC的dealloc
        uintptr_t shiftcls          : 33; // 存放着 Class、Meta-Class 對象的內存地址信息
        uintptr_t magic             : 6;  // 用於在調試時分辨對象是否未完成初始化
        uintptr_t weakly_referenced : 1;  // 是否被弱引用指向
        uintptr_t deallocating      : 1;  // 對象是否正在釋放
        uintptr_t has_sidetable_rc  : 1;  // 是否需要使用 sidetable 來存儲引用計數
        uintptr_t extra_rc          : 19;  // 引用計數能夠用 19 個二進制位存儲時,直接存儲在這裏
    };
    
    

    這裏的 has_sidetable_rc 和 extra_rc,has_sidetable_rc 表明該指針是否引用了 sidetable 散列表,之所以有這個選項,是因爲少量的引用計數是不會直接存放在 SideTables 表中的,對象的引用計數會先存放在 extra_rc 中,當其被存滿時,纔會存入相應的 SideTables 散列表中,SideTables 中有很多張 SideTable,每個 SideTable 也都是一個散列表,而引用計數表就包含在 SideTable 之中。

  • 散列表(引用計數表、弱引用表)

    引用計數要麼存放在 isa 的 extra_rc 中,要麼存放在引用計數表中,而引用計數表包含在一個叫 SideTable 的結構中,它是一個散列表,也就是哈希表。而 SideTable 又包含在一個全局的 StripeMap 的哈希映射表中,這個表的名字叫 SideTables。

    當一個對象訪問 SideTables 時:

    • 首先會取得對象的地址,將地址進行哈希運算,與 SideTables 中 SideTable 的個數取餘,最後得到的結果就是該對象所要訪問的 SideTable

    • 在取得的 SideTable 中的 RefcountMap 表中再進行一次哈希查找,找到該對象在引用計數表中對應的位置

    • 如果該位置存在對應的引用計數,則對其進行操作,如果沒有對應的引用計數,則創建一個對應的 size_t 對象,其實就是一個 uint 類型的無符號整型

    弱引用表也是一張哈希表的結構,其內部包含了每個對象對應的弱引用表 weak_entry_t,而 weak_entry_t 是一個結構體數組,其中包含的則是每一個對象弱引用的對象所對應的弱引用指針。

11.循環引用

1. 概述

iOS內存中的分區有:堆、棧、靜態區。其中,棧和靜態區是操作系統自己管理回收,不會造成循環引用。在堆中的相互引用無法回收,有可能造成循環引用。

循環引用的實質:多個對象相互之間有強引用,不能施放讓系統回收。

解決循環引用一般是將 strong 引用改爲 weak 引用。

2. 循環引用場景分析及解決方法

1)父類與子類

如:在使用UITableView 的時候,將 UITableView 給 Cell 使用,cell 中的 strong 引用會造成循環引用。

// controller
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TestTableViewCell *cell =[tableView dequeueReusableCellWithIdentifier:@"UITableViewCellId" forIndexPath:indexPath];
    cell.tableView = tableView;
    return cell;
}

// cell
@interface TestTableViewCell : UITableViewCell
@property (nonatomic, strong) UITableView *tableView; // strong 造成循環引用
@end

解決:strong 改爲 weak

// cell
@interface TestTableViewCell : UITableViewCell
@property (nonatomic, weak) UITableView *tableView; // strong 改爲 weak
@end

2)block

block在copy時都會對block內部用到的對象進行強引用的。

self.testObject.testCircleBlock = ^{
   [self doSomething];
};

self將block作爲自己的屬性變量,而在block的方法體裏面又引用了 self 本身,此時就很簡單的形成了一個循環引用。

應該將 self 改爲弱引用

__weak typeof(self) weakSelf = self;
 self.testObject.testCircleBlock = ^{
      __strong typeof (weakSelf) strongSelf = weakSelf;
      [strongSelf doSomething];
};

在 ARC 中,在被拷貝的 block 中無論是直接引用 self 還是通過引用 self 的成員變量間接引用 self,該 block 都會 retain self。

  • 快速定義宏
    // weak obj
    /#define WEAK_OBJ(type)  __weak typeof(type) weak##type = type;

    // strong obj
    /#define STRONG_OBJ(type)  __strong typeof(type) str##type = weak##type;

3)Delegate

delegate 屬性的聲明如下:

@property (nonatomic, weak) id <TestDelegate> delegate;

如果將 weak 改爲 strong,則會造成循環引用

// self -> AViewController
BViewController *bVc = [BViewController new];
bVc = self; 
[self.navigationController pushViewController: bVc animated:YES];

   // 假如是 strong 的情況
   // bVc.delegate ===> AViewController (也就是 A 的引用計數 + 1)
   // AViewController 本身又是引用了 <BViewControllerDelegate> ===> delegate 引用計數 + 1
   // 導致: AViewController <======> Delegate ,也就循環引用啦

4)NSTimer

NSTimer 的 target 對傳入的參數都是強引用(即使是 weak 對象)

解決辦法: 《Effective Objective-C 》中的52條方法

#import <Foundation/Foundation.h>

@interface NSTimer (YPQBlocksSupport)

+ (NSTimer *)ypq_scheduledTimeWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                       repeats:(BOOL)repeats;

@end

#import "NSTimer+YPQBlocksSupport.h"

@implementation NSTimer (YPQBlocksSupport)

+ (NSTimer *)ypq_scheduledTimeWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                       repeats:(BOOL)repeats
{
    return [self scheduledTimerWithTimeInterval:interval
                                         target:self
                                       selector:@selector(ypq_blockInvoke:) userInfo:[block copy]
                                        repeats:repeats];
}

- (void)ypq_blockInvoke:(NSTimer *)timer
{
    void (^block)() = timer.userInfo;
    if(block)
    {
        block();
    }
}

@end

使用方式:

__weak ViewController * weakSelf = self;
[NSTimer ypq_scheduledTimeWithTimeInterval:4.0f
                                     block:^{
                                         ViewController * strongSelf = weakSelf;
                                         [strongSelf afterThreeSecondBeginAction];
                                     }
                                   repeats:YES];

計時器保留其目標對象,反覆執行任務導致的循環,確實要注意,另外在dealloc的時候,不要忘了調用計時器中的 invalidate方法。

其他面試題篇章:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章