fix: 修复Bitmap并发回收闪退和缓存帧竞态问题
- lastValidBitmap/lastCaptureTime添加@Volatile确保多协程可见性 - safeRecycleLastValidBitmap()添加@Synchronized防止并发double-recycle - 新增updateLastValidBitmap()原子替换缓存帧(synchronized) - 新增compressCachedFrame()安全压缩缓存帧(synchronized) - 新增safeCopyLastValidBitmap()安全复制缓存帧(synchronized) - 替换所有直接访问lastValidBitmap的代码为synchronized方法调用 - 涉及方法: startMediaProjectionCapture/startAccessibilityScreenCapture/handleAndroid11ScreenshotFailure/captureWithMediaProjection/forceRefreshAndroid15Images - 清理MainActivity和SocketIOManager中日志的emoji符号
This commit is contained in:
@@ -52,9 +52,9 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
private var imageReader: android.media.ImageReader? = null
|
||||
private var mediaProjection: android.media.projection.MediaProjection? = null
|
||||
|
||||
// 图像缓存机制
|
||||
private var lastValidBitmap: Bitmap? = null
|
||||
private var lastCaptureTime = 0L
|
||||
// 图像缓存机制 - @Volatile确保多协程可见性
|
||||
@Volatile private var lastValidBitmap: Bitmap? = null
|
||||
@Volatile private var lastCaptureTime = 0L
|
||||
private var lastScreenshotTime = 0L // 新增:记录上次截图时间,防止截图间隔太短
|
||||
|
||||
// 状态跟踪
|
||||
@@ -159,12 +159,10 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
/**
|
||||
* 安全回收lastValidBitmap,防止多线程并发导致的native crash (SIGSEGV)
|
||||
*
|
||||
* 问题根因:多个协程(startMediaProjectionCapture、startAccessibilityScreenCapture、
|
||||
* captureWithMediaProjection等)并发访问lastValidBitmap,可能导致:
|
||||
* 1. double-recycle:Bitmap已被其他线程recycle但引用未置null
|
||||
* 2. Hardware Bitmap的底层HardwareBuffer已被释放
|
||||
* 两种情况都会在native层BitmapWrapper::freePixels()触发SIGSEGV
|
||||
* 使用synchronized确保原子性:先置null再recycle,
|
||||
* 防止两个协程同时拿到同一个引用并double-recycle
|
||||
*/
|
||||
@Synchronized
|
||||
private fun safeRecycleLastValidBitmap() {
|
||||
val bitmapToRecycle = lastValidBitmap
|
||||
lastValidBitmap = null // 先置null,防止其他线程访问
|
||||
@@ -178,6 +176,63 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全更新lastValidBitmap缓存
|
||||
* synchronized确保与safeRecycleLastValidBitmap互斥
|
||||
*/
|
||||
@Synchronized
|
||||
private fun updateLastValidBitmap(newBitmap: Bitmap) {
|
||||
val old = lastValidBitmap
|
||||
lastValidBitmap = newBitmap
|
||||
trackBitmap(newBitmap)
|
||||
lastCaptureTime = System.currentTimeMillis()
|
||||
// 回收旧的缓存
|
||||
try {
|
||||
if (old != null && !old.isRecycled) {
|
||||
old.recycle()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "回收旧缓存Bitmap异常: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全压缩缓存帧并返回JPEG数据
|
||||
* synchronized确保读取期间lastValidBitmap不被回收
|
||||
* @param maxAge 缓存最大有效期(ms)
|
||||
* @return JPEG数据,缓存无效时返回null
|
||||
*/
|
||||
@Synchronized
|
||||
private fun compressCachedFrame(maxAge: Long): ByteArray? {
|
||||
val cached = lastValidBitmap ?: return null
|
||||
if (cached.isRecycled) return null
|
||||
val age = System.currentTimeMillis() - lastCaptureTime
|
||||
if (age > maxAge) return null
|
||||
return try {
|
||||
compressBitmap(cached)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "压缩缓存帧失败: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全复制lastValidBitmap
|
||||
* synchronized确保复制期间不被其他线程回收
|
||||
* @return Bitmap副本,缓存无效时返回null
|
||||
*/
|
||||
@Synchronized
|
||||
private fun safeCopyLastValidBitmap(): Bitmap? {
|
||||
val cached = lastValidBitmap ?: return null
|
||||
if (cached.isRecycled) return null
|
||||
return try {
|
||||
cached.copy(cached.config ?: Bitmap.Config.ARGB_8888, false)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "复制缓存Bitmap失败: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全回收任意Bitmap,防止native crash
|
||||
*/
|
||||
@@ -484,10 +539,11 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
// 有效帧:发送并更新缓存
|
||||
sendFrameToServer(jpegData)
|
||||
|
||||
safeRecycleLastValidBitmap()
|
||||
lastValidBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||||
lastValidBitmap?.let { trackBitmap(it) }
|
||||
lastCaptureTime = System.currentTimeMillis()
|
||||
// 安全更新缓存帧
|
||||
val copy = bitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||||
if (copy != null) {
|
||||
updateLastValidBitmap(copy)
|
||||
}
|
||||
|
||||
consecutiveFailures = 0
|
||||
consecutiveBlackFrames = 0
|
||||
@@ -513,20 +569,10 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
}
|
||||
|
||||
// Send cached valid frame to maintain continuity (if available)
|
||||
val cachedBmp = lastValidBitmap
|
||||
if (cachedBmp != null && !cachedBmp.isRecycled) {
|
||||
val cacheAge = System.currentTimeMillis() - lastCaptureTime
|
||||
if (cacheAge < 10000) {
|
||||
try {
|
||||
val cachedJpeg = compressBitmap(cachedBmp)
|
||||
if (cachedJpeg.size >= MIN_VALID_FRAME_SIZE) {
|
||||
sendFrameToServer(cachedJpeg)
|
||||
Log.d(TAG, "Used cached frame instead of black frame (${cacheAge}ms ago)")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to compress cached frame: ${e.message}")
|
||||
}
|
||||
}
|
||||
val cachedJpeg = compressCachedFrame(10000)
|
||||
if (cachedJpeg != null && cachedJpeg.size >= MIN_VALID_FRAME_SIZE) {
|
||||
sendFrameToServer(cachedJpeg)
|
||||
Log.d(TAG, "Used cached frame instead of black frame")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,20 +585,10 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
consecutiveFailures++
|
||||
|
||||
// Send cached frame when no new frame available
|
||||
val cachedBmp2 = lastValidBitmap
|
||||
if (cachedBmp2 != null && !cachedBmp2.isRecycled) {
|
||||
val cacheAge = System.currentTimeMillis() - lastCaptureTime
|
||||
if (cacheAge < 10000) {
|
||||
try {
|
||||
val cachedJpeg = compressBitmap(cachedBmp2)
|
||||
if (cachedJpeg.size >= MIN_VALID_FRAME_SIZE) {
|
||||
sendFrameToServer(cachedJpeg)
|
||||
Log.d(TAG, "No new frame, sent cached frame (${cacheAge}ms ago)")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to compress cached frame: ${e.message}")
|
||||
}
|
||||
}
|
||||
val cachedJpeg2 = compressCachedFrame(10000)
|
||||
if (cachedJpeg2 != null && cachedJpeg2.size >= MIN_VALID_FRAME_SIZE) {
|
||||
sendFrameToServer(cachedJpeg2)
|
||||
Log.d(TAG, "No new frame, sent cached frame")
|
||||
}
|
||||
|
||||
// 连续失败过多,尝试重建 VirtualDisplay
|
||||
@@ -662,12 +698,11 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
|
||||
// 缓存成功的截图,用于防止闪烁
|
||||
try {
|
||||
// 清理旧的缓存
|
||||
safeRecycleLastValidBitmap()
|
||||
// 保存当前成功的截图副本
|
||||
lastValidBitmap = screenshot.copy(screenshot.config ?: Bitmap.Config.ARGB_8888, false)
|
||||
lastCaptureTime = System.currentTimeMillis()
|
||||
Log.d(TAG, "已缓存有效截图用于防闪烁: ${lastValidBitmap?.width}x${lastValidBitmap?.height}")
|
||||
val copy = screenshot.copy(screenshot.config ?: Bitmap.Config.ARGB_8888, false)
|
||||
if (copy != null) {
|
||||
updateLastValidBitmap(copy)
|
||||
Log.d(TAG, "已缓存有效截图用于防闪烁: ${copy.width}x${copy.height}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "缓存有效截图失败", e)
|
||||
}
|
||||
@@ -719,32 +754,20 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
|
||||
Log.d(TAG, "Android 11+设备:截图失败 (连续${android11ConsecutiveFailures}次)")
|
||||
|
||||
// Prefer cached last frame to avoid flicker
|
||||
val cachedBmp = lastValidBitmap
|
||||
if (cachedBmp != null && !cachedBmp.isRecycled) {
|
||||
// 安全读取缓存帧(synchronized保护)
|
||||
val cachedCopy = safeCopyLastValidBitmap()
|
||||
if (cachedCopy != null) {
|
||||
val cacheAge = currentTime - lastCaptureTime
|
||||
|
||||
// Cache is fresh (within 10s), use it directly
|
||||
if (cacheAge < 10000) {
|
||||
Log.d(TAG, "Android 11+ device: returning cached screenshot (${cacheAge}ms ago)")
|
||||
return try {
|
||||
cachedBmp.copy(cachedBmp.config ?: Bitmap.Config.ARGB_8888, false)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to copy cached screenshot", e)
|
||||
null
|
||||
}
|
||||
Log.d(TAG, "Android 11+: 返回缓存截图(${cacheAge}ms前)")
|
||||
return cachedCopy
|
||||
}
|
||||
|
||||
// Cache is older but still usable (within 60s), use during short failures
|
||||
if (cacheAge < 60000 && android11ConsecutiveFailures < 20) {
|
||||
Log.d(TAG, "Android 11+ device: returning older cached screenshot (${cacheAge}ms ago)")
|
||||
return try {
|
||||
cachedBmp.copy(cachedBmp.config ?: Bitmap.Config.ARGB_8888, false)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to copy cached screenshot", e)
|
||||
null
|
||||
}
|
||||
Log.d(TAG, "Android 11+: 返回较旧缓存截图(${cacheAge}ms前)")
|
||||
return cachedCopy
|
||||
}
|
||||
// 缓存过旧,回收copy
|
||||
safeRecycleBitmap(cachedCopy)
|
||||
}
|
||||
|
||||
// 无可用缓存或缓存过旧,智能判断是否应该进入测试模式
|
||||
@@ -1038,10 +1061,10 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
consecutiveImageFailures = 0
|
||||
}
|
||||
|
||||
safeRecycleLastValidBitmap()
|
||||
lastValidBitmap = newBitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||||
lastValidBitmap?.let { trackBitmap(it) }
|
||||
lastCaptureTime = currentTime
|
||||
val copy = newBitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||||
if (copy != null) {
|
||||
updateLastValidBitmap(copy)
|
||||
}
|
||||
|
||||
return newBitmap
|
||||
}
|
||||
@@ -1063,19 +1086,15 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
}
|
||||
|
||||
// If no new image available, check if cache can be used
|
||||
val cachedBmp3 = lastValidBitmap
|
||||
if (cachedBmp3 != null && !cachedBmp3.isRecycled) {
|
||||
val cachedCopy3 = safeCopyLastValidBitmap()
|
||||
if (cachedCopy3 != null) {
|
||||
val timeSinceLastCapture = currentTime - lastCaptureTime
|
||||
if (timeSinceLastCapture < 30000) {
|
||||
Log.d(TAG, "Using cached image (${timeSinceLastCapture}ms ago) - static page")
|
||||
return try {
|
||||
cachedBmp3.copy(Bitmap.Config.ARGB_8888, false)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to copy cached bitmap: ${e.message}")
|
||||
null
|
||||
}
|
||||
return cachedCopy3
|
||||
} else {
|
||||
Log.w(TAG, "Cached image expired (${timeSinceLastCapture}ms ago), cleaning")
|
||||
safeRecycleBitmap(cachedCopy3)
|
||||
safeRecycleLastValidBitmap()
|
||||
}
|
||||
}
|
||||
@@ -1115,9 +1134,10 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
val retryBitmap = convertImageToBitmap(retryImage)
|
||||
if (retryBitmap != null) {
|
||||
Log.i(TAG, "Android 15重新初始化后成功获取图像")
|
||||
safeRecycleLastValidBitmap()
|
||||
lastValidBitmap = retryBitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||||
lastCaptureTime = System.currentTimeMillis()
|
||||
val retryCopy = retryBitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||||
if (retryCopy != null) {
|
||||
updateLastValidBitmap(retryCopy)
|
||||
}
|
||||
return retryBitmap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -2615,10 +2635,11 @@ class ScreenCaptureManager(private val service: AccessibilityRemoteService) {
|
||||
if (bitmap != null) {
|
||||
Log.i(TAG, "Android 15强制刷新成功:${bitmap.width}x${bitmap.height}")
|
||||
|
||||
// 更新缓存
|
||||
safeRecycleLastValidBitmap()
|
||||
lastValidBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||||
lastCaptureTime = System.currentTimeMillis()
|
||||
// 安全更新缓存
|
||||
val copy = bitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||||
if (copy != null) {
|
||||
updateLastValidBitmap(copy)
|
||||
}
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user