【问题标题】:How to avoid memory leaks in Android/Kotlin activities如何避免 Android/Kotlin 活动中的内存泄漏
【发布时间】:2021-12-05 09:18:55
【问题描述】:

我的应用模块启动了一个作为前台服务运行的位置跟踪器/提示器。我工作得很好,但 Android Studio 给了我这个警告:

private lateinit var locationActivity: Activity
=> Do not place Android context classes in static fields; this is a memory leak

这是模块代码:

package com.DevID.AppID

import android.Manifest
import android.app.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.location.Location
import android.net.Uri
import android.os.Build
import android.os.IBinder
import android.os.Looper
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import com.android.volley.Request
import com.android.volley.RequestQueue
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import com.google.android.gms.location.*
import java.util.*
import kotlin.concurrent.fixedRateTimer

private var createCount = 0
private var initialized = false
private var LocationServiceRunning = false
private var locationService: LocationService = LocationService()
private lateinit var locationIntent: Intent

private lateinit var locationCallback: LocationCallback
private lateinit var VolleyQueue: RequestQueue
private lateinit var locationActivity: Activity // **WARNING: Do not place Android context classes in static fields; this is a memory leak**

fun startLocationService(activity: Activity, enable: Boolean) {
    LocationService.Enabled = enable
    locationIntent = Intent(activity, locationService.javaClass)
    VolleyQueue = Volley.newRequestQueue(activity)
    locationActivity = activity
    if (LocationService.Enabled) {
        if (!LocationServiceRunning) {
            if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
                reportEvent("NOPERMIS")
                val errorNotif = createNotification(
                    activity,
                    "Error: location not available",
                    "Click to open settings and allow location access",
                    NotificationCompat.PRIORITY_MAX,
                    Notification.CATEGORY_ERROR,
                    Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", activity.packageName, null))
                )
                (activity.getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager).notify(99, errorNotif)
            } else {
                reportEvent("STARTING")
                createCount = 0
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Oreo = Android 8.0 = API level 26
                    activity.startForegroundService(locationIntent)
                } else {
                    activity.startService(locationIntent)
                }
            }
        }
    } else {
        if (LocationServiceRunning) {
            reportEvent("STOPPING")
            activity.stopService(locationIntent)
        } else {
            (activity.getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager).cancel(99) // Clean up any error notifications
        }
    }
}

fun reportEvent(event: String) {
    VolleyQueue.add(StringRequest(Request.Method.GET, "${LocationService.ReportURL}&report=$event", {}, {}))
}

fun createNotification(activity: Activity, title: String, text: String, priority: Int, category: String?, intent: Intent): Notification {
    val channelID = "LOCATION_SERVICE"
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val channelName = "Location Service"
        val chan = NotificationChannel(channelID, channelName, NotificationManager.IMPORTANCE_LOW)
        chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
        val manager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        manager.createNotificationChannel(chan)
    }
    return NotificationCompat.Builder(activity, channelID)
        .setOngoing(true)
        .setSmallIcon(R.drawable.ic_stat_ic_notification)
        .setContentTitle(title)
        .setContentText(if (text == "") null else text)
        .setPriority(priority)
        .setCategory(category)
        .setContentIntent(
            TaskStackBuilder.create(activity)
                .addNextIntent(intent)
                .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
        )
        .setShowWhen(true)
        .setWhen(System.currentTimeMillis())
        .build()
}

class LocationService : Service() {
    companion object {
        var ReportURL: String = ""
        var ReportInterval: Long = 60000 // 60 Seconds
        var ReportTitle: String = "App is running"
        var ReportText: String = ""
        var Enabled: Boolean = false
    }

    private val startTime = now()
    private var locGPS: String = ""
    private var locCount = 0
    private var locTime = startTime
    private lateinit var locationTimer: Timer

    private fun now(): Int {
        return (System.currentTimeMillis() / 1000).toInt()
    }

    override fun onCreate() {
        super.onCreate()
        LocationServiceRunning = true
        createCount++
        if (Enabled) {
            showNotification()
            startLocationUpdates()
        }
    }

    private fun showNotification() {
        val locNotif = createNotification(
            locationActivity,
            ReportTitle,
            ReportText,
            NotificationCompat.PRIORITY_MIN,
            Notification.CATEGORY_SERVICE,
            Intent(this, MainActivity::class.java)
        )
        startForeground(99, locNotif)
    }

    private fun startLocationUpdates() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            return
        }
        locationTimer = fixedRateTimer("timer", false, 5000, ReportInterval * 1000) {
            val start = now() - startTime
            val last = now() - locTime
            VolleyQueue.add(StringRequest(  // https://developer.android.com/training/volley/simple
                Request.Method.GET,
                "${ReportURL}&gps=$locGPS&count=$locCount&start=$start&last=$last&wait=${ReportInterval}&agent=App",
                { response ->
                    val lines = response.lines()
                    lines.forEach { line ->
                        when {
                            line == "start" -> startLocationService(applicationContext as MainActivity, true)
                            line == "stop" -> startLocationService(applicationContext as MainActivity, false)
                            line.startsWith("text:") -> ReportText = line.substring(5)
                            line.startsWith("title:") -> ReportTitle = line.substring(6)
                            line == "show" -> showNotification()
                            else -> Log.w("ø_LocationService", "bad response line: $line")
                        }
                    }
                },
                {
                    ReportTitle = "Error: connection lost"
                    ReportText = ""
                    showNotification()
                }
            ))
        }
        val request = LocationRequest.create().apply {
            interval = ReportInterval
            fastestInterval = 20000
            priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        }
        locationCallback = object : LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult) {
                val location: Location = locationResult.lastLocation
                locGPS = location.latitude.toString() + "," + location.longitude.toString()
                locCount++
                locTime = now()
            }
        }
        LocationServices
            .getFusedLocationProviderClient(this)
            .requestLocationUpdates(request, locationCallback, Looper.getMainLooper())
        initialized = true
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        super.onStartCommand(intent, flags, startId)
        return START_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    override fun onDestroy() {
        super.onDestroy()
        LocationServiceRunning = false
        if (initialized) {
            LocationServices
                .getFusedLocationProviderClient(this)
                .removeLocationUpdates(locationCallback)
            locationTimer.cancel()
        }
        if (Enabled) {
            reportEvent("RESTARTING")
            val broadcastIntent = Intent()
            broadcastIntent.action = "restartservice"
            broadcastIntent.setClass(this, LocationServiceRestarter::class.java)
            this.sendBroadcast(broadcastIntent)
        }
    }

}

class LocationServiceRestarter : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        Log.i("ø_LocationService", "LocationServiceRestarter triggered")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            context!!.startForegroundService(Intent(context, LocationService::class.java))
        } else {
            context!!.startService(Intent(context, LocationService::class.java))
        }
    }
}

我能否在不完全重写代码的情况下避免这种内存泄漏?

【问题讨论】:

    标签: android kotlin memory-leaks


    【解决方案1】:

    删除

    private lateinit var locationActivity: Activity
    

    您需要的是Context,而您的LocationService 是Context。

    改变

    fun startLocationService(activity: Activity, enable: Boolean) { ... }
    fun createNotification(activity: Activity, ...) { ... }
    

    到

    fun startLocationService(context: Context, enable: Boolean) { ... }
    fun createNotification(context: Context, ...) { ... }
    

    并调用createNotification 作为第一个参数传递this 而不是locationActivity。

    您也可以使用applicationContext 代替locationActivity,就像您在调用startLocationService 时所做的那样。

    【讨论】:

    • 这是一个巨大的帮助。我对 Android 应用程序非常缺乏经验,整个“上下文与活动”的事情对我来说仍然很困惑。非常感谢!
    • @W3Coder,不客气!强烈建议一次参加一次 Google 的 Android 培训,例如 developer.android.com/courses 和核心开发人员主题 here。不要试图一次全部理解。那只会让你感到困惑。谷歌还有大量的 Codelabs 供你学习:codelabs.developers.google.com/?cat=android
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-13
    • 1970-01-01
    • 1970-01-01
    • 2015-08-27
    相关资源
    最近更新 更多