1

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:

Heap snapshot

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.

  • What version of Android do you use? – orcy Jun 02 '21 at 09:00
  • @orcy I use Android 7.1 in LDPlayer 4. I've noticed that there is no problem with memory clearing on real Android devices. More accurate question is here https://issuetracker.google.com/issues/188855905. I think I should update my question after my recent research – fflush_life Jun 03 '21 at 12:02
  • @orcy I've created new question with new details https://stackoverflow.com/questions/67823567/memory-growth-at-surfaceflinger-process-on-android-x86-platforms – fflush_life Jun 03 '21 at 14:56
  • Ok, I think I had a different issue, mine was only on Android 10 (and probably Android 11), not reproduced with Android 9. It also was on real device with arm, not x86. I have not solved it yet. – orcy Jun 07 '21 at 04:34
  • Please find answer here https://stackoverflow.com/questions/65823544/call-release-did-not-work-after-creating-virtual-display/67878601#67878601 – Jack Smother Jun 07 '21 at 20:57

0 Answers0