Android View 绘制流程 优化 (Bitmap 复用+内容变化检测+防抖调度策略)

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 方法每次调用都会:

  1. 创建新的 Bitmap.createBitmap(120, 80, Bitmap.Config.ARGB_8888)
  1. 创建新的 Canvas(bitmap)
  1. 这导致频繁的内存分配和垃圾回收

优化方案

我们实现了 Bitmap 和 Canvas 的复用机制:

添加复用变量:
  1. 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
        }
    }

内容变化检测优化 ✅

  • 问题:即使内容没有变化也会生成缩略图
  • 解决:添加哈希检测机制,检查矩形内容和视图变换是否变化
  • 效果:只有在内容真正变化时才生成缩略图

检测机制

  1. 矩形内容哈希:基于矩形数量、颜色、位置、尺寸计算哈希值
  2. 视图变换检测:检查 offsetX、offsetY、scaleFactor 是否变化
  3. 智能跳过:如果内容没有变化,直接返回,不执行缩略图生成

防抖调度策略

首先添加防抖相关的变量:

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 资源

现在你可以测试一下,观察日志来确认防抖机制是否正常工作:

  1. 拖动画布,应该看到频繁的取消和重新调度日志
  1. 缩放画布,应该看到类似的防抖效果
  1. 操作结束后,应该立即生成缩略图

这个实现应该能显著减少不必要的缩略图生成,提高应用性能!

结束!!!