You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

不使用第三方库实现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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 14:15:03