Ketika kita membuat sebuah aplikasi khususnya aplikasi Mobile, Manajemen Memori merupakan salah satu hal yang harus kita perhatikan, karena kesalahan dalam Manajemen Memori bisa menyebabkan Memory Leak, akibatnya performa aplikasi kita menjadi sangat lambat, bahkan bisa mengakibatkan crash tak terduga.
Oleh karena itulah pada artikel kali ini kita akan belajar mengenai Manajemen Memori pada bahasa pemrograman Swift, meskipun topik ini terdengar agak advanced, tapi menurut saya Manajemen Memori itu cukup penting untuk dipelajari sejak awal.
Pada saat aplikasi kita dijalankan, sebuah file executable yang berisi instruksi-instruksi bagaimana aplikasi kita berkerja akan dimuat ke dalam RAM. pada saat itu juga lah aplikasi kita akan mendapatkan bagian (chunk) dari RAM. bagian tersebut bernama Heap (tempat dimana instances dari class-class kita berada ketika aplikasi kita sedang berjalan).
Jadi, apa yang dimaksud dengan Manajemen Memori? Manajemen Memori adalah proses bagaimana kita memanajemen memori Heap. proses tersebut mencakup bagaimana kita memanajemen object-object pada memori Heap dan memastikan bahwa object-object tersebut ter-deallocated dari ruang memori ketika sudah tidak dipakai lagi, sehingga ruang tersebut bisa dipakai lagi nantinya.
Pada bahasa pemrograman Swift, untuk manajemen memorinya itu menggunakan konsep yang bernama Automatic Reference Counting (ARC), konsep ARC ini sebenarnya cukup sederhana, yaitu melacak instance pada suatu class dan memutuskan kapan waktu yang tepat untuk melakukan deallocate, ARC melacaknya dengan cara menghitung berapa banyak references (reference count) pada tiap instance, jika nilainya telah mencapai 0, artinya instance tersebut sudah tak dipakai lagi, maka instance tersebut pun akan di-deallocate.
Agar lebih jelas, perhatikan kode berikut :
Automatic Reference Counting (ARC)1class Book {2 let title: String34 init(title: String, author: String) {5 self.title = title6 }78 deinit {9 print("\(title) instance deallocated.")10 }11}1213var myBook1: Book? = Book(title: "Sapiens")14var myBook2: Book? = myBook11516myBook1 = nil17myBook2 = nil
Penjelasan kode :
Pertama, Kita membuat instance dari class Book, dan kita tampung ke dalam variabel myBook1, reference count menjadi 1.
1var myBook1: Book? = Book(title: "Sapiens")
Kita membuat reference dari myBook1 dan menampungnya ke dalam variabel myBook2, jadi, variabel myBook1 dan myBook2 mengacu pada instance Book yang sama. sekarang, reference count menjadi 2.
1var myBook2: Book? = myBook1
Saat kita set nilai myBook1 menjadi nil, reference count berkurang menjadi 1.
1myBook1 = nil
Terakhir, kita set nilai myBook2 menjadi nil juga. dan instance Book tersebut pun akan di-deallocate, karena sekarang reference count nilainya sudah mencapai 0.
1myBook2 = nil
Untuk memastikan bahwa instance tersebut benar-benar ter-deallocated ketika kita set nilai myBook2 menjadi nil, silakan jalankan kodenya, maka kita akan melihat output seperti ini :
Output1Sapiens instance deallocated.
Sebelum adanya Automatic Reference Counting (ARC), yaitu pada Xcode versi 4.2 ke bawah, kita harus melakukan increment dan decrement reference count secara manual, konsep tersebut bernama Manual Retain Release (MRR).
Sebelum kita membahas mengenai Strong Reference Cycle, kita harus berkenalan terlebih dahulu dengan Strong reference.
Jadi, Strong reference adalah salah satu tipe reference yang sifatnya mencegah Object untuk di-deallocate selama reference tersebut masih ada. selain Strong reference, ada 2 tipe reference lain yaitu Weak dan Unowned yang akan kita bahas nanti. Ketika kita membuat reference baru, secara default tipe reference-nya adalah Strong. Contoh Strong reference bisa kita lihat pada kode di bawah ini :
Automatic Reference Counting (ARC)1class Book {2 let title: String34 init(title: String, author: String) {5 self.title = title6 }78 deinit {9 print("\(title) instance deallocated.")10 }11}1214var myBook1: Book? = Book(title: "Sapiens")15var myBook2: Book? = myBook11718myBook1 = nil19myBook2 = nil
Seperti yang telah kita ketahui, Strong reference sifatnya mencegah Object untuk di-deallocate selama reference tersebut masih ada, dan itu bisa menyebabkan salah satu masalah Memory Leak yang bernama Strong Reference Cycle.
Strong Reference Cycle terjadi ketika ARC gagal melakukan deallocated suatu Object karena ada 2 reference yang saling mempunyai tipe reference Strong (Ingat, tipe reference Strong akan mencegah suatu Object ter-deallocated selama reference tersebut masih ada). agar lebih jelas saya akan memberikan beberapa contoh kasus Strong Reference Cycle yang mungkin akan kita jumpai dalam pembuatan aplikasi nantinya :
Strong Reference Cycle bisa disebabkan oleh 2 Object yang saling mempunyai tipe reference Strong. Pada kode di bawah, class Book mempunyai dependency Author, dan sebaliknya Author mempunyai dependency Book.
Dependencies1class Author {2 let name: String3 var book: Book?45 init(name: String) {6 self.name = name7 print("\(name) instance initialized.")8 }910 deinit {11 print("\(name) instance deallocated.")12 }13}1415class Book {16 let title: String17 var author: Author?1819 init(title: String) {20 self.title = title21 print("\(title) instance initialized.")22 }2324 deinit {25 print("\(title) instance deallocated.")26 }27}2829var yuval: Author? = Author(name: "Yuval")30var sapiens: Book? = Book(title: "Sapiens")3132sapiens?.author = yuval33yuval?.book = sapiens
Ketika kita set nilainya menjadi nil, method deinit() tidak akan terpanggil. karena saat kita set nilainya menjadi nil, reference count masing-masing Object nilainya belum mencapai 0.
Karena awalnya kan ada 2 reference (reference dari membuat variabel dan reference dari set property), lalu Object tersebut kita set nilainya menjadi nil, dan reference count sekarang bernilai 1. karena belum mencapai 0, Object tersebut pun tidak ter-deallocate dari ruang memori dan method deinit() pun tidak terpanggil.
1var yuval: Author? = Author(name: "Yuval")2var sapiens: Book? = Book(title: "Sapiens")34sapiens?.author = yuval5yuval?.book = sapiens68yuval = nil9sapiens = nil
Output1Yuval instance initialized.2Sapiens instance initialized.
Solusi :
Untuk menangani masalah tersebut, caranya sangat mudah! kita tinggal ubah tipe reference dari yang awalnya Strong menjadi Weak atau Unowned. Berbeda dengan Strong, tipe reference Weak atau Unowned tidak melakukan increment terhadap reference count, sehingga jika kita set variabel yuval dan sapiens menjadi nil, Object Book dan Author pun akan ter-deallocated.
1class Book {2 let title: String4 weak var author: Author?56 init(title: String) {7 self.title = title8 print("\(title) instance initialized.")9 }1011 deinit {12 print("\(title) instance deallocated.")13 }14}
Kita hanya perlu ubah menjadi Weak reference salah satunya saja. pada contoh di atas kita ubah reference author pada class Book menjadi Weak. jadi, ketika variabel yuval kita set nilainya menjadi nil, maka Object yuval akan ter-deallocated, sekarang nilai dari sapiens?.author juga akan berubah menjadi nil.
Weak Reference1print(sapiens?.title)2print(sapiens?.author)
Output1Optional("Sapiens")2nil
dan ketika kita juga set nilai dari variabel sapiens menjadi nil, maka Object sapiens juga akan ter-deallocated. ๐
Output1Yuval instance initialized.2Sapiens instance initialized.4Yuval instance deallocated.5Sapiens instance deallocated.
Apabila kita ubah tipe reference nya menjadi Unowned, itu mirip seperti Implicitly Unwrapped Optionals, jadi, kita tidak perlu lagi menggunakan Optional Chaining.
Unowned Reference1class Author {2 let name: String3 var book: Book!45 init(name: String) {6 self.name = name7 print("\(name) instance initialized.")8 }910 deinit {11 print("\(name) instance deallocated.")12 }13}1415class Book {16 let title: String18 unowned var author: Author1920 init(title: String, author: Author) {21 self.title = title22 self.author = author23 print("\(title) instance initialized.")24 }2526 deinit {27 print("\(title) instance deallocated.")28 }29}3032var yuval: Author? = Author(name: "Yuval")33yuval!.book = Book(title: "Sapiens", author: yuval!)3435yuval = nil
Output1Yuval instance initialized.2Sapiens instance initialized.4Yuval instance deallocated.5Sapiens instance deallocated.
Namun jika reference author pada class Book ter-deallocated, dan kita mengakses property author tersebut, maka program kita akan crash saat dijalankan! hati-hati ๐ฑ
1var yuval: Author? = Author(name: "Yuval")2var sapiens: Book = Book(title: "Sapiens", author: yuval!)34yuval!.book = sapiens56yuval = nil8print(sapiens.author)
Delegation Pattern sering kita jumpai pada Apple Platforms, contohnya pada UITableView atau UICollectionView.
Delegation Pattern pada UITableView1import UIKit23class ViewController: UIViewController {4 @IBOutlet var tableView: UITableView! {5 didSet {7 tableView.delegate = self8 tableView.dataSource = self9 }10 }11}
Tujuan utama dari Delegation Pattern adalah agar suatu object dapat berkomunikasi dengan Owner Class-nya secara decoupled atau terpisah, serta tanpa perlu tahu Concrete Type yang dimiliki oleh Owner Class-nya, sehingga kode kita menjadi lebih Reusable dan Testable.
Namun jika kita kurang tepat dalam menggunakan Delegation Pattern, aplikasi yang kita buat bisa terkena Memory Leak!
Perhatikan kode berikut :
Delegation Pattern1protocol ExampleDelegate: AnyObject {}23class FirstVC {5 var delegate: ExampleDelegate?67 init() {8 print("FirstVC instance initialized.")9 }1011 deinit{12 print("FirstVC instance deallocated.")13 }14}1516class SecondVC: ExampleDelegate {17 let vc = FirstVC()1819 init() {21 vc.delegate = self22 print("SecondVC instance initialized.")23 }2425 deinit{26 print("SecondVC instance deallocated.")27 }28}2930var secondVC: SecondVC? = SecondVC()
Ketika kita set nilai dari variabel secondVC menjadi nil, harapannya kan instance SecondVC dan FirstVC akan ter-deallocated oleh ARC kan? karena sudah tidak digunakan lagi. namun kalau kita jalankan kode nya, method deinit() tidak terpanggil dua-duanya, ๐ค
1secondVC = nil
Kenapa? karena pada class SecondVC kita membuat Strong reference terhadap FirstVC,
SecondVC1let vc = FirstVC()
FirstVC1var delegate: ExampleDelegate?
juga sebaliknya, pada class FirstVC kita membuat Strong reference terhadap SecondVC, dannn.. kita pun terjebak dalam Strong Reference Cycle! ๐ฑ
Solusi :
Untuk menangani masalah tersebut, caranya sangat mudah, kita tinggal ganti tipe reference pada property delegate menjadi weak.
Delegation Pattern1protocol ExampleDelegate: AnyObject {}23class FirstVC {5 weak var delegate: ExampleDelegate?67 init() {8 print("FirstVC instance initialized.")9 }1011 deinit{12 print("FirstVC instance deallocated.")13 }14}1516class SecondVC: ExampleDelegate {17 let vc = FirstVC()1819 init() {20 vc.delegate = self21 print("SecondVC instance initialized.")22 }2324 deinit{25 print("SecondVC instance deallocated.")26 }27}2829var secondVC: SecondVC? = SecondVC()3031secondVC = nil
Sekarang, jalankan programnya dan kita akan melihat output seperti ini :
Output1FirstVC instance initialized.2SecondVC instance initialized.4SecondVC instance deallocated.5FirstVC instance deallocated.
Terakhir, kita bisa temukan Strong Reference Cycle pada Closure, karena Closure merupakan tipe data Reference, dan di dalam Closure kita dapat mengakses object-object yang berada di luar Closure.
Agar lebih jelas, perhatikan kode berikut :
1class Person {2 var name: String = "Steve"4 var sayHello: (() -> ())?56 init() {8 sayHello = {9 print("Hello, my name is \(self.name)!")10 }12 }1314 deinit{15 print("Person instance deallocated.")16 }17}1819var person: Person? = Person()20person?.sayHello!()
Pada contoh kode di atas, class Person dan closure sayHello sama-sama mempunyai Strong reference, jadi ketika kita set nilai dari variabel person menjadi nil, maka method deinit() tidak akan terpanggil.
1person = nil
Solusi :
Untuk menangani masalah tersebut, caranya mirip seperti menangani Strong Reference Cycle pada Dependencies dan Delegation Pattern, yaitu mengubah tipe reference menjadi Weak atau Unowned. namun pada Closure agak berbeda, kita akan menggunakan Capture List, yaitu menuliskan property apa saja yang ingin kita pakai di dalam Closure, serta mengubah tipe reference-nya menjadi non-Strong.
Sekarang, ubah closure sayHello menjadi seperti ini :
1init() {2 sayHello = {4 [unowned self] in6 print("Hello, my name is \(self.name)!")7 }8}
Lalu jalankan kodenya, dan kita akan melihat output seperti ini :
1Hello, my name is Steve!3Person instance deallocated.
Bagaimana jika Closure kita menerima parameter? kita bisa menempatkan parameter tersebut di antara Capture List dan keyword in, seperti ini :
Capturing List with parameter1class Person {2 var name: String = "Steve"4 var sayHello: ((_ withEmoji: Bool) -> ())?56 init() {7 sayHello = {9 [unowned self] (_ withEmoji: Bool) in10 print("Hello, my name is \(self.name)! \(withEmoji ? "๐" : "")")12 }13 }1415 deinit{16 print("Person instance deallocated.")17 }18}
Sampai di sini, kita telah mengetahui perbedaan antara tipe reference Strong, Weak, dan Unowned. namun agar lebih jelas dan mudah diingat, saya akan memberikan poin-poin penting perbedaan antara ketiga tipe reference tersebut :
Strong : โ
Weak : โ
Unowned : โ
Strong : โ
Weak : โ (ARC akan mengubah nilainya menjadi nil jika Object yang di-refer ter-deallocated)
Unowned : โ
Strong : โ (Strong reference tidak akan pernah terdeallocated)
Weak : โ
Unowned : โ
Kita bisa gunakan tipe reference Weak jika Object yang kita refer berpotensi akan ter-deallocated lebih dulu daripada Object yang me-refer.
Kita bisa gunakan tipe reference Unowned jika kita yakin bahwa Object yang kita refer akan ter-deallocated lebih lama atau sama dengan Object / Closure yang me-refer. (Misalnya seperti contoh kasus Strong Reference Cycle pada Closure di atas, lifetime dari class Person adalah sama dengan lifetime dari closure sayHello).
Kalau masih kurang yakin mau pakai Weak atau Unowned, saya sarankan pakai Weak saja. karena lebih aman dan tidak akan menyebabkan Crash seperti tipe reference Unowned apabila digunakan dengan cara yang kurang tepat.
Sekarang, kita telah belajar mengenai Automatic Reference Counting (ARC), Strong Reference Cycle, and Capture List in Swift. Selanjutnya mungkin kamu bisa belajar bagaimana cara mendeteksi memory leaks menggunakan Memory Graph Debugger yang ada pada Xcode atau menggunakan fitur Allocation & Memory Leaks detector pada Instruments.
Oke mungkin itu saja yang bisa saya bagikan kali ini, kalau kamu merasa artikel ini bermanfaat silakan Like & Share artikel ini ke teman-teman kamu atau jika kamu punya pertanyaan, tulis aja di kolom komentar, Thank you! ๐ ๐
Always open to new ideas. ๐๏ธ
Articles that you might want to read.
Cara baru untuk mengelola memori di Swift ๐๏ธ
let, with, run, apply, dan also pada Kotlin.
Menulis kode yang mudah untuk di-test pada Swift.