Pagtagas ng memorya (memory leak) — sitwasyon kung saan hindi binibigyang-laya ng application ang memoryang ginagamit ng mga bagay na hindi na kailangan. Sa mobile development, ito ay partikular na kritikal: limitadong heap at kawalan ng swap ay humahantong sa OutOfMemoryError at pag-crash ng application. Ayon sa Purdue University (2022), 35% ng mga Android application sa Google Play ay naglalaman ng kahit isang pagtagas ng memorya. Tatalakayin natin ang mga tipikal na sitwasyon, diagnostic tool, at paraan ng pag-aayos.
Mga pangunahing punto
Pagtagas ng memorya — sitwasyon kung saan ang nakalaang memorya ay hindi ibinabalik sa sistema matapos ang bagay ay hindi na kailangan ng programa. Itinuturing ng garbage collector na buhay ang naturang bagay dahil may aktibong chain ng referensya mula sa GC Root patungo dito.
Sa Java/Kotlin, awtomatikong gumagana ang garbage collector, ngunit hindi nito matukoy na ang isang bagay ay lohikal na hindi kailangan kung mayroong teknikal na referensya dito. Dapat na tahasang putulin ng developer ang mga hindi kinakailangang koneksyon. Sa Swift/Objective-C, awtomatikong binibilang ng ARC ang mga referensya, ngunit hinaharangan ng retain cycles ang pag-reset ng counter.
Ang pangunahing panganib ng pagtagas ay ang cumulative effect. Bawat pagtagas ay kumokonsumo ng maliit na halaga ng memorya, ngunit sa maraming paglipat sa pagitan ng mga screen (pag-ikot ng screen, pagbubukas/pagsasara ng Activity), ang mga pagtagas ay nag-iipon hanggang sa maubos ang limitasyon ng heap.
Pagtagas — ang bagay ay hindi naa-access ng code, ngunit hindi tinanggal ng GC. Pamamaga — ang bagay ay lohikal na kailangan, ngunit nakaimbak sa labis na dami. Halimbawa ng pamamaga: cache ng mga larawan na 100 MB na may working set na 30 MB. Parehong problema ay humahantong sa OOM, ngunit magkaiba ang mga sanhi at paraan ng paggamot.
ART (Android Runtime) ay gumagamit ng generational garbage collection na may concurrent compaction. Ang memorya ay nahahati sa batang henerasyon (Young), luma (Old), at malalaking bagay (Large). Ang mga bagay na nakaligtas sa ilang GC cycle ay inililipat sa Old generation, kung saan mas madalang ang pagtitipon — pinapabilis nito ang mga normal na cycle.
Nagsisimula ang GC kapag ang heap ay umabot sa isang tiyak na threshold ng occupancy (karaniwan 75-85%). Sa panahon ng GC, lahat ng thread ng application ay pinipigilan (STW — Stop The World). Kung mas maraming buhay na bagay, mas mahaba ang pause. Ang pagtagas ay nagpapataas ng bilang ng mga buhay na bagay, na nagpapahaba ng mga GC pause.
Tinutukoy ng kolektor ang mga buhay na bagay sa pamamagitan ng pagtawid sa graph mula sa GC Roots: static field, stack variable ng mga aktibong thread, JNI referensya. Anumang bagay na maaabot sa pamamagitan ng referensya mula sa mga ugat na ito ay itinuturing na buhay — kahit na alam ng developer na hindi na ito kailangan.
// Halimbawa: static collection bilang GC Root — permanenteng pagtagas
object GlobalHolder {
val listeners = mutableListOf<WeakReference<Any>>()
}
class LeakingFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
GlobalHolder.listeners.add(WeakReference(this))
// WeakReference ay hindi humaharang sa GC — tamang pag-uugali
}
}
WeakReference ay lumulutas ng problema: binabalewala ng GC ang weak referensya kapag tinutukoy ang mga buhay na bagay. Kung weak referensya na lamang ang natitira sa isang bagay, ito ay titipunin sa susunod na GC cycle.
Activity Context — ang pinakamalawak na sitwasyon ng pagtagas sa Android. Kung ang singleton, static field, o long-lived service ay nag-iimbak ng referensya sa Activity Context, ang buong Activity kasama ang lahat ng View ay hindi maaaring kolektahin ng GC. Solusyon: gamitin ang Application Context para sa mga long-lived na bagay.
Handler at mga ipinadalang mensahe — Ang Handler.postDelayed(runnable, delay) ay naglalagay ng mensahe sa queue ng Main Looper. Kung ang Activity ay nawasak bago matapos ang delay, ang mensahe ay nasa queue pa rin at humahawak ng referensya sa pamamagitan ng Runnable → anonymous class → panlabas na klase (Activity).
class SafeActivity : AppCompatActivity() {
private val mainHandler = Handler(Looper.getMainLooper())
private val callback = Runnable { /* update UI */ }
override fun onResume() {
super.onResume()
mainHandler.postDelayed(callback, 5000)
}
override fun onPause() {
mainHandler.removeCallbacks(callback) // sapilitan: linisin ang queue
super.onPause()
}
}
Inner Classes — ang non-static inner class ay may implicit na referensya sa instance ng panlabas na klase. Kung ang panlabas na klase ay Activity at ang inner class ay ipinasa sa labas (halimbawa, sa RecyclerView.Adapter), hindi maaaring kolektahin ang Activity.
Android Studio Memory Profiler — ang built-in na tool para sa real-time na pagsubaybay ng heap. Nagpapakita ng graph ng ginagamit na memorya, bilang ng mga alokasyon at bagay ayon sa uri. Pinapayagan ang pag-record ng heap dump at pag-export sa HPROF format para sa pagsusuri sa MAT.
Eclipse MAT (Memory Analyzer Tool) — desktop analyzer ng heap dump. Awtomatikong gumagawa ng Leak Suspects Report na nagha-highlight ng mga bagay na may pinakamalaking retained size at nagmumungkahi ng posibleng GC root chain para sa bawat kahina-hinalang bagay.
Xcode Memory Graph Debugger — para sa iOS. Pinipigilan ang application at vi-visualize ang graph ng mga bagay. Ang retain cycles ay naka-highlight sa pula, maaaring mag-click sa anumang bagay upang makita ang retain count at mga referensya nito.
| Tool | Mga kakayahan | Kompleksidad |
|---|---|---|
| Memory Profiler | Real-time graph, heap dump, pagsubaybay ng alokasyon ng bagay | Mababa |
| Eclipse MAT | Dominator tree, Leak Suspects, OQL query | Katamtaman |
| LeakCanary | Awtomatikong pagtuklas, trace ng pagtagas sa notification | Minimal |
| Xcode Memory Graph | Biswal na graph ng retain cycles, listahan ng buhay na bagay | Mababa |
Ayon sa Uber Engineering Blog, ang pagpapatupad ng awtomatikong profiling ng memorya (LeakCanary + heap dump analysis) sa CI/CD pipeline ay nagbabawas ng bilang ng mga insidenteng nauugnay sa memorya sa produksyon ng 60% sa loob ng 3 buwan.
Pagpapalit ng Context — kung ang bagay ay nabubuhay nang mas mahaba kaysa Activity, gamitin ang applicationContext. Lahat ng long-lived na bagay (singleton, repository, database helpers) ay dapat tumanggap ng Application Context, hindi Activity Context. Exception: mga UI component na nangangailangan ng access sa tema o resource na partikular sa Activity.
Lifecycle-aware component — ang paggamit ng LifecycleObserver, DefaultLifecycleObserver o reactivex ay awtomatikong kinakansela ang mga subscription sa onDestroy. Ang Android Jetpack ay nagbibigay ng lifecycleScope at viewModelScope, na nililinis ng kaukulang event.
Static inner class — kung ang inner class ay hindi nangangailangan ng access sa field ng panlabas na klase, gawin itong static. Ang static inner class ay walang implicit na referensya sa panlabas na klase. Kung kailangan ang access, gamitin ang WeakReference para sa tahasang referensya.
class MyActivity : AppCompatActivity() {
// ❌ Non-static inner class — implicit na referensya sa MyActivity
inner class BadListener : SomeListener {
override fun onEvent() { /*...*/ }
}
// ✅ Static inner class — walang implicit na referensya
class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
override fun onEvent() { /*...*/ }
}
}
Sa iOS, gumamit ng capture lists: [weak self] sa mga closure na maaaring mabuhay nang mas mahaba kaysa sa lumikha. Para sa mga delegate, gumamit ng mahinang referensya (weak var delegate). Para sa mga closure na garantisadong tatawagin lamang habang nabubuhay ang self, maaaring gamitin ang [unowned self], ngunit mag-ingat — ang pag-access sa pinalayang bagay ay magdudulot ng crash.
Mga madalas itanong
Sa Android, magsagawa ng ilang paglipat sa pagitan ng mga screen (Activity A → B → A → B) at suriin ang adb shell dumpsys meminfo package_name. Kung ang Total PSS ay patuloy na tumataas at hindi bumabalik sa orihinal na halaga — may pagtagas. Sa iOS katulad: gamitin ang Debug Memory Graph sa Xcode para sa biswal na pagsusuri.
Oo, kung ang CoroutineScope ay hindi nakansela kapag ang component ay nawasak. Ang coroutine na inilunsad sa GlobalScope ay patuloy na tumatakbo kahit pagkatapos ng finish() ng Activity. Solusyon: gamitin ang viewModelScope (kinakansela sa onCleared) o lifecycleScope (kinakansela sa onDestroy). Para sa sariling Scope, gumawa ng lifecycle-aware scopes sa pamamagitan ng LifecycleOwner.
Bitmap ay nag-iimbak ng pixel data sa native heap, hindi sa Java heap. Ibig sabihin, hindi nakikita ng Java GC ang aktwal na laki ng Bitmap. Kung ang Bitmap ay hindi tinawagan ng recycle() o ang referensya ay hindi ni-null, ang native memorya ay hindi mapapalaya. Gamitin ang BitmapFactory na may inSampleSize para mag-load ng pinaliit na kopya at Glide/Coil para sa awtomatikong pamamahala ng cache.
Static field — ito ay GC Root. Ito ay nabubuhay hangga't ang klase ay na-load (sa Android — hangga't nabubuhay ang Process). Kung ang static field ay tumutukoy sa Activity, Bitmap, View, o anumang mabigat na bagay, ang bagay na ito ay hindi kailanman kokolektahin ng GC. Static field — walang hanggang referensya. Solusyon: mag-imbak lamang ng WeakReference o i-null ang static field sa onDestroy.
ARC ay awtomatikong nagpapalaya ng mga bagay kapag ang bilang ng malakas na referensya ay bumaba sa zero. Retain cycle — ang tanging paraan ng pagtagas sa ARC. Palaging gumamit ng weak para sa parent→child referensya, kung saan ang child ay dapat mabuhay nang mas mahaba kaysa parent (mga delegate, data source). Para sa mga closure, gumamit ng capture list [weak self] at suriin ang self para sa nil sa loob ng closure.
Buod
Gagawa kami ng mobile application na turnkey
Gumagawa ang IT Sectr ng mga iOS at Android application para sa mga startup at negosyo mula noong 2017. Magpapayo kami sa iyo at magmumungkahi ng pinakamahusay na solusyon.
Basahin din