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:
wdvipa
2026-02-15 14:57:38 +08:00
parent 0c516f7307
commit 410219f382
3 changed files with 183 additions and 152 deletions

View File

@@ -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-recycleBitmap已被其他线程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
}