美文网首页
12-2-3 ImageLoader 的实现

12-2-3 ImageLoader 的实现

作者: Yue_Q | 来源:发表于2019-01-10 19:32 被阅读0次

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

    • 图片的同步加载:以同步的方式加载图片,可能是从内存中拉取,也可能是从磁盘中读取的,还可能是从网络拉取。
    • 图片的异步加载:调用者不想在单独的线程中以同步的方式来获取图片,这个时候 ImageLoader 内部需要自己在线程中加载图片,降低 OOM 概率的有效手段。
    • 图片压缩
    • 内存缓存
    • 磁盘缓存
    • 网络拉取

    注意:
    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 方式实现异步任务,但终归是不太自然的方式实现。

    相关文章

      网友评论

          本文标题:12-2-3 ImageLoader 的实现

          本文链接:https://www.haomeiwen.com/subject/ouporqtx.html