一般来说,一个优秀的 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 方式实现异步任务,但终归是不太自然的方式实现。
网友评论