對於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 } }
上面還有兩個方法未有寫,因為目前架子還不完善,復用池目前也不存在,所以下節再繼續。