I'm developing an Android app that uses screenshot mechanism and then sends the captured data over network in infinite loop. After some time of work the system kills my app without any exception. I made an experiment where the whole app was cut off besides the screen shooter and the problem is still here. Android profiler does not show any issues but I can see very large AbstractList$Itr and byte[] as on image:
If I increase the RAM then the app lives longer. I can't find the leak for two weaks...
Full source code of ScreenShooter class:
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.PixelFormat
import android.graphics.Point
import android.hardware.display.VirtualDisplay
import android.media.ImageReader
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Handler
import android.os.HandlerThread
import android.os.Process
import android.view.Display
import android.view.WindowManager
import my_module.service.network.Networking
import java.io.ByteArrayOutputStream
import java.util.concurrent.locks.ReentrantLock
class ScreenShooter(private val network: Networking, private val context: Context): ImageReader.OnImageAvailableListener {
private val handlerThread: HandlerThread
private val handler: Handler
private var imageReader: ImageReader? = null
private var virtualDisplay: VirtualDisplay? = null
private var projection: MediaProjection? = null
private val mediaProjectionManager: MediaProjectionManager
private var latestBitmap: Bitmap? = null
private val byteStream = ByteArrayOutputStream()
private val mut: ReentrantLock = ReentrantLock()
private var width: Int
private var height: Int
private val TAG = ScreenShooter::class.java.simpleName
private val DELAY = 1000L
init {
handlerThread = HandlerThread(
javaClass.simpleName,
Process.THREAD_PRIORITY_BACKGROUND
).apply { start() }
handler = Handler(handlerThread.looper)
val windowManager = (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager)
val display: Display = windowManager.defaultDisplay
val size = Point()
display.getRealSize(size)
var width = size.x
var height = size.y
while (width * height > 2 shl 19) {
width = (width * 0.9).toInt()
height = (height * 0.9).toInt()
}
this.width = width
this.height = height
mediaProjectionManager = context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
}
override fun onImageAvailable(reader: ImageReader?) {
mut.lock()
val image = imageReader?.acquireLatestImage() ?: return
var buffer = image.planes[0].buffer
buffer.rewind()
latestBitmap?.copyPixelsFromBuffer(buffer)
latestBitmap?.compress(Bitmap.CompressFormat.JPEG, 80, byteStream)
network.sendScreen(byteStream.toByteArray())
buffer.clear()
buffer = null
image.close()
byteStream.flush()
byteStream.reset()
byteStream.close()
mut.unlock()
Thread.sleep(DELAY)
}
fun startCapture(resultCode: Int, resultData: Intent) {
latestBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
imageReader = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2)
} else {
ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
}
imageReader?.setOnImageAvailableListener(this, handler)
projection = mediaProjectionManager.getMediaProjection(resultCode, resultData)
virtualDisplay = projection!!.createVirtualDisplay(
"shooter",
width,
height,
context.resources.displayMetrics.densityDpi,
VIRT_DISPLAY_FLAGS,
imageReader?.surface,
null,
handler
)
projection?.registerCallback(projectionCallback, handler)
}
fun stopCapture() {
mut.lock()
imageReader?.setOnImageAvailableListener(null, null)
imageReader?.acquireLatestImage()?.close()
imageReader = null
projection?.unregisterCallback(projectionCallback)
projection?.stop()
virtualDisplay?.release()
imageReader?.close()
latestBitmap?.recycle()
mut.unlock()
}
private val projectionCallback = object : MediaProjection.Callback() {
override fun onStop() {
virtualDisplay?.release()
}
}
}
Methods startCapture() and stopCapture() are called from my background service in mainthread. network.sendScreen(byteStream.toByteArray())
just pushes the byte array (screenshot) to okhttp websocket queue:
fun sendScreen(bytes: ByteArray) {
val timestamp = System.currentTimeMillis()
val mss = Base64
.encodeToString(bytes, Base64.DEFAULT)
.replace("\n", "")
val message = """{ "data": {"timestamp":"$timestamp", "image":"$mss"} }"""
.trimMargin()
ws?.send(message.trimIndent())
}
Also I often get messages like Background concurrent copying GC freed but I got them without screenshooter with only network part and everything is Ok. I see no errors in logcat, no leaks in profiler.
I tried to recreate Screenshooter object in my service, but the app started to crash more often:
val t = HandlerThread(javaClass.simpleName).apply { start() }
Handler(t.looper).post {
while (true) {
Thread.sleep(10 * 1000L)
Log.d(TAG, "recreating the shooter")
Handler(mainLooper).post {
shooter!!.stopCapture()
shooter = null
shooter = ScreenShooter(network!!, applicationContext)
shooter!!.startCapture(resultCode, resultData!!)
}
}
}
However I supposed that this method would drop the previous created object out of GC Root and all its memory.
I've already no forces to search the leak. Thank you in advance.