บทความนี้จะแสดงให้เห็นจุดที่เรามักจะตกหลุมพรางจนอาจทำให้เกิดสาเหตุที่ตัวจัดการ Memory ผิดพลาดได้

เราจะได้เรียนรู้

  1. ARC ทำงานอย่างไร
  2. Strong Reference Cycle คืออะไร
  3. การใช้งาน weak และ unowned

การจัดการหน่วยความจำของ struct เป็นดังนี้ เมื่อเราทำการพาสค่าที่เป็นประเภทสตรัคไปยังเมธอดใดๆ ระบบจะทำการคัดลอกค่าใหม่เก็บไว้ในหน่วยความจำ แล้วค่าเก่าจะถูกเคลียร์ทิ้งคืนค่าหน่วยความจำให้กับระบบ เพราะเนื่องจากสตรัคเป็นประเภท Value type

แต่ Class นั้นไม่ใช่ เนื่องจากเป็นประเภท Reference type คลาสจะถูกสร้างไว้ในหน่วยความจำ เมื่อเราพาสค่าประเภทนี้ไปยังเมธอด จะเป็นการพาสตัว Reference ของคลาสไป (ตัวที่บ่งบอกว่าคลาสหรือ Instance อยู่ที่ใดในหน่วยความจำ)
คลาสไม่สามารถถูกเคลียร์เพื่อคืนค่าให้กับหน่วยความจำโดยอัตโนมัติได้ เพราะอาจจะมีการพาสค่าไปที่อื่นได้อีก ดังนั้นแล้วจึงมี ARC (Automatic Reference Counting) ในการติดตามเฝ้าดูตัว Reference เหล่านี้ เพื่อทำการเคลียร์และคืนค่ากลับไปที่หน่วยความจำ เมื่อไม่มีการใช้งานคลาสแล้ว

ARC ใช้หลักการอย่างง่ายโดยการเฝ้าดู 1 Reference หรือมากกว่าที่ผูกกับคลาสหรืออินสแตนซ์ไว้ เมื่อไม่มี Reference มาผูกในระยะเวลานาน ก็จะทำลายอินสแตนซ์นั้นและคืนค่าพื้นที่ของหน่วยความจำ

ARC ทำงานอย่างไร

เมื่อเราสร้างคลาส ตัว ARC จะจองพื้นที่ในหน่วยความจำเพื่อเก็บคลาสไว้ และแน่นอนว่ามีพื้นที่ของหน่วยความจำเพียงพอในการเก็บข้อมูลที่เชื่อมโยงกับคลาสที่สร้างไว้ด้วย และ ARC จะล็อคพื้นที่นั้นไว้เพื่อไม่ให้มีใครมาเก็บข้อมูลที่พื้นที่นี้ได้ และเมื่อคลาสไม่ได้ถูกใช้งานแล้ว (ไม่มี Reference) ARC ก็จะทำลายคลาสและคืนพื้นที่หน่วยความจำเพื่อเอาไปใช้ประโยชน์อื่นต่อได้

และแน่นอนว่าถ้าหาก ARC ได้ทำลายคลาสและคืนพื้นที่ไปแล้ว เป็นไปไม่ได้เลยที่จะอ่านข้อมูลของคลาสนั้นได้อีก แล้วถ้าหากเราพยายามที่จะอ่านข้อมูลของคลาสนั้นให้ได้ แอพพลิเคชั่นจะแครชเนื่องจากไม่มีข้อมูลของคลาสนั้นอยู่

เพื่อให้แน่ใจว่าจะไม่ทำลายคลาสทิ้ง ARC จะเฝ้านับว่าคลาสถูก Reference ไปที่อื่นกี่ครั้งแล้ว ตัวอย่าง มี properties ที่กำลังทำงานอยู่กี่ตัว หรือตัวแปร หรือคลาสที่ Reference กับคลาสด้วยกัน จนกระทั่งนับได้ว่าไม่มี Reference ที่ผูกกับคลาสแล้ว หรือนับได้ 0 ARC จะทำเครื่องหมายเพื่อทำลายและคืนค่าหน่วยความจำ

นิยาม

class MyClass {
  var name = ""
  
  init(name: String) {  
    self.name = name 
    print("Initializing class with name \(self.name)") 
  }
 
  deinit { 
    print("Releasing class with name \(self.name)") 
  } 
}
var class1ref1: MyClass? = MyClass(name: "One")  
var class2ref1: MyClass? = MyClass(name: "Two")  
var class2ref2: MyClass? = class2ref1 
 
print("Setting class1ref1 to nil")  
class1ref1 = nil

print("Setting class2ref1 to nil")  
class2ref1 = nil

print("Setting class2ref2 to nil")  
class2ref2 = nil

คอนโซลที่แสดง

Initializing class with name One  
Initializing class with name Two 
Setting class1ref1 to nil  
Releasing class with name One  
Setting class2ref1 to nil 
Setting class2ref2 to nil  
Releaseing class with name Two

เราสร้างคลาส class1ref1 ซึ่งมี 1 Reference และคลาส class2ref1 มี 2 Reference เนื่องจากมีคลาสที่ชื่อ class2ref2 มาเชื่อมโยงอยู่ด้วย
และเมื่อเราทำลายคลาส class1ref1 = nil จะสามารถทำลายได้เลยเนื่องจากมีอยู่ 1 Reference
แต่เมื่อสั่ง class2ref1 = nil จะไม่สามารถทำลายได้เนื่องจากมี Reference เชื่อมโยงไปยังคลาส class2ref2 อยู่ ดังนั้นเมื่อสั่ง class2ref2 = nil ก็จะทำให้ class2ref1 โดนทำลายไปได้

Strong Reference Cycle คืออะไร

อินสแตนซ์ของคลาสที่ถือ Strong Reference ร่วมกัน ทำให้ถูกมองข้ามที่จะทำลายจาก ARC ตัวอย่าง

นิยาม

class MyClass1_Strong {
   var name = ""
   var class2: MyClass2_Strong?
   
   init(name: String) {
      self.name = name
      print("Initializing class1_Strong with name (self.name)")
   }
   
   deinit {
      print("Releasing class1_Strong with name (self.name)")
   }
}
class MyClass2_Strong {
   var name = ""
   var class1: MyClass1_Strong?
   
   init(name: String) {
      self.name = name  
      print("Initializing class1_Strong with name (self.name)")
   }
   
   deinit {
      print("Releasing class1_Strong with name (self.name)")  
   }
}
var class1: MyClass1_Strong? = MyClass1_Strong(name: "Class1_Strong")  
var class2: MyClass2_Strong? = MyClass2_Strong(name: "Class2_Strong") 
 
class1?.class2 = class2  
class2?.class1 = class1 
 
print("Setting classes to nil")  
class2 = nil 
class1 = nil

จากโค้ดข้างบนจะเห็นว่า หากเราให้ class2 = nil จะไม่สามารถคืนค่าหน่วยความจำได้ เนื่องจากคลาสมี Reference เชื่อมโยงไปยังคลาส class2ref2 อยู่ class1 อยู่ และในทางกลับกันเราให้ class1 = nil ก็จะไม่สามารถทำได้เนื่องจากเหตุผลข้อเดียวกัน กล่าวคือการนับ Reference ของตัว ARC จะไม่มีทางเป็น 0 เราสามารถใช้ weak หรือ unowned ในการแก้ปัญหาได้

การใช้งาน weak และ unowned

class MyClass1_Strong {
   var name = ""
   weak var class2: MyClass2_Strong?
   
   init(name: String) {
      self.name = name
      print("Initializing class1_Strong with name (self.name)")
   }
   
   deinit {
      print("Releasing class1_Strong with name (self.name)")
   }
}
class MyClass1_Strong {
   var name = ""
   unowned var class2: MyClass2_Strong
   
   init(name: String) {
      self.name = name
      print("Initializing class1_Strong with name (self.name)")
   }
   
   deinit {
      print("Releasing class1_Strong with name (self.name)")
   }
}

เพื่อให้ ARC สามารถทำลายคลาสทั้งสองที่ถือ Strong Reference ร่วมกันได้ Swift ให้ใช้ weak, unowned เพื่อให้คลาสไม่ถือ Strong Reference
ความต่างระหว่าง weak, unowned คือสามารถทำให้ property เป็น optional และ non optional ได้