在iOS app中,記憶體管理是基於引用計數模型運作。當創建一個物件的時候,記憶體會在Heap(堆)上分配,並將其引用計數設置為1。隨著其他物件對此物件建立強引用(strong reference),其引用計數會增加1。

iOS : 記憶體管理

在iOS app中,記憶體管理是基於引用計數模型運作。當創建一個物件的時候,記憶體會在Heap(堆)上分配,並將其引用計數設置為1。隨著其他物件對此物件建立強引用(strong reference),其引用計數會增加1。

相反的,如果擁有物件的持有者放棄了強引用,引用計數將會減少1。一旦引用計數變為0,該物件的記憶體就會被自動釋放。

當啟用了ARC(Automatic Reference Counting)自動引用計數功能的編譯器編寫程式碼時,編譯器會分析你創建的引用,並自動插入對底層記憶體管理機制的調用,我們無須手動設置引用計數。

隨著自動引用計數(ARC)的引入,我們只需要在引用物件時指定所有權的類型:

強引用(strong reference):確保被引用的物件只要引用仍然有效,就會一直保留在記憶體中。例如,我們宣告一個控制器屬性 strong var myView: UIView,表示控制器強引用 myView 物件,直到控制器釋放之前,myView 都會存在記憶體中。

弱引用 (weak reference):對被引用的物件的生存期沒有影響。例如,我們宣告一個閉包中的局部變數 weak var capturedView: UIView,表示閉包弱引用 capturedView 物件,即使 capturedView 被釋放,閉包中的 capturedView 也會變成 nil 而不會崩潰。

非擁有引用 (unowned reference):與弱引用類似,對被引用的物件的生存期沒有影響,但與弱引用的不同之處是,非擁有引用 預期總是擁有 「非 nil 的值」,**ARC 不會自動將其設置為 ****nil**。例如,我們宣告一個子視圖中的屬性 unowned var parentViewController: UIViewController,表示子視圖非擁有引用父控制器,而父控制器通常擁有比子視圖更長的生存期,因此子視圖可以安全地訪問父控制器。

需要注意的是:

  • 當被引用的物件被釋放時,弱引用會被設定為 nil,而非擁有引用則會變成一個懸浮指標 (dangling pointer)。向懸浮指標發送訊息會導致程式崩潰。
  • 使用非擁有引用時,要確保另一個物件擁有相同的或更長的生存期,避免懸浮指標問題。

簡單來說,在以下情況下使用不同的引用類型:

  • strong: 當你想要確保物件一直存在,直到不再需要它為止;
  • weak: 會在被引用的物件被釋放時自動設為 nil,因此它們可以用來避免循環引用。例如,在代理關係中,委托者通常會使用弱引用來引用代理,這樣當委托者被釋放時,代理也不會被保留。;
  • unowned: 不會在被引用的物件被釋放時自動設為 nil,因此它們只能用於指向那些生命週期一定會比引用它的物件長的物件。例如,子視圖可以使用非擁有引用來引用父視圖,因為父視圖通常會比子視圖存在得更久。
  • assign: 當你只需要一個指向簡單數據的指標,不需要跟踪它的生命周期。

使用 weak 和 assign 引用可以避免循環引用,即兩個物件互相引用導致彼此都無法被釋放的情況。

  • 使用弱引用來避免循環引用:

class Delegate { weak var delegate: SomeObject? }

class SomeObject { weak var delegate: Delegate? }在上述例子中,DelegateSomeObject 之間存在循環引用。如果使用強引用,則兩個物件都會一直存在,直到程式結束。使用弱引用後,當 Delegate 被釋放時,delegate 指標會變為 nil,從而打破循環引用。

  • 使用非擁有引用來指向父視圖:

class ViewController: UIViewController { unowned var childView: UIView

init(childView: UIView) { self.childView = childView } }在上述例子中,ViewController 使用非擁有引用來引用 childView。因為 ViewController 的生命週期通常會比 childView 短,因此 childView 不會在 ViewController 被釋放時被釋放。

總而言之,在選擇使用弱引用還是非擁有引用時,需要考慮以下因素:

  • 是否需要避免循環引用? 如果需要,則使用弱引用。
  • 被引用的物件的生命週期是否一定會比引用它的物件長? 如果是,則使用非擁有引用。

避免強引用循環:

如果對 ARC(自動引用計數)背後的引用計數機制有良好的理解,那麼理解引用循環的概念就會很容易。如果兩個物件之間存在一個由強引用構成的循環,則即使沒有其他強引用指向它們,它們也會相互保持對方的存活,導致無法被釋放。

閉包/Block 中的強引用循環

在 iOS 開發中,常見的強引用循環之一發生在使用閉包/Block 時。如果將閉包/Block 分配給類實例的屬性,並且閉包/Block 的內部捕獲了該實例(self),則可能會導致強引用循環。需要注意的是,如果只是創建一個新的閉包/Block 而不將其分配給屬性,則不會導致任何引用循環。

更詳細的解釋如下:

  • 閉包/Block 是一種可以捕獲外部變量的程式碼塊。在 Swift 和 Objective-C 中,閉包/Block 會隱式地捕獲它們所使用的所有變量,包括 self 指標。
  • 強引用循環 是指兩個或多個物件互相保持強引用,導致它們無法被釋放的情況。

閉包/Block 中的強引用循環示例:

class MyClass { var closure: () -> Void = { // 這個閉包捕獲了 self,可能導致強引用循環 print(self) } }

let myObject = MyClass()
// 這裡可能會導致強引用循環如何避免閉包/Block 中的強引用循環:
  • 使用弱引用 (weak) 或非擁有引用 (unowned): 在閉包/Block 內部使用 weakunowned 關鍵字來引用 self,可以避免強引用循環。
  • 使用捕獲列表 (capture list): 在閉包/Block 的定義中使用捕獲列表,可以明確指定要捕獲的變量,並可以指定它們是強引用還是弱引用。

示例:

class MyClass { var closure: () -> Void = { [weak self] in // 使用弱引用來引用 self guard let strongSelf = self else { return } print(strongSelf) } }在使用閉包/Block 時,需要注意避免強引用循環。使用弱引用或非擁有引用以及捕獲列表是常用的避免強引用循環的方法。

在 Objective-C 中使用 Copy 属性

在 Objective-C 的實踐中,對於像 NSString 和 NSArray 這樣具有可變版本的類,我們通常會使用 copy 属性。這麼做的原因是為了確保我們的属性擁有獨立的副本,不會受到原始可變變數更新的影響。

更詳細的解釋如下:

  • Copy 属性: 當你將一個物件賦值給具有 copy 属性的變量時,會自動複製該物件,並將副本賦值給變數。這意味著變數持有的是原始物件的一個獨立副本,而不是直接指向原始物件。
  • 可變版本類: 像 NSString 和 NSArray 這樣的類,既有不可變版本(NSString、NSArray),也有可變版本(NSMutableString、NSMutableArray)。可變版本的物件可以被修改,而不可變版本的物件則不能被修改。

為何要使用 Copy 屬性呢?

  • 保護數據完整性: 當你將一個可變物件賦值給一個屬性時,如果不使用 copy,那麼該屬性就會直接指向原始物件。如果之後原始物件被修改,那麼屬性所指向的物件也會被修改,導致數據不一致。使用 copy 屬性可以避免這個問題,因為它會創建一個獨立的副本,就算原始物件被修改,属性所指向的副本也不會受到影響。
  • 防止意外修改: 在某些情况下,你可能希望確保一個屬性不可被修改。例如,你可能有一个表示用戶名的屬性,你不希望其他人可以修改它。在這種情况下,可以使用 copy 屬性來確保属性值不可被修改。

示例:

@property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSArray *items;在 Objective-C 中,對於具有可變版本的類,通常建議使用 copy 屬性来保護資料的完整性和防止意外修改。

電腦記憶體中的五大區域:

  1. 棧/stack : 後進先出(LIFO)的儲存結構,用於存放函數的局部變量、參數和返回地址。.
  2. 堆/heap : 用於存放動態分配的記憶體。先進後出的(FILO)的儲存結構,工程師手動申請的字節空間 malloc calloc realoc函數. 當需要分配記憶體時,可以使用malloc()calloc()函數從堆中分配記憶體。當不需要使用記憶體時,可以使用free()函數釋放記憶體。
  3. BSS段/Block Started by Symbol : 儲存未被初始化的全局變量 靜態變數. BSS段的內容在程序啟動時會被初始化為0。
  4. 常數段/constant segment : 儲存已被初始化的全局 靜態變量 常量資料.
  5. 代碼段/ code segment : 儲存程式的代碼.

C 語言程式記憶體配置

**C 語言程式記憶體配置 (LibreOffice 原始檔)**Reference: Apple官方文件: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/