07/09/2018, 15:46

Quản lý bộ nhớ trong Swift

Tại sao cần phải biết quản lý bộ nhớ? Nhà giàu mà không biết tiêu tiền cũng sạt nghiệp. Mặc dù phần cứng máy tính/điện thoại ngày càng phát triển, nhưng cứ tiêu xài hoan phí bộ nhớ thì dẫn đến app rất chậm, lag. Users chửi, khách hàng chửi Biết để đi phỏng vấn. Mình chưa đi phỏng vấn lần nào ...

Tại sao cần phải biết quản lý bộ nhớ?

  1. Nhà giàu mà không biết tiêu tiền cũng sạt nghiệp. Mặc dù phần cứng máy tính/điện thoại ngày càng phát triển, nhưng cứ tiêu xài hoan phí bộ nhớ thì dẫn đến app rất chậm, lag. Users chửi, khách hàng chửi
  2. Biết để đi phỏng vấn. Mình chưa đi phỏng vấn lần nào nhưng dám chắc mấy câu này rất dễ bị hỏi
  3. Học để biết. Kiến thức bao la, biết thêm một kiến thức không bổ bề ngang cũng tràn bề dọc

Rốt cuộc Stack và Heap là gì?

Abstraction, em là ai?

Trước khi bắt đầu vào phần Stack và Heap, mình muốn nói về sự trừu tượng trong học thuật.

Trong cuộc sống, sẽ có rất nhiều thứ bạn xài hằng ngày nhưng không thể giải thích được cách hoạt động của nó. Khi ai đó hỏi bạn "xe ô tô chạy sao mày? ", dù không chạy ô tô bạn vẫn trả lời được "ừ thì cắm chìa khóa rồi khởi động xe rồi chạy thôi". Bạn hiểu là ô tô có động cơ có bánh xe, có phanh, có đèn. Bạn phân biệt được ô tô với xe bò dù không chạy 2 loại này.

Đó chính là sự trừu tượng khóa - Abstraction. Abstraction rất tốt, nó giúp chúng ta hiểu, phân biệt sự vật hiện tượng như xe bò với ô tô ở trên. Trong IT, Abstraction lại càng được sử dụng nhiều hơn.

Tại sao chúng ta cần Abstraction ?

Để hiểu được thực sự 100% Stack, Heap là gì, tại sao thanh Ram lại có 2 cái này, vòng đời, phạm vi của chúng, rồi thằng nào con nào "chi phối" 2 thứ này: Hệ điều hành, complier hay language runtimes, vv là cả một quá trình. Chúng ta phải tự tay viết hệ điều hành, complier, học sâu kiến trúc máy tính mới hiểu được rõ.

Ơi giời, nhưng đã có Abstraction đây rồi, chúng ta không cần hiểu rõ đến mức như vậy. Mục tiêu của chúng ta là lái ô tô chở gấu đi chơi, không phải thiết kế ô tô. Nhưng hiểu một xíu về cách hoạt động sẽ giúp ta lái mượt hơn, ví dụ bẻ cua nên tăng tốc hay giảm tốc.

Stack và Heap?

Stack Heap
  • Vùng nhớ stack được sử dụng cho việc thực thi thread. Khi gọi hàm, các biến cục bộ của hàm được lưu trữ vào block của stack (theo kiểu LIFO). Cho đến khi hàm trả về giá trị, block này sẽ được xóa tự động. Hay nói cách khác, các biến cục bộ được lưu trữ ở vùng nhớ stack và tự động được giải phóng khi kết thúc hàm.
Vùng nhớ heap được dùng cho cấp phát bộ nhớ động Vùng nhớ được cấp phát tồn tại đến khi lập trình viên giải phóng vùng nhớ đó
  • Kích thước vùng nhớ stack được fix cố định. Chúng ta không thể tăng hoặc giảm kích thước vùng nhớ stack. Nếu không đủ vùng nhớ stack, gây ra stack overflow. Hiện tượng này xảy ra khi nhiều hàm lồng nhau hoặc đệ quy nhiều lần dẫn đến không đủ vùng nhớ.
Hệ điều hành sẽ có cơ chế tăng kích thước vùng nhớ heap.

Thực sự các anh hùng cũng cái nhau dữ dội, bắt bẻ từng câu chữ về Stack vs Heap trên stackoverflow, mọi người có thể xem thêm tại đây nha:

http://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap

Value Types và Reference types

Biến là một trong những thứ sử dụng bộ nhớ nhiều nhất trong lập trình. Không nói qua khi 50% thời gian lập trình là dùng biến.

Ở các ngôn ngữ lập trình, kiểu dữ liệu của được chia làm 2 loại và value type (tham trị) và reference type ( tham chiếu ).

Ví dụ:

  • Value type Int, Double, Struct, vv.
  • Reference type là Closure, Class

Điểm khác biệt quan trọng nhất chúng ta cần nhớ giữa 2 loại này:

  • Value type lưu ngay giá trị của biến trên Stack
  • Reference type chỉ lưu địa chỉ đến vùng nhớ trong Heap.

Vẽ data model là gì?

Trước khi đi sâu vào vấn đề cũng như hiểu rõ behind-the-scene tại sao có sự khác nhau như vậy. Chúng ta nên ôn lại cách vẽ data model .Data model mô phỏng lại cách thức lưu trữ của value typereference type

Vẽ data model cho Value Type

Cho đoạn code sau:

<pre class="lang:swift decode:true" title="Draw value type data">var number1: Int
number1 = 69
var number2: Int
number2 = number1
number1 = 96</pre>

Đầu tiên với:

var number1: Int

Khai báo biến number kiểu Int. Tưởng tượng, bạn đang yêu cầu: "Swift, cho tao một chỗ trống để lưu biến kiểu Int, tao đặt tên là number1 nha". vẽ data model

Hình chữ nhật tượng trưng cho một ô nhớ, number1 kế bên là tên biến.

Tiếp theo:

number1 = 69

"Ê Swift, tao muốn bỏ giá trị 69 vô biến number1". Thấy dấu = , Swift sẽ lấy giá trị bên phải dấu = bỏ vô cái hộp lúc trước đã tạo để lưu trữ dữ liệu.

var number2: Int

Tiếp tục tạo một biến number2:

vẽ data model

Tiếp theo:

number2 = number1

Theo như chúng ta suy nghĩ, khi thực hiện phép gán =, Swift liệu có lấy giá trị bên phải bỏ vào hộp (ô nhớ) như trước?

Nhưng không, Swift sẽ tạo lấy một bản copy của 69 từ number1 rồi bỏ vào ô nhớ ở number2 như hình bên dưới

vẽ data model

Như ta thấy,không có sự liên kết nào giữa number1number2.

Và dòng code cuối cùng:

number1 = 96

Quản lý bộ nhớ trong Swift

Do không có sự liên kết nào giữa number1number2 nên khi thay đổi giá trị của number1, number2 không bị ảnh hưởng gì hết.

Vẽ data model cho Reference Type

Ta có đoạn code sau:

class User{
    var age = 1
    init(age: Int){
        self.age = age
    }
}
var user1: User
user1 = User(age: 21)
user1.age = 22
var user2 = User(age: 25)
user2 = user1

user1.age = 30
var user1: User

Đầu tiên, khởi tạo biến:

Tiếp theo là khởi tạo đối tượng từ class User:

user1 = User(age: 21)

Lúc này, khác với Value Type,  Swift sẽ tạo một instance User ở trong Heap.

Sau khi tạo instance xong, Swift sẽ lấy địa chỉ của instance này bỏ vào ô nhớ của user1. Trong hình trên, instance có địa chỉ là @0x69

Đương nhiên địa chỉ @0x69 là mình bịa ra, để không phải bịa lung tung như vậy nữa. Ta để dấu mũi tên cho dễ nhìn, giống vầy:

Tiếp theo:

user1.age = 22

Swift sẽ

  1. Lấy địa chỉ trong ô nhớ user1
  2. Tìm instance có địa chỉ đó trong Heap
  3. Thay đổi ô nhớ age trong instance tìm được thành 22.

Hãy nhìn sơ đồ mình họa bên dưới

Tiếp theo tại một biến mới là user2 và khởi tạo luôn:

var user2 = User(age: 25)

user2 = user1

Trước khi toán tử gán = thực thi, ô nhớ user2 vẫn đang lưu địa chỉ 0x60. Nhưng sau khi phép gán = thực thi, nó sẽ lưu địa chỉ lấy từ ô nhớ user1 là 0x69. Ta sẽ phải vẽ lại mũi tên như sau:

Đọc nãy giờ vẫn không thấy quản lý bộ nhớ đâu?

Quản lý bộ nhớ chỗ nào???

Từ từ cháo mới nhừ được. Câu hỏi đặt ra là cái instance còn lại có địa chỉ 0x60 trong Heap sẽ được xử lý thế nào, nó tự động được xóa đi, hay ta phải code để xóa nó?

Strong Reference, Reference counting là gì

Mặc định, một mũi tên trỏ đến một instance trong Heap ở những ví dụ trên được xem  là một Strong Reference. Còn reference counting thì đếm mũi tên trỏ đến instance.

quan ly bo nho

Automatic Reference Counting (ARC)

ARC là cơ chế quản lý bộ nhớ của Swift. Cơ chế hoạt động của nó rất giống việc chúng ta vẽ data model nãy giờ. Đó là lý do nãy giờ mình muốn bạn ôn lại Stack, Heap, và vẽ vời như vậy.

Nội dung thì dài dòng nhưng đại ý là:

Nếu một instance không có còn strong reference nào hay được hiểu là reference counting = 0 thì cơ chế ARC sẽ xóa và giải phóng bộ nhớ cho instance đó trong Heap

Như ví dụ trên, instance có địa chỉ 0x60 sẽ bị xóa vì có reference counting = 0

quản lý bộ nhớ swift

Ví dụ ARC

Quick note về vòng đời của object trong Swift:

  1. Allocation: Giai đoạn cấp phát bộ nhớ. Stack và Heap sẽ đảm nhận việc này.
  2. Initialization: Khởi tạo đối tượng. Hàm init được chạy
  3. Usage: Dùng đối tượng đó
  4. Deinitialization: Hàm deinit chạy
  5. Deallocation: Giải phóng bộ nhớ. Stack hoặc Heap lấy lại vùng nhớ không xài nữa

Stack và Value Type thì tự động giải phóng rồi, nên ta chỉ quan tâm Heap và Reference Type.

Để mô phỏng lại quá trình hủy object, có 2 cách:

  • Khai báo biến kiểu optional để ta có thể gán nó = nil
  • Cho đoạn code cần test vào một hàm, chạy hàm đó. Hết scope của hàm đó, những biến trong hàm sẽ bị hủy

Lấy luôn ví dụ User từ đầu đến giờ, mình thêm hàm deinit để track xem instance nào trong Heap bị delete nhé:

class User{
    var age = 1
    init(age: Int){
        self.age = age
    }
    deinit {
        print("user has age: (age) was deallocated")
    }
}
var user1: User?
user1 = User(age: 21)
user1?.age = 22
var user2: User? = User(age: 25)
user2 = user1
và kết quả:

user has age: 25 was initialized

Đúng như những gì chúng ta vẽ nãy giờ phải không nào?

Thêm một ví dụ từ Official Guide:

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("(name) is being initialized")
    }
    deinit {
        print("(name) is being deinitialized")
    }
}


var reference1: Person?
var reference2: Person?
var reference3: Person?

reference1 = Person(name: "John Appleseed")
reference2 = reference1
reference3 = reference1


// instance đang có 3 strong reference, xóa hết mới giải phóng bộ nhớ cho instance đó được, bỏ comment để xóa
//reference1 = nil
//reference2 = nil
//reference3 = nil

Ví dụ trên mình không vẽ data model nữa vì nó tương tự như ví dụ User rồi.

Strong Reference Cycles

Cho đoạn code:

class Person {
    let name: String
    init(name: String) {
        self.name = name
    }
    var apartment: Apartment?
    deinit { print("(name) is being deinitialized") }
}

class Apartment {
    let name: String
    init(name: String) {
        self.name = name
    }
    var owner: Person?
    deinit { print("Apartment (name) is being deinitialized") }
}

var person: Person? = Person(name: "Khoa dep trai")
var apartment: Apartment? = Apartment(name: "Novaland")


person?.apartment = apartment
apartment?.owner = person
Dựa vào đoạn code ta có thể vẽ data model sau: strong reference la gi Bây giờ ta muốn hủy 2 biến personapartment:
person = nil
apartment = nil
  Một lần nữa vẽ data model để xem chuyện gì xảy ra, và tại sao không xóa được 2 biến trên?   "Hiện tượng" này được gọi là Strong Reference Cycle.

Memory Leak là gì?

Nguyên nhân bị Strong Reference Cycle do cả 2 instance vẫn còn lại strong reference = 1. Mà theo cơ chế ARC thì trong reference = 0 thì instance mới bị hủy được. Trường hợp ta muốn hủy instance nhưng thực tế instance vẫn còn trong Heap như vầy, người ta gọi mà Memory Leak.  Để giải quyết vấn đề này, Swift cung cấp 2 cách đó là Weak ReferenceUnowned Reference

Weak Reference

Về chức năng thì Weak Reference giống Strong Reference. Nhưng với cơ chế ARC, thì instance có nhiều weak reference cũng sẽ bị xóa. Đề xài weak reference, ta chỉ cần thêm keyword weak là được:
class Person {
    let name: String
    init(name: String) {
        self.name = name
    }
    var apartment: Apartment?
    deinit { print("(name) is being deinitialized") }
}
class Apartment {
    let name: String
    init(name: String) {
        self.name = name
    }
    weak var owner: Person?
    deinit { print("Apartment (name) is being deinitialized") }
}
var person: Person? = Person(name: "Khoa dep trai")
var apartment: Apartment? = Apartment(name: "Novaland")

person?.apartment = apartment
apartment?.owner = person!

person = nil
apartment = nil
  Ta sẽ được kết quả:

Khoa dep trai is being deinitialized

Apartment Novaland is being deinitialized

Để hiểu rõ, ta cứ vẽ data model ra thôi, dùng mũi tên nét đứt ( -------> ) để biểu diễn weak reference:

Như vậy, instance Person sẽ bị xóa trước do strong reference = 0. Tiếp theo do nó bị xóa nên strong reference đến instance Apartment cũng bị xóa luôn. Dẫn đến instance Apartment có strong reference = 0. Đó là lý do ta thấy

Khoa dep trai is being deinitialized

xuất hiện trước:

Apartment Novaland is being deinitialized

Bạn thử thêm weak reference vào class Person, và xóa weak reference ở class Apartment thì output sẽ ngược lại:

 weak var apartment: Apartment?
Output lúc này:

Apartment Novaland is being deinitialized

Khoa dep trai is being deinitialized

Tương tự, bạn tự vẽ data model nhé.

Unowned Reference

unowned reference cũng giống như weak reference. ARC chỉ giữ lại instance có strong reference >= 1. Còn instance có unownd reference hay weak reference đều bị xóa Điểm khác là:
Một instance A unowned reference ( trỏ ) đến một instance B khi mà instance B đó có vòng đời bằng hoặc dài hơn instance A
Là sao? Hãy cùng xem ví dụ sau: Có 2 class và Customer và CreditCard mô phỏng lại ứng dụng ngân hàng:
class Customer {
    let name: String
    weak var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("(name) is being deinitialized") }
}

class CreditCard {
    let number: Int
    unowned let customer: Customer
    init(number: Int, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #(number) is being deinitialized") }
}


var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 123456789, customer: john!)
john = nil
  Vẽ data model cho dễ nhìn: Một Customer có thể có CreditCard, tuy nhiên CreditCard chỉ tồn tại khi nó gắn với một Customer. Instance của CarditCard có vòng đời ngắn hơn instance của Customer là cái chắc. ( A trỏ đến B mà vòng đời B dài hơn hoặc bằng A.  A là CreditCard B là Customer  )   Thực ra ví dụ này từ Official Guide của Apple. Nó không rõ ràng lắm lại vì trong trường hợp này xài weak cũng được mà. Cùng tìm hiểu thêm về memory managment trong Closure để phân biệt rõ weak với unowned nhé

Memory managment trong Closure

Closure cũng như class là reference types. Trong một class, nếu một property là closure và trong closure đó lại dùng property/method của class nữa ( xài self.property ) thì sẽ xảy ra "hiện tượng" strong reference cycle như những ví dụ ở trên. Thôi trăm nghe không bằng một thấy, hãy nhìn qua ví dụ tính Fibonacci dưới đây:
class Fibonacci{
    var value: Int
    
    init(value: Int) {
        self.value = value
    }
    lazy var fibonacci: () -> Int = {
        var a = 0
        var b = 1
        
        for _ in 0..<self.value{
            let temp = a
            a = b
            b = temp + a
        }
        return a
    }
    deinit {
        print("(value) was deinitialized")
    }

}
var fi: Fibonacci? = Fibonacci(value: 7)
fi?.fibonacci()

fi = nil
Vẽ data model ta được: Dĩ nhiên trường hợp closure này không thể dùng weak/unowned trước property như những ví dụ trước. Để giải quyết vấn đề này, Swift cung cấp một giải pháp: closure capture list

Closure capture list

Capture list sẽ quy định luật để lấy giá trị của property trong closure. Tức là lấy self.property/self.method như thế nào. Mặc định là strong reference như hình ở trên rồi Ta dùng syntax sau ở phần body của closure:
[weak self ] in
hoặc:
[ unowned self ] in
hoặc lấy nhiều property/method cũng được:
[weak self, unowned self.property, .....] in
Cùng xem qua sự khác nhau giữa chúng nhé, xem dòng comment bên dưới:
lazy var fibonacci: () -> Int = {
        
        [ weak self ] in
        
        
        var a = 0
        var b = 1
    
        // lúc này self có thể nil, nên phải check optional
        
        guard let max = self?.value else {
            fatalError() // return luôn không cần return type
        }
        
        for _ in 0..<max{
            let temp = a
            a = b
            b = temp + a
        }
        return a
    }

Và output lúc này:

7 was deinitialized

Bạn tự vẽ data model để minh họa nhé.

Ở ví dụ này, ta xài unowned là hợp lý nhất vì cả class và closure có vòng đời bằng nhau.

lazy var fibonacci: () -> Int = {
        
        [ unowned self ] in
        var a = 0
        var b = 1
    
        // xài unowned lúc này self.value không thể nil được vì vòng đời closure bằng với class

        
        for _ in 0..<self.value{
            let temp = a
            a = b
            b = temp + a
        }
        return a
    }

Từ những ví dụ trên, ta có thể rút ra kết luận sau:

  • Unowned thì không thể nil được vì vòng đời cái instance trỏ đi bằng với cái instance nó trỏ đến

  • Weak ngược lại có thể nil, suy ra không thể là hằng được.

Nguồn: Raywenderlich

Strong reference cycle trong IOS

Để tránh strong reference cycle, IOS dùng cơ chế ARC này ở nhiều chỗ.

Dễ thấy nhất là @IBOutlet:

@IBOutlet weak var tableView : UITableView!

và delegate:

Ví dụ homeVC có một tableView:

Đa số delegate nên xài weak vì delegate có thể có hoặc không có.

Thêm một trường hợp hay gặp nữa là: Ví dụ a có thuộc tính delegate tới b, b có thuộc tính delegate tới c.

Nếu để strong reference thì nếu muốn hủy b, c sẽ không hủy được b. Vì b còn strong reference tới a.

What's next:

Còn một số cái như unsafe unowned reference mình chưa đề cập. Tuy nhiên với kĩ năng vẽ data model, bạn có thể đọc document trên Apple, vẽ lại data model và hy vọng nó giúp bạn dễ hiểu những gì đang diễn ra hơn.

Một điểm nữa, document của Apple dùng ví dụ khá trực quan và thực tế, không phải kiểu Foo, Bar như những document khác. Bạn nên đọc lại phần này vài lần để củng cố lại kiến thức nhé.

Resources:

https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html

https://krakendev.io/blog/weak-and-unowned-references-in-swift

teamtreehouse.com

https://www.raywenderlich.com/134411/arc-memory-management-swift

Bài viết gốc:

http://niviki.com/2017/04/quan-ly-bo-nho-trong-swift/

0