对于Android的图片加载框架,早些前用得最普遍的则是Android-Universal-Image-Loader,github地址:https://github.com/nostra13/Android-Universal-Image-Loader,不过它现在也依然被广泛应用着,但是如今的项目如果要选取一款图片加载框架用到工程中,glide算是一个首选了,为啥呢?肯定是好用嘛,而对于一个程序员来说,这么好用的框架也应该去了解它里面的一些核心原理才行,所以接下来准备剖析它的核心机制。
Glide4.11.0基本使用:
再分析原理之前,先来对Glide的基本使用有一个了解,它的github地址为:https://github.com/bumptech/glide:
下面先来读一下官网对于它的一个描述,这样能让咱们对其有一个大体的认识,知道glide都能干嘛:
通过这官方的说明,确实能感受到它的强大,居然能解析视频~~有点6,接下来咱们来看一下它的简单使用,注意只是简单哈,因为通过简单的使用,接下来我们就会手动实现这样的一个功能,能把简单的自己手动实现出来,那基本上对于这个框架的一个原理也了解比较透了,至于它的一些各种用法不在本次的讨论中,学习框架就要紧抓它的核心原理,好,先来集成它:
目前最新版本是4.11.0,如下:
接下来简单的使用官网也有说明:
咱们就以它为例,一个是在显示Activity中显示一个图片,一个是在列表当中显示,下面开始来使用:
package com.android.glidearcstudy; import android.os.Bundle; import android.os.Environment; import android.widget.ImageView; import androidx.appcompat.app.AppCompatActivity; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import java.io.File; public class MainActivity extends AppCompatActivity { private ImageView imageView; private ImageView imageView1; private ImageView imageView2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initViews(); load(); } private void initViews() { imageView = findViewById(R.id.imageView); imageView1 = findViewById(R.id.imageView1); imageView2 = findViewById(R.id.imageView2); } private void load() { //从网络加载 Glide.with(this).load("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/" + "it/u=1753838773,1743607588&fm=26&gp=0.jpg") .apply(new RequestOptions().error(R.drawable.ic_launcher_background).placeholder (R.mipmap.ic_launcher).override(500, 500)) .into(imageView); //从sdcard加载 Glide.with(this).load(Environment.getExternalStorageDirectory()+"/test.jpg") .into(imageView1); Glide.with(this).load(new File(Environment.getExternalStorageDirectory()+"/test2.jpg")) .into(imageView2); } }
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_horizontal" android:orientation="vertical"> <ImageView android:id="@+id/imageView" android:layout_width="100dp" android:layout_height="100dp" /> <ImageView android:id="@+id/imageView1" android:layout_width="100dp" android:layout_marginTop="5dp" android:layout_height="100dp" /> <ImageView android:id="@+id/imageView2" android:layout_width="100dp" android:layout_marginTop="5dp" android:layout_height="100dp" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="toNext" android:text="进入列表界面" /> </LinearLayout>
添加权限:
由于有2张是从sdcard上来加载的,所以导两个图片到sdcard上:
接下来运行一下,比较简单,关于sdcard权限的问题就不多说了,6.0以上的需要主动申请权限才能用的,运行如下:
接下来则来弄一个在列表中显示图片的界面:
package com.android.glidearcstudy; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; public class SecondActivity extends AppCompatActivity { String[] url = {"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1581846474183&di=d5545c6a3f65a" + "2fd4a60e94216415f6f&imgtype=0&src=http%3A%2F%2Fimg.wxcha.com%2Ffile%2F201801%2F20%2F93494f56ae.jpg", "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1581846474181&di=046af070c0469f08c0524ce4a48cb1cc&imgtype=0&src=http%" + "3A%2F%2Fimg.wxcha.com%2Ffile%2F201903%2F27%2F8492923169.jpg%3Fdown", "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1581846523830&di=b0048998113fd960deb13604b3a61eba&imgtype=0&src=h" + "ttp%3A%2F%2Fwww.chinazoyo.com.cn%2Fimg.php%3Fwww.qqju.com%2Fpic%2Ftx%2Ftx20737.jpg"}; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); RecyclerView recyclerView = findViewById(R.id.recycler_view); LinearLayoutManager layoutManager = new LinearLayoutManager(this); recyclerView.setLayoutManager(layoutManager); ImageAdapter adapter = new ImageAdapter(); recyclerView.setAdapter(adapter); } private final class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> { @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new ViewHolder(LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_test, parent, false)); } @Override public void onBindViewHolder(ViewHolder holder, int position) { holder.title.setText( String.valueOf(position)); holder.imageView.setTag(position); Glide.with(SecondActivity.this) .load(url[position % url.length]) .into(holder.imageView); } @Override public int getItemCount() { return 100; } public final class ViewHolder extends RecyclerView.ViewHolder { private final ImageView imageView; private final TextView title; ViewHolder(View itemView) { super(itemView); title = itemView.findViewById(R.id.text); imageView = itemView.findViewById(R.id.icon); } } } }
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:contextpackageNamckage="MainActivity"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical" /> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:orientation="horizontal"> <TextView android:id="@+id/text" android:layout_width="80dp" android:layout_height="80dp" android:gravity="center" /> <ImageView android:id="@+id/icon" android:layout_width="80dp" android:layout_height="80dp" /> </LinearLayout>
运行:
如丝般顺滑~~加载了100个图,完全没有闪白的感觉,性能确实是挺不错的,好了,关于Glide的基本使用就到这了,接下来,咱们手动来实现这样的效果,别看目前应用时只有简单的一句话,底层涉及到的东东还是相当相当多的,颇有些挑战,还是跟之前学习Arouter一样,先不分析官网的源码,在自己手写完之后再来看源码就会非常简单的。
手写实现Glide核心功能---缓存与解码复用
缓存机制:
概念:
对于图片加载来说缓存是一个非常重要的功能,在实际面试啥的要问到图片加载框架的一些底层基本上都逃不开它,所以接下来手写从Glide的缓存开刀,搞清楚了它那其实上对于Glide的认识也比较深刻了。
对于图片缓存我们以前常听的是三级缓存(内存、磁盘、网络),而对于Glide是4级,关于这块的东东参考这篇博客:https://www.jianshu.com/p/97fd67720b34,如博主所说:
默认情况下,Glide 会在开始一个新的图片请求之前检查以下多级的缓存: 1. 活动资源 (Active Resources) 2. 内存缓存 (Memory Cache) 3. 资源类型(Resource Disk Cache) 4. 原始数据 (Data Disk Cache) 活动资源:如果当前对应的图片资源正在使用,则这个图片会被Glide放入活动缓存。 内存缓存:如果图片最近被加载过,并且当前没有使用这个图片,则会被放入内存中 资源类型: 被解码后的图片写入磁盘文件中,解码的过程可能修改了图片的参数(如:inSampleSize、inPreferredConfig) 原始数据: 图片原始数据在磁盘中的缓存(从网络、文件中直接获得的原始数据)
上面的概念咋一看肯定有些抽象,不过不着急,接下来会一一进行探究,并且会手动来实现的,目前先对其调用顺序有一个了解,对于代码这块:
Glide会首先从Active Resources查找当前是否有对应的活跃图片,没有则查找内存缓存,没有则查找资源类型,没有则查找数据来源。用时序图来表达一下整个调用过程:
各层概念了解:
第一层活动资源(Active Resources):
先来看一下博主对它的理论化的描述:
举个我们的demo例子来进一步说明下,还记得我们有一个列表加载图片的界面么,里面其实就是只有3张图,然后整个列表加载了100张:
那貌似其实就是内存缓存嘛,其实是有区别的,内存缓存要用的话肯定是有一个内存上限策略,如果到达了内存上限那对于可能在未来要使用的这张图片就有可能被清理掉,也就是博主说的这句话:
那可能想,回收就回收了呗,大不了再重新加载不就是了,非得再搞个这个区域来存放,你想想用户体验,是不是重载加载就会加载中的情况,体验还能丝般顺滑么?好,先对其有个大概的了解既可,没有代码的见证概念是显示何等得空洞~~
第二层内存缓存(Memory Cache):
这里直接看博主的说明:
第三四层磁盘缓存(资源类型 Resource Disk Cache、原始数据 Data Disk Cache):
上面文字中提到了图片的两个尺寸:ARGB_8888和RGB_565,其实还有很多种格式,可以通过这个类来查看:
咱们目前只来分析RGB_565和ARGB_8888,这俩有啥区别呢?这里从计算图片内存的角度来分析一下,对于一张图都是由一个个像素点组成的,比如:
所以整张图片的内存大小应该是"长的像素个数 * 高的像素个数",而如果加上图片格式情况就不一样了,先来看一下RGB_565:
所以如果是一个RGB_565格式的Bitmap总内存大小的计算公式就变为:“长的像素个数 * 高的像素个数 * 2”。
而如果是ARGB_8888呢?
所以如果是一个ARGB_8888格式的Bitmap总内存大小的计算公式就变为:“长的像素个数 * 高的像素个数 * 4”。
手写实现:
内存缓存:
首先从内存缓存开撸,先来对原生的Bitmap进行封装一下:
package com.android.glidearcstudy.glide.recycle; import android.graphics.Bitmap; public class Resource { private Bitmap bitmap; /*引用计数,使用+1,不使用-1, 当为0时则代表引bitmap不需要使用了,此时就可以将其保存到内存当中了 */ private int acquired; private OnResourceListener onResourceListener; public void setOnResourceListener(OnResourceListener onResourceListener) { this.onResourceListener = onResourceListener; } /** * 引用计数-1 */ public void release() { if (--acquired == 0 && onResourceListener != null) { onResourceListener.onResourceReleased(this); } } /** * 引用计数+1 */ public void acquire() { if (bitmap.isRecycled()) { throw new IllegalStateException("Acquire a recycled resource"); } ++acquired; } /** * 当acquired 为0的时候 回调 onResourceReleased */ public interface OnResourceListener { void onResourceReleased(Resource resource); } }
那如何用它呢?这里用单元测试来调用一下:
接下来还需要定义一个Key,关于这Key有啥用,待之后再来看:
package com.android.glidearcstudy.glide.recycle; import android.graphics.Bitmap; import com.android.glidearcstudy.glide.Key; public class Resource { private Bitmap bitmap; /*引用计数,使用+1,不使用-1, 当为0时则代表引bitmap不需要使用了,此时就可以将其保存到内存当中了 */ private int acquired; private OnResourceListener onResourceListener; private Key key; public Resource(Bitmap bitmap) { this.bitmap = bitmap; } public Bitmap getBitmap() { return bitmap; } public void setOnResourceListener(Key key, OnResourceListener onResourceListener) { this.key = key; this.onResourceListener = onResourceListener; } /** * 释放 */ public void recycle() { if (acquired > 0) { return; } if (!bitmap.isRecycled()) { bitmap.recycle(); } } /** * 引用计数-1 */ public void release() { if (--acquired == 0 && onResourceListener != null) { onResourceListener.onResourceReleased(this); } } /** * 引用计数+1 */ public void acquire() { if (bitmap.isRecycled()) { throw new IllegalStateException("Acquire a recycled resource"); } ++acquired; } /** * 当acquired 为0的时候 回调 onResourceReleased */ public interface OnResourceListener { void onResourceReleased(Resource resource); } }
好,资源封装好了之后,接下来则需要封装内存缓存操作类了,如之前理论所描述它会使用LRUCache,先定义内存缓存的操作接口:
package com.android.glidearcstudy.glide; import com.android.glidearcstudy.glide.recycle.Resource; public interface MemoryCache { void clearMemory(); void trimMemory(int level); interface ResourceRemoveListener { void onResourceRemoved(Resource resource); } Resource put(Key key, Resource resource); void setResourceRemoveListener(ResourceRemoveListener listener); Resource remove2(Key key);//为啥这里要定义成remove2,而非remove,因为对于LruCache也有remove方法 }
接下来新建一个具体类来实现它:
package com.android.glidearcstudy.glide; import android.util.LruCache; import com.android.glidearcstudy.glide.recycle.Resource; public class LruMemoryCache extends LruCache<Key, Resource> implements MemoryCache { private ResourceRemoveListener listener; private boolean isRemoved; public LruMemoryCache(int maxSize) { super(maxSize); } @Override protected int sizeOf(Key key, Resource value) { return -1; } @Override protected void entryRemoved(boolean evicted, Key key, Resource oldValue, Resource newValue) { } @Override public Resource remove2(Key key) { return null; } @Override public void clearMemory() { } @Override public void trimMemory(int level) { } @Override public void setResourceRemoveListener(ResourceRemoveListener listener) { } }
其中是继承了androidx.collection.LruCache,而非java.util中的LruCache了:
LRuCache原理阐述:
下面简单对它有一个了解,如之前描述它其实是一个双向链表:
而双向链表的结构如:
然后看一下LRU中put方法:
此时又需要子类进行重写了,如下:
注意:前提前不对map进行排序则是按最先插入的则先移除的原则,而如果排了序的则不是按这个规则啦。
那LRU的整个使用的原理是怎么的呢?下面以一个图例来说明一下:
目前插入了一个map key=1的元素,接着再put一个map key=2的元素则会加入到集合的尾部,形态为:
接下来从中取出map key=1元素,也就是get(1),此时会变为:
很明显使用了的则会移动到队列的尾部,每次都这样的话,列表的头部数据就是最近最不常使用的了,当缓存空间不足时,就会删除列表头部的缓存数据,因为原则是最少使用的最先被清理嘛,假设这个集合的最大存放元素的个数就是2个,接下来再来一个map key=3的元素,此时由于内存超限了则需要将头部的一个元素给清理掉,然后插到尾部,所以就为:
好,明白了LruCache的原理之后,接下来继续来写咱们的内存缓存。
实现咱们的内存的LruCache:
首先重写相关的父类的方法:
其中对于4.4以上机型为啥要用getAllocationByteCount(),而不用getByteCount(),这里就得先理解这俩的区别,下面阐述一下:
紧接着又要显示一张图,但是这图呢比原来复用池的那张要小,如下:
而在大于4.4的系统上,这俩调用之后获取图片大小的区别如下:
也就是getByteCount()只会获取新图的大小,而getAllocationByCount()是获取原来复用池整张图的大小,而我们希望的是要后者。
好,继续来编写,接下来来实现setResourceRemoveListener()这个方法,很简单赋值既可:
而上面监听的调用是在entryRemoved()方法中,实现一下:
好,关于内存缓存暂且写到这。
活动资源:
对于正在使用的图片还需要添加到活动资源中,所以接下来实现这块的逻辑,这块涉及到了弱引用的使用了,写法还是很精妙的,如下:
package com.android.glidearcstudy.glide; import com.android.glidearcstudy.glide.recycle.Resource; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; /** * 正在使用的图片资源 */ public class ActiveResources { private ReferenceQueue<Resource> queue; private final Resource.OnResourceListener resourceListener; private Map<Key, ResourceWeakReference> activeResources = new HashMap<>(); private Thread cleanReferenceQueueThread; private boolean isShutdown; public ActiveResources(Resource.OnResourceListener resourceListener) { this.resourceListener = resourceListener; } /** * 加入活动缓存 */ public void activate(Key key, Resource resource) { resource.setOnResourceListener(key, resourceListener); activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue())); } /** * 引用队列,通知我们弱引用被回收了 * 让我们得到通知的作用 */ private ReferenceQueue<Resource> getReferenceQueue() { if (null == queue) { queue = new ReferenceQueue<>(); cleanReferenceQueueThread = new Thread() { @Override public void run() { while (!isShutdown) {//这边开启清理循环来将回收的资源给从缓存中移除掉 try { //被回收掉的引用 ResourceWeakReference ref = (ResourceWeakReference) queue.remove(); activeResources.remove(ref.key); } catch (InterruptedException e) { } } } }; cleanReferenceQueueThread.start(); } return queue; } static final class ResourceWeakReference extends WeakReference<Resource> { final Key key; public ResourceWeakReference(Key key, Resource referent, ReferenceQueue<? super Resource> queue) { super(referent, queue); this.key = key; } } }
其中关于软引用队列的作用这里先用单元测试看一下效果:
嗯,比较简单,接下来继续完善活动资源缓存的逻辑:
好,接下来再来调用一下,看怎么使用它们:
package com.android.glidearcstudy.glide; import com.android.glidearcstudy.glide.recycle.Resource; public class CacheTest implements Resource.OnResourceListener, MemoryCache.ResourceRemoveListener { LruMemoryCache lruMemoryCache; ActiveResources activeResource; public Resource test(Key key) { //内存缓存 lruMemoryCache = new LruMemoryCache(10); lruMemoryCache.setResourceRemoveListener(this); //活动资源缓存 activeResource = new ActiveResources(this); /** * 第一步 从活动资源中查找是否有正在使用的图片 */ Resource resource = activeResource.get(key); if (null != resource) { //当不使用的时候 release resource.acquire(); return resource; } /** * 第二步 从内存缓存中查找 */ resource = lruMemoryCache.get(key); if (null != resource) { //1.为什么从内存缓存移除? // 因为lru可能移除此图片 我们也可能recycle掉此图片 // 如果不移除,则下次使用此图片从活动资源中能找到,但是这个图片可能被recycle掉了 lruMemoryCache.remove2(key);//从内存缓存中移除 resource.acquire(); activeResource.activate(key, resource);//再加入到活动资源缓存中 return resource; } return null; } /** * 这个资源没有正在使用了 * 将其从活动资源移除 * 重新加入到内存缓存中 */ @Override public void onResourceReleased(Resource resource) { //TODO } /** * 从内存缓存被动移除 * 此时得放入复用池 */ @Override public void onResourceRemoved(Resource resource) { //TODO } }
上面还有两个方法未有写,因为目前架子还不完善,复用池目前也不存在,所以下节再继续。