在我们项目中,有一个使用率很高的功能,对于多张图片进行批量处理的功能。从23年开始到现在2年时间经历了多位研发的努力,越来越多的用户使用和离不开这个功能,但是必须要说的是,随着用户量的上升和移动设备性能的发展,越来越多的用户对于我们的这个功能不满意,特别是在加载速度方面。说句题外话,这也从侧面说明了一点,有时候核心需求的「精准命中」远胜技术完美,功能的价值最终由用户的「实际使用」而非「技术指标」来定义。
一、背景与问题现象
该功能场景是用户进入一个批量处理页面,系统会自动加载本地图片资源,并添加统一水印/处理图片。
大致流程如下
- 点击图片处理,开始加载所需的资料,例如时间、地址
- 循环图片集合,获取每一张图片的bitmap
- 通过第一步的资料生成需要打上去的水印/生成需要的图片处理操作
- 保存图片
- 切换下一张图片和下一个需要加载的水印/图片处理操作
- 结束时延迟通知媒体库刷新相册
在实际的功能中有以下的耗时点:
- Bitmap加载延迟触发:需要开始批量处理时才开始加载图片资源为 Bitmap,将耗时操作暴露给用户体验中
- 强行等待 ImageView 绘制完成:为了避免view没有完全加载导致最后生成的图片没有完全显示或者显示错乱,使用 delay(600) 和 postDelayed() 等非精准控制方式等待 ImageView 完全显示。
- 通知媒体库刷新采用固定延时:处理完图片后,使用 delay(500) 再进行媒体库更新,存在不确定性。
- 图片加载失败没有超时机制:导致流程可能卡在某一步。

二、为何会出现"强制等待 ImageView 刷新完成"的写法?
对于其他问题,其实都是很好理解的,唯独第二点我认为可能需要单独拿出来细说一下,也是很多地方老生常谈的内容,「View的绘制时机 」。在Android中View的绘制时异步的,依赖于View的绘制流程,并不是触发了相关的修改就会立马改变,例如在我们的项目中切换图片这个操作中,调用「imageView.setImageBitmap()」,图像其实并不是马上渲染成功,如果在调用
这个方法之后立马获取imageView,实际这个时候的imageView会出现:
1、View需要走完measure() → layout() → draw() 三阶段;
2、设置 Bitmap 后,仅仅标记需要重绘;
3、真正的图像渲染发生在下一帧的 draw() 阶段;
4、如果此时立即对该图像内容进行处理(如截图、再次绘制等),容易出现"像加载不全"或"只加载一半"的问题。
所以在我们项目中之前的研发采用了delay的操作并不是完全没有道理的,他希望避免出现问题,只是这个处理方案,不可控、不可精准、影响性能。
三、优化方案
1、Bitmap预加载
使用以下方式预加载 Bitmap 并在处理流程中挂起等待,当用户真正需要使用 bitmap 时,如果尚未加载完成,将自动挂起等待,避免了 race condition,也不需要阻止用户必须等待全部加载完成才能执行操作:
js
private val bitmapDeferredList = pathList.map {
CompletableDeferred<Bitmap>()
}
fun preloadBitmaps() {
pathList.forEachIndexed { index, path ->
scope.launch(Dispatchers.IO) {
val bitmap = BitmapFactory.decodeFile(path)
bitmapDeferredList[index].complete(bitmap)
}
}
}
2、使用suspendCancellableCoroutine挂起函数加上ViewTreeObserver精准监听ImageView绘制完成
通过监听 View 的绘制事件来替代传统的 delay(),这样可以精准感知 View 已完成渲染,确保图片已完全加载至 ImageView,从而进行下一步截图或绘制操作。
js
suspend fun ImageView.awaitDrawComplete(): Boolean =
suspendCancellableCoroutine { continuation ->
val observer = viewTreeObserver
val listener = object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
observer.removeOnPreDrawListener(this)
continuation.resume(true)
return true
}
}
observer.addOnPreDrawListener(listener)
continuation.invokeOnCancellation {
observer.removeOnPreDrawListener(listener)
}
}
3、媒体库刷新逻辑统一&&解耦
原本的执行逻辑:处理完成所有图片后delay 500ms后刷新媒体库
修改为:弹出弹窗后用户点击下一步操作时触发刷新+页面退出后立即触发刷新
四、优化效果
优化阶段 | 平均耗时 | 优化手段 |
---|---|---|
线上版本 | 30s | 无预加载,手动延时 |
优化后、立即触发操作 | 6.4s | 异步预加载未完全完成 + 挂起控制 + 准确监听 View 渲染 |
优化后,等待1s后触发操作 | 4.4s | 异步预加载全部完成 + 挂起控制 + 准确监听 View 渲染 |
而在实际用户体验中,用户进入该页面后会进行需要如何操作进行调整,所以基本上都会在1s后才会触发批量的操作
五、未来可以继续优化的方向
为了追求速度,其实是可以有很多操作的,但是可能会付出一定的代价,如果将代价控制在可以接受的程度,是可以继续压缩处理时间的
1、压缩图片尺寸处理
对于高分辨率图片(如 4K)可先压缩至处理尺寸(如 1080p)再添加水印,显著减少 Bitmap 操作耗时。
可通过 inSampleSize 参数实现:
js
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(path, options)
options.inSampleSize = calculateInSampleSize(options, 1080, 1920)
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeFile(path, options)
2、多线程并发处理
- 使用 Dispatchers.IO 启动多个协程并行处理 Bitmap,但是需要注意内存控制与最大线程数限制,避免出现协程开启过多,频繁切换上下文和调度反而导致速度变慢的情况。
- 防止内存膨胀导致 OOM,可配合 bitmap 压缩与 LRU 缓存。
- 同时,如果有操作需要在主线程执行的,例如每次都需要执行View.draw(canvas),协程是并发的,但是因为这个需要等待所以还是串行的操作
3、对于代码的细致优化
这一项处理的重点方向就是之前的研发留下来的代码结构的优化,例如用内联函数省去调用函数的开销、将重复操作提取至循环外获取一次、按需切换上下文而不是切换上下文之后执行判断发现是false再退出
这些处理其实不会很大的影响加载速度,并且大部分属于项目中的遗留的、不好的代码习惯和一些当时没有考虑到性能的情况,所以并不在这里详细讲述具体的优化。
六、思考
为什么会决定写这篇博客,其实除了分享解决方案,还有一些主要的原因
第一就是,这个功能的优化确实难倒了公司很多的研发,原本以为会是一场硬仗,但是结果似乎很轻松的就达到了提升85%的性能优化。
第二是因为,我们每个人一直都在成长,在看过很多技术文章的时候,都会发现他们讲的很厉害,有很多"八股"知识,在真正的经历相关的工作时才会体验到里面的玄妙的地方,以及回想起曾经学习过和看过的技术文章的内容,再回过头来去翻的喜悦。
最重要 是因为,在我的职业生涯中写过很蠢的代码,也写过我自认为很优秀的代码,遇到过完全没有架构和设计思想的伙伴,也遇到过知识底蕴很扎实的同事。有很多功能是我们加了很多班,改了很多版,代码质量方面完全没话说的,但是可能达不到我们的日活的目标。
而像今天优化的这个功能,在问世到现在的2年时间里一直都是处于很"烂"的代码结构下,但是使用的用户一直保持在一个很高的量级下,做好一个产品确实不是一个简单的事情。
我也一直在思考,代码质量是设计出来的还是演进出来的?
现实项目中,很多模块是在"不确定"的混乱中快速上线的:"产品还在摸索 "、"资源不足,时间紧张 "、"没有足够信息进行完美设计"。很多功能在一开始设计时会因为各种客观因素而"简陋"和"乱",而同时再厉害的架构设计也无法完美的遇见未来的需求变化和用户对于我们的功能的期望。
再看我们项目的这个功能,其实他的问题不是整体架构一开始设计的简陋,而是历经这么长时间没有研发真正去解决一系列的问题。
对于未来项目的代码架构上的设计,或许下次我会先问问自己:
- 这个功能的用户价值有多大?(需求优先级)
- 它的架构复杂度是否值得?(投入产出比)
- 是否可以暂时接受技术债,然后在合适的时间去清理?
而对于与我共事的人,也是我成长的最大变量,每一个人其实都是"镜子":
技术弱但务实的人,让我理解什么叫"能上线"
理论强但保守的人,让我理解什么叫"架构的代价"
优秀且务实的人,让我明白什么叫"技术是为产品服务的工具"
需求的不确定性,也让我明白什么叫"产品决策权的重要性"