Pagtagas ng Memorya sa mga Mobile Application — ano ito, mga sanhi at paraan ng pagtuklas

May-akda: IT Sectr Nai-publish: 2026-03-29 Oras ng pagbabasa: 9 min

Pagtagas ng memorya (Memory Leak) — sitwasyon kung saan ang isang app ay nagpapanatili ng mga reference sa mga bagay na hindi na kailangan, na pumipigil sa garbage collector na palayain ang occupied memory. Ayon sa datos ng LeakCanary, kahit sa mahusay na pagkakasulat na mga app ay may 3–5 na pagtagas bawat 10,000 linya ng code. Bawat pagtagas ay unti-unting binabawasan ang available na memorya, na humahantong sa pagbagal at OutOfMemoryError.

Mga Pangunahing Punto

  • Memory Leak — nananatili ang bagay sa memorya kahit na walang aktibong reference mula sa lohika ng app
  • Static na reference sa Activity o Context — pinakakaraniwang sanhi ng pagtagas sa Android
  • LeakCanary — karaniwang tool para sa awtomatikong pagtuklas ng pagtagas sa Android
  • WeakReference at Application Context — pangunahing teknolohiya para maiwasan ang pagtagas
  • Mga Lifecycle-aware na component nag-aalis ng buong klase ng pagtagas na nauugnay sa mga subscription

Ano ang pagtagas ng memorya

Pagtagas ng memorya (Memory Leak) — sitwasyon kung saan ang isang bagay ay nananatiling maaabot sa pamamagitan ng chain ng malalalakas na reference (Strong Reference), kahit na lohikal na ito ay hindi na kailangan ng app. Itinuturing ng garbage collector (GC) na buhay ang naturang bagay at hindi pinapalaya ang memoryang inookupahan nito. Bilang resulta, ang available na heap memory ay patuloy na bumababa at ang dalas ng mga paghinto ng GC ay tumataas.

Hindi tulad ng mga wikang may manual memory management (C, C++), sa Java/Kotlin ang pagtagas ay hindi isang nakalimutang free(), kundi isang nakalimutang reference. Hangga't mayroong strong reference mula sa root object (GC Root) hanggang sa tumatagas na bagay, itinuturing ito ng GC na kailangan. Karaniwang GC Root: static field, aktibong thread, stack ng tawag, JNI-global na reference.

Panganib ng pagtagas ay ang pinagsama-samang epekto nito. Isang pagtagas na 100 KB ay hindi napapansin, ngunit 100 tulad ng pagtagas ay kumukuha ng 10 MB at ang app ay nagsisimulang bumagal dahil sa madalas na GC. Ang kritikal na masa ng pagtagas ay humahantong sa OutOfMemoryError at pag-crash ng app. Sintomas ng pagtagas: patuloy na pagtaas ng konsumo ng memorya sa graph ng Profiler, madalas na paghinto ng GC na may STW (Stop The World) at pagbaba ng performance ng UI.

Mga karaniwang uri ng pagtagas sa mobile apps

Limang uri ng pagtagas ay sumasaklaw sa 95% ng mga kaso sa mobile development. Bawat isa ay may sariling sanhi at katangian na pattern sa code.

Static na reference sa Activity o Context

Pinaka kilalang pagtagas sa Android — pag-iimbak ng static na reference sa Activity o Context. Karaniwang code: isang static na field ng Activity na hindi nireretrieve sa onDestroy(). Habang buhay ang static na field, nananatiling buhay ang buong Activity kasama ang View tree nito na maaaring kumuha ng 1–10 MB. Ito ay klasikong pagtagas na unang natatagpuan ng LeakCanary.

Solusyon: huwag kailanman mag-imbak ng Activity o Context sa mga static na field. Gamitin ang Application Context para sa mga singleton na mas mahaba ang buhay kaysa Activity. Kung kailangan ng reference sa Activity — gumamit ng WeakReference<Activity>.

kotlin
object MySingleton {
    private var weakActivity: WeakReference<Activity>? = null

    fun attach(activity: Activity) {
        weakActivity = WeakReference(activity)
    }
}

Panloob na klase na may implicit na reference

Anonymous na klase at non-static na nested na klase ay implicitly na nag-iimbak ng reference sa klase na naglalaman ng mga ito. Isang Runnable na ipinadala sa Handler na isinasagawa pagkatapos ng onDestroy() ay humahawak sa buong Activity. Isang Retrofit callback na nagsasara ng Activity ay gumagawa ng pareho. Ito ang pinakamapanlinlang na uri ng pagtagas — ang implicit na reference ay hindi nakikita sa code.

Ang Kotlin object expression at lambda ay kumukuha rin ng mga reference sa panlabas na klase. Gawing static (o top-level sa Kotlin) ang mga nested na klase at ipasa ang mga panlabas na reference sa pamamagitan ng WeakReference. Para sa mga lambda, gamitin ang Lifecycle-aware approach na may viewLifecycleOwner.

Hindi na-unsubscribe na listener at subscription

Pag-subscribe sa mga system service nang hindi nag-a-unsubscribe — direktang pagtagas. SensorManager, LocationManager, NotificationListener na nirehistro sa onResume() nang hindi tinatawag ang unregister sa onPause() ay humahawak sa Activity. Katulad: RxJava Disposable na hindi idinagdag sa CompositeDisposable at coroutine na inilunsad sa pamamagitan ng GlobalScope.

Gumamit ng Lifecycle-aware na component: observe() na may LifecycleOwner ay awtomatikong nag-a-unsubscribe sa onDestroy(). Para sa RxJava — viewLifecycleOwner.lifecycle.addObserver na may DisposableObserver. Para sa coroutine — lifecycleScope.launch() ay nakatali sa lifecycle.

kotlin
// awtomatikong pag-unsubscribe sa pamamagitan ng Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
    updateUI(data)
}

// coroutine na may lifecycleScope
lifecycleScope.launch {
    viewModel.loadData().collect { render(it) }
}

Bitmap na walang recycle

Bitmap ay kumukuha ng malaking halaga ng heap memory: isang FullHD-bitmap — 1920 × 1080 × 4 byte = 8.3 MB. Kung ang Bitmap ay ginagawa para sa bawat elemento ng listahan at ang recycle() ay hindi tinatawag kapag nagtatago, mabilis na nauubos ang memorya. Sa lumang bersyon ng Android (bago ang 3.0), ang Bitmark ay naka-imbak sa native memory, ngunit sa modernong bersyon — sa Dalvik/ART heap, at ang GC ay maaari lamang itong palayain kung walang strong reference.

Gumamit ng Glide o Coil para sa pag-load ng mga larawan — ang mga library na ito ay awtomatikong namamahala ng caching at recycle. Kung direktang nagtatrabaho sa Bitmap, tawagan ang bitmap.recycle() para sa malalaking larawan na hindi na ipinapakita at gamitin ang inSampleSize para sa pag-load ng maliliit na kopya.

Reference sa Fragment pagkatapos ng onDestroyView

Fragment ay may dalawang lifecycle: ang Fragment mismo at ang View nito. Pagkatapos ng onDestroyView() ang View tree ay nawasak, ngunit ang Fragment mismo ay maaaring manatili sa memorya kung mayroong panlabas na reference. Karaniwang error — pag-iimbak ng reference sa Fragment sa adapter ng ViewPager o sa navigation graph na hindi nalilinis kapag nawasak.

Huwag kailanman mag-imbak ng reference sa Fragment sa mga field ng mahabang-buhay na bagay. Gamitin ang childFragmentManager para sa nested na fragment at observe() na may LifecycleOwner para sa paglilipat ng data sa pagitan nila. Nalutas ng ViewPager2 ang problemang ito sa antas ng API: ang FragmentTransactionAdapter ay wastong namamahala ng lifecycle.

Paano matukoy ang pagtagas ng memorya

Pagtuklas ng pagtagas ay nangangailangan ng pag-verify ng dalawang katotohanan: ang memorya ay hindi bumabalik pagkatapos ng expected lifetime at ang bilang ng mga bagay ng isang partikular na uri ay tumataas nang hindi bumababa. Ang proseso ng diagnostic ay may tatlong yugto.

Unang yugto — visual na pagsusuri sa pamamagitan ng Memory Profiler sa Android Studio. Buksan ang tab na Memory, gawin ang target na aksyon (buksan at isara ang screen), pindutin ang GC (Garbage Collection) at tingnan kung bumalik ang memorya sa orihinal na antas. Kung pagkatapos ng 3–4 na cycle ng pagbukas-pagsara ang memorya ay patuloy na tumataas — may pagtagas.

Ikalawang yugto — pagkuha ng Heap Dump. Sa Memory Profiler pindutin ang Dump Java Heap. Ang nakuhang .hprof file ay buksan sa Android Studio: makikita mo ang lahat ng bagay sa heap na may mga sukat at reference. Hanapin ang mga klase na ang bilang ay dapat zero pagkatapos isara ang screen. Halimbawa, MainActivity na may bilang na 2 pagkatapos isara — malinaw na pagtagas.

Ikatlong yugto — pagsusuri ng Retained Size at GC Root. Sa Android Studio suriin ang Retained Size: gaano karaming memorya ang mapapalaya kung tatanggalin mo ang bagay na ito. Ang landas mula sa GC Root hanggang sa bagay ay nagpapakita kung ano ang humahawak dito: Static field → HashMap → Activity — at makikita mo ang punto ng pagtagas. Ang Reference widget panel ay nagpapakita ng lahat ng may hawak ng bagay.

Mga tool para sa paghahanap ng pagtagas

Apat na tool ay sumasaklaw sa paghahanap ng pagtagas mula sa awtomatikong pagtuklas hanggang sa malalim na pagsusuri ng Heap Dump.

ToolParaanFormat ng Resulta
LeakCanaryAwtomatikong pagmonitorHeap Dump + stack trace ng pagtagas
Android Memory ProfilerManu-manong pagmonitorGraph ng memorya + Heap Dump
MAT (Eclipse)Malalim na pagsusuriUlat ng Dominator Tree + landas ng GC Root
PerfettoSystem-wide tracingTimeline + native memory

LeakCanary — must-have para sa bawat proyekto sa Android. Awtomatikong natutukoy ang pagtagas pagkatapos ng lifecycle ng Activity/Fragment at ipinapakita ang eksaktong lokasyon ng pagtagas na may stack trace. Integrasyon: isang linya sa build.gradle. Ang LeakCanary 2.x ay hindi nangangailangan ng manual na initialisasyon — awtomatikong nagrerehistro ng Application Watcher.

Paano maiwasan ang pagtagas ng memorya

Pag-iwas sa pagtagas ay isinama sa proseso ng pag-develop sa pamamagitan ng isang set ng mga patakaran at tool na sumusuri ng code sa bawat yugto.

Patakaran ng malakas na reference

Huwag kailanman mag-imbak ng reference sa Activity, Fragment o View sa static field, singleton o mahabang-buhay na bagay. Kung hindi maiiwasan ang reference — gumamit ng WeakReference o mag-imbak ng data sa pamamagitan ng ViewModel na nabubuhay nang eksakto hangga't kailangan at hindi direktang humahawak ng View.

Lifecycle-aware na arkitektura

ViewModel at LiveData mula sa Android Architecture Components ay nilulutas ang problema ng lifecycle sa antas ng arkitektura. Ang ViewModel ay nabubuhay sa pag-ikot ng screen at hindi naglalaman ng mga reference sa View. Ang LiveData ay awtomatikong nag-a-unsubscribe ng observer sa onDestroy(). Gamitin ang mga ito sa halip na manu-manong pag-subscribe sa mga system service.

Code Review na may pokus sa GC Root

Sa code review bigyang pansin ang: static field na may uri ng Context/View, anonymous na klase, lambda na nagsasara ng Activity, manual na subscription, RxJava disposable na walang composite, pag-imbak ng Fragment sa pamamagitan ng Bundle. Sa Kotlin, suriin din ang coroutine para sa launch na walang koneksyon sa lifecycle.

Awtomatikong pagsusuri sa CI

LeakCanary ay maaaring gumana bilang bahagi ng pipeline ng pagsubok: patakbuhin ang acceptance test na may LeakCanary at markahan ang build na failed kung may natagpuang pagtagas. Ito ay pumipigil sa pagpasok ng pagtagas sa produksyon. Dagdagan ang pagsusuri ng Android Lint na may patakaran na StaticFieldLeak — ito ay nakakahanap ng potensyal na pagtagas sa antas ng static analysis.

kotlin
// LeakCanary sa mga pagsubok
class LeakTest {
    @Test
    fun activityShouldNotLeak() {
        ActivityScenario.launch(MainActivity::class.java)
            .close()
        LeakAssertions.assertNoLeak() // fail kung may pagtagas
    }
}

Mga Madalas Itanong

Ano ang pagkakaiba ng pagtagas ng memorya at OutOfMemoryError?

Pagtagas ay ang sanhi, at OutOfMemoryError ay ang resulta. Isang pagtagas ay hindi humahantong sa OOM, ngunit ang akumulasyon ng dose-dosenang pagtagas ay nauubos ang Heap. Ang OOM ay isang nakamamatay na exception, at ang pagtagas ay isang pattern na humahantong dito sa paglipas ng panahon.

Paano makahanap ng pagtagas nang walang LeakCanary?

Sa pamamagitan ng Android Memory Profiler: buksan at isara ang screen ng 5 beses, pagkatapos ng bawat pagsara ay tawagan ang GC. Kung ang memorya ay hindi bumabalik sa baseline — may pagtagas. Kumuha ng Heap Dump at hanapin sa listahan ang klase ng Activity na ang bilang ay higit sa 0 pagkatapos isara.

Maaari bang pigilan ng Kotlin ang pagtagas sa antas ng wika?

Bahagyang. Nilulutas ng Kotlin ang problema ng null-safety, ngunit hindi nito pinamamahalaan ang strong references. Ang coroutine na may lifecycleScope at viewModelScope ay pumipigil sa pagtagas mula sa background task, at ang sealed class at data class ay nagbabawas ng bilang ng mga estado na humahantong sa pagtagas. Ang pangunahing proteksyon — mga architectural pattern, hindi mga function ng wika.

Bakit nakakahanap ang LeakCanary ng pagtagas na wala naman?

LeakCanary minsan ay nagbibigay ng false positive: ang isang bagay ay maaaring pansamantalang hawak ng system (halimbawa, InputMethodManager ay humahawak ng huling View). Suriin nang manu-mano: kung ang Retained Size < 1 KB at ang GC Root ay isang system service, malamang na ito ay isang maling alarma.

Ang pagtagas ba ng memorya ay nangyayari lang sa Android?

Hindi. Ang pagtagas ay posible sa anumang platform na may GC: iOS (Swift/Objective-C), Flutter (Dart), web browser (JavaScript). Ang mekanismo ay pareho — strong reference mula sa GC Root. Sa iOS, awtomatikong pinamamahalaan ng ARC ang memorya, ngunit ang retain cycle sa pagitan ng mga bagay ay lumilikha ng parehong pagtagas.

Buod

  • Memory Leak — bagay na hindi mapalaya ng GC dahil sa nakalimutang strong reference
  • Static na reference sa Activity at Context — pinakakaraniwang sanhi ng pagtagas
  • Implicit na reference sa pamamagitan ng anonymous na klase, lambda, at RxJava subscription ay mas mapanlinlang kaysa sa explicit
  • LeakCanary awtomatikong nakakahanap ng pagtagas at nagpapakita ng eksaktong stack trace
  • Lifecycle-aware na component (ViewModel, LiveData, lifecycleScope) nag-aalis ng isang klase ng pagtagas
  • Heap Dump at pagsusuri ng Retained Size — pangunahing paraan ng manual na diagnostic
  • Pag-iwas ay may kasamang code review sa strong reference at CI check sa LeakCanary

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.

Pag-usapan ang proyekto

Basahin din