不使用第三方库实现Android URL加载图片及现有代码问题排查
现有代码存在的问题
- 线程资源滥用:每次触发
BindingAdapter都会新建一个独立的SingleThreadExecutor,RecyclerView滑动时会瞬间创建数十个线程,CPU持续高负载,是设备发热的核心原因。 - 无缓存机制:同一张图片反复下载、解码,大量重复IO和计算操作,不仅额外消耗性能,也直接导致列表滑动时图片加载慢。
- 未适配RecyclerView复用逻辑:ImageView被复用到新条目时,旧的异步下载任务完成后仍会给当前ImageView设置旧图片,是图片错位的直接原因。
- 未做Bitmap采样压缩:直接加载原图尺寸的Bitmap到ImageView,占用内存过高,解码速度慢,也会提升列表卡顿概率。
- 无用任务无取消逻辑:条目滑出屏幕后,对应的未完成下载任务仍会继续执行,浪费系统资源。
纯Android SDK实现的图片加载方案
不需要引入任何第三方库,通过全局复用线程池、内存缓存、ImageView标记防错位、Bitmap采样压缩几个核心优化点即可解决所有问题,实现代码如下:
1. 全局图片加载单例实现
import android.graphics.Bitmap import android.graphics.BitmapFactory import android.os.Handler import android.os.Looper import android.util.LruCache import android.widget.ImageView import java.net.URL import java.util.concurrent.Executors object NativeImageLoader { // 内存缓存:取应用最大可用内存的1/8作为缓存容量 private val memoryCache: LruCache<String, Bitmap> = run { val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() val cacheSize = maxMemory / 8 object : LruCache<String, Bitmap>(cacheSize) { override fun sizeOf(key: String, bitmap: Bitmap): Int { return bitmap.byteCount / 1024 } } } // 复用固定线程池,核心线程数和CPU核心数匹配,避免频繁创建线程的开销 private val executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) private val mainHandler = Handler(Looper.getMainLooper()) fun loadImage(url: String, imageView: ImageView, placeholderRes: Int) { // 先设置占位图 imageView.setImageResource(placeholderRes) // 给ImageView打标记,记录当前需要加载的URL imageView.tag = url // 优先读取内存缓存,命中直接返回 val cachedBitmap = memoryCache.get(url) if (cachedBitmap != null) { imageView.setImageBitmap(cachedBitmap) return } // 异步执行下载解码逻辑 executor.execute { try { // 第一次校验:如果ImageView已被复用,直接终止任务 if (imageView.tag != url) return@execute // 先读取图片尺寸计算采样率,不加载到内存 val options = BitmapFactory.Options() options.inJustDecodeBounds = true val inputStream = URL(url).openStream() BitmapFactory.decodeStream(inputStream, null, options) inputStream.close() // 按ImageView的目标尺寸计算采样率 val reqWidth = imageView.width val reqHeight = imageView.height options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) options.inJustDecodeBounds = false // 二次请求流,解码压缩后的Bitmap val newInputStream = URL(url).openStream() val bitmap = BitmapFactory.decodeStream(newInputStream, null, options) newInputStream.close() bitmap?.let { // 存入内存缓存 memoryCache.put(url, it) // 切主线程设置图片,第二次校验避免错位 mainHandler.post { if (imageView.tag == url) { imageView.setImageBitmap(it) } } } } catch (e: Exception) { e.printStackTrace() } } } // 计算Bitmap采样率,压缩图片尺寸到和ImageView匹配 private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { val rawHeight = options.outHeight val rawWidth = options.outWidth var inSampleSize = 1 if (rawHeight > reqHeight || rawWidth > reqWidth) { val halfHeight = rawHeight / 2 val halfWidth = rawWidth / 2 while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { inSampleSize *= 2 } } return inSampleSize } }
2. 修改BindingAdapter逻辑
@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false) fun ImageView.bindImage(imgUrl: String?, placeholderRes: Int? = null) { imgUrl ?: return // 替换为你自己的默认占位图资源 val defaultPlaceholder = placeholderRes ?: R.drawable.img_album_cover // 转换为HTTPS链接的逻辑保留 val httpsUrl = imgUrl.toUri().buildUpon().scheme("https").build().toString() NativeImageLoader.loadImage(httpsUrl, this, defaultPlaceholder) }
3. XML布局用法(可兼容原有写法,也可自定义占位图)
<androidx.appcompat.widget.AppCompatImageView android:id="@+id/iv_album_image" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" app:imageUrl="@{albumsObject.albumImage}" app:placeholder="@{@drawable/img_album_cover}" android:src="@drawable/img_album_cover" />
优化说明
- 全局复用固定线程池,避免频繁创建销毁线程,大幅降低CPU负载,解决设备发热问题。
- Lru内存缓存自动管理,同一张图片仅需下载解码一次,大幅提升列表滑动时的加载速度。
- ImageView Tag双重校验机制,完全解决RecyclerView复用导致的图片错位问题。
- Bitmap自适应采样压缩,仅加载和ImageView尺寸匹配的Bitmap,降低内存占用,提升解码速度。
- 如果需要持久化缓存,可自行扩展本地文件存储逻辑,将下载的图片存入应用私有缓存目录,加载时优先读取本地文件即可。
内容的提问来源于stack exchange,提问作者Ahmed Mamdouh
相关产品推荐
相关产品推荐

