如何避免 Android/Kotlin 活动中的内存泄漏
How to avoid memory leaks in Android/Kotlin activities
我的应用程序模块启动一个位置 tracker/prompter 运行 作为前台服务。我工作得很好,但是 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))
}
}
}
我可以在不完全重写我的代码的情况下避免这种内存泄漏吗?
移除
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
时所做的那样。
我的应用程序模块启动一个位置 tracker/prompter 运行 作为前台服务。我工作得很好,但是 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))
}
}
}
我可以在不完全重写我的代码的情况下避免这种内存泄漏吗?
移除
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
时所做的那样。