目录
我的学习手册 - 热更新了解了一下下
我的学习手册 - Glide了解了一下下
我的学习手册 - 进程保活了解了一下下
我的学习手册 - EventBus了解了一下下
我的学习手册 - ARouter了解了一下下
Glide
概况
首先为什么要用Glide呢,最主要的原因就是缓存机制和它的生命周期绑定
一、缓存机制
所有的图片缓存机制都是使用的三级缓存,即内存缓存,磁盘缓存,服务器存储。
三级缓存.png
除此之外,Glide的缓存的数据是由Glide的各种设置决定的,例如一张图片100x100设置宽高80x80和50x50的图片两种类型展示,那么缓存中会存在有80x80和50x50的两张图片,当你再次设置的时候就直接读取这两张图片,而不是读取原图重新设置宽高,这里采用的是使用空间换取时间的策略,这就是见仁见智了,由于目前手机的内存会变得越来越大,所以这种方案也是没有问题的。
二、生命周期绑定
Glide.with(context)
在使用Glide的时候会传入当前上下文,但是通过上下文是无法获取当前Activity生命周期回调的,所以Glide使用的策略是在当前Activity中添加一个空白的Fragment,通过该Fragment来进行回调当前界面的生命周期,在当前Fragment的销毁的时候,将内存中的绑定当前Activity的bitmap回收,可以将占用的内存进行释放。这个也是我最喜欢Glide的原因,其他的图片加载框架都是事先占用部分的手机内存,只有在App退出的时候才会进行释放,内存允许的情况下,可以达到当前App所有的图片都放在内存中,加载效率更快。可是在实际情况中,不同页面出现相同图片的概率还是比较小的,而且就算是出现相同的图片,那也只是很少的一部分,所以我更欣赏Glide的做法,将bitmap和生命周期绑定,生命周期结束bitmap回收,这也是更符合我们的编程思想。
这里有一个比较重要的一点就是如何将bitmap与当前的Activity进行绑定。这里使用的方案是创建一个HashMap<Int, ArrayList<String>>,第一个参数放入Activity的hashCode,用来对应Activity,第二个参数放入内存缓存中存入bitmap的key,那么我们可以通过找到当前Activity的hashCode来获取所有的内存缓存的bitmap的key集合,再通过这些key来获取bitmap进行回收。
手撸Glide (非官方)
缓存
那么首先仿照Glide的样子来创建一个Glide
Glide.with(this)
.load(url)
.placeHolder(R.mipmap.loading)
.error(R.mipmap.error)
.listener({
log("图片加载成功 url = $url " +
"width = ${it.width} " +
"height = ${it.height}")
}, {
log("图片加载失败")
})
.into(imageView)
那么对于Glide来说with方法创建了一个BitmapRequest,所以with方法返回一个bitmap的请求实体
Glide
object Glide {
fun with(activity: AppCompatActivity): BitmapRequest {
return BitmapRequest(activity)
}
}
BitmapRequest
对于一个BitmapRequest来说我们需要知道这一张图片的基本数据,地址url,图片imageView,然后我们可以添加一些友好设置,加载占位图placeHolderRes,加载失败图errorRes,加载监听listener成功和失败。对于图片来说,我们并不想直接持有,所以采用软引用的方式SoftReference<ImageView>,此外对于缓存来说需要一个key所以还需要一个参数uriMd5
class BitmapRequest(private var activity: Activity) {
private var imageUrl: String? = null
private var uriMd5: String? = null
private var mSoftImageView: SoftReference<ImageView>? = null
private var requestListener: BitmapRequestListener? = null
private var placeHolderRes: Int? = null
private var errorRes: Int? = null
fun load(imageUrl: String): BitmapRequest {
this.imageUrl = imageUrl
uriMd5 = MD5Utils.toMD5(imageUrl)
return this
}
fun placeHolder(placeHolderRes: Int): BitmapRequest {
this.placeHolderRes = placeHolderRes
return this
}
fun error(errorRes: Int): BitmapRequest {
this.errorRes = errorRes
return this
}
fun listener(requestListener: BitmapRequestListener): BitmapRequest {
this.requestListener = requestListener
return this
}
fun listener(onResourceReady: (bitmap: Bitmap) -> Unit, onException: () -> Unit): BitmapRequest {
listener(object : BitmapRequestListener {
/**
* 图片加载成功
*/
override fun onResourceReady(bitmap: Bitmap) {
onResourceReady(bitmap)
}
/**
* 图片加载异常
*/
override fun onException() {
onException()
}
})
return this
}
fun into(imageView: ImageView) {
mSoftImageView = SoftReference(imageView)
imageView.tag = uriMd5
RequestManager.INSTANCE.addBitmapRequest(this)
}
fun getActivity():Activity = activity
@Nullable
fun getImageUrl(): String? = imageUrl
@Nullable
fun getUriMd5(): String? = uriMd5
@Nullable
fun getPlaceHolder(): Int? = placeHolderRes
@Nullable
fun getError(): Int? = errorRes
@Nullable
fun getRequestListener(): BitmapRequestListener? = requestListener
@Nullable
fun getImageView(): ImageView? = mSoftImageView?.get()
override fun toString(): String {
return "BitmapRequest(activity=$activity, imageUrl=$imageUrl, uriMd5=$uriMd5, mSoftImageView=$mSoftImageView, requestListener=$requestListener, placeHolderRes=$placeHolderRes, errorRes=$errorRes)"
}
}
interface BitmapRequestListener {
/**
* 图片加载成功
*/
fun onResourceReady(bitmap:Bitmap)
/**
* 图片加载异常
*/
fun onException()
}
BitmapManager
由于图片加载肯定不会是一张一张的加载,这里就存在并发的问题,那么我们就需要构建一个加载队列,将一个个图片加载请求放入这个请求队列去进行加载图片。那么我们可以看手机的cpu核心数来进行分配几条流水线来进行加载。
class RequestManager private constructor() {
/*** 请求调度 给予多个来解决并发问题 ***/
private val dispatchers: ArrayList<RequestDispatcher> = arrayListOf()
companion object {
val INSTANCE: RequestManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
val manager = RequestManager()
manager.start()
manager
}
}
/*** 请求队列 ***/
private val requestQueue = LinkedBlockingQueue<BitmapRequest>()
/**
* 添加一个网络请求
*/
fun addBitmapRequest(request: BitmapRequest) {
//如果队列中没有这个图片请求 添加
if (!requestQueue.contains(request)) {
requestQueue.add(request)
} else {
log("当前任务已存在\n$request")
}
}
/**
* 开启请求,通过RequestDispatcher进行调度 获取bitmap
*/
private fun start() {
stop()
val threadCount: Int = Runtime.getRuntime().availableProcessors()
for (i in 0 until threadCount) {
val requestDispatcher = RequestDispatcher(requestQueue)
//开始加载bitmap
requestDispatcher.start()
dispatchers.add(requestDispatcher)
}
}
/**
* 关闭线程
*/
private fun stop() {
if (dispatchers.isNotEmpty()) {
for (dispatcher: RequestDispatcher in dispatchers) {
//如果线程没有被中断 中断线程
if (!dispatcher.isInterrupted) {
dispatcher.interrupt()
}
}
dispatchers.clear()
}
}
}
RequestDispatcher
在执行加载图片的时候是需要开启一个死循环,由于是阻塞式的所以不糊造成界面的卡顿,无法理解的可以想想Handler的死循环读取message,异曲同工。
在这里我们进行一些对于图片加加载,从内存、磁盘、网络中读取图片,分别进行不同的操作来存放bitma
我们在BitmapRequest中也放置了一些参数,在这里都可以用上
!!!这里是继承Thread的,所以更新ui的操作都需要转到主线程中,这里采用handler来进行线程调度
class RequestDispatcher(requestQueue: BlockingQueue<BitmapRequest>) : Thread() {
/*** Handler 用户跳转到主线程进行展示图片 ***/
private val handler by lazy { Handler(Looper.getMainLooper()) }
/*** 请求队列 ***/
private val queue: BlockingQueue<BitmapRequest> = requestQueue
/**
* 运行
*/
override fun run() {
//死循环 由于是阻塞式 在子线程中 所以不会发生界面卡顿
while (!isInterrupted) {
//获取当前的任务
try {
val bitmapRequest: BitmapRequest = queue.take()
//给图片设置占位图
showPlaceHolder(bitmapRequest)
//1、获取bitmap 先从缓存中获取 没有再从网络获取
val bitmap: Bitmap? = getBitmap(bitmapRequest)
//2、通过Handler到ui线程展示图片
showImageInUiThread(bitmapRequest, bitmap)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
/**
* 显示占位图
*/
private fun showPlaceHolder(request: BitmapRequest) {
request.getPlaceHolder()?.let { placeHolder ->
request.getImageView()?.let { imageView ->
handler.post {
imageView.setImageResource(placeHolder)
}
}
}
}
/**
* 从缓存中获取bitmap
*/
private fun getBitmap(bitmapRequest: BitmapRequest): Bitmap? {
//从缓存中获取bitmap 先从内存中获取 再从磁盘中获取
var bitmap: Bitmap? = DoubleCacheManager.get(bitmapRequest.getUriMd5())
log("缓存读取成功? ${bitmap != null} ")
if (bitmap != null) {
return bitmap
} else {
//从网络中获取
bitmap = downloadImage(bitmapRequest.getImageUrl())
return bitmap
}
}
/**
* 网络下载缓存
*/
private fun downloadImage(uri: String?): Bitmap? {
if (uri == null || uri.isEmpty()) {
return null
}
var inputStream: InputStream? = null
var bitmap: Bitmap? = null
try {
val url = URL(uri)
val conn: URLConnection = url.openConnection()
inputStream = conn.getInputStream()
bitmap = BitmapFactory.decodeStream(inputStream)
//!!!将图片放入缓存
if (bitmap != null) {
val key: String = MD5Utils.toMD5(uri)
log("key = $key 存入缓存")
DoubleCacheManager.put(key, bitmap)
}
} catch (e: java.lang.Exception) {
e.printStackTrace()
} finally {
try {
inputStream?.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
return bitmap
}
/**
* 在ui线程中展示图片
*/
private fun showImageInUiThread(request: BitmapRequest, bitmap: Bitmap?) {
//需要判断图片是否为空 因为是软应用
//bitmap是否为空
//tag是否匹配
handler.post {
val imageView: ImageView? = request.getImageView()
val errorRes: Int? = request.getError()
if (imageView != null && imageView.tag == request.getUriMd5()) {
if (bitmap != null) {
imageView.setImageBitmap(bitmap)
} else {
//bitmap为空 说明加载失败 显示错误图片
errorRes?.let {
imageView.setImageResource(it)
}
}
}
}
request.getRequestListener()?.let {
if (bitmap != null) {
handler.post {
it.onResourceReady(bitmap)
}
} else {
handler.post {
it.onException()
}
}
}
}
}
生命周期
那么到这里图片的加载和缓存就已经完成了,接下来就是重点的生命周期的绑定,其实生命周期的绑定很简单,就往简单了想,不要去想的复杂,思路:
1.通过传入的上下文创建一个Fragment
2.先自定义一个HashMap<Int, ArrayList<String>>,内部参数<Activity的HashCode,ArrayList<内存缓存中bitmap的key,就是地址的MD5>>
获取到BitmapRequest的上下文activity,获取activity的HashCode
获取通过地址得到三级缓存中提供的bitmap之后,将key(BitmapRequest的uriMd5)放入上方HashMap的HashCode对应的列表中
3.在创建的Fragment被销毁的时候,移除内存中的bitmap并且回收,调用gc
LifeCycleObservable
class LifeCycleObservable private constructor() {
private val activityMap: HashMap<Int, ArrayList<String>> by lazy { hashMapOf<Int, ArrayList<String>>() }
private val addCode: StringBuilder by lazy { StringBuilder() }
private val removeCode: StringBuilder by lazy { StringBuilder() }
companion object {
val instance: LifeCycleObservable by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
LifeCycleObservable()
}
}
/**
* 将ActivityCode 和 对应的bitmap的imageUrlMd5集合相对应
*/
fun add(activityCode: Int, imageUrlMd5: String) {
addCode.append("activityCode = $activityCode imageUrlMd5 = $imageUrlMd5 \n")
var urlMd5List: ArrayList<String>? = activityMap[activityCode]
if (urlMd5List == null) {
urlMd5List = arrayListOf()
}
if (!urlMd5List.contains(imageUrlMd5)) {
urlMd5List.add(imageUrlMd5)
}
activityMap[activityCode] = urlMd5List
log("add activityCode = $activityCode imageUrlMd5 = $imageUrlMd5 urlMd5List = ${urlMd5List.size}")
}
fun remove(activityCode: Int) {
val urlMd5List: ArrayList<String>? = activityMap[activityCode]
urlMd5List?.let {
for (imageUrlMd5 in it) {
//只是从内存中将所有的缓存获取
val bitmap = DoubleCacheManager.get(imageUrlMd5, DoubleCacheManager.LRU)
DoubleCacheManager.remove(imageUrlMd5, DoubleCacheManager.LRU)
//回收
if (bitmap != null && !bitmap.isRecycled) {
bitmap.recycle()
}
removeCode.append("activityCode = $activityCode imageUrlMd5 = $imageUrlMd5 \n")
}
//系统回收 有些手机会执行 有些手机不会执行 可以自己手动GC查看内存
System.gc()
//目前添加的必删除的多
log("\nremoveCode \n$removeCode")
}
activityMap.clear()
}
}
RequestDispatcher的run()中,添加
override fun run() {
//死循环 由于是阻塞式 在子线程中 所以不会发生界面卡顿
while (!isInterrupted) {
//获取当前的任务
try {
val bitmapRequest: BitmapRequest = queue.take()
showPlaceHolder(bitmapRequest)
//1、获取bitmap 先从缓存中获取 没有再从网络获取
val bitmap: Bitmap? = getBitmap(bitmapRequest)
//2、通过Handler到ui线程展示图片
showImageInUiThread(bitmapRequest, bitmap)
//生命周期
//3、此时已经把缓存放入 那么把缓存的key uriMd5和Activity结合
val uriMd5 = bitmapRequest.getUriMd5()
uriMd5?.let {
LifeCycleObservable.instance.add(bitmapRequest.getActivity().hashCode(), uriMd5)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
RequestManagerFragment 销毁
class RequestManagerFragment : Fragment() {
private val activityCode: Int by lazy { activity!!.hashCode() }
override fun onDetach() {
super.onDetach()
LifeCycleObservable.instance.remove(activityCode)
}
}
三、内存变化效果图
首先是进入首页,添加存储权限,点击跳转到另一个界面,点击加载30多张图片,不进行复用,内存会暴涨到300M,然后点击退出,将当前界面绑定的bitmap全部回收,并且调用gc,内存恢复到初始状态
内存变化.GIF
网友评论