Android Academy · Oppitunti

Muistivuodot ja niiden korjaaminen

Etsikää vuodot ja estäkää ne.

Oppitunti 3/413 vaihetta

Muistivuodot ja niiden korjaaminen on ilmainen Android Academy-oppitunti CoddyKitissä. Tämä on oppitunti 3/4. Voit lukea tästä oppimispolusta kokonaan mitkä tahansa 3 oppituntia ilmaiseksi — sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä käytännön harjoittelun sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. Oppitunti kuuluu Android Academy-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. Android Academy-kurssilla on yhteensä 4 oppituntia.

Mikä on muistivuoto?

Muistivuoto tapahtuu, kun tarpeettomia olioita ei voida vapauttaa roskienkeruulla, koska jokin pitää niihin edelleen viitettä. Androidissa tyypillinen uhri on Activity tai Context, joka jää elämään näytön tuhoutumisen jälkeen.

Vuodot kasvattavat kekoa ajan mittaan, aiheuttavat roskienkeruun useammin (mikä pysäyttää käyttöliittymän ja aiheuttaa jankia) ja kaatavat sovelluksen lopulta OutOfMemoryError-virheeseen. Tässä oppitunnissa näytetään, miten vuodot löydetään ja korjataan.

Miten roskienkeruu päättää, mitä säilytetään

Androidin roskienkeruu säilyttää kaikki oliot, jotka ovat saavutettavissa GC-juuresta, kuten elävistä säikeistä tai staattisista kentistä. Kaikki saavuttamattomat oliot vapautetaan.

Vuoto on yksinkertaisesti ei-toivottu saavutettavuuspolku: pitkäikäinen olio pitää viitettä lyhytikäiseen olioon. Alla oleva puhdas Kotlin-esimerkki näyttää, miten saavutettavuus pitää olion elossa.

object GlobalCache {
    val items = mutableListOf<ByteArray>()
}

fun cacheSomething() {
    // This 1MB array stays alive forever because GlobalCache
    // (a static singleton, a GC root) keeps referencing it.
    GlobalCache.items.add(ByteArray(1_000_000))
}

fun main() {
    repeat(3) { cacheSomething() }
    println("Held arrays: ${GlobalCache.items.size}") // never freed
}

Klassikko: Activityn vuotaminen

Yleisin Android-vuoto syntyy, kun staattinen tai pitkäikäinen objekti säilyttää Context-olion. Kun Activity tuhotaan (esimerkiksi näytön kääntämisen tai siirtymisen yhteydessä), järjestelmä haluaisi vapauttaa sen, mutta staattinen viite pitää sen ikuisesti elossa.

Alla oleva koodi aiheuttaa vuodon: singleton tallentaa Activity-kontekstin välimuistiin.

// LEAK: static field holds an Activity Context
object Analytics {
    var context: Context? = null   // <- keeps Activity alive
    fun init(ctx: Context) { context = ctx }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Analytics.init(this) // passing the Activity -> leak on rotation
    }
}

Korjaus: käytä Application Contextia

Kun Context-olio on tallennettava pitkäikäiseen objektiin, tallenna application context, joka elää koko prosessin ajan ja jonka säilyttäminen on turvallista.

Säilytä Activity/View-kontekstia vain niin kauan kuin kyseinen näyttö on olemassa. Tämä yksittäinen sääntö estää useimmat Android-vuodot.

object Analytics {
    private var appContext: Context? = null
    fun init(ctx: Context) {
        // applicationContext is process-scoped and safe to keep
        appContext = ctx.applicationContext
    }
}

// Call site is now leak-free:
Analytics.init(this) // stores applicationContext, not the Activity

Sisäluokat ja Handlerit

Ei-staattinen sisäluokka (mukaan lukien useimmat kuuntelijat, Runnable-oliot ja Handler-takaisinkutsut) sisältää implisiittisen viitteen ulompaan luokkaansa. Jos kyseinen työ jatkuu näyttöä pidempään, se vuotaa näytön.

Viivästetty Handler.postDelayed on yleinen syyllinen: odottava viesti pitää Activityn elossa, kunnes se suoritetaan.

// LEAK: the posted Runnable holds the Activity for 60 seconds
handler.postDelayed({ updateUi() }, 60_000)

// FIX: cancel pending work when the screen goes away
override fun onDestroy() {
    super.onDestroy()
    handler.removeCallbacksAndMessages(null)
}

Coroutinet: rajaa työn laajuus

Coroutine, joka jatkuu näyttöä pidempään, vuotaa kaiken kaappaamansa. Ratkaisu on structured concurrency: käynnistä työ elinkaareen sidotussa scopessa, jotta se peruutetaan automaattisesti.

Käytä ViewModelissa viewModelScope-scopea ja käyttöliittymäohjaimessa lifecycleScope-scopea. Kun omistaja tuhotaan, scope peruuttaa työn ja viitteet vapautetaan.

class FeedViewModel : ViewModel() {
    fun load() {
        // Cancelled automatically when the ViewModel is cleared
        viewModelScope.launch {
            val feed = repository.fetchFeed()
            _state.value = feed
        }
    }
}

// In Compose, collect tied to the lifecycle:
val state by viewModel.state.collectAsStateWithLifecycle()

DisposableEffect Composessa

Composessa kaikki rekisteröitävä on myös poistettava rekisteristä, kun composable poistuu. DisposableEffect tarjoaa juuri tätä varten onDispose-lohkon: kuuntelijoille, tarkkailijoille, sensoreille ja broadcast receiver -vastaanottajille.

Siivouksen unohtaminen tässä vuotaa takaisinkutsun ja kaiken, minkä se kaappaa.

@Composable
fun LocationDisplay(manager: LocationManager) {
    DisposableEffect(manager) {
        val listener = LocationListener { /* update */ }
        manager.register(listener)
        onDispose { manager.unregister(listener) } // prevents the leak
    }
}

Vuotojen etsiminen LeakCanarylla

LeakCanary on vakiotyökalu vuotojen automaattiseen havaitsemiseen debug-koontiversioissa. Lisää riippuvuus, niin se tarkkailee tuhottuja objekteja. Jos objektia ei kerätä roskienkeruussa, työkalu tallentaa heapin ja näyttää tarkan viiteketjun, joka pitää objektin elossa.

Ketju kertoo, mitä pitää korjata: se osoittaa suoraan ongelmalliseen kenttään tai kuuntelijaan.

// build.gradle.kts (app module)
dependencies {
    debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
// No code needed: on a debug build, navigate away from a screen and
// LeakCanary posts a notification with the leak trace, e.g.:
//   Analytics.context -> MainActivity (leaked)

Heap dumpit Profilerissa

Käytä vuotoihin, joita LeakCanary ei havaitse (hidas kasvu, natiivimuisti tai suuret välimuistit), Memory Profileria. Ota heap dump, pakota roskienkeruu ja tarkista, millä luokilla on yllättävän paljon ilmentymiä.

Activityn, Fragmentin tai ViewModelin kasvava ilmentymämäärä siirtymisen jälkeen on selvä merkki ongelmasta. Profiler näyttää myös polun GC-juuriin, joten voit jäljittää viitteen.

Bitmapit ja suuret varaukset

Kaikki muistiongelmat eivät ole viitevuotoja. Suuret bitmapit ja rajoittamattomat välimuistit voivat täyttää heapin itsestään. Täyden tarkkuuden valokuva voi viedä muistista kymmeniä megatavuja.

Anna kuvakirjaston (Coil/Glide) pienentää kuva näytettävään kokoon ja rajoita luomasi välimuistin koko määrittämällä sille enimmäiskoko. Katkelmassa näytetään kooltaan rajoitettu LRU-välimuisti.

// Bound memory: keep at most ~1/8 of available app memory
val maxKb = (Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()
val bitmapCache = object : LruCache<String, Bitmap>(maxKb) {
    override fun sizeOf(key: String, value: Bitmap) = value.byteCount / 1024
}
// An unbounded HashMap<String, Bitmap> would grow until OutOfMemoryError.

Vuotojen ehkäisyn tarkistuslista

Ota nämä tavat käyttöön, niin useimmat vuodot eivät pääse syntymään:

  • Tallenna pitkäikäisiin objekteihin applicationContext, älä koskaan Activity- tai View-kontekstia.
  • Suorita asynkroninen työ elinkaareen sidotussa coroutinessa (viewModelScope, lifecycleScope).
  • Poista kuuntelijat ja vastaanottajat aina rekisteristä (onDispose, onDestroy).
  • Peruuta viivästetyt Handlerit ja ajastimet.
  • Rajoita välimuistien kokoa ja pienennä bitmapit.
  • Pidä LeakCanary mukana debug-koontiversioissa ja reagoi sen jäljitystietoihin.

Pikatarkistus

Sinun on säilytettävä Context-olio singleton-objektissa koko sen elinkaaren ajan. Mitä kontekstia sen pitäisi säilyttää, jotta näyttö ei vuotaisi?

Kertaus: pidä muisti siistinä

Opit, mitä vuodot ovat ja miten niitä estetään:

  • Vuodot ovat ei-toivottuja saavutettavuuspolkuja, jotka pitävät kuolleet objektit elossa.
  • Klassinen vuoto on staattinen tai pitkäikäinen viite Activity- tai Context-objektiin — tallenna sen sijaan applicationContext.
  • Sisäluokkien kuuntelijat, viivästetyt Handlerit ja ilman scopea käynnistetyt coroutinet vuotavat omistajansa — rajaa niiden scope ja peruuta ne.
  • Käytä Composessa DisposableEffect-rakennetta takaisinkutsujen poistamiseen rekisteristä.
  • LeakCanary ja Memory Profiler paljastavat viiteketjun.
  • Rajoita välimuistien kokoa ja pienennä bitmapit.

Seuraavaksi: sovelluksen nopea käynnistäminen käynnistyksen optimoinnin ja baseline-profiilien avulla.

Aloita maksutta

Opi Kotlin tekoälytuutorin avulla — ilmaiseksi

Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.

Kurssit
36
Oppitunnit
152

Usein kysytyt kysymykset

Onko oppitunti ”Muistivuodot ja niiden korjaaminen” ilmainen?

Kyllä — voit lukea täällä verkossa kokonaan ilmaiseksi mitkä tahansa Android Academy-oppimispolun 3 oppituntia, myös oppitunnin “Muistivuodot ja niiden korjaaminen”. Sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä interaktiiviset harjoitukset sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. Android Academy-kurssilla on yhteensä 4 oppituntia.

Mitä opin oppitunnilla ”Muistivuodot ja niiden korjaaminen”?

Etsikää vuodot ja estäkää ne. Harjoittelet Android Academy-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.

Tarvitsenko kokemusta aloittaakseni Android Academy-opiskelun?

Aiempi kokemus ei ole tarpeen. CoddyKitin Android Academy-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 3/4.

Kuinka kauan ”Muistivuodot ja niiden korjaaminen”-oppitunnin suorittaminen kestää?

Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.

Voinko kirjoittaa ja suorittaa koodia tällä Android Academy-oppitunnilla?

Kyllä. Jokainen Android Academy-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.

Kaikki tämän kurssin oppitunnit

  1. Suorituskyvyn mittaaminen
  2. Uudelleenkokoonpanon hallinta
  3. Muistivuodot ja niiden korjaaminen
  4. Käynnistys ja baseline-profiilit
← Takaisin: Android Academy