Strong Reference (การอ้างอิงแบบเข้มแข็ง) — เป็นกลไกมาตรฐานในการจัดการหน่วยความจำที่วัตถุยังคงอยู่ในหน่วยความจำตราบใดที่ยังมีการอ้างอิงที่ทำงานอยู่ชี้ไปยังวัตถุนั้น แตกต่างจากการอ้างอิงแบบอ่อน การอ้างอิงแบบเข้มแข็งจะเพิ่มตัวนับการอ้างอิงของวัตถุและป้องกันการปลดปล่อยหน่วยความจำโดยอัตโนมัติ ตามข้อมูลจาก Apple Developer Documentation ARC จะจัดการอายุของวัตถุใน Swift และ Objective-C โดยอัตโนมัติ การเข้าใจการทำงานของการอ้างอิงแบบเข้มแข็งเป็นสิ่งสำคัญในการป้องกันการรั่วไหลของหน่วยความจำและการพึ่งพาแบบวงจรในแอปพลิเคชันมือถือ
หัวข้อหลัก
Strong Reference เป็นประเภทของการอ้างอิงไปยังวัตถุที่ป้องกันไม่ให้วัตถุถูกทำลายโดย garbage collector หรือระบบจัดการหน่วยความจำ ตราบใดที่ยังมีการอ้างอิงแบบเข้มแข็งอย่างน้อยหนึ่งรายการชี้ไปยังวัตถุ หน่วยความจำของวัตถุนั้นจะไม่ถูกปลดปล่อย นี่เป็นกลไกพื้นฐานที่ ARC ใน Swift และ Objective-C และ garbage collection ใน Java และ Kotlin ทำงาน
แนวคิดของการอ้างอิงแบบเข้มแข็งเป็นพื้นฐานสำหรับทุกภาษาที่มีการจัดการหน่วยความจำอัตโนมัติ ในระบบที่มี ARC การอ้างอิงแบบเข้มแข็งแต่ละครั้งจะเพิ่มตัวนับการอ้างอิงของวัตถุ เมื่อตัวนับลดลงถึงศูนย์ วัตถุจะถูกยกเลิกการจัดสรรทันที ใน Java และ Kotlin ที่มี garbage collector การอ้างอิงแบบเข้มแข็งรับประกันว่าวัตถุสามารถเข้าถึงได้และจะไม่ถูกเก็บโดย GC
ตามข้อมูลจาก WWDC 2021 ประมาณ 35% ของการรั่วไหลของหน่วยความจำในแอป iOS เกี่ยวข้องกับการใช้งานการอ้างอิงแบบเข้มแข็งที่ไม่ถูกต้องและการอ้างอิงแบบวงจร ในการพัฒนา Android การรั่วไหลผ่าน implicit strong reference ใน closures และ callbacks เป็นสาเหตุอันดับสองของปัญหาหน่วยความจำรองจาก Context Leak
เพื่อการทำงานกับหน่วยความจำอย่างมีประสิทธิภาพ จำเป็นต้องเข้าใจความแตกต่างระหว่าง strong, weak และ unowned reference และเลือกประเภทการอ้างอิงให้ถูกต้องตามความสัมพันธ์ของการเป็นเจ้าของและอายุของวัตถุ
ก่อนการนำ ARC มาใช้ นักพัฒนาต้องเรียก retain และ release ด้วยตนเองสำหรับทุกวัตถุ ซึ่งนำไปสู่ข้อผิดพลาดมากมาย ARC ที่ Apple เปิดตัวในปี 2011 พร้อมกับการเปิดตัว LLVM 3.0 ทำให้กระบวนการนี้เป็นอัตโนมัติโดยการวิเคราะห์กราฟความเป็นเจ้าของในขั้นตอนการคอมไพล์ คอมไพเลอร์จะแทรกการเรียก retain, release และ autorelease ในตำแหน่งที่เหมาะสมด้วยตนเอง
ตามข้อมูลจาก Clang Static Analyzer การนำ ARC มาใช้ลดจำนวนบั๊กที่เกี่ยวข้องกับหน่วยความจำในแอป iOS ลง 70% สำหรับนักพัฒนา นี่หมายความว่าการจัดการหน่วยความจำปลอดภัยขึ้น แต่ในขณะเดียวกันก็จำเป็นต้องเข้าใจว่าการอ้างอิงแบบเข้มแข็งทำงานอย่างไรภายใต้ฝาครอบ — เพื่อหลีกเลี่ยง retain cycles
ใน Kotlin และ Java บทบาทของ ARC จะถูกดำเนินการโดย garbage collector แต่หลักการของการอ้างอิงแบบเข้มแข็งยังคงเหมือนเดิม: GC Roots — คือจุดเริ่มต้นที่วัตถุถูกยึดไว้โดยการอ้างอิงแบบเข้มแข็ง ตราบใดที่วัตถุสามารถเข้าถึงได้ผ่านห่วงโซ่ของการอ้างอิงแบบเข้มแข็งจาก GC Root วัตถุนั้นจะไม่ถูกเก็บ
ARC (Automatic Reference Counting) ทำงานบนหลักการนับจำนวนการอ้างอิงสำหรับแต่ละวัตถุในฮีป เมื่อมีการสร้างการอ้างอิงแบบเข้มแข็งใหม่ไปยังวัตถุ ตัวนับจะเพิ่มขึ้น (retain) เมื่อการอ้างอิงถูกทำลายหรือถูกเขียนทับ ตัวนับจะลดลง (release) เมื่อตัวนับถึงศูนย์ วัตถุจะถูกลบออกจากหน่วยความจำทันที
ลองดูตัวอย่างใน Swift เมื่อสร้างอินสแตนซ์ของคลาส ARC จะจัดสรรหน่วยความจำและตั้งค่า retain count เป็น 1 การกำหนดค่าให้กับตัวแปรอื่นแต่ละครั้งจะเพิ่มตัวนับ เมื่อตัวแปรออกนอกขอบเขตการมองเห็น ตัวนับจะลดลง:
class ProfileViewController {
var nameLabel: String?
var avatarImage: UIImage?
func loadProfile() {
// retain count = 1 สำหรับอินสแตนซ์ใหม่
let user = User(name: "Ivan")
// retain count = 2 หลังจากการกำหนดค่า nameLabel
nameLabel = user.name
// ออกจากเมธอด — user ออกจาก scope, retain count = 1
}
}
ในโค้ดนี้ ARC รับประกันว่าวัตถุ User ยังคงอยู่ในหน่วยความจำตราบใดที่ยังมีการอ้างอิงแบบเข้มแข็งอย่างน้อยหนึ่งรายการชี้ไปยังวัตถุนั้น เมื่อฟังก์ชัน loadProfile เสร็จสิ้น ตัวแปรท้องถิ่น user จะถูกทำลาย แต่ nameLabel ยังคงยึดวัตถุไว้ หน่วยความจำจะถูกปลดปล่อยก็ต่อเมื่อ nameLabel หยุดอยู่หรือถูกเขียนทับ
ใน Kotlin พฤติกรรมที่คล้ายกันนี้เกิดขึ้นผ่าน GC Roots ตราบใดที่ยังมีห่วงโซ่ strong references ที่สามารถติดตามได้จากรากของ garbage collector (เช่น ฟิลด์ static หรือเธรดที่ทำงานอยู่) วัตถุจะยังคงอยู่ในหน่วยความจำ ข้อแตกต่างคือ GC ไม่ได้ปลดปล่อยหน่วยความจำทันที — สิ่งนี้เกิดขึ้นแบบอะซิงโครนัสหลังจากการวิเคราะห์การเข้าถึงได้
ใน ARC การปลดปล่อยเกิดขึ้นแบบซิงโครนัสทันทีที่ตัวนับถึงศูนย์ ใน Swift และ Objective-C คุณรู้แน่ชัดว่าวัตถุจะถูกลบเมื่อใด ใน Kotlin และ Java ช่วงเวลาของการปลดปล่อยไม่สามารถคาดเดาได้ แต่สิ่งนี้ถูกชดเชยด้วยรูปแบบที่ยืดหยุ่นกว่าในการตรวจจับการพึ่งพาแบบวงจรในระดับ garbage collector
Retain cycle (วงจรการยึด) — สถานการณ์ที่วัตถุสองชิ้นขึ้นไปมีการอ้างอิงแบบเข้มแข็งซึ่งกันและกัน ผลลัพธ์คือ retain count ของพวกมันไม่เคยลดลงถึงศูนย์ และหน่วยความจำจะไม่ถูกปลดปล่อยแม้ว่าวัตถุเหล่านั้นจะไม่จำเป็นสำหรับแอปพลิเคชันอีกต่อไป
ตัวอย่างคลาสสิก: parent view controller ยึด child object ด้วยการอ้างอิงแบบเข้มแข็ง และ child ก็ยึด parent ด้วยการอ้างอิงแบบเข้มแข็งเช่นกัน นี่เป็นเรื่องปกติในสถานการณ์ที่มี delegates, closures และ lambda expressions ที่ซ้อนกัน ตามข้อมูลจาก Instruments Leaks retain cycles คิดเป็นสัดส่วนถึง 60% ของการรั่วไหลของหน่วยความจำทั้งหมดในแอปที่ใช้ ARC
class ParentViewController: UIViewController {
var child: ChildViewController?
func setupChild() {
child = ChildViewController()
// retain cycle: parent เก็บ child, child เก็บ parent ผ่าน closure
child?.onEvent = {
self.handleEvent()
}
}
func handleEvent() {}
}
ปัญหาที่นี่คือ closure onEvent จับ self (ParentViewController) ด้วยการอ้างอิงแบบเข้มแข็ง ในขณะที่ ParentViewController เองก็ยึด child ด้วยการอ้างอิงแบบเข้มแข็ง วัตถุทั้งสองจะไม่ถูกปลดปล่อยเลย ทางออกคือการใช้ weak self ใน closure เพื่อตัดวงจร
ใน Kotlin วงจรที่คล้ายกันเกิดขึ้นเมื่อใช้ lambdas ที่จับวัตถุภายนอก JVM garbage collector อาจตรวจจับวงจรดังกล่าวได้ในที่สุด แต่เฉพาะเมื่อวัตถุไม่สามารถเข้าถึงได้จาก GC Roots หากวงจรเกี่ยวข้องกับเธรดที่ทำงานอยู่หรือบริบท UI การรั่วไหลจะคงอยู่ตลอดอายุการทำงานของแอปพลิเคชัน
การเข้าใจความแตกต่างระหว่างประเภทของการอ้างอิงเป็นกุญแจสำคัญในการจัดการหน่วยความจำอย่างปลอดภัย Strong Reference เพิ่ม retain count Weak Reference ไม่เพิ่ม retain count และจะกลายเป็น nil โดยอัตโนมัติเมื่อวัตถุถูกปลดปล่อย Unowned Reference ก็ไม่เพิ่ม retain count เช่นกัน แต่ไม่ถูกตั้งเป็น nil — การเข้าถึงมันหลังจากการปลดปล่อยจะทำให้เกิด crash
| ประเภทการอ้างอิง | Retain count | ความปลอดภัย | เมื่อใดควรใช้ |
|---|---|---|---|
| Strong | +1 | ปลอดภัย (ค่าเริ่มต้น) | การเป็นเจ้าของวัตถุ ความสัมพันธ์ parent → child |
| Weak | ไม่เปลี่ยนแปลง | การตั้งเป็น nil อัตโนมัติ (ปลอดภัย) | Delegates, callback, การอ้างอิงย้อนกลับ |
| Unowned | ไม่เปลี่ยนแปลง | เสี่ยงต่อ crash เมื่อเข้าถึงช้า | เมื่อวัตถุมีอายุยืนกว่าเจ้าของอย่างแน่นอน |
การเลือกประเภทการอ้างอิงถูกกำหนดโดยความสัมพันธ์ของการเป็นเจ้าของ หากวัตถุ B เป็นส่วนหนึ่งของ A และไม่สามารถดำรงอยู่ได้หากไม่มี A — ให้ใช้ Strong หาก B สามารถดำรงอยู่ได้โดยอิสระและอ้างอิงถึง A เพื่อการแจ้งเตือน — ให้ใช้ Weak Unowned ถูกใช้น้อยครั้ง — เฉพาะเมื่ออายุของวัตถุลูกไม่เกินอายุของวัตถุพ่อแม่อย่างเคร่งครัด
Apple Developer Documentation แนะนำ: โดยค่าเริ่มต้นให้ใช้ strong สำหรับความสัมพันธ์ของการเป็นเจ้าของทั้งหมด หากจำเป็นต้องหลีกเลี่ยง retain cycle — ให้ระบุว่าการอ้างอิงใดควรเป็นแบบอ่อน โดยปกติแล้วนี่คือการอ้างอิงย้อนกลับในลำดับชั้น (child → parent) ใน Kotlin บทบาทที่คล้ายกันนี้เป็นของ WeakReference จาก java.lang.ref ซึ่งใช้สำหรับ caches และ observer patterns
การตรวจจับ retain cycles เป็นขั้นตอนแรก ขั้นตอนที่สองคือการแก้ไขอย่างถูกต้อง เครื่องมือหลักในการต่อสู้กับวงจรของการอ้างอิงแบบเข้มแข็งคือการเปลี่ยนการอ้างอิงหนึ่งรายการเป็น weak หรือ unowned ในภาษาที่มี garbage collector จะใช้ WeakReference เพิ่มเติมพร้อมกับการตรวจสอบ null ด้วยตนเองก่อนการเข้าถึงแต่ละครั้ง
ใน Swift และ Objective-C การแก้ไขที่พบบ่อยที่สุดคือการเพิ่ม [weak self] ใน closures สิ่งนี้รับประกันว่า closure จะไม่ยึดวัตถุไว้หลังจากที่วัตถุถูกปลดปล่อยแล้ว ใน Kotlin เพื่อวัตถุประสงค์ที่คล้ายกันจะใช้ wrapper WeakReference หรือการล้างการอ้างอิงอย่างชัดเจนใน onDestroy
class NetworkService {
func fetchData(completion: @escaping (Data?) -> Void) {
// การจับภาพผ่าน weak self — retain cycle ถูกตัดออก
URLSession.shared.dataTask(
with: URL(string: "https://api.example.com")!
) { [weak self] data, response, error in
guard let self else { return }
completion(data)
}.resume()
}
}
ในตัวอย่างนี้ [weak self] รับประกันว่า NetworkService จะไม่ถูกยึดโดย closure หลังจากที่ไม่จำเป็นอีกต่อไป หาก self ถูกปลดปล่อยก่อนที่คำขอจะเสร็จสิ้น — guard let self else { return } จะออกจาก closure โดยไม่เรียก completion
สำหรับการวินิจฉัย retain cycles ให้ใช้ Instruments Leaks สำหรับ iOS หรือ Android Profiler + LeakCanary สำหรับ Android เครื่องมือเหล่านี้แสดงกราฟการยึดที่แม่นยำและระบุว่าการอ้างอิงแบบเข้มแข็งใดที่ขัดขวางการปลดปล่อยวัตถุ การทำโปรไฟล์หน่วยความจำอย่างสม่ำเสมอควรเป็นส่วนหนึ่งของ CI/CD pipeline ของทุกโปรเจกต์มือถือ
Swift และ Kotlin ใช้กลไกการจัดการหน่วยความจำที่แตกต่างกันโดยพื้นฐาน แต่แนวคิดของการอ้างอิงแบบเข้มแข็งมีอยู่ในทั้งสองภาษา ใน Swift ใช้ ARC ที่มีการปลดปล่อยแบบซิงโครนัสเมื่อ retain count = 0 ใน Kotlin ใช้ tracing GC ที่ทำความสะอาดวัตถุที่ไม่สามารถเข้าถึงได้แบบอะซิงโครนัส
| พารามิเตอร์ | Swift (ARC) | Kotlin (JVM GC) |
|---|---|---|
| กลไก | การนับการอ้างอิง (retain count) | การติดตามการเข้าถึงได้ (GC Roots) |
| การปลดปล่อย | แบบซิงโครนัส (เมื่อตัวนับถึงศูนย์) | แบบอะซิงโครนัส (ตามรอบ GC) |
| Retain cycle | ไม่ถูกตรวจจับโดยอัตโนมัติ | GC อาจตรวจจับได้ แต่ไม่ทันที |
| Weak ref | weak (การตั้งเป็น nil อัตโนมัติ) | WeakReference (การตรวจสอบด้วยตนเอง) |
ข้อแตกต่างในทางปฏิบัติหลัก: ใน Swift retain cycle — คือการรั่วไหลที่แน่นอน ใน Kotlin GC สามารถตัดวงจรได้หากวัตถุไม่สามารถเข้าถึงได้จากราก แต่อายุของวัตถุที่รั่วไหลยังคงไม่สามารถคาดเดาได้ ดังนั้นในทั้งสองภาษา กลยุทธ์ที่ดีที่สุดคือการหลีกเลี่ยงวงจรของการอ้างอิงแบบเข้มแข็งในขั้นตอนการออกแบบ
สำหรับ Swift ให้ใช้ weak ใน delegate patterns และ closures สำหรับ Kotlin — WeakReference หรือ Lifecycle-aware components ที่ล้างการอ้างอิงโดยอัตโนมัติเมื่อเจ้าของถูกทำลาย ในทั้งสองแนวทาง เป้าหมายคือหนึ่งเดียว — ไม่รวมการอ้างอิงแบบเข้มแข็งในที่ที่พวกมันสร้างห่วงโซ่การยึดที่ไม่สามารถแตกหักได้
คำถามที่พบบ่อย
Strong Reference เพิ่ม retain count ของวัตถุและป้องกันการปลดปล่อยตราบใดที่การอ้างอิงยังคงอยู่ Weak Reference ไม่เปลี่ยน retain count และจะถูกตั้งเป็น nil โดยอัตโนมัติเมื่อวัตถุถูกลบออกจากหน่วยความจำ การอ้างอิงแบบเข้มแข็งใช้สำหรับการเป็นเจ้าของ การอ้างอิงแบบอ่อนใช้สำหรับการเชื่อมต่อย้อนกลับและ delegates
Retain cycle — คือการล็อกซึ่งกันและกันที่วัตถุสองชิ้นยึดกันด้วยการอ้างอิงแบบเข้มแข็ง retain count ของพวกมันไม่เคยลดลงถึงศูนย์ หน่วยความจำไม่ถูกปลดปล่อย สิ่งนี้นำไปสู่การรั่วไหลของหน่วยความจำ: วัตถุยังคงอยู่ในฮีปตลอดไป แอปพลิเคชันใช้ทรัพยากรมากขึ้นเรื่อย ๆ และในที่สุดก็ล่มด้วย OutOfMemory
ใช้ Instruments Leaks จาก Xcode — เริ่มต้นการทำโปรไฟล์ด้วยเทมเพลต Leaks ดำเนินการตามสถานการณ์ในแอปและตรวจสอบตัวบ่งชี้การรั่วไหล สำหรับการวินิจฉัยที่แม่นยำ ให้สลับไปที่แท็บ Cycles & Roots — มันจะแสดงกราฟของ strong reference ซึ่งกันและกันที่ก่อให้เกิดวงจรที่ไม่สามารถแตกหักได้
ใช้ Unowned เมื่ออายุของวัตถุลูกไม่เกินอายุของวัตถุพ่อแม่อย่างแน่นอน — ตัวอย่างเช่น เมื่อผูกวัตถุกับขอบเขตที่กำหนดไว้อย่างเคร่งครัด หากมีข้อสงสัย — ให้ใช้ Weak เนื่องจากการเข้าถึง unowned ที่ถูกปลดปล่อยแล้วจะทำให้แอป crash
ทางอ้อม — ใช่ Retain และ release แต่ละครั้งใน ARC เป็นการดำเนินการแบบอะตอมมิกที่มีค่าใช้จ่ายเพิ่มเติม หากมีวัตถุจำนวนมากในวงจร สิ่งนี้อาจส่งผลต่อประสิทธิภาพ อย่างไรก็ตาม ปัญหาหลักไม่ใช่ความเร็วของ ARC แต่คือการรั่วไหลของหน่วยความจำเนื่องจากการเลือกประเภทการอ้างอิงที่ไม่ถูกต้อง
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม