1. 程式人生 > >Android Glide 的使用總結

Android Glide 的使用總結

Android Glide 的使用總結


初識

在泰國舉行的谷歌開發者論壇上,谷歌為我們介紹了一個名叫 Glide 的圖片載入庫,作者是bumptech。這個庫被廣泛的運用在google的開源專案中,包括2014年google I/O大會上釋出的官方app。

之前很多同學都看到過這篇介紹Glide的文章,原文在這裡。文中從各個方面介紹和比較了Glide與Picasso,總體來說二者極為相似,有著近乎相同的API的使用風格。但Glide在快取策略和載入GIF方面略勝一籌。最後作者也極力推薦了這個庫。

學習


推薦一個Glide系列是文章,全面對Glide的瞭解,

Glide - 系列綜述

使用總結


遇到的問題


在使用過程中發現了一些問題,也在其他部落格上看到了相關的解決方案,例如:

  1. 為什麼 有的圖片第一次載入的時候只顯示佔位圖,第二次才顯示正常的圖片呢?
  2. 為什麼 我總會得到類似You cannot start a load for a destroyed activity這樣的異常呢?
  3. 為什麼 我不能給載入的圖片setTag()呢?

第一個問題第一次只顯示佔位符:

            Glide.with(AppContext.context())
                .load(url)
                .placeholder(R.mipmap.placeholdermid) //佔位符
.error(R.mipmap.placeholdermid) //錯誤佔位符 .dontAnimate()//沒有任何淡入淡出效果 .override(640, 428)//調整圖片大小 .diskCacheStrategy(DiskCacheStrategy.ALL) .priority(Priority.HIGH)//優先順序 .into(iv);

AppContext.context() 這裡是對Application封裝獲取getApplicationContext的一個單例。

這是我在listview當中使用到的相關配置選項,當我發現有時候只需改一改Glide載入的相關選項就能解決掉這個只顯示佔位符的問題。
其他的解決方案:

  • 不使用Glide的預設動畫;
  • 不設定佔位;
  • 使用Glide的Transformation API自定義圓形Bitmap的轉換,例如這裡

第二個問題:

不要再非主執行緒裡面使用Glide載入圖片,如果真的使用了,請把context引數換成getApplicationContext。上面的使用方式上我全部使用getApplicationContext這個上下文。

第三個問題:

從Glide的3.6.0之後,新添加了全域性設定的方法。具體方法如下:
先實現GlideMoudle介面,全域性設定ViewTaget的tagId:

public class MyGlideMoudle implements GlideModule{
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        ViewTarget.setTagId(R.id.glide_tag_id);
    }

    @Override
    public void registerComponents(Context context, Glide glide) {

    }
}

同樣,也需要在ids.xml下新增id

<item name="glide_tag_id" type="id"/>

最後在AndroidManifest.xml檔案裡面新增

<meta-data
    android:name="com.yourpackagename.MyGlideMoudle"
    android:value="GlideModule" />

Glide獲取快取大小並清除快取


清除Glide快取

Glide自帶清除快取的功能;

Glide.get(context).clearDiskCache();//(清除磁碟快取)
Glide.get(context).clearMemory();//(清除記憶體快取)

其中clearDiskCache()方法必須執行在子執行緒,clearMemory()方法必須執行在主執行緒,這是這兩個方法所強制要求的,詳見原始碼。

獲取Glide快取空間大小

以下方法適合在Glide為預設的快取目錄的情況,不論是內部儲存空間還是外部。因為我們可以通過

InternalCacheDiskCacheFactory.DEFAULT_DISK_CACHE_DIR
ExternalCacheDiskCacheFactory.DEFAULT_DISK_CACHE_DIR

獲取到位於內部與外部儲存的快取資料夾的資料夾名,並通過

context.getCacheDir()
context.getExternalCacheDir()

獲取內部與外部儲存的路徑.進而可以通過遍歷資料夾內的檔案進行快取檔案大小求和與全部清除。

工具類如下:

import android.content.Context;
import android.os.Looper;
import android.text.TextUtils;

import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.cache.ExternalCacheDiskCacheFactory;
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;

import java.io.File;
import java.math.BigDecimal;

/**
 * Created by wman on 2017/2/23.
 */

public class GlideCacheUtil {

    private static GlideCacheUtil inst;

    public static GlideCacheUtil getInstance() {
        if (inst == null) {
            inst = new GlideCacheUtil();
        }
        return inst;
    }

    /**
     * 清除圖片磁碟快取
     */
    public void clearImageDiskCache(final Context context) {
        try {
            if (Looper.myLooper() == Looper.getMainLooper()) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Glide.get(context).clearDiskCache();
                        //BusUtil.getBus().post(new GlideCacheClearSuccessEvent());
                    }
                }).start();
            } else {
                Glide.get(context).clearDiskCache();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 清除圖片記憶體快取
     */
    public void clearImageMemoryCache(Context context) {
        try {
            if (Looper.myLooper() == Looper.getMainLooper()) { //只能在主執行緒執行
                Glide.get(context).clearMemory();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 清除圖片所有快取
     */
    public void clearImageAllCache(Context context) {
        clearImageDiskCache(context);
        clearImageMemoryCache(context);
        String ImageExternalCatchDir = context.getExternalCacheDir() + ExternalCacheDiskCacheFactory.DEFAULT_DISK_CACHE_DIR;
        deleteFolderFile(ImageExternalCatchDir, true);
    }

    /**
     * 獲取Glide造成的快取大小
     *
     * @return CacheSize
     */
    public String getCacheSize(Context context) {
        try {
            return getFormatSize(getFolderSize(new File(context.getCacheDir() + "/" + InternalCacheDiskCacheFactory.DEFAULT_DISK_CACHE_DIR)));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * 獲取指定資料夾內所有檔案大小的和
     *
     * @param file file
     * @return size
     * @throws Exception
     */
    private long getFolderSize(File file) throws Exception {
        long size = 0;
        try {
            File[] fileList = file.listFiles();
            for (File aFileList : fileList) {
                if (aFileList.isDirectory()) {
                    size = size + getFolderSize(aFileList);
                } else {
                    size = size + aFileList.length();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return size;
    }

    /**
     * 刪除指定目錄下的檔案,這裡用於快取的刪除
     *
     * @param filePath       filePath
     * @param deleteThisPath deleteThisPath
     */
    private void deleteFolderFile(String filePath, boolean deleteThisPath) {
        if (!TextUtils.isEmpty(filePath)) {
            try {
                File file = new File(filePath);
                if (file.isDirectory()) {
                    File files[] = file.listFiles();
                    for (File file1 : files) {
                        deleteFolderFile(file1.getAbsolutePath(), true);
                    }
                }
                if (deleteThisPath) {
                    if (!file.isDirectory()) {
                        file.delete();
                    } else {
                        if (file.listFiles().length == 0) {
                            file.delete();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 格式化單位
     *
     * @param size size
     * @return size
     */
    private static String getFormatSize(double size) {
        double kiloByte = size / 1024;
        if (kiloByte < 1) {
            return size + "Byte";
        }
        double megaByte = kiloByte / 1024;
        if (megaByte < 1) {
            BigDecimal result1 = new BigDecimal(Double.toString(kiloByte));
            return result1.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "KB";
        }
        double gigaByte = megaByte / 1024;
        if (gigaByte < 1) {
            BigDecimal result2 = new BigDecimal(Double.toString(megaByte));
            return result2.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "MB";
        }
        double teraBytes = gigaByte / 1024;
        if (teraBytes < 1) {
            BigDecimal result3 = new BigDecimal(Double.toString(gigaByte));
            return result3.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "GB";
        }
        BigDecimal result4 = new BigDecimal(teraBytes);
        return result4.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "TB";
    }
}

利用策略模式對圖片載入的封裝


這個段落的答案,摘抄自Stormzhang的文章 如何正確使用開源專案?

對於開源專案,我們知道有些庫設計的確實很棒,使用者呼叫起來非常方便,一行程式碼直接搞定,拿圖片載入庫 Glide 舉個例子:

Glide.with(context)
    .load("http://inthecheesefactory.com/uploads/source/glidepicasso/cover.jpg")
    .into(ivImg);

使用起來是不是特簡單?你也許問我,都封裝的這麼好了還用得著再封裝一層麼?那你錯了,哪怕他已經很完美了,我都會這麼做:

public class ImageLoader {
    public static void with(Context context, String imageUrl, ImageView imageView) {
        Picasso.with(context).load(imageUrl).into(imageView); 
    }
}

這樣我所有專案呼叫的方式直接就是 ImageLoader.with() ,這樣做的好處是:

入口統一,所有圖片載入都在這一個地方管理,一目瞭然,即使有什麼改動我也只需要改這一個類就可以了。

具體的實現詳見這邊文章:網路圖片載入的封裝【從零開始搭建android框架系列(4)】

  • ImageUtil類

可以看到ImageUtil提供的是單例模式

public class ImageLoaderUtil {

    public static final int PIC_LARGE = 0;
    public static final int PIC_MEDIUM = 1;
    public static final int PIC_SMALL = 2;

    public static final int LOAD_STRATEGY_NORMAL = 0;
    public static final int LOAD_STRATEGY_ONLY_WIFI = 1;

    private static ImageLoaderUtil mInstance;
    private BaseImageLoaderStrategy mStrategy;

    public ImageLoaderUtil() {
        mStrategy = new GlideImageLoaderStrategy();
    }

    //single instance
    public static ImageLoaderUtil getInstance() {
        if (mInstance == null) {
            synchronized (ImageLoaderUtil.class) {
                if (mInstance == null) {
                    mInstance = new ImageLoaderUtil();
                    return mInstance;
                }
            }
        }
        return mInstance;
    }


    public void loadImage(Context context, ImageLoader img) {
        mStrategy.loadImage(context, img);
    }

    public void setLoadImgStrategy(BaseImageLoaderStrategy strategy) {
        mStrategy = strategy;
    }
}

  • BaseImageLoaderProvider類

可以看到我們ImageUtil中是採用這個類的loadImage方法去載入圖片的。這裡是一個介面。由具體的子類(GlideImageLoaderProvider)去實現loadImage方法。

public interface BaseImageLoaderStrategy {
    void loadImage(Context ctx, ImageLoader img);
}

  • GlideImageLoaderProvider類

是BaseImageLoaderProvider的實現類,完成具體的載入圖片操作。這裡面會有wifi下載入圖片的判斷。具體判斷將放在util工具類中進行實現。這裡也是利用圖片載入庫Glide進行實現。後期如果工程專案決定使用其他的圖片載入框架,當然可以採用其他類繼承BaseImageLoaderProvider。

public class GlideImageLoaderStrategy implements BaseImageLoaderStrategy {
    @Override
    public void loadImage(Context ctx, ImageLoader img) {
        int strategy = img.getWifiStrategy();
        if (strategy == ImageLoaderUtil.LOAD_STRATEGY_ONLY_WIFI) {
            int netType = AppContext.getNetworkType();
            //如果是在wifi下才載入圖片,並且當前網路是wifi,直接載入
            if (1 == netType) {
                loadNormal(ctx, img);
            } else {
                //如果是在wifi下才載入圖片,並且當前網路不是wifi,載入快取
                loadCache(ctx, img);
            }
        } else {
            //如果不是在wifi下才載入圖片
            loadNormal(ctx, img);
        }
    }


    /**
     * load image with Glide
     */
    private void loadNormal(Context ctx, ImageLoader img) {
        Glide.with(ctx)
                .load(img.getUrl())
                .placeholder(img.getPlaceHolder()) //佔位符
                .error(img.getPlaceHolder())       //錯誤佔位符
                .crossFade()//平滑和養眼動畫預設的持續時間是 100毫秒
                .dontAnimate()//沒有任何淡入淡出效果
                .diskCacheStrategy(DiskCacheStrategy.ALL)
                .priority(Priority.HIGH)//優先順序
                .into(img.getImgView());
    }


    /**
     * load cache image with Glide
     */
    private void loadCache(Context ctx, ImageLoader img) {
        Glide.with(ctx).using(new StreamModelLoader<String>() {
            @Override
            public DataFetcher<InputStream> getResourceFetcher(final String model, int i, int i1) {
                return new DataFetcher<InputStream>() {
                    @Override
                    public InputStream loadData(Priority priority) throws Exception {
                        throw new IOException();
                    }

                    @Override
                    public void cleanup() {

                    }

                    @Override
                    public String getId() {
                        return model;
                    }

                    @Override
                    public void cancel() {

                    }
                };
            }
        }).load(img.getUrl())
                .placeholder(img.getPlaceHolder()) //佔位符
                .error(img.getPlaceHolder())       //錯誤佔位符
                .crossFade()//平滑和養眼動畫預設的持續時間是 100毫秒
                .dontAnimate()//沒有任何淡入淡出效果
                .override(640, 428)//調整圖片大小
                .diskCacheStrategy(DiskCacheStrategy.ALL)
                .priority(Priority.HIGH)//優先順序
                .into(img.getImgView());
    }
}

  • ImageLoader類

在ImageUtil的load方法中進行圖片載入,第一個引數是Context,那麼第二個引數呢?正是這裡的ImageLoader,採用Builder建造者模式。Builder模式可以將一個複雜物件的構建和它的表示分離,使得同樣的構建過程可以構建不同的物件。

因為在圖片載入中,會處理到的資料必定有圖片的url,必定有ImageView的例項,可能有載入策略(是否wifi下載入),可能有圖片載入型別(大圖,中圖,小圖),也會有圖片載入沒有成功時候的佔位符。那麼這麼多資料操作,所以用到了Builder模式,一步一步的建立一個複雜物件的建立者模式,它允許使用者在不知道內部構建細節的情況下,可以更精細的控制物件的構建流程。比如這裡的ImageLoader。

public class ImageLoader {
    private int type;           //型別 (大圖,中圖,小圖)
    private String url;         //需要解析的url
    private int placeHolder;    //當沒有成功載入的時候顯示的圖片
    private ImageView imgView;  //ImageView的例項
    private int wifiStrategy;   //載入策略,是否在wifi下才載入

    private ImageLoader(Builder builder) {
        this.type = builder.type;
        this.url = builder.url;
        this.placeHolder = builder.placeHolder;
        this.imgView = builder.imgView;
        this.wifiStrategy = builder.wifiStrategy;
    }

    public int getType() {
        return type;
    }

    public String getUrl() {
        return url;
    }

    public int getPlaceHolder() {
        return placeHolder;
    }

    public ImageView getImgView() {
        return imgView;
    }

    public int getWifiStrategy() {
        return wifiStrategy;
    }

    public static class Builder {
        private int type;
        private String url;
        private int placeHolder;
        private ImageView imgView;
        private int wifiStrategy;

        public Builder() {
            this.type = ImageLoaderUtil.PIC_SMALL;
            this.url = "";
            this.placeHolder = R.mipmap.ic_launcher;
            this.imgView = null;
            this.wifiStrategy = ImageLoaderUtil.LOAD_STRATEGY_NORMAL;
        }

        public Builder type(int type) {
            this.type = type;
            return this;
        }

        public Builder url(String url) {
            this.url = url;
            return this;
        }

        public Builder placeHolder(int placeHolder) {
            this.placeHolder = placeHolder;
            return this;
        }

        public Builder imgView(ImageView imgView) {
            this.imgView = imgView;
            return this;
        }

        public Builder strategy(int strategy) {
            this.wifiStrategy = strategy;
            return this;
        }

        public ImageLoader build() {
            return new ImageLoader(this);
        }
    }
}

ImageLoader的公共訪問入口,以後使用的方式,將會是:

            ImageLoaderUtil.getInstance().loadImage(context, new ImageLoader.Builder()
                .url(url)
                .placeHolder(resourceId)
                .imgView(iv)
                .build());

如果需要其他圖片載入策略,比如PicassoImageLoaderStrategy使用Picasso框架來載入圖片,實現BaseImageLoaderStrategy的loadImage介面即可。