12-2-3 ImageLoader 的实现

2019-01-10  本文已影响0人  Yue_Q

一般来说,一个优秀的 ImageLoader 应该具备如下功能

注意:
ImageLoader 还要处理特殊清空,比如 ListView 或者 GridView 中,View 的复用即是它们的优点也是它们的缺点。
例如:在 ListView 中,一个 itemA 正在从网络拉取图片,当用户快速拉取列表,很有可能 itemB 复用了 ImageView A,图片下载完之后,由于 ImageView A 被 item B 所复用,所以 item B 显示的是 item A 的图片,造成了列表错位问题。

ImageLoader kotlin 版本

/**
 * Provides retrieving of {@link InputStream} of image by URI from network or
 * file system or app resources.<br />
 * {@link URLConnection} is used to retrieve image stream from network.
 *
 * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
 * @since 1.8.0
 */
class JImageLoader private constructor(context: Context) {
    private var mIsDiskLruCacheCreated = false
    // Handler sends an update message to the main thread
    private val mMainHandler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            val result = msg.obj as LoaderResult
            val imageView = result.imageView
            val uri = imageView.getTag(TAG_KEY_URI) as String
            if (uri == result.uri) {
                imageView.setImageBitmap(result.bitmap)
            } else {
                Log.w(TAG, "set image bitmap, but url has changed, ignored!")
            }
        }
    }

    private val mContext: Context = context.applicationContext
    private val mImageResizer = ImageResizer()
    private val mMemoryCache: LruCache<String, Bitmap>
    private var mDiskLruCache: DiskLruCache? = null

    init {
        val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
        // cacheSize is the maximum size to cache
        // cacheSize is one eight
        val cacheSize = maxMemory / 8
        mMemoryCache = object : LruCache<String, Bitmap>(cacheSize) {
            override fun sizeOf(key: String, bitmap: Bitmap): Int {
                return bitmap.rowBytes * bitmap.height / 1024
            }
        }
        // cache path
        val diskCacheDir = getDiskCacheDir(mContext, "bitmap")
        if (!diskCacheDir.exists()) {
            // diskCache does is not exist to create
            diskCacheDir.mkdirs()
        }
        if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
            try {
                mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE)
                mIsDiskLruCacheCreated = true
            } catch (e: IOException) {
                e.printStackTrace()
            }

        }
    }

    private fun addBitmapToMemoryCache(key: String, bitmap: Bitmap) {
        if (getBitmapFromMemoryCache(key) == null) {
            mMemoryCache.put(key, bitmap)
        }
    }

    private fun getBitmapFromMemoryCache(key: String): Bitmap? {
        return mMemoryCache.get(key)
    }

    /**
     * Asynchronous load bitmap from memory cache or disk cache or network.
     * @dec note: 1. Thread loading images may be OOM
     *            2. AsyncTask 3.0 cannot implement concurrency
     * @param uri http url
     * @param imageView ImageView Resource file
     * @param reqWidth the width ImageView desired
     * @param reqHeight the height ImageView desired
     * @return bitmap, maybe null.
     */
    @JvmOverloads
    fun bindBitmap(uri: String, imageView: ImageView, reqWidth: Int = 0, reqHeight: Int = 0) {
        imageView.setTag(TAG_KEY_URI, uri)
        val bitmap = loadBitmapFromMemCache(uri)
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap)
            return
        }

        val loadBitmapTask = Runnable {
            val bitmap = loadBitmap(uri, reqWidth, reqHeight)
            if (bitmap != null) {
                val result = LoaderResult(imageView, uri, bitmap)
                mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget()
            }
        }
        THREAD_POOL_EXECUTOR.execute(loadBitmapTask)
    }

    /**
     * Synchronize load bitmap from memory cache or disk cache or network.
     * @param uri http url
     * @param reqWidth the width ImageView desired
     * @param reqHeight the height ImageView desired
     * @return bitmap, maybe null.
     */
    private fun loadBitmap(uri: String, reqWidth: Int, reqHeight: Int): Bitmap? {
        var bitmap = loadBitmapFromMemCache(uri)
        if (bitmap != null) {
            return bitmap
        }

        try {
            bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight)
            if (bitmap != null) {
                return bitmap
            }
            bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight)
        } catch (e: IOException) {
            e.printStackTrace()
        }

        if (bitmap == null && !mIsDiskLruCacheCreated) {
            bitmap = downloadBitmapFromUrl(uri)
        }

        return bitmap
    }

    private fun loadBitmapFromMemCache(url: String): Bitmap? {
        val key = hashKeyFormUrl(url)
        return getBitmapFromMemoryCache(key)
    }

    /**
     * File submit or undo operations
     * @param url http url
     * @param reqWidth the width ImageView desired
     * @return bitmap, maybe null.
     */
    @Throws(IOException::class)
    private fun loadBitmapFromHttp(url: String, reqWidth: Int, reqHeight: Int): Bitmap? {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw RuntimeException("can not visit network from UI thread.")
        }
        if (mDiskLruCache == null) {
            return null
        }
        // url uses MD5 encryption to prevent exceptions
        val key = hashKeyFormUrl(url)
        val editor = mDiskLruCache!!.edit(key)
        if (editor != null) {
            val outputStream = editor.newOutputStream(DISK_CACHE_INDEX)
            // network pull
            if (downloadUrlToStream(url, outputStream)) {
                editor.commit()
            } else {
                editor.abort()
            }
            mDiskLruCache!!.flush()
        }
        return loadBitmapFromDiskCache(url, reqWidth, reqHeight)
    }

    /** 
     * Compression image into memory
     * File submit or undo operations
     * @param url http url
     * @param reqWidth the width ImageView desired
     * @return bitmap, maybe null.
     */
    @Throws(IOException::class)
    private fun loadBitmapFromDiskCache(url: String, reqWidth: Int, reqHeight: Int): Bitmap? {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Log.w(TAG, "load bitmap from UI Thread, it's not recommended!")
        }
        if (mDiskLruCache == null) {
            return null
        }

        var bitmap: Bitmap? = null
        val key = hashKeyFormUrl(url)
        val snapshot = mDiskLruCache!!.get(key)
        if (snapshot != null) {
            val fileInputStream = snapshot.getInputStream(DISK_CACHE_INDEX) as FileInputStream
            val fileDescriptor = fileInputStream.fd
            bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fileDescriptor,reqWidth, reqHeight)
            if (bitmap != null) {
                addBitmapToMemoryCache(key, bitmap)
            }
        }

        return bitmap
    }

    private fun downloadUrlToStream(urlString: String, outputStream: OutputStream): Boolean {
        var urlConnection: HttpURLConnection? = null
        var out: BufferedOutputStream? = null
        var in1: BufferedInputStream? = null

        try {
            val url = URL(urlString)
            urlConnection = url.openConnection() as HttpURLConnection
            in1 = BufferedInputStream(urlConnection.inputStream, IO_BUFFER_SIZE)
            out = BufferedOutputStream(outputStream, IO_BUFFER_SIZE)

            var b: Int = in1.read()
            while (b != -1) {
                out.write(b)
                b = in1.read()
            }
            return true
        } catch (e: IOException) {
            Log.e(TAG, "downloadBitmap failed.$e")
        } finally {
            urlConnection?.disconnect()
            try {
                out?.close()
                in1?.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }

        }
        return false
    }

    private fun downloadBitmapFromUrl(urlString: String): Bitmap? {
        var bitmap: Bitmap? = null
        var urlConnection: HttpURLConnection? = null
        var `in`: BufferedInputStream? = null

        try {
            val url = URL(urlString)
            urlConnection = url.openConnection() as HttpURLConnection
            `in` = BufferedInputStream(urlConnection.inputStream, IO_BUFFER_SIZE)
            bitmap = BitmapFactory.decodeStream(`in`)
        } catch (e: IOException) {
            Log.e(TAG, "Error in downloadBitmap: $e")
        } finally {
            urlConnection?.disconnect()
            try {
                `in`?.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }

        }
        return bitmap
    }

    private fun hashKeyFormUrl(url: String): String {
        val cacheKey: String
        cacheKey = try {
            val mDigest = MessageDigest.getInstance("MD5")
            mDigest.update(url.toByteArray())
            bytesToHexString(mDigest.digest())
        } catch (e: NoSuchAlgorithmException) {
            url.hashCode().toString()
        }

        return cacheKey
    }

    private fun bytesToHexString(bytes: ByteArray): String {
        val sb = StringBuilder()
        for (i in bytes.indices) {
            val hex = Integer.toHexString(0xFF and bytes[i].toInt())
            if (hex.length == 1) {
                sb.append('0')
            }
            sb.append(hex)
        }
        return sb.toString()
    }

    private fun getDiskCacheDir(context: Context, uniqueName: String): File {
        val externalStorageAvailable = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
        val cachePath: String
        cachePath = if (externalStorageAvailable) {
            context.externalCacheDir!!.path
        } else {
            context.cacheDir.path
        }
        return File(cachePath + File.separator + uniqueName)
    }

    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    private fun getUsableSpace(path: File): Long {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
            return path.usableSpace
        }
        val stats = StatFs(path.path)
        return stats.blockSize.toLong() * stats.availableBlocks.toLong()
    }

    private class LoaderResult(var imageView: ImageView, var uri: String, var bitmap: Bitmap)

    companion object {
        //Kotlin static variables and static method

        private val TAG = "ImageLoader"

        private const val MESSAGE_POST_RESULT = 1

        private val CPU_COUNT = Runtime.getRuntime().availableProcessors()
        private val CORE_POOL_SIZE = CPU_COUNT + 1
        private val MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1
        private const val KEEP_ALIVE = 10L

        private val TAG_KEY_URI = 123 // R.id.imageView
        private const val DISK_CACHE_SIZE = (1024 * 1024 * 50).toLong()
        private const val IO_BUFFER_SIZE = 8 * 1024
        private const val DISK_CACHE_INDEX = 0

        private val sThreadFactory = object : ThreadFactory {
            private val mCount = AtomicInteger(1)

            override fun newThread(r: Runnable): Thread {
                return Thread(r, "ImageLoader#" + mCount.getAndIncrement())
            }
        }

        val THREAD_POOL_EXECUTOR: Executor = ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
                KEEP_ALIVE, TimeUnit.SECONDS,
                LinkedBlockingDeque(), sThreadFactory
        )

        /**
         * build a new instance of ImageLoader
         * @param context
         * @return a new instance of ImageLoader
         */
        fun build(context: Context): JImageLoader {
            return JImageLoader(context)
        }
    }

}

采用线程池的原因:
(1)使用线程可能会创建大量的线程造成 OOM
(2)AsyncTask 3.0 之后无法实现并发效果,虽然可以通过 executeOnExecutor 方式实现异步任务,但终归是不太自然的方式实现。

上一篇下一篇

猜你喜欢

热点阅读