perf(image): 优化图片加载性能

- 使用 BitmapDecodeHelper 替换 BitmapFactory 进行采样解码
- 添加单线程执行器进行异步图片解码处理
- 实现图片视图布局完成后再进行尺寸计算和加载
- 防止因 Activity 销毁导致的内存泄漏问题
- 在 SprayingFinishActivity、SprayingOCRActivity 和 SprayingResultActivity 中统一图片加载逻辑
这个提交包含在:
徐勤民 2026-04-14 22:59:25 +08:00
父节点 ff6a0c3a33
当前提交 7310127d56
共有 3 个文件被更改,包括 58 次插入7 次删除

查看文件

@ -2,10 +2,11 @@ package com.nova.brain.glass.ui
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory import androidx.core.view.doOnLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.nova.brain.glass.R import com.nova.brain.glass.R
import com.nova.brain.glass.databinding.ActivitySprayingFinishBinding import com.nova.brain.glass.databinding.ActivitySprayingFinishBinding
import com.nova.brain.glass.helper.BitmapDecodeHelper
import com.nova.brain.glass.helper.OfflineCmdListener import com.nova.brain.glass.helper.OfflineCmdListener
import com.nova.brain.glass.helper.OfflineCmdServiceHelper import com.nova.brain.glass.helper.OfflineCmdServiceHelper
import com.nova.brain.glass.helper.SprayingPhotoManager import com.nova.brain.glass.helper.SprayingPhotoManager
@ -15,6 +16,7 @@ import com.xuqm.base.adapter.BasePagedAdapter
import com.xuqm.base.adapter.CommonPagedAdapter import com.xuqm.base.adapter.CommonPagedAdapter
import com.xuqm.base.adapter.ViewHolder import com.xuqm.base.adapter.ViewHolder
import com.xuqm.base.ui.BaseListFormLayoutNormalActivity import com.xuqm.base.ui.BaseListFormLayoutNormalActivity
import java.util.concurrent.Executors
class SprayingFinishActivity : class SprayingFinishActivity :
BaseListFormLayoutNormalActivity<ItemItem, SprayingFinishVM, ActivitySprayingFinishBinding>() { BaseListFormLayoutNormalActivity<ItemItem, SprayingFinishVM, ActivitySprayingFinishBinding>() {
@ -22,6 +24,7 @@ class SprayingFinishActivity :
override fun fullscreen(): Boolean = true override fun fullscreen(): Boolean = true
override fun getRecyclerOrientation(): Int = RecyclerView.VERTICAL override fun getRecyclerOrientation(): Int = RecyclerView.VERTICAL
private val imageDecodeExecutor = Executors.newSingleThreadExecutor()
private val listener = object : OfflineCmdListener { private val listener = object : OfflineCmdListener {
override fun onOfflineCmd(cmd: String) { override fun onOfflineCmd(cmd: String) {
@ -52,7 +55,21 @@ class SprayingFinishActivity :
val imageViews = listOf(binding.photo1, binding.photo2, binding.photo3) val imageViews = listOf(binding.photo1, binding.photo2, binding.photo3)
imageViews.forEach { it.setImageDrawable(null) } imageViews.forEach { it.setImageDrawable(null) }
photos.forEachIndexed { index, path -> photos.forEachIndexed { index, path ->
imageViews.getOrNull(index)?.setImageBitmap(BitmapFactory.decodeFile(path)) imageViews.getOrNull(index)?.let { imageView ->
imageView.doOnLayout {
val targetWidth = imageView.width.coerceAtLeast(1)
val targetHeight = imageView.height.coerceAtLeast(1)
imageDecodeExecutor.execute {
val bitmap = BitmapDecodeHelper.decodeSampledBitmap(path, targetWidth, targetHeight)
runOnUiThread {
if (isFinishing || isDestroyed) {
return@runOnUiThread
}
imageView.setImageBitmap(bitmap)
}
}
}
}
} }
} }

查看文件

@ -1,13 +1,14 @@
package com.nova.brain.glass.ui package com.nova.brain.glass.ui
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory
import android.os.CountDownTimer import android.os.CountDownTimer
import android.os.Environment import android.os.Environment
import android.view.WindowManager import android.view.WindowManager
import androidx.core.view.doOnLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.nova.brain.glass.R import com.nova.brain.glass.R
import com.nova.brain.glass.databinding.ActivitySprayingOcrBinding import com.nova.brain.glass.databinding.ActivitySprayingOcrBinding
import com.nova.brain.glass.helper.BitmapDecodeHelper
import com.nova.brain.glass.helper.OfflineCmdListener import com.nova.brain.glass.helper.OfflineCmdListener
import com.nova.brain.glass.helper.OfflineCmdServiceHelper import com.nova.brain.glass.helper.OfflineCmdServiceHelper
import com.nova.brain.glass.helper.SprayingPhotoManager import com.nova.brain.glass.helper.SprayingPhotoManager
@ -24,6 +25,7 @@ import com.xuqm.base.common.LogHelper
import com.xuqm.base.extensions.showMessage import com.xuqm.base.extensions.showMessage
import com.xuqm.base.ui.BaseListFormLayoutNormalActivity import com.xuqm.base.ui.BaseListFormLayoutNormalActivity
import java.io.File import java.io.File
import java.util.concurrent.Executors
import java.util.UUID import java.util.UUID
class SprayingOCRActivity : class SprayingOCRActivity :
@ -63,6 +65,7 @@ class SprayingOCRActivity :
} }
private val photoCallbackId = UUID.randomUUID().toString() private val photoCallbackId = UUID.randomUUID().toString()
private var resultCountdown: CountDownTimer? = null private var resultCountdown: CountDownTimer? = null
private val imageDecodeExecutor = Executors.newSingleThreadExecutor()
private val mPhotoFileCallback = object : PhotoFileCallback.Stub() { private val mPhotoFileCallback = object : PhotoFileCallback.Stub() {
override fun onTakePhoto(path: String) { override fun onTakePhoto(path: String) {
@ -139,8 +142,20 @@ class SprayingOCRActivity :
override fun adapter(): BasePagedAdapter<ItemItem> = adapter override fun adapter(): BasePagedAdapter<ItemItem> = adapter
private fun showPhoto(path: String) { private fun showPhoto(path: String) {
binding.content.setImageBitmap(BitmapFactory.decodeFile(path)) binding.content.doOnLayout {
restartResultCountdown(path) val targetWidth = it.width.coerceAtLeast(1)
val targetHeight = it.height.coerceAtLeast(1)
imageDecodeExecutor.execute {
val bitmap = BitmapDecodeHelper.decodeSampledBitmap(path, targetWidth, targetHeight)
runOnUiThread {
if (isFinishing || isDestroyed) {
return@runOnUiThread
}
binding.content.setImageBitmap(bitmap)
restartResultCountdown(path)
}
}
}
} }
private fun restartResultCountdown(path: String) { private fun restartResultCountdown(path: String) {

查看文件

@ -2,14 +2,15 @@ package com.nova.brain.glass.ui
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Paint import android.graphics.Paint
import android.os.Environment import android.os.Environment
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.doOnLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.nova.brain.glass.R import com.nova.brain.glass.R
import com.nova.brain.glass.databinding.ActivitySprayingResultBinding import com.nova.brain.glass.databinding.ActivitySprayingResultBinding
import com.nova.brain.glass.helper.BitmapDecodeHelper
import com.nova.brain.glass.helper.OfflineCmdListener import com.nova.brain.glass.helper.OfflineCmdListener
import com.nova.brain.glass.helper.OfflineCmdServiceHelper import com.nova.brain.glass.helper.OfflineCmdServiceHelper
import com.nova.brain.glass.helper.SprayingPhotoManager import com.nova.brain.glass.helper.SprayingPhotoManager
@ -25,6 +26,7 @@ import com.xuqm.base.common.LogHelper
import com.xuqm.base.extensions.showMessage import com.xuqm.base.extensions.showMessage
import com.xuqm.base.ui.BaseListFormLayoutNormalActivity import com.xuqm.base.ui.BaseListFormLayoutNormalActivity
import java.io.File import java.io.File
import java.util.concurrent.Executors
import java.util.UUID import java.util.UUID
class SprayingResultActivity : class SprayingResultActivity :
@ -33,6 +35,7 @@ class SprayingResultActivity :
override fun fullscreen(): Boolean = true override fun fullscreen(): Boolean = true
override fun getRecyclerOrientation(): Int = RecyclerView.HORIZONTAL override fun getRecyclerOrientation(): Int = RecyclerView.HORIZONTAL
private val imageDecodeExecutor = Executors.newSingleThreadExecutor()
private var status = true private var status = true
private val manualResultLauncher = private val manualResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@ -157,7 +160,7 @@ class SprayingResultActivity :
super.initData() super.initData()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
intent.getStringExtra("path")?.apply { intent.getStringExtra("path")?.apply {
binding.iv.setImageBitmap(BitmapFactory.decodeFile(this)) showResultImage(this)
setStatusImage() setStatusImage()
} }
} }
@ -231,4 +234,20 @@ class SprayingResultActivity :
} }
override fun adapter(): BasePagedAdapter<ItemItem> = adapter override fun adapter(): BasePagedAdapter<ItemItem> = adapter
private fun showResultImage(path: String) {
binding.iv.doOnLayout {
val targetWidth = it.width.coerceAtLeast(1)
val targetHeight = it.height.coerceAtLeast(1)
imageDecodeExecutor.execute {
val bitmap = BitmapDecodeHelper.decodeSampledBitmap(path, targetWidth, targetHeight)
runOnUiThread {
if (isFinishing || isDestroyed) {
return@runOnUiThread
}
binding.iv.setImageBitmap(bitmap)
}
}
}
}
} }