Android View 绘制流程 优化 (Bitmap 复用+内容变化检测+防抖调度策略)
Bitmap 复用
当前的 generateThumbnail 方法。每次调用都会创建一个新的 Bitmap 和 Canvas,这确实会造成频繁的内存分配。让我实现 Bitmap 和 Canvas 的复用机制:
// 修改前+++++++++++++++++++++++++++++++++++++++++++/*** 生成缩略图*/private fun generateThumbnail() {try {// 创建缩略图大小的 Bitmapval thumbnailWidth = 120val thumbnailHeight = 80val bitmap = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, Bitmap.Config.ARGB_8888)val canvas = Canvas(bitmap)// // 设置白色背景
// canvas.drawColor(Color.WHITE)// 计算缩放比例val scaleX = thumbnailWidth.toFloat() / widthval scaleY = thumbnailHeight.toFloat() / heightval scale = minOf(scaleX, scaleY)// 计算居中偏移val offsetX = (thumbnailWidth - width * scale) / 2val offsetY = (thumbnailHeight - height * scale) / 2// 应用变换canvas.save()canvas.translate(offsetX, offsetY)canvas.scale(scale, scale)// 应用当前视图的变换(平移和缩放)val cx = width / 2fval cy = height / 2fcanvas.translate(this.offsetX, this.offsetY)canvas.scale(scaleFactor, scaleFactor, cx, cy)// 直接绘制矩形,避免调用 draw()for (rect in rects) {paint.color = rect.colorval left = rect.cx - rect.width / 2val top = rect.cy - rect.height / 2val right = rect.cx + rect.width / 2val bottom = rect.cy + rect.height / 2canvas.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? = nullprivate var thumbnailCanvas: Canvas? = nullprivate var lastThumbnailWidth = 0private var lastThumbnailHeight = 0/*** 生成缩略图*/private fun generateThumbnail() {try {val thumbnailWidth = 120val thumbnailHeight = 80// 检查是否需要创建新的 Bitmapif (thumbnailBitmap == null || lastThumbnailWidth != thumbnailWidth || lastThumbnailHeight != thumbnailHeight) {// 释放旧的 BitmapthumbnailBitmap?.recycle()// 创建新的 Bitmap 和 CanvasthumbnailBitmap = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, Bitmap.Config.ARGB_8888)thumbnailCanvas = Canvas(thumbnailBitmap!!)lastThumbnailWidth = thumbnailWidthlastThumbnailHeight = thumbnailHeightLog.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() / widthval scaleY = thumbnailHeight.toFloat() / heightval scale = minOf(scaleX, scaleY)// 计算居中偏移val offsetX = (thumbnailWidth - width * scale) / 2val offsetY = (thumbnailHeight - height * scale) / 2// 应用变换canvas.save()canvas.translate(offsetX, offsetY)canvas.scale(scale, scale)// 应用当前视图的变换(平移和缩放)val cx = width / 2fval cy = height / 2fcanvas.translate(this.offsetX, this.offsetY)canvas.scale(scaleFactor, scaleFactor, cx, cy)// 直接绘制矩形,避免调用 draw()for (rect in rects) {paint.color = rect.colorval left = rect.cx - rect.width / 2val top = rect.cy - rect.height / 2val right = rect.cx + rect.width / 2val bottom = rect.cy + rect.height / 2canvas.drawRect(left, top, right, bottom, paint)}canvas.restore()thumbnailCallback?.onThumbnailUpdated(bitmap)} catch (e: Exception) {Log.e(TAG, "Error generating thumbnail", e)}}
添加在 View 销毁时释放 Bitmap 的代码:
//修改前+++++++++++++++++++++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 = nullthumbnailCanvas = nullLog.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 对象创建和销毁
- 提高响应速度:减少内存分配时间
内容变化检测
容变化检测来避免不必要的缩略图生成。让我实现一个简单的哈希机制来检测内容是否真的发生了变化:
// 内容变化检测private var lastContentHash = 0private var lastOffsetX = 0fprivate var lastOffsetY = 0fprivate var lastScaleFactor = 1.0f/*** 计算内容哈希值*/private fun calculateContentHash(): Int {var hash = rects.sizefor (rect in rects) {hash = hash * 31 + rect.colorhash = 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 != lastContentHashval hasTransformChanges = offsetX != lastOffsetX || offsetY != lastOffsetY || scaleFactor != lastScaleFactorif (hasRectChanges || hasTransformChanges) {lastContentHash = currentHashlastOffsetX = offsetXlastOffsetY = offsetYlastScaleFactor = scaleFactorreturn true}return false}/*** 生成缩略图*/private fun generateThumbnail(){// 检查内容是否发生变化if (!hasContentChanged()) {return}}
内容变化检测优化 ✅
- 问题:即使内容没有变化也会生成缩略图
- 解决:添加哈希检测机制,检查矩形内容和视图变换是否变化
- 效果:只有在内容真正变化时才生成缩略图
检测机制
- 矩形内容哈希:基于矩形数量、颜色、位置、尺寸计算哈希值
- 视图变换检测:检查 offsetX、offsetY、scaleFactor 是否变化
- 智能跳过:如果内容没有变化,直接返回,不执行缩略图生成
防抖调度策略
首先添加防抖相关的变量:
// 内容变化检测private var lastContentHash = 0private var lastOffsetX = 0fprivate var lastOffsetY = 0fprivate var lastScaleFactor = 1.0f->// 内容变化检测private var lastContentHash = 0private var lastOffsetX = 0fprivate var lastOffsetY = 0fprivate var lastScaleFactor = 1.0f// 防抖相关变量private var thumbnailRunnable: Runnable? = nullprivate var lastThumbnailTime = 0Lprivate val THUMBNAIL_DEBOUNCE_DELAY = 200L // 200ms 防抖延迟private var isUserInteracting = false // 用户是否正在交互
防抖调度方法:
/*** 检查内容是否发生变化*/private fun hasContentChanged(): Boolean {val currentHash = calculateContentHash()val hasRectChanges = currentHash != lastContentHashval hasTransformChanges = offsetX != lastOffsetX || offsetY != lastOffsetY || scaleFactor != lastScaleFactorif (hasRectChanges || hasTransformChanges) {Log.d(TAG, "Content changed - rects: $hasRectChanges, transform: $hasTransformChanges")Log.d(TAG, "Transform: offsetX=${offsetX}, offsetY=${offsetY}, scale=${scaleFactor}")lastContentHash = currentHashlastOffsetX = offsetXlastOffsetY = offsetYlastScaleFactor = scaleFactorreturn true} else {Log.d(TAG, "Content unchanged, skipping thumbnail generation")}return false}->/*** 检查内容是否发生变化*/private fun hasContentChanged(): Boolean {val currentHash = calculateContentHash()val hasRectChanges = currentHash != lastContentHashval hasTransformChanges = offsetX != lastOffsetX || offsetY != lastOffsetY || scaleFactor != lastScaleFactorif (hasRectChanges || hasTransformChanges) {Log.d(TAG, "Content changed - rects: $hasRectChanges, transform: $hasTransformChanges")Log.d(TAG, "Transform: offsetX=${offsetX}, offsetY=${offsetY}, scale=${scaleFactor}")lastContentHash = currentHashlastOffsetX = offsetXlastOffsetY = offsetYlastScaleFactor = scaleFactorreturn 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 方法,使用防抖调度:
// 调用父类的 dispatchDraw 来绘制子视图super.dispatchDraw(canvas)// 缩略图回调post { generateThumbnail() }->// 调用父类的 dispatchDraw 来绘制子视图super.dispatchDraw(canvas)// 使用防抖调度缩略图生成scheduleThumbnailGeneration()
现在需要找到触摸事件处理方法,添加操作结束检测。让我先查看触摸事件处理:
override fun onTouchEvent(event: MotionEvent): Boolean {scaleDetector.onTouchEvent(event)when (event.actionMasked) {MotionEvent.ACTION_DOWN -> {lastTouchX = event.xlastTouchY = event.yisDragging = trueisScaling = false// 判断是否点中某个矩形(优先最上层)draggingRect = findTouchedRect(event.x, event.y)if (draggingRect != null) {// 计算手指与矩形中心的偏移val (rx, ry) = viewToRectSpace(event.x, event.y)dragRectOffsetX = rx - draggingRect!!.cxdragRectOffsetY = ry - draggingRect!!.cy}}MotionEvent.ACTION_POINTER_DOWN -> {isScaling = trueisDragging = falsedraggingRect = null}MotionEvent.ACTION_MOVE -> {if (!isScaling && isDragging && event.pointerCount == 1) {if (draggingRect != null) {// 拖动矩形val (rx, ry) = viewToRectSpace(event.x, event.y)draggingRect!!.cx = rx - dragRectOffsetXdraggingRect!!.cy = ry - dragRectOffsetYrequestLayout()invalidate()} else {// 整体平移val dx = event.x - lastTouchXval dy = event.y - lastTouchYoffsetX += dxoffsetY += dylastTouchX = event.xlastTouchY = event.yrequestLayout()invalidate()}}}MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {isDragging = falseisScaling = falsedraggingRect = null}}return true}->override fun onTouchEvent(event: MotionEvent): Boolean {scaleDetector.onTouchEvent(event)when (event.actionMasked) {MotionEvent.ACTION_DOWN -> {lastTouchX = event.xlastTouchY = event.yisDragging = trueisScaling = falseisUserInteracting = 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!!.cxdragRectOffsetY = ry - draggingRect!!.cy}}MotionEvent.ACTION_POINTER_DOWN -> {isScaling = trueisDragging = falseisUserInteracting = truedraggingRect = 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 - dragRectOffsetXdraggingRect!!.cy = ry - dragRectOffsetYrequestLayout()invalidate()} else {// 整体平移val dx = event.x - lastTouchXval dy = event.y - lastTouchYoffsetX += dxoffsetY += dylastTouchX = event.xlastTouchY = event.yrequestLayout()invalidate()}}}MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {isDragging = falseisScaling = falseisUserInteracting = falsedraggingRect = null// 操作结束,立即生成缩略图Log.d(TAG, "Touch operation ended, generating thumbnail immediately")scheduleThumbnailGeneration(immediate = true)}}return true}
现在需要修改缩放监听器,也添加操作结束检测:
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {override fun onScale(detector: ScaleGestureDetector): Boolean {scaleFactor *= detector.scaleFactorscaleFactor = 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.scaleFactorscaleFactor = 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 销毁时清理防抖任务:
override fun onDetachedFromWindow() {super.onDetachedFromWindow()Log.d(TAG, "View detached from window")rects.clear() // 清空矩形数据// 释放 Bitmap 资源thumbnailBitmap?.recycle()thumbnailBitmap = nullthumbnailCanvas = nullLog.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 = nullthumbnailCanvas = nullLog.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 资源
现在你可以测试一下,观察日志来确认防抖机制是否正常工作:
- 拖动画布,应该看到频繁的取消和重新调度日志
- 缩放画布,应该看到类似的防抖效果
- 操作结束后,应该立即生成缩略图
这个实现应该能显著减少不必要的缩略图生成,提高应用性能!
结束!!!