Bitmap 复用
当前的 generateThumbnail 方法。每次调用都会创建一个新的 Bitmap 和 Canvas,这确实会造成频繁的内存分配。让我实现 Bitmap 和 Canvas 的复用机制:
Kotlin
// 修改前+++++++++++++++++++++++++++++++++++++++++++
/**
* 生成缩略图
*/
private fun generateThumbnail() {
try {
// 创建缩略图大小的 Bitmap
val thumbnailWidth = 120
val thumbnailHeight = 80
val bitmap = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
// // 设置白色背景
// canvas.drawColor(Color.WHITE)
// 计算缩放比例
val scaleX = thumbnailWidth.toFloat() / width
val scaleY = thumbnailHeight.toFloat() / height
val scale = minOf(scaleX, scaleY)
// 计算居中偏移
val offsetX = (thumbnailWidth - width * scale) / 2
val offsetY = (thumbnailHeight - height * scale) / 2
// 应用变换
canvas.save()
canvas.translate(offsetX, offsetY)
canvas.scale(scale, scale)
// 应用当前视图的变换(平移和缩放)
val cx = width / 2f
val cy = height / 2f
canvas.translate(this.offsetX, this.offsetY)
canvas.scale(scaleFactor, scaleFactor, cx, cy)
// 直接绘制矩形,避免调用 draw()
for (rect in rects) {
paint.color = rect.color
val left = rect.cx - rect.width / 2
val top = rect.cy - rect.height / 2
val right = rect.cx + rect.width / 2
val bottom = rect.cy + rect.height / 2
canvas.drawRect(left, top, right, bottom, paint)
}
canvas.restore()
thumbnailCallback?.onThumbnailUpdated(bitmap)
} catch (e: Exception) {
Log.e(TAG, "Error generating thumbnail", e)
}
}
// 修改后+++++++++++++++++++++++++++++++++++++++++++
// Bitmap 复用相关变量
private var thumbnailBitmap: Bitmap? = null
private var thumbnailCanvas: Canvas? = null
private var lastThumbnailWidth = 0
private var lastThumbnailHeight = 0
/**
* 生成缩略图
*/
private fun generateThumbnail() {
try {
val thumbnailWidth = 120
val thumbnailHeight = 80
// 检查是否需要创建新的 Bitmap
if (thumbnailBitmap == null ||
lastThumbnailWidth != thumbnailWidth ||
lastThumbnailHeight != thumbnailHeight) {
// 释放旧的 Bitmap
thumbnailBitmap?.recycle()
// 创建新的 Bitmap 和 Canvas
thumbnailBitmap = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, Bitmap.Config.ARGB_8888)
thumbnailCanvas = Canvas(thumbnailBitmap!!)
lastThumbnailWidth = thumbnailWidth
lastThumbnailHeight = thumbnailHeight
Log.d(TAG, "Created new thumbnail bitmap: ${thumbnailWidth}x${thumbnailHeight}")
}
val bitmap = thumbnailBitmap!!
val canvas = thumbnailCanvas!!
// 清空画布
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
// 计算缩放比例
val scaleX = thumbnailWidth.toFloat() / width
val scaleY = thumbnailHeight.toFloat() / height
val scale = minOf(scaleX, scaleY)
// 计算居中偏移
val offsetX = (thumbnailWidth - width * scale) / 2
val offsetY = (thumbnailHeight - height * scale) / 2
// 应用变换
canvas.save()
canvas.translate(offsetX, offsetY)
canvas.scale(scale, scale)
// 应用当前视图的变换(平移和缩放)
val cx = width / 2f
val cy = height / 2f
canvas.translate(this.offsetX, this.offsetY)
canvas.scale(scaleFactor, scaleFactor, cx, cy)
// 直接绘制矩形,避免调用 draw()
for (rect in rects) {
paint.color = rect.color
val left = rect.cx - rect.width / 2
val top = rect.cy - rect.height / 2
val right = rect.cx + rect.width / 2
val bottom = rect.cy + rect.height / 2
canvas.drawRect(left, top, right, bottom, paint)
}
canvas.restore()
thumbnailCallback?.onThumbnailUpdated(bitmap)
} catch (e: Exception) {
Log.e(TAG, "Error generating thumbnail", e)
}
}
添加在 View 销毁时释放 Bitmap 的代码:
Kotlin
//修改前+++++++++++++++++++++
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
Log.d(TAG, "View detached from window")
rects.clear() // 清空矩形数据
}
//修改后+++++++++++++++++++++
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
Log.d(TAG, "View detached from window")
rects.clear() // 清空矩形数据
// 释放 Bitmap 资源
thumbnailBitmap?.recycle()
thumbnailBitmap = null
thumbnailCanvas = null
Log.d(TAG, "Released thumbnail bitmap resources")
}
Bitmap 复用优化总结
问题分析
之前的 generateThumbnail 方法每次调用都会:
- 创建新的 Bitmap.createBitmap(120, 80, Bitmap.Config.ARGB_8888)
- 创建新的 Canvas(bitmap)
- 这导致频繁的内存分配和垃圾回收
优化方案
我们实现了 Bitmap 和 Canvas 的复用机制:
添加复用变量:
-
private var thumbnailBitmap: Bitmap? = null
private var thumbnailCanvas: Canvas? = null
private var lastThumbnailWidth = 0
private var lastThumbnailHeight = 0
智能创建 Bitmap:
- 只在首次调用或尺寸变化时创建新的 Bitmap
- 复用现有的 Bitmap 和 Canvas
- 使用 canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) 清空画布
资源管理:
- 在 onDetachedFromWindow() 中调用 bitmap.recycle() 释放内存
- 避免内存泄漏
性能提升
- 减少内存分配:从每次创建新 Bitmap 改为复用
- 减少垃圾回收压力:避免频繁的 Bitmap 对象创建和销毁
- 提高响应速度:减少内存分配时间
内容变化检测
容变化检测来避免不必要的缩略图生成。让我实现一个简单的哈希机制来检测内容是否真的发生了变化:
Kotlin
// 内容变化检测
private var lastContentHash = 0
private var lastOffsetX = 0f
private var lastOffsetY = 0f
private var lastScaleFactor = 1.0f
/**
* 计算内容哈希值
*/
private fun calculateContentHash(): Int {
var hash = rects.size
for (rect in rects) {
hash = hash * 31 + rect.color
hash = hash * 31 + rect.cx.toInt()
hash = hash * 31 + rect.cy.toInt()
hash = hash * 31 + rect.width.toInt()
hash = hash * 31 + rect.height.toInt()
}
return hash
}
/**
* 检查内容是否发生变化
*/
private fun hasContentChanged(): Boolean {
val currentHash = calculateContentHash()
val hasRectChanges = currentHash != lastContentHash
val hasTransformChanges = offsetX != lastOffsetX ||
offsetY != lastOffsetY ||
scaleFactor != lastScaleFactor
if (hasRectChanges || hasTransformChanges) {
lastContentHash = currentHash
lastOffsetX = offsetX
lastOffsetY = offsetY
lastScaleFactor = scaleFactor
return true
}
return false
}
/**
* 生成缩略图
*/
private fun generateThumbnail(){
// 检查内容是否发生变化
if (!hasContentChanged()) {
return
}
}
内容变化检测优化 ✅
- 问题:即使内容没有变化也会生成缩略图
- 解决:添加哈希检测机制,检查矩形内容和视图变换是否变化
- 效果:只有在内容真正变化时才生成缩略图
检测机制
- 矩形内容哈希:基于矩形数量、颜色、位置、尺寸计算哈希值
- 视图变换检测:检查 offsetX、offsetY、scaleFactor 是否变化
- 智能跳过:如果内容没有变化,直接返回,不执行缩略图生成
防抖调度策略
首先添加防抖相关的变量:
Kotlin
// 内容变化检测
private var lastContentHash = 0
private var lastOffsetX = 0f
private var lastOffsetY = 0f
private var lastScaleFactor = 1.0f
->
// 内容变化检测
private var lastContentHash = 0
private var lastOffsetX = 0f
private var lastOffsetY = 0f
private var lastScaleFactor = 1.0f
// 防抖相关变量
private var thumbnailRunnable: Runnable? = null
private var lastThumbnailTime = 0L
private val THUMBNAIL_DEBOUNCE_DELAY = 200L // 200ms 防抖延迟
private var isUserInteracting = false // 用户是否正在交互
防抖调度方法:
Kotlin
/**
* 检查内容是否发生变化
*/
private fun hasContentChanged(): Boolean {
val currentHash = calculateContentHash()
val hasRectChanges = currentHash != lastContentHash
val hasTransformChanges = offsetX != lastOffsetX ||
offsetY != lastOffsetY ||
scaleFactor != lastScaleFactor
if (hasRectChanges || hasTransformChanges) {
Log.d(TAG, "Content changed - rects: $hasRectChanges, transform: $hasTransformChanges")
Log.d(TAG, "Transform: offsetX=${offsetX}, offsetY=${offsetY}, scale=${scaleFactor}")
lastContentHash = currentHash
lastOffsetX = offsetX
lastOffsetY = offsetY
lastScaleFactor = scaleFactor
return true
} else {
Log.d(TAG, "Content unchanged, skipping thumbnail generation")
}
return false
}
->
/**
* 检查内容是否发生变化
*/
private fun hasContentChanged(): Boolean {
val currentHash = calculateContentHash()
val hasRectChanges = currentHash != lastContentHash
val hasTransformChanges = offsetX != lastOffsetX ||
offsetY != lastOffsetY ||
scaleFactor != lastScaleFactor
if (hasRectChanges || hasTransformChanges) {
Log.d(TAG, "Content changed - rects: $hasRectChanges, transform: $hasTransformChanges")
Log.d(TAG, "Transform: offsetX=${offsetX}, offsetY=${offsetY}, scale=${scaleFactor}")
lastContentHash = currentHash
lastOffsetX = offsetX
lastOffsetY = offsetY
lastScaleFactor = scaleFactor
return true
} else {
Log.d(TAG, "Content unchanged, skipping thumbnail generation")
}
return false
}
/**
* 防抖调度缩略图生成
*/
private fun scheduleThumbnailGeneration(immediate: Boolean = false) {
val currentTime = System.currentTimeMillis()
// 如果距离上次生成时间太短,且不是立即生成,则使用延迟
if (!immediate && currentTime - lastThumbnailTime < THUMBNAIL_DEBOUNCE_DELAY) {
// 取消之前的任务
thumbnailRunnable?.let {
removeCallbacks(it)
Log.d(TAG, "Cancelled previous thumbnail task")
}
// 重新调度延迟任务
thumbnailRunnable = Runnable {
Log.d(TAG, "Executing delayed thumbnail generation")
generateThumbnail()
thumbnailRunnable = null
}
postDelayed(thumbnailRunnable!!, THUMBNAIL_DEBOUNCE_DELAY)
Log.d(TAG, "Scheduled delayed thumbnail generation in ${THUMBNAIL_DEBOUNCE_DELAY}ms")
return
}
// 立即生成或时间间隔足够
Log.d(TAG, "Executing immediate thumbnail generation")
generateThumbnail()
lastThumbnailTime = currentTime
}
修改 dispatchDraw 方法,使用防抖调度:
Kotlin
// 调用父类的 dispatchDraw 来绘制子视图
super.dispatchDraw(canvas)
// 缩略图回调
post { generateThumbnail() }
->
// 调用父类的 dispatchDraw 来绘制子视图
super.dispatchDraw(canvas)
// 使用防抖调度缩略图生成
scheduleThumbnailGeneration()
现在需要找到触摸事件处理方法,添加操作结束检测。让我先查看触摸事件处理:
Kotlin
override fun onTouchEvent(event: MotionEvent): Boolean {
scaleDetector.onTouchEvent(event)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
lastTouchX = event.x
lastTouchY = event.y
isDragging = true
isScaling = false
// 判断是否点中某个矩形(优先最上层)
draggingRect = findTouchedRect(event.x, event.y)
if (draggingRect != null) {
// 计算手指与矩形中心的偏移
val (rx, ry) = viewToRectSpace(event.x, event.y)
dragRectOffsetX = rx - draggingRect!!.cx
dragRectOffsetY = ry - draggingRect!!.cy
}
}
MotionEvent.ACTION_POINTER_DOWN -> {
isScaling = true
isDragging = false
draggingRect = null
}
MotionEvent.ACTION_MOVE -> {
if (!isScaling && isDragging && event.pointerCount == 1) {
if (draggingRect != null) {
// 拖动矩形
val (rx, ry) = viewToRectSpace(event.x, event.y)
draggingRect!!.cx = rx - dragRectOffsetX
draggingRect!!.cy = ry - dragRectOffsetY
requestLayout()
invalidate()
} else {
// 整体平移
val dx = event.x - lastTouchX
val dy = event.y - lastTouchY
offsetX += dx
offsetY += dy
lastTouchX = event.x
lastTouchY = event.y
requestLayout()
invalidate()
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
isDragging = false
isScaling = false
draggingRect = null
}
}
return true
}
->
override fun onTouchEvent(event: MotionEvent): Boolean {
scaleDetector.onTouchEvent(event)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
lastTouchX = event.x
lastTouchY = event.y
isDragging = true
isScaling = false
isUserInteracting = true
// 取消之前的延迟任务
thumbnailRunnable?.let {
removeCallbacks(it)
Log.d(TAG, "Cancelled thumbnail task on touch down")
}
// 判断是否点中某个矩形(优先最上层)
draggingRect = findTouchedRect(event.x, event.y)
if (draggingRect != null) {
// 计算手指与矩形中心的偏移
val (rx, ry) = viewToRectSpace(event.x, event.y)
dragRectOffsetX = rx - draggingRect!!.cx
dragRectOffsetY = ry - draggingRect!!.cy
}
}
MotionEvent.ACTION_POINTER_DOWN -> {
isScaling = true
isDragging = false
isUserInteracting = true
draggingRect = null
// 取消之前的延迟任务
thumbnailRunnable?.let {
removeCallbacks(it)
Log.d(TAG, "Cancelled thumbnail task on pointer down")
}
}
MotionEvent.ACTION_MOVE -> {
if (!isScaling && isDragging && event.pointerCount == 1) {
if (draggingRect != null) {
// 拖动矩形
val (rx, ry) = viewToRectSpace(event.x, event.y)
draggingRect!!.cx = rx - dragRectOffsetX
draggingRect!!.cy = ry - dragRectOffsetY
requestLayout()
invalidate()
} else {
// 整体平移
val dx = event.x - lastTouchX
val dy = event.y - lastTouchY
offsetX += dx
offsetY += dy
lastTouchX = event.x
lastTouchY = event.y
requestLayout()
invalidate()
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
isDragging = false
isScaling = false
isUserInteracting = false
draggingRect = null
// 操作结束,立即生成缩略图
Log.d(TAG, "Touch operation ended, generating thumbnail immediately")
scheduleThumbnailGeneration(immediate = true)
}
}
return true
}
现在需要修改缩放监听器,也添加操作结束检测:
Kotlin
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
scaleFactor *= detector.scaleFactor
scaleFactor = scaleFactor.coerceIn(0.2f, 5.0f)
requestLayout()
invalidate()
return true
}
}
->
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
isUserInteracting = true
// 取消之前的延迟任务
thumbnailRunnable?.let {
removeCallbacks(it)
Log.d(TAG, "Cancelled thumbnail task on scale begin")
}
return true
}
override fun onScale(detector: ScaleGestureDetector): Boolean {
scaleFactor *= detector.scaleFactor
scaleFactor = scaleFactor.coerceIn(0.2f, 5.0f)
requestLayout()
invalidate()
return true
}
override fun onScaleEnd(detector: ScaleGestureDetector) {
isUserInteracting = false
// 缩放结束,立即生成缩略图
Log.d(TAG, "Scale operation ended, generating thumbnail immediately")
scheduleThumbnailGeneration(immediate = true)
}
}
最后,在 View 销毁时清理防抖任务:
Kotlin
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
Log.d(TAG, "View detached from window")
rects.clear() // 清空矩形数据
// 释放 Bitmap 资源
thumbnailBitmap?.recycle()
thumbnailBitmap = null
thumbnailCanvas = null
Log.d(TAG, "Released thumbnail bitmap resources")
}
->
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
Log.d(TAG, "View detached from window")
rects.clear() // 清空矩形数据
// 取消防抖任务
thumbnailRunnable?.let {
removeCallbacks(it)
Log.d(TAG, "Cancelled thumbnail task on detach")
}
thumbnailRunnable = null
// 释放 Bitmap 资源
thumbnailBitmap?.recycle()
thumbnailBitmap = null
thumbnailCanvas = null
Log.d(TAG, "Released thumbnail bitmap resources")
}
防抖机制实现总结
核心变量
Apply to DeepLearnVie...
private var thumbnailRunnable: Runnable? = null
private var lastThumbnailTime = 0L
private val THUMBNAIL_DEBOUNCE_DELAY = 200L // 200ms 防抖延迟
private var isUserInteracting = false // 用户是否正在交互
防抖调度策略
- 时间间隔检查:如果距离上次生成时间小于 200ms,使用延迟
- 取消机制:新操作开始时取消之前的延迟任务
- 立即生成:操作结束后立即生成缩略图
操作结束检测
- 触摸操作:ACTION_UP 和 ACTION_CANCEL 时立即生成
- 缩放操作:onScaleEnd 时立即生成
- 操作开始:ACTION_DOWN、ACTION_POINTER_DOWN、onScaleBegin 时取消延迟任务
性能提升
- 减少频率:从每秒几十次降到每秒几次
- 保持响应性:操作结束后立即更新
- 避免卡顿:减少主线程负担
- 用户体验:缩略图更新更平滑
清理机制
- View 销毁:取消所有延迟任务
- 内存管理:释放 Bitmap 资源
现在你可以测试一下,观察日志来确认防抖机制是否正常工作:
- 拖动画布,应该看到频繁的取消和重新调度日志
- 缩放画布,应该看到类似的防抖效果
- 操作结束后,应该立即生成缩略图
这个实现应该能显著减少不必要的缩略图生成,提高应用性能!
结束!!!