第7章 图片

0.463字数 12473阅读 1227

7.1 压缩图片

一、基础知识

1、图片的格式

  • jpg:最常见的图片格式。色彩还原度比较好,可以支持适当压缩后保持比较好的色彩度。使用jpg格式,可以使生成的图片大小比较小而不会使图片看起来很模糊(失真)。如果你图片颜色很多的,建议使用。
  • jpeg:与jpg格式相似,经常在拷相片的时候看到,但我们制作图片的时候一般是保存jpg,文件相对jpg较大,因为里面存储了很多相机里的拍摄参数,像色相、饱和度、对比度等,可用于直接打印和印刷(相对于jpg要好)
  • png:在网页中用的较多的一种格式,支持透明
格式类型 支持压缩 支持透明 支持动画 非矢量
jpg
jpeg
png

2、px、dp和dpi

  • px (pixels)像素:就是屏幕上实际的像素点单位。
  • dip或dp (device independent pixels):设备独立像素,与设备屏幕有关。
  • dpi(dot per inch):屏幕像素密度,每英寸多少像素。

换算公式:
dip = px * (dpi / 160)
DisplayMetrics#density = dpi / 160
DisplayMetrics#densityDpi = dpi

3、各种宽度之间的区别

ImageView#getWidth() 显示的ImageView控件的宽度。
ImageView#getMeasureWidth() 显示的ImageView控件的测量宽度,在布局之前计算出来的。
ImageView#getMinimumWidth() 显示的ImageView控件的最小宽度,是XML参数定义里的minWidth。

Drawable#getIntrinsicWidth()decode进来之后,没有进行缩放的值,若为BitmapDrawable则
getIntrinsicWidth() = ((BitmapDrawable)d).getBitmap().getWidth(),但是decode的时候,可能会根据图片所在的文件夹和设备屏幕dpi进行缩放,因此不一定等于原始图片的宽度。

注意:若切换横竖屏,组件的宽高会互换,但是图片本身的固有宽高不会变

//获取ImageView显示的图片在设备上的真实尺寸,注意调用的时机,一定要在layout完成之后
void getImgDisplaySize() {  
    Drawable imgDrawable = imageView.getDrawable();  
    if (imgDrawable != null) {  
        //获得ImageView中Image的真实宽高,等价于getIntrinsicWidth()
        int dw = mCurrentImage.getDrawable().getBounds().width();  
        int dh = mCurrentImage.getDrawable().getBounds().height();  
  
        //获得ImageView中Image的变换矩阵  
        Matrix m = mCurrentImage.getImageMatrix();  
        float[] values = new float[10];  
        m.getValues(values);  
  
        //Image在绘制过程中的变换矩阵,从中获得x和y方向的缩放系数,比如设置scaleType为centerCrop也会导致缩放 
        float sx = values[0];  
        float sy = values[4];  
  
        //计算Image在屏幕上实际绘制的宽高  
        realImgShowWidth = (int) (dw * sx);  
        realImgShowHeight = (int) (dh * sy);  
    }  
}  
ResourceId -> Uri
public static final String ANDROID_RESOURCE = "android.resource://";
public static final String FOREWARD_SLASH = "/";

private static Uri resourceIdToUri(Context context, int resourceId) {
    return Uri.parse(ANDROID_RESOURCE + context.getPackageName() + FOREWARD_SLASH + resourceId);
}
Uri -> InputStream

ContentResolver#openInputStream(Uri uri) InputStream

4、ScaleType

该属性用以表示显示图片的方式,默认值是FIT_CENTER
参考:图片说明Andorid中ImageView的不同属性ScaleType的区别

  • CENTER:图片按原来的大小居中显示
  • CENTER_CROP:等比例缩放,使得图片长(宽)大于等于ImageView的 长(宽),一定会充满ImageView
  • CENTER_INSIDE:图片居中显示;若图片比较大,按比例缩小使得长(宽)小于等于ImageView的 长(宽);若图片较小,直接居中显示。
  • FIT_CENTER FIT_START FIT_END:大图等比例缩小,使整幅图能够居中显示在ImageView中,小图等比例放大,同样
    要整体居中显示在ImageView中,显示居中/左上/右下
  • FIT_XY:不按比例缩放图片,把图片塞满整个View
  • MATRIX:由Matrix来决定,配合方法:ImageView#setImageMatrix (Matrix matrix)

二、图片的基本认识

1、图片的存在形式

  • 文件形式(即以二进制形式存在于硬盘上)
    获取大小(Byte):File.length()
  • 流的形式(即以二进制形式存在于内存中)
    获取大小(Byte):new FileInputStream(File).available()
    和文件形式获得的大小应该是一样的
  • Bitmap形式
    获取大小(Byte):Bitmap.getByteCount() 不准确

2、BitmapFactory.Options

用于解码Bitmap时的各种参数控制

2.1、inPreferredConfig

设置色彩模式。默认值是ARGB_8888,一个像素点占用4 bytes空间;一般对透明度不做要求的话,一般采用RGB_565(5+6+5=16)模式,一个像素点占用2 bytes。

bitmap占用内存大小=图片长度(px)*图片宽度(px)*单位像素占用的字节数

例如:若一张图片加载之后的宽高23684224,采用默认的色彩模式ARGB_8888,
则占用内存大小:2368
4224*4/1024/1024=38.15625MB。看到bitmap占用这么大,所以用完调用Bitmap.recycle()是个好习惯(推荐),不调用也没关系,因为GC进程会自动回收。

注意:如果inPreferredConfig不为null,解码器会尝试使用此参数指定的颜色模式来对图片进行解码,如果inPreferredConfig为null或者在解码时无法满足此参数指定的颜色模式,解码器会自动根据原始图片的特征以及当前设备的屏幕位深,选取合适的颜色模式来解码,例如,如果图片中包含透明度,那么对该图片解码时使
用的配置就需要支持透明度,默认会使用ARGB_8888来解码。

2.2、inJustDecodeBounds

若为true,那么在解码的时候将不会返回bitmap,只会返回这个bitmap的尺寸。这个属性的目的:如果你只想知道一个bitmap的尺寸,但又不想将其加载到内存时使用。这是一个非常有用的属性。

2.3、inSampleSize

这个值是一个int,当它小于1的时候,将会被当做1处理,如果大于1,那么就会按照比例(1 /inSampleSize)缩小bitmap的宽和高、降低分辨率,大于1时这个值将会被处置为2的倍数。例如,width=100,height=100,inSampleSize=2,那么就会将bitmap处理为,width=50,height=50,宽高降为1 / 2,像素数降为1 / 4。

2.4、inScaled、inDensity和inTargetDensity

inScaled:设置这个Bitmap是否可以被缩放,默认值是true,表示可以被缩放。

inDensity:
若图片放在drawable文件夹中,inDensity属性会根据drawable文件夹的分辨率来赋值,drawable文件夹(不指定分辨率,即文件夹名后不跟分辨率),则默认的inDensity就是160,对应关系如下:
ldpi -----> 120
mdpi -----> 160
hdpi -----> 240
xhdpi -----> 320
xxhdpi -----> 480
xxxhdpi -----> 640

inTartgetDensity:
会根据屏幕的像素密度来赋值,就是DisplayMetrics#densityDpi

输出图片的宽高 = 原图片的宽高 / inSampleSize * (inTargetDensity / inDensity)

注意:还与inScaled有关。若inJustDecodeBounds=true,将不受影响。
仅仅影响decodeResourcedecodeResourceStream方法,此时若inTargetDensity = 0,则设置为DisplayMetrics#densityDpi。若inDensity = 0,将被设置为上面表格的值
也就是说bitmap占用内存的大小,还与设备和所在的文件夹有关,因为宽高可能会进行缩放。

三、图片压缩

问:我们为什么要压缩图片呢?
答:一,避免占用内存过多。二,可能要上传图片,如果图片太大,浪费流量。(有时候需要上传原图除外)

1、避免内存过多的压缩方法

归根结底,图片是要显示在界面组件上的,所以还是要用到bitmap,从上面可得出Bitmap的在内存中的大小只和图片尺寸和色彩模式有关,那么要想改变Bitmap在内存中的大小,要么改变尺寸,要么改变色彩模式。

2、避免上传浪费流量的压缩方法

改变图片尺寸,改变色彩模式,改变图片质量都行。正常情况下,先改变图片尺寸和色彩模式,再改变图片质量。

注意:如果是Bitmap#compress(CompressFormat.PNG, quality, baos),这样的png格式,quality就没有作用了,bytes.length不会变化,因为png图片是无损的,不能进行压缩。
CompressFormat还有一个属性是,CompressFormat.WEBP格式,该格式是google自己推出来一个图片格式。

3、改变图片质量的压缩方法

它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的

/** 
 * 根据bitmap压缩图片质量 
 * @param bitmap 未压缩的bitmap 
 * @return 压缩后的bitmap 
 */  
public static Bitmap cQuality(Bitmap bitmap){  
    ByteArrayOutputStream bOut = new ByteArrayOutputStream();  
    int beginRate = 100;  
    //第一个参数 :图片格式 ,第二个参数: 图片质量,100为最高,0为最差 ,第三个参数:保存压缩后的数据的流  
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bOut);  
    while(bOut.size()/1024/1024>100){  //如果压缩后大于100Kb,则提高压缩率,重新压缩  
        beginRate -=10;  
        bOut.reset();  
        bitmap.compress(Bitmap.CompressFormat.JPEG, beginRate, bOut);  
    }  
    ByteArrayInputStream bInt = new ByteArrayInputStream(bOut.toByteArray());  
    Bitmap newBitmap = BitmapFactory.decodeStream(bInt);  
    if(newBitmap!=null){  
        return newBitmap;  
    }else{  
        return bitmap;  
    }  
}  

4、改变图片大小的压缩算法

4.1、采样率法
public static boolean getCacheImage(String filePath,String cachePath){  
    OutputStream out = null;  
    BitmapFactory.Options option = new BitmapFactory.Options();  
    option.inJustDecodeBounds = true;  //设置为true,只读尺寸信息,不加载像素信息到内存  
    Bitmap bitmap = BitmapFactory.decodeFile(filePath, option);  //此时bitmap为空  
    option.inJustDecodeBounds = false;  
    int bWidth = option.outWidth;  
    int bHeight= option.outHeight;  
    int toWidth = 400;  
    int toHeight = 800;  
    int be = 1;  //be = 1代表不缩放  
    if(bWidth/toWidth>bHeight/toHeight&&bWidth>toWidth){  
        be = (int)bWidth/toWidth;  
    }else if(bWidth/toWidth<bHeight/toHeight&&bHeight>toHeight){  
        be = (int)bHeight/toHeight;  
    }  
    option.inSampleSize = be; //设置缩放比例  
    bitmap  = BitmapFactory.decodeFile(filePath, option);  
    try {  
        out = new FileOutputStream(new File(cachePath));  
    } catch (IOException e) {  
        e.printStackTrace();  
    }  
    return bitmap.compress(CompressFormat.JPEG, 100, out);  
    }  
4.2、缩放法压缩(martix)
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
bm = Bitmap.createBitmap(bit, 0, 0, bit.getWidth(),
    bit.getHeight(), matrix, true);

4.3、RGB_565法
BitmapFactory.Options options2 = new BitmapFactory.Options();
options2.inPreferredConfig = Bitmap.Config.RGB_565;
bm = BitmapFactory.decodeFile(Environment
      .getExternalStorageDirectory().getAbsolutePath()
       + "/DCIM/Camera/test.jpg", options2);
4.4、createScaledBitmap(固定宽高,内部就是martix压缩法)
bm = Bitmap.createScaledBitmap(bit, 150, 150, true);

其实说白了,Bitmap压缩都是围绕这个来做文章:Bitmap所占用的内存 = 图片长度 x 图片宽度 x 一个像素点占用的字节数。3个参数,任意减少一个的值,就达到了压缩的效果。

5、质量和大小结合压缩

正常情况下我们应该把两者相结合的,所以有了下面的算法(在项目中直接用,清晰度在手机上没问题)

public static File scal(Uri fileUri){  
    String path = fileUri.getPath();  
    File outputFile = new File(path);  
    long fileSize = outputFile.length();  
    final long fileMaxSize = 200 * 1024;  
    if (fileSize >= fileMaxSize) {  
        BitmapFactory.Options options = new BitmapFactory.Options();  
        options.inJustDecodeBounds = true;  
        BitmapFactory.decodeFile(path, options);  
        int height = options.outHeight;  
        int width = options.outWidth;  
  
        double scale = Math.sqrt((float) fileSize / fileMaxSize);  
        options.outHeight = (int) (height / scale);  
        options.outWidth = (int) (width / scale);  
        options.inSampleSize = (int) (scale + 0.5);  
        options.inJustDecodeBounds = false;  
  
        Bitmap bitmap = BitmapFactory.decodeFile(path, options);  
        outputFile = new File(PhotoUtil.createImageFile().getPath());  
        FileOutputStream fos = null;  
        try {  
            fos = new FileOutputStream(outputFile);  
            bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fos);  
            fos.close();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
        Log.d("", "sss ok " + outputFile.length());  
        if (!bitmap.isRecycled()) {  
            bitmap.recycle();  
        }else{  
            File tempFile = outputFile;  
            outputFile = new File(PhotoUtil.createImageFile().getPath());  
            PhotoUtil.copyFileUsingFileChannels(tempFile, outputFile);  
        }  
    }  
    return outputFile;  
}  

public static Uri createImageFile(){  
    // Create an image file name  
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());  
    String imageFileName = "JPEG_" + timeStamp + "_";  
    File storageDir = Environment.getExternalStoragePublicDirectory(  
        Environment.DIRECTORY_PICTURES);  
    File image = null;  
    try {  
        image = File.createTempFile(  
            imageFileName,  /* prefix */  
            ".jpg",         /* suffix */  
            storageDir      /* directory */  
        );  
    } catch (IOException e) {  
        e.printStackTrace();  
    }  
    // Save a file: path for use with ACTION_VIEW intents  
    return Uri.fromFile(image);  
}  

public static void copyFileUsingFileChannels(File source, File dest){  
    FileChannel inputChannel = null;  
    FileChannel outputChannel = null;  
    try {  
        try {  
            inputChannel = new FileInputStream(source).getChannel();  
            outputChannel = new FileOutputStream(dest).getChannel();  
            outputChannel.transferFrom(inputChannel, 0, inputChannel.size());  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    } finally {  
        try {  
            inputChannel.close();  
            outputChannel.close();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
}  

四、高清加载长图或大图方案 —— 局部加载

在Android开发中,加载图片是很常见的情况,我们一般选择传统的加载图片框架如universalimageloader,picasso,fresco等。但是加载巨图怎么解决,就是一个图片很大,比如清明上河图,世界地图等,一个屏幕显示不完,又不能缩小,压缩,该怎么解决?

android早就给我们解决方案 —— BitmapRegionDecoder。这个类就是用来显示指定区域的图像,当原始图像大,你只需要部分图像时,BitmapRegionDecoder特别有用

1、使用

最主要的就是BitmapRegionDecoder#newInstance方法获取一个对象,然后通过这个对象去调用decodeRegion(Rect rect, BitmapFactory.Options options)得到Bitmap,最后就可以
显示在屏幕上了。考虑到用户可以触摸移动图像,我们用手势控制器GestureDetector来控制图片显示的区域。

2、方法

  • BitmapRegionDecoder.newInstance(InputStream is, boolean isShareable) BitmapRegionDecoder
  • decodeRegion(Rect rect, BitmapFactory.Options options) Bitmap

3、实例

public class LargeImageView extends View implements GestureDetector.OnGestureListener {
    private final String TAG = this.getClass().getSimpleName();

    private BitmapRegionDecoder mDecoder;

    //绘制的区域
    private volatile Rect mRect = new Rect();

    private int mScaledTouchSlop;

    // 分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;

    //图片的宽度和高度
    private int mImageWidth, mImageHeight;
    //手势控制器
    private GestureDetector mGestureDetector;
    private BitmapFactory.Options options;

    public LargeImageView(Context context) {
        super(context);
        init(context, null);
    }

    public LargeImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public LargeImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        //设置显示图片的参数,如果对图片质量有要求,就选择ARGB_8888模式
        options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;


        mScaledTouchSlop = ViewConfiguration.get(getContext())
                .getScaledTouchSlop();
        Log.d(TAG, "sts:" + mScaledTouchSlop);
        //初始化手势控制器
        mGestureDetector = new GestureDetector(context, this);

        //获取图片的宽高
        InputStream is = null;
        try {
            is = context.getResources().getAssets().open("timg.jpg");
            //初始化BitmapRegionDecode,并用它来显示图片
            mDecoder = BitmapRegionDecoder
                    .newInstance(is, false);
            BitmapFactory.Options tmpOptions = new BitmapFactory.Options();
            // 设置为true则只获取图片的宽高等信息,不加载进内存
            tmpOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(is, null, tmpOptions);
            mImageWidth = tmpOptions.outWidth;
            mImageHeight = tmpOptions.outHeight;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //把触摸事件交给手势控制器处理
        return mGestureDetector.onTouchEvent(event);
    }

    @Override
    public boolean onDown(MotionEvent e) {
        mLastX = (int) e.getRawX();
        mLastY = (int) e.getRawY();
        return true;
    }

    @Override
    public void onShowPress(MotionEvent e) {

    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        int x = (int) e2.getRawX();
        int y = (int) e2.getRawY();
        move(x, y);
        return true;
    }

    /**
     * 移动的时候更新图片显示的区域
     *
     * @param x
     * @param y
     */
    private void move(int x, int y) {
        int deltaX = x - mLastX;
        int deltaY = y - mLastY;
        Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY);
        //如果图片宽度大于屏幕宽度
        if (mImageWidth > getWidth()) {
            //移动rect区域
            mRect.offset(-deltaX, 0);
            //检查是否到达图片最右端
            if (mRect.right > mImageWidth) {
                mRect.right = mImageWidth;
                mRect.left = mImageWidth - getWidth();
            }

            //检查左端
            if (mRect.left < 0) {
                mRect.left = 0;
                mRect.right = getWidth();
            }
            invalidate();
        }
        //如果图片高度大于屏幕高度
        if (mImageHeight > getHeight()) {
            mRect.offset(0, -deltaY);

            //是否到达最底部
            if (mRect.bottom > mImageHeight) {
                mRect.bottom = mImageHeight;
                mRect.top = mImageHeight - getHeight();
            }

            if (mRect.top < 0) {
                mRect.top = 0;
                mRect.bottom = getHeight();
            }
            //重绘
            invalidate();
        }
        mLastX = x;
        mLastY = y;
    }

    @Override
    public void onLongPress(MotionEvent e) {
        mLastX = (int) e.getRawX();
        mLastY = (int) e.getRawY();
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        int x = (int) e2.getRawX();
        int y = (int) e2.getRawY();
        move(x, y);
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //显示图片
        Bitmap bm = mDecoder.decodeRegion(mRect, options);
        canvas.drawBitmap(bm, 0, 0, null);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = getMeasuredWidth();
        int height = getMeasuredHeight();

        int imageWidth = mImageWidth;
        int imageHeight = mImageHeight;

        //默认显示图片的中心区域,开发者可自行选择
        mRect.left = imageWidth / 2 - width / 2;
        mRect.top = imageHeight / 2 - height / 2;
        mRect.right = mRect.left + width;
        mRect.bottom = mRect.top + height;
    }
}

五、BitmapShader —— 实现圆形、圆角图片

1、相关方法

BitmapShader继承自Shader,在给Paint设置了Shader之后,Paint就类似于PS里面的笔刷,刷出来的是设置的Bitmap。

涉及的相关方法:

  • BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY)
  • Shader#setLocalMatrix(@Nullable Matrix localM)
  • Paint#setShader(Shader shader)

2、TileMode的取值

CLAMP 拉伸
REPEAT 重复
MIRROR 镜像

如果大家给电脑屏幕设置屏保的时候,如果图片太小,可以选择重复、拉伸、镜像;
重复:就是横向、纵向不断重复这个bitmap
镜像:横向不断翻转重复,纵向不断翻转重复;
拉伸:这个和电脑屏保的模式应该有些不同,这个拉伸的是图片最后的那一个像素;横向的最后一个横行像素,不断的重复,纵项的那一列像素,不断的重复;

现在大概明白了,BitmapShader通过设置给mPaint,然后用这个mPaint绘图时,就会根据你设置的TileMode,对绘制区域进行着色。
这里需要注意一点:就是BitmapShader是从你的画布的左上角开始绘制的,不在view的右下角绘制个正方形,它不会在你正方形的左上角开始。

7.2 Glide讲解

一、简介

在泰国举行的谷歌开发者论坛上,谷歌为我们介绍了一个名叫Glide的图片加载库,作者是bumptech。这个库被广泛的运用在Google的开源项目中,包括2014年Google I/O大会上发布的官方App。

Glide是一款由Bump Technologies开发的图片加载框架,使得我们可以在Android平台上以极度简单的方式加载和展示图片。Glide默认使用HttpUrlConnection进行网络请求,为了让App保持一致的网络请求形式,可以让Glide使用我们指定的网络请求形式请求网络资源。

二、依赖

1.jar包

Github地址:https://github.com/bumptech/glide/releases

2.Gradle

dependencies {  
    compile 'com.github.bumptech.glide:glide:3.7.0'  
    compile 'com.android.support:support-v4:23.3.0'  
}

三、权限

<uses-permission android:name="android.permission.INTERNET" />

四、混淆

-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
    **[] $VALUES;
    public *;
}

五、使用

  • Glide.with(context).load(imageUrl).into(imageView); //从URL中加载
  • Glide.with(context).load(R.mipmap.ic_launcher).into(imageView); //从Res资源中加载
  • File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Running.jpg");
    Glide.with(context).load(file).into(imageView); //从文件加载
  • Uri uri = resourceIdToUri(context, R.mipmap.ic_launcher);
    Glide.with(context).load(uri).into(imageView);//从Uri加载

Glide.with()方法用于创建一个加载图片的实例。with()方法可以接收Context、Activity、Fragment或者FragmentActivity类型的参数。特别需要注意的是with()方法中传入的实例会决定Glide加载图片的生命周期,如果传入的是Activity、Fragment或者FragmentActivity的实例,那么当其被销毁时图片加载也会停止,如果传入的是ApplicationContext时只有当应用程序被杀掉的时候图片加载才会停止。

使用Glide加载图片不用担心内存浪费,甚至是内存溢出的问题。因为Glide不会直接将图片的完整尺寸全部加载到内存中,而是用多少加载多少。Glide会自动判断ImageView的大小,然后只将这么大的图片像素加载到内存当中,帮助我们节省内存开支。

下面一个小的工具函数可以将资源id转换为一个Uri:

public static final String ANDROID_RESOURCE = "android.resource://";
public static final String FOREWARD_SLASH = "/";
private static Uri resourceIdToUri(Context context, int resourceId) {
    return Uri.parse(ANDROID_RESOURCE + context.getPackageName() + FOREWARD_SLASH + resourceId);
}

六、方法

1、指定图片格式

如果调用了.asBitmap()方法,则.load()中的参数指向的可以是一个静态图片也可以是GIF图片,如果是一张GIF图片,则加载之后只会展示GIF图片的第一帧。
如果调用的.asGif()方法,则.load()方法中的参数指向的必须是一个GIF图片,如果是一张静态图片,则图片加载完成之后展示的只是出错占位符(如果没有设置出错占位符,则什么也不展示)。

//显示静态图片(若加载的是gif图那么就会显示第一帧的图片)
.asBitmap()
//显示动态图片(若加载的是静态图会加载失败)
.asGif()

2、指定占位图显示

//加载时显示的图片
.placeholder(R.drawable.image_load)
//加载失败时显示的图片
.error(R.drawable.image_error)

3、设置缓存

Android应用中一个较好的图片处理框架,会最小化网络请求的消耗。Glide也是一样,默认使用内存和磁盘缓存来避免不必要的网络请求。然而,如果你的图片变化的非常快,你需要禁止一些缓存。
比如你请求一个1000x1000像素的图片,你的ImageView是500x500像素,Glide会保存两个版本的图片到缓存里。

//禁止内存缓存,但仍然会缓存到磁盘
.skipMemoryCache(true)
//禁止磁盘缓存(Glide默认缓存策略是:DiskCacheStrategy.RESULT)
.diskCacheStrategy(DiskCacheStrategy.NONE)
//缓存参数
//ALL:缓存源资源和转换后的资源(即缓存所有版本图像,默认行为)
//NONE:不作任何磁盘缓存,然而默认的它将仍然使用内存缓存
//SOURCE:仅缓存源资源(原来的全分辨率的图像),上面例子里的1000x1000像素的图片
//RESULT:缓存转换后的资源(最终的图像,即降低分辨率后的或者是转换后的)

如果你有一个图片你需要经常处理它,会生成各种不同的版本的图片,缓存它的原始的分辨率图片才有意义。我们
使用DiskCacheStrategy.SOURCE去告诉Glide只缓存原始版本:

Glide.with(context).load("url").diskCacheStrategy(DiskCacheStrategy.SOURCE).into(imageView);

4、设置加载尺寸

Glide在缓存和内存里自动限制图片的大小去适配ImageView的尺寸。用Glide时,如果图片不需要自动适配ImageView,调用override(horizontalSize, verticalSize),
它会在将图片显示在ImageView之前调整图片的大小。

//加载图片为100*100像素的尺寸
.override(100, 100)

5、设置图片缩放

如果调用了.centerCrop()方法,则显示图片的时候短的一边填充容器,长的一边跟随缩放;
如果调用了.fitCenter()方法,则显示图片的时候长的一边填充容器,短的一边跟随缩放;
这两个方法可以都调用,如果都调用,则最终显示的效果是后调用的方法展示的效果。

//它是一个裁剪技术,即缩放图像让它填充到ImageView界限内并且裁剪额外的部分,ImageView可能会完全填充,但图像可能不会完整显示
.centerCrop()
//它是一个裁剪技术,即缩放图像让图像都测量出来等于或小于ImageView的边界范围,该图像将会完全显示,但可能不会填满整个ImageView
.fitCenter()
Glide
    .with(context)
    .load(UsageExampleListViewAdapter.eatFoodyImages[0])
    .override(600, 200) // resizes the image to these dimensions (in pixel)
    .centerCrop() // this cropping technique scales the image so that it fills the requested bounds and then crops the extra.
    .into(imageViewResizeCenterCrop);

6、设置资源加载优先级

假设你正在创建一个信息展示界面,包含顶部的一个主要照片,还有底部的2个并不重要的小图。对于用户体验,我们最好先加载主角照片,然后再加载底部不紧急的图片。Glide里的.priority()方法和Priority的枚举变量支持你的想法。

Priority枚举变量,以递增方式列出:

  • Priority.LOW
  • Priority.NORMAL
  • Priority.HIGH
  • Priority.IMMEDIATE

你应当明白优先级并不是非常严格的。Glide会将它们作为一个指导来最优化处理请求。但并不意味着所有的图片都能够按请求的顺序加载。

.priority(Priority.HIGH)

7、设置圆角或圆形图片

//圆角图片
.transform(new GlideRoundTransform(this))
//圆形图片
.transform(new GlideCircleTransform(this))

8、设置缩略图

缩略图的优点

缩略图不同于前面提到的占位图。占位图应当是跟app绑定在一起的资源。缩略图是一个动态的占位图,可以从网络加载。缩略图也会被先加载,直到实际图片请求加载完毕。如果因为某些原因,缩略图获得的时间晚于原始图片,它并不会替代原始图片,而是简单地被忽略掉。

提示:另外一个非常棒的平滑图片显示的方法,通过加载图片主色调的占位图。

Glide提供了2个不同的方法产生缩略图。

第一种:简单的缩略图

通过在加载的时候指定一个小的分辨率,产生一个缩略图。这个方法在ListView和详细视图的组合中非常有用。如果你已经在ListView中用到了250x250像素的图片,那么在在详细视图中会需要一个更大分辨率的图片。然而从用户的角度,我们已经看见了一个小版本的图片,为什么需要好几秒,同样的图片(高分辨率的)才能被再次加载出来呢?
在这种情况下,从显示250x250像素版本的图片平滑过渡到详细视图里查看大图更有意义。Glide里的.thumbnail()方法让这个变为可能。这里,.thumbnal()的参数是一个(0,1)之间浮点数:

Glide.with(context).load("url")
    .thumbnail(0.1f)//系数需在(0,1)之间,这样会先加载缩略图然后加载全图
    .into(imageView);

这里传递一个0.1f作为参数,Glide会加载原始图片大小的10%的图片。如果原始图片有1000x1000像素,缩略图的分辨率为100x100像素。由于图片将会比ImageView小,你需要确保缩放类型是否正确。
注意到你所有的请求设置都会影响到你的缩略图。例如,如果你使用了一个变换让你的图片变为灰度图,缩略图也同样将会是灰度图。

第二种:高级缩略图请求(原图与缩略图完全不同 )

.thumbnail()传入一个浮点类型的参数,非常简单有效,但并不是总是有意义。如果缩略图需要从网络加载同样全分辨率图片,可能根本都不快。这样,Glide提供了另一个方法去加载和显示缩略图:传递一个新的Glide请求作为参数。

private void loadImageThumbnailRequest() {  
    // setup Glide request without the into() method
    DrawableRequestBuilder<String> thumbnailRequest = Glide
        .with( context )
        .load( eatFoodyImages[2] );

    // pass the request as a a parameter to the thumbnail request
    Glide
        .with( context )
        .load( UsageExampleGifAndVideos.gifUrl )
        .thumbnail( thumbnailRequest )
        .into( imageView3 );
}

区别在于第一个缩略图请求是完全独立于第二个原始请求的。缩略图可以来自不同资源或者图片URL,你可以在它上面应用不同的变换。

9、设置动画

加载图片时所展示的动画,可以是Animator类型的属性动画,也可以是int类型的动画资源。这个动画只在第一次加载的时候会展示,以后都会从缓存中获取图片,因此也就不会展示动画了(图片的改变时才会有用)。

//设置加载动画
.animate(R.anim.alpha_in)
//实现ViewPropertyAnimation.Animator接口
.animate(ViewPropertyAnimation.Animator animator)
//淡入淡出动画,也是默认动画,动画默认的持续时间是300毫秒。类似:.crossFade(int duration)
.crossFade() 
//移除所有动画
.dontAnimate()
ViewPropertyAnimation.Animator animationObject = new ViewPropertyAnimation.Animator() {  
    @Override
    public void animate(View view) {
        // if it's a custom view class, cast it here
        // then find subviews and do the animations
        // here, we just use the entire view for the fade animation
        view.setAlpha( 0f );

        ObjectAnimator fadeAnim = ObjectAnimator.ofFloat( view, "alpha", 0f, 1f );
        fadeAnim.setDuration( 2500 );
        fadeAnim.start();
    }
};

10、加载本地视频(相当于一张缩略图)

//只能加载本地视频(显示的只是视频的第一帧图像,相当于一张缩略图,不能播
//放视频),网络视频无法加载。如果你想要从网络URL播放视频,参考VideoView
String files = Environment.getExternalStorageDirectory().getAbsolutePath() + "/glide.avi";
Glide.with(this).load(files).into(view);

11、定制view中使用SimpleTarget和ViewTarget

Glide中的回调:Target

假设我们并没有ImageView作为图片加载的目标。我们只需要Bitmap本身。Glide提供了一个用Target获取Bitmap资源的方法。Target只是用来回调,它会在所有的加载和处理完毕时返回想要的结果。

Glide提供了多种多样有各自明确目的Target。先从SimpleTarget介绍。

SimpleTarget
private SimpleTarget target = new SimpleTarget<Bitmap>() {  
    @Override
    public void onResourceReady(Bitmap bitmap, GlideAnimation glideAnimation) {
        // do something with the bitmap
        // for demonstration purposes, let's just set it to an ImageView
        imageView1.setImageBitmap( bitmap );
    }
};

    Glide
        .with(context) // could be an issue!
        .load(eatFoodyImages[0])
        .asBitmap()
        .into(target);

使用Target注意事项:

  • 第一个是SimpleTarget对象的定义。java/Android可以允许你在.into()内匿名定义,但这会显著增加在Glide处理完
    图片请求前Android垃圾回收清理匿名target对象的可能性。最终,会导致图片被加载了,但是回调永远不会被调用。
    所以,请确保将你的回调定义为一个字段对象,防止被万恶的Android垃圾回收给清理掉。
  • 第二个关键部分是Glide的.with( context )。这个问题实际上是Glide一个特性问题:当你传递了一个context,例如
    当前app的activity,当activity停止后,Glide会自动停止当前的请求。这种整合到app生命周期内是非常有用的,但也
    是很难处理的。如果你的target是独立于app的生命周期。这里的解决方案是使用application的context:.with(context.getApplicationContext())。当app自己停止运行的时候,Glide会只取消掉图片的请求。
特定大小的Target

另外一个潜在问题是Target没有一个明确的大小。如果你传递一个ImageView作为.into()的参数,Glide会使用ImageView的
大小来限制图片的大小。例如如果要加载的图片是1000x1000像素,但是ImageView的尺寸只有250x250像素,Glide会降低图片到小尺寸,以节省处理时间和内存。显然,由于target没有具体大小,这对target并不起效。但是,如果你有个期望的具体大小,你可以增强回调。如果你知道图片应当为多大,那么在你的回调定义里应当指明,以节省内存:

private SimpleTarget target2 = new SimpleTarget<Bitmap>(250, 250) {  
    @Override
    public void onResourceReady(Bitmap bitmap, GlideAnimation glideAnimation) {
        imageView2.setImageBitmap(bitmap);
    }
};
ViewTarget

有很多原因导致我们不能直接使用ImageView。前面已经介绍了如何获取Bitmap。现在,我们将更深入学习。假设你有个自定义的View。由于没有已知的方法在哪里设置图片,Glide并不支持加载图片到定制的View内。然而用ViewTarget会让这个更简单。

让我们看一个简单的定制View,它继承于FrameLayout,内部使用了一个ImageView:

public class FutureStudioView extends FrameLayout {  
    ...
    public void setImage(Drawable drawable) {
        iv = (ImageView) findViewById(R.id.custom_view_image);
        iv.setImageDrawable(drawable);
    }
}

由于我们定制的View并不是继承自ImageView,这里不能使用常规的.into()方法。因此,我们只能创建一个ViewTarget,用来传递给.into()方法:

void loadImageViewTarget() {  
    FutureStudioView customView = (FutureStudioView) findViewById(R.id.custom_view);

    viewTarget = new ViewTarget<FutureStudioView, GlideDrawable>(customView) {
        @Override
        public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> glideAnimation) {
            this.view.setImage(resource.getCurrent());
        }
    };

    Glide.with(context.getApplicationContext()) // safer!
        .load(eatFoodyImages[2])
        .into(viewTarget);
}

在target的回调方法中,我们在定制view上使用我们创建的setImage(Drawable drawable)方法设置图片。同时,确保你注意到我们已经在ViewTarget的构造方法里传递了我们的定制view:new ViewTarget<FutureStudioView, GlideDrawable>(customView)。

12、设置监听请求接口

首先,创建一个listener作为一个字段对象,避免被垃圾回收:

private RequestListener<String, GlideDrawable> requestListener 
    = new RequestListener<String, GlideDrawable>() {  
    @Override
    public boolean onException(Exception e, String model, 
    Target<GlideDrawable> target, boolean isFirstResource) {
        // todo log exception
        // important to return false so the error placeholder can be placed
        //加载异常
        return false;
    }

    @Override
    public boolean onResourceReady(GlideDrawable resource, String model, 
    Target<GlideDrawable> target, boolean isFromMemoryCache, 
    boolean isFirstResource) {
            //加载成功
            //view.setImageDrawable(resource);
        return false;
    }
};

在onException方法中,你可以抓取问题,并决定你需要做什么,比如记录日志。如果Glide应当处理这个后果,比如显示一个出错占位图,在onException方法中返回false是很重要的。

Glide  
    .with( context )
    .load(UsageExampleListViewAdapter.eatFoodyImages[0])
    .listener( requestListener )
    .error( R.drawable.cupcake )
    .into( imageViewPlaceholder );

只有在listener的onException方法里返回false,R.drawable.cupcake才会显示出来。

13、设置取消或恢复请求

以下两个方法是为了保证用户界面的滑动流畅而设计的。当在ListView中加载图片的时候,如果用户滑动ListView的时候继续加载图片,就很有可能造成滑动不流畅、卡顿的现象,这是由于Activity需要同时处理滑动事件以及Glide加载图片。Glide为我们提供了这两个方法,让我们可以在ListView等滑动控件滑动的过程中控制Glide停止加载或继续加载,可以有效的保证界面操作的流畅。

//当列表在滑动的时候可以调用pauseRequests()取消请求
Glide.with(context).pauseRequests();
//当列表滑动停止时可以调用resumeRequests()恢复请求
Glide.with(context).resumeRequests();

// ListView滑动时触发的事件
lv.setOnScrollListener(new AbsListView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        switch (scrollState) {
            case AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
            case AbsListView.OnScrollListener.SCROLL_STATE_FLING:
                // 当ListView处于滑动状态时,停止加载图片,保证操作界面流畅
                Glide.with(MainActivity.this).pauseRequests();
                break;
            case AbsListView.OnScrollListener.SCROLL_STATE_IDLE:
                // 当ListView处于静止状态时,继续加载图片
                Glide.with(MainActivity.this).resumeRequests();
                break;
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    }
});

14、获取缓存大小

new GetDiskCacheSizeTask(textView).execute(new File(getCacheDir(), DiskCache.Factory.DEFAULT_DISK_CACHE_DIR));

private class GetDiskCacheSizeTask extends AsyncTask<File, Long, Long> {

    private final TextView resultView;

    public GetDiskCacheSizeTask(TextView resultView) {
        this.resultView = resultView;
    }

    @Override
    protected void onPreExecute() {
        resultView.setText("Calculating...");
    }

    @Override
    protected void onProgressUpdate(Long... values) {
        super.onProgressUpdate(values);
    }

    @Override
    protected Long doInBackground(File... dirs) {
        try {
            long totalSize = 0;
            for (File dir : dirs) {
                publishProgress(totalSize);
                totalSize += calculateSize(dir);
            }
            return totalSize;
        } catch (RuntimeException ex) {
            final String message = String.format("Cannot get size of %s: %s", Arrays.toString(dirs), ex);
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    resultView.setText("error");
                    Toast.makeText(resultView.getContext(), message, Toast.LENGTH_LONG).show();
                }
            });
        }
        return 0L;
    }

    @Override
    protected void onPostExecute(Long size) {
        String sizeText = android.text.format.Formatter.formatFileSize(resultView.getContext(), size);
        resultView.setText(sizeText);
    }

    private long calculateSize(File dir) {
        if (dir == null) return 0;
        if (!dir.isDirectory()) return dir.length();
        long result = 0;
        File[] children = dir.listFiles();
        if (children != null)
            for (File child : children)
                result += calculateSize(child);
        return result;
    }
}

15、清除内存缓存

//可以在UI主线程中进行
Glide.get(this).clearMemory();

16、清除磁盘缓存

//需要在子线程中执行
Glide.get(this).clearDiskCache();

17、图片裁剪、模糊、滤镜等处理

变换

在图片显示出之前可以对图片进行变换处理。例如,如果你的app需要显示一张灰度图,但只能获取到一个原始全色彩的版本,你可以使用一个变换去将图片从有明艳色彩的版本转换成惨淡的黑白版。不要误会我们,变换不仅限于颜色。你可以改变图片的很多属性:大小、边框、色彩、像素点,等等!在之前介绍用Glide调整图片大小时,已经介绍了自带的两个
变换fitCenter和 centerCrop。这两个方案都有一个显著的特征,他们有他们自己的Glide转换方法,所以,这篇文章不再介绍了。

实现自己的变换

为了实现你自己自定义的变换,你需要创建一个新的类去实现变换接口。这个方法需要实现的内容还是相当复杂的,你需要深入探索Glide的内部结构才能让其工作好。如果你只是想要常规的图片(不包括Gif和视频)变换,我们建议只要处理抽象的BitmapTransformation类。它简化了相当多的实现,能覆盖95%的使用范围。

所以,让我们先看一下BitmapTransformation实现的一个例子。用Renderscript去模糊图片。我们可以用之前用过的代码去实现一个Glide变换。我们的框架必须继承BitmapTransformation类:

public class BlurTransformation extends BitmapTransformation {

    public BlurTransformation(Context context) {
        super( context );
    }

    @Override
    protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
        return null; // todo
    }

    @Override
    public String getId() {
        return null; // todo
    }
}

现在,我们用前面文章的代码,借助Renderscript来实现图片的模糊处理。

public class BlurTransformation extends BitmapTransformation {

    private RenderScript rs;

    public BlurTransformation(Context context) {
        super( context );

        rs = RenderScript.create( context );
    }

    @Override
    protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
        Bitmap blurredBitmap = toTransform.copy( Bitmap.Config.ARGB_8888, true );

        // Allocate memory for Renderscript to work with
        Allocation input = Allocation.createFromBitmap(
            rs, 
            blurredBitmap, 
            Allocation.MipmapControl.MIPMAP_FULL, 
            Allocation.USAGE_SHARED
        );
        Allocation output = Allocation.createTyped(rs, input.getType());

        // Load up an instance of the specific script that we want to use.
        ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
        script.setInput(input);

        // Set the blur radius
        script.setRadius(10);

        // Start the ScriptIntrinisicBlur
        script.forEach(output);

        // Copy the output to the blurred bitmap
        output.copyTo(blurredBitmap);

        toTransform.recycle();

        return blurredBitmap;
    }

    @Override
    public String getId() {
        return "blur";
    }
}

getId()方法为这个变换描述了一个独有的识别。Glide使用那个关键字作为缓存系统的一部分。防止出现异常问题,确保其唯一。

应用一个简单的变换

Glide有两个不同的方式进行变换。第一个是传递一个你的类的实例作为.transform()的参数。不管是图片还是gif,都可以进行变换。另一个则是使用.bitmapTransform(),
它只接受bitmap的变换。由于我们的实现都是基于bitmap,我们可以使用第一个:

Glide.with(context)
    .load(eatFoodyImages[0])
    .transform(new BlurTransformation(context))
    //.bitmapTransform(new BlurTransformation(context)) // this would work too!
    .into(imageView1);

这足够让Glide从网络下载的图片自动实现模糊算法。非常有用!

实现多重变换

通常,Glide的流接口(fluent interface)允许方法被连接在一起,然而变换并不是这样的。确保你只调用.transform()或者.bitmapTransform()一次,不然,之前的设置将会被覆盖!然而,你可以通过传递多个转换对象当作参数到.transform()(或者.bitmapTransform())中来进行多重变换:

Glide.with(context)
    .load(eatFoodyImages[1])
    .transform(new GreyscaleTransformation(context), new BlurTransformation(context))
    .into(imageView2);

在这段代码中,我们先对图片进行了灰度变换,然后模糊处理。Glide会为你自动进行两个转换。牛逼吧!

提示:当你使用变换的时候,你不能使用.centerCrop()或者.fitCenter()。

Glide的变换集合

如果你已经对你的app里要用什么变换有了想法,在花点时间看看下面的库吧:Glide-transformations(https://github.com/wasabeef/glide-transformations)。它提供了许多变换的集合。值得去看一下你的idea是否已经被实现了。

这个库有2个不同版本。扩展库包括更多的变换,并且是用手机的GPU进行计算。需要一个额外的依赖,所以这两个版本的设置还有点不一样。你应当看看支持的变换的列表,再决定你需要用哪个版本。

Glide变换的设置

设置是很简单的!对于基本版,你可以在你的build.gradle里加一行:

dependencies {  
    compile 'jp.wasabeef:glide-transformations:2.0.0'
}

如果你想要使用GPU变换:

repositories {  
    jcenter()
    mavenCentral()
}

dependencies {  
    compile 'jp.wasabeef:glide-transformations:2.0.0'
    compile 'jp.co.cyberagent.android.gpuimage:gpuimage-library:1.3.0'
}

Glide变换的使用

在你同步了Android Studio的builde.gradle文件后,你已经可以进行使用变换集合了。使用方式与使用自定义变换一样。假如我们要用glide变换集合去模糊图片:

Glide  
    .with( context )
    .load( eatFoodyImages[2] )
    .bitmapTransform( new jp.wasabeef.glide.transformations.BlurTransformation( context, 25 ) )
    .into( imageView3 );

你也可以像上面一样应用一组变换。一个单独的变换或者一组变换,.bitmapTransform()都可以接受!

示例:圆角处理

 Glide.with(mContext)
    .load(R.drawable.image_example)
    .bitmapTransform(new RoundedCornersTransformation(mContext, 30, 0, RoundedCornersTransformation.CornerType.BOTTOM))
    .into(imageView);

可实现Transformation接口,进行更灵活的图片处理,如进行简单地圆角处理。

public class RoundedCornersTransformation implements Transformation<Bitmap> {

    private BitmapPool mBitmapPool;
    private int mRadius;

    public RoundedCornersTransformation(Context context, int mRadius) {
        this(Glide.get(context).getBitmapPool(), mRadius);
    }

    public RoundedCornersTransformation(BitmapPool mBitmapPool, int mRadius) {
        this.mBitmapPool = mBitmapPool;
        this.mRadius = mRadius;
    }

    @Override
    public Resource<Bitmap> transform(Resource<Bitmap> resource, int outWidth, int outHeight) {
        //从其包装类中拿出Bitmap
        Bitmap source = resource.get();
        int width = source.getWidth();
        int height = source.getHeight();
        Bitmap result = mBitmapPool.get(width, height, Bitmap.Config.ARGB_8888);
        if (result == null) {
            result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        }
        Canvas canvas = new Canvas(result);
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setShader(new BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
        canvas.drawRoundRect(new RectF(0, 0, width, height), mRadius, mRadius, paint);
        //返回包装成Resource的最终Bitmap
        return BitmapResource.obtain(result, mBitmapPool);
    }

    @Override
    public String getId() {
        return "com.wiggins.glide.widget.GlideCircleTransform(radius=" + mRadius + ")";
    }
}

自定义图片处理时为了避免创建大量Bitmap以及减少GC,可以考虑重用Bitmap,这就需要使用BitmapPool,例如从Bitmap池中取一个Bitmap,用这个Bitmap生成一个Canvas,然后在这个Canvas上画初始的Bitmap并使用Matrix、Paint或者Shader处理这张图片。为了有效并正确重用Bitmap需要遵循以下三条准则:

  • 永远不要把transform()传给你的原始resource或原始Bitmap给recycle()了,更不要放回BitmapPool,因为这些都自动完成了。值得注意的是,任何从BitmapPool取出的用于自定义图片变换的辅助Bitmap,如果不经过transform()方法返回,就必须主动放回BitmapPool或者调用recycle()回收。
  • 如果你从BitmapPool拿出多个Bitmap或不使用你从BitmapPool拿出的一个Bitmap,一定要返回extras给BitmapPool。
  • 如果你的图片处理没有替换原始resource(例如由于一张图片已经匹配了你想要的尺寸,你需要提前返回),transform()方法就返回原始resource或原始Bitmap。例如:
private static class MyTransformation extends BitmapTransformation {
    public MyTransformation(Context context) {
        super(context);
    }

    @Override
    protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
        Bitmap result = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888);
        // 如果BitmapPool中找不到符合该条件的Bitmap,get()方法会返回null,就需要我们自己创建Bitmap了
        if (result == null) {
            // 如果想让Bitmap支持透明度,就需要使用ARGB_8888
            result = Bitmap.createBitmap(outWidth, outHeight, Bitmap.Config.ARGB_8888);
        }
        //创建最终Bitmap的Canvas.
        Canvas canvas = new Canvas(result);
        Paint paint = new Paint();
        paint.setAlpha(128);
        // 将原始Bitmap处理后画到最终Bitmap中
        canvas.drawBitmap(toTransform, 0, 0, paint);
        // 由于我们的图片处理替换了原始Bitmap,就return我们新的Bitmap就行。
        // Glide会自动帮我们回收原始Bitmap。
        return result;
    }

    @Override
     public String getId() {
         // Return some id that uniquely identifies your transformation.
         return "com.wiggins.glide.MyTransformation";
     }
}

七、GlideModule使用

GlideModule是一个接口,全局改变Glide行为的一种方式,通过全局GlideModule配置Glide(GlideModule#applyOptions),用GlideBuilder设置选项,用Glide注册ModelLoader(GlideModule#registerComponents)等。你需要创建Glide的实例,来访问GlideBuilder。可以通过创建一个公共的类,实现GlideModule的接口来定制Glide。所有的GlideModule实现类必须是public的,并且只拥有一个空的构造器,以便在Glide延迟初始化时,可以通过反射将它们实例化。

1、自定义一个GlideModule

public class MyGlideModule implements GlideModule {

    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        // Apply options to the builder here.
    }

    @Override
    public void registerComponents(Context context, Glide glide) {
        // register ModelLoaders here.
    }
}

2、AndroidManifest.xml注册

<meta-data
    android:name="com.wiggins.glide.MyGlideModule"
    android:value="GlideModule" />//value是固定的

3、混淆处理

-keepnames class com.wiggins.glide.MyGlideModule
# or more generally
#-keep public class * implements com.bumptech.glide.module.GlideModule

4、多个GlideModule冲突问题

GlideModule不能指定调用顺序,所以应该避免不同的GlideModule之间有冲突的选项设置,可以考虑将所有的设置都放到一个GlideModule里面,或者排除掉某个manifest文件的某个Module。

<meta-data
    android:name="com.wiggins.glide.MyGlideModule"
    tools:node="remove" />

5、更改Glide配置

已经知道如何使用Glide module去自定义Glide。现在我们看一下接口的第一个方法:applyOptions(Context context, GlideBuilder builder)。这个方法将GlideBuilder的对象当作参数,并且是void返回类型,所以你在这个方法里能调用GlideBuilder可以用的方法。

.setMemoryCache(MemoryCache memoryCache)
.setBitmapPool(BitmapPool bitmapPool)
.setDiskCache(DiskCache.Factory diskCacheFactory)
.setDiskCacheService(ExecutorService service)
.setResizeService(ExecutorService service)
.setDecodeFormat(DecodeFormat decodeFormat)

显而易见,GlideBuilder对象可以让你访问到Glide的核心部分。使用文中的方法,你可以改变磁盘缓存、内存缓存等等。

5.1 设置Glide内存缓存大小

MemoryCache用来把resources缓存在内存里,以便能马上能拿出来显示。默认情况下Glide使用LruResourceCache,我们可以通过它的构造器设置最大缓存内存大小。

//获取系统分配给应用的总内存大小
int maxMemory = (int) Runtime.getRuntime().maxMemory();
//设置图片内存缓存占用八分之一
int memoryCacheSize = maxMemory / 8;
//设置内存缓存大小
builder.setMemoryCache(new LruResourceCache(memoryCacheSize));

获取默认的使用内存

//MemoryCache和BitmapPool的默认大小由MemorySizeCalculator类决定,MemorySizeCalculator会根据给定屏幕大小可用内存算出合适的缓存大小,
这也是推荐的缓存大小,我们可以根据这个推荐大小做出调整
MemorySizeCalculator calculator = new MemorySizeCalculator(context);
int defaultMemoryCacheSize = calculator.getMemoryCacheSize();
int defaultBitmapPoolSize = calculator.getBitmapPoolSize();
5.2 设置Glide磁盘缓存大小
//方式一
//指定的是数据的缓存地址
File cacheDir = context.getExternalCacheDir();
//最多可以缓存多少字节的数据
int diskCacheSize = 1024 * 1024 * 30;
//设置磁盘缓存大小
builder.setDiskCache(new DiskLruCacheFactory(cacheDir.getPath(), "glide", diskCacheSize));
//方式二
//存放在data/data/xxxx/cache/
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, "glide", diskCacheSize));
//方式三
//存放在外置文件
builder.setDiskCache(new ExternalCacheDiskCacheFactory(context, "glide", diskCacheSize));
5.3 设置图片解码格式

默认格式RGB_565相对于ARGB_8888的4字节/像素可以节省一半的内存,但是图片质量就没那么高了,而且不支持透明度。

builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
5.4 设置BitmapPool缓存内存大小

Bitmap池用来允许不同尺寸的Bitmap被重用,这可以显著地减少因为图片解码像素数组分配内存而引发的垃圾回收。默认情况下Glide使用LruBitmapPool作为Bitmap池,LruBitmapPool采用Lru算法保存最近使用的尺寸的Bitmap,我们可以通过它的构造器设置最大缓存内存大小。

builder.setBitmapPool(new LruBitmapPool(memoryCacheSize));
5.5 设置用来检索cache中没有的Resource的ExecutorService
//为了使缩略图请求正确工作,实现类必须把请求根据Priority优先级排好序
builder.setDiskCacheService(ExecutorService service);
builder.setResizeService(ExecutorService service);

6、集成网络框架

Glide包含一些小的、可选的集成库,目前Glide集成库当中包含了访问网络操作的Volley和OkHttp,也可以通过Glide的ModelLoader接口自己写网络请求。

6.1 将OkHttp集成到Glide当中

a)添加依赖

dependencies {
    //OkHttp 2.x
    compile 'com.github.bumptech.glide:okhttp-integration:1.4.0@aar'
    compile 'com.squareup.okhttp:okhttp:2.7.5'

    //OkHttp 3.x
    compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
    compile 'com.squareup.okhttp3:okhttp:3.2.0'
}

结尾的@aar可以将库中的AndroidManifest.xml文件一起导出,Gradle自动合并必要的GlideModule到AndroidManifest.xml,然后使用所集成的网络连接,所以不用再
将以下文本添加到项目的AndroidManifest.xml文件中:

<meta-data
    android:name="com.bumptech.glide.integration.okhttp.OkHttpGlideModule"
    android:value="GlideModule" />

b)创建OkHttp集成库的GlideModule

<meta-data
    android:name="com.wiggins.glide.okhttp.OkHttpGlideModule"
    android:value="GlideModule" />

c)混淆配置

-keep class com.wiggins.glide.okhttp.OkHttpGlideModule
#or
-keep public class * implements com.bumptech.glide.module.GlideModule

注意:
a.OkHttp 2.x和OkHttp 3.x需使用不同的集成库;
b.Gradle会自动将OkHttpGlideModule合并到应用的manifest文件中;
c.如果你没有对所有的GlideModule配置混淆规则(即没有使用-keep public class * implements com.bumptech.glide.module.GlideModule),则需要把OkHttp的GlideModule进行混淆配置:-keep class com.wiggins.glide.okhttp.OkHttpGlideModule

6.2 将Volley集成到Glide当中

a)添加依赖

dependencies {
    compile 'com.github.bumptech.glide:volley-integration:1.4.0@aar'
    compile 'com.mcxiaoke.volley:library:1.0.8'
}

b)创建Volley集成库的GlideModule

<meta-data
    android:name="com.wiggins.glide.volley.VolleyGlideModule"
    android:value="GlideModule" />

c)混淆配置

-keep class com.wiggins.glide.volley.VolleyGlideModule
#or
-keep public class * implements com.bumptech.glide.module.GlideModule

7、替换Glide组件、使用ModelLoader自定义数据源

7.1、替换Glide组件

替换Glide组件功能需要在自定义模块的registerComponents()方法中加入具体的替换逻辑。相比于更改Glide配置,替换Glide组件这个功能的难度就明显大了不少。Glide中的组件非常繁多,也非常复杂,但其实大多数情况下并不需要我们去做什么替换。不过,有一个组件却有着比较大的替换需求,那就是Glide的HTTP通讯组件。

替换Glide组件功能需要在自定义模块的GlideModule#registerComponents(Context context, Glide glide)方法中加入具体的替换逻辑,需要在方法中调用Glide#register(Class<T> modelClass, Class<Y> resourceClass, ModelLoaderFactory<T, Y> factory),其中modelClass表示 数据模型的类型,一般为GlideUrl
,Glide.with(context).load("url")底层就是将转化为了GlideUrl;resourceClass表示URL所指向的资源的类型,一般为InputStream。

默认情况下,Glide使用的是基于原生HttpURLConnection进行订制的HTTP通讯组件,但是现在大多数的Android开发者都更喜欢使用OkHttp,因此将Glide中的HTTP通讯组件修改成OkHttp的这个需求比较常见,那么今天我们也会以这个功能来作为例子进行讲解。

Model:数据模型,一般为URL字符串
Resource:URL所指向的网络资源

它主要和三个接口有关:

ModelLoader:数据模型Loader,将任意复杂的数据模型转化为具体的可被DataFetcher使用的数据类型。需要返回一个从url拉取数据的DataFetcher,泛型类型为上面指定的类型。

public interface ModelLoader<T, Y> {
    DataFetcher<Y> getResourceFetcher(T var1, int var2, int var3);
}

ModelLoaderFactory:ModelLoader的工厂,build方法返回ModelLoader。

public interface ModelLoaderFactory<T, Y> {
    ModelLoader<T, Y> build(Context var1, GenericLoaderFactory var2);

    void teardown();
}

DataFetcher:获取 由model表示的resource要解码的数据

public interface DataFetcher<T> {
    T loadData(Priority var1) throws Exception;  //重要方法,返回给glide的数据

    void cleanup();

    String getId();

    void cancel();
}

默认地,Glide内部使用标准的HTTPUrlConnection去下载图片。Glide也提供两个集成库。这三个方法优点是在安全设置上都是相当严格的。唯一的不足之处是当你从一个使用HTTPS,还是self-signed的服务器下载图片时,Glide并不会下载或者显示图片,因为self-signed认证会被认为存在安全问题。

首先创建一个跳过SSL认证的OkHttpClient
public class UnsafeOkHttpClient {
    public static OkHttpClient getUnsafeOkHttpClient() {
        try {
            // Create a trust manager that does not validate certificate chains
            final TrustManager[] trustAllCerts = new TrustManager[]{
                    new X509TrustManager() {
                        @Override
                        public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
                        }

                        @Override
                        public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
                        }

                        @Override
                        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                            return new X509Certificate[0];
                            //return null;//删除这行,多谢下面评论的几位小伙伴指出空指针问题,并提供解决方案。
                        }
                    }
            };

            // Install the all-trusting trust manager
            final SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, trustAllCerts, new java.security.SecureRandom());

            // Create an ssl socket factory with our all-trusting manager
            final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

            OkHttpClient okHttpClient = new OkHttpClient();
            okHttpClient.setSslSocketFactory(sslSocketFactory);
            okHttpClient.setProtocols(Arrays.asList(Protocol.HTTP_1_1));
            okHttpClient.setHostnameVerifier(new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            });

            return okHttpClient;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

创建的OkHttpClient关闭了所有的SSL认证检查。

集成到 Glide

Glide的OkHTTP集成库做的都是一样的工作,所以我们可以跟随他们的步骤。首先,我们需要在GlideModule里声明我们的定制。你应该想到,我们需要在registerComponents()方法里做适配。我们可以调用.register()方法去交换Glide基础构成。Glide使用一个ModelLoader去链接到数据模型创建一个具体的数据类型。我们的例子中,我们需要创建一个ModelLoader,它连接到一个URL,通过GlideUrl类响应并转化为输入流。Glide需要能够创建我们的新ModelLoader的实例,所以我们在.register()方法中传入一个工厂:

public class UnsafeOkHttpGlideModule implements GlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder glideBuilder) {

    }

    @Override
    public void registerComponents(Context context, Glide glide) {
        glide.register(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
    }
}

public class OkHttpUrlLoader implements ModelLoader<GlideUrl, InputStream> {

    @Override
    public DataFetcher<InputStream> getResourceFetcher(GlideUrl glideUrl, int i, int i1) {
        return new OkHttpStreamFetcher(client, glideUrl);
    }

    private final OkHttpClient client;

    public OkHttpUrlLoader(OkHttpClient client) {
        this.client = client;
    }

    /**
     * The default factory for {@link OkHttpUrlLoader}s.
     */
    public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
        private static volatile OkHttpClient internalClient;
        private OkHttpClient client;

        private static OkHttpClient getInternalClient() {
            if (internalClient == null) {
                synchronized (Factory.class) {
                    if (internalClient == null) {
                        internalClient = UnsafeOkHttpClient.getUnsafeOkHttpClient();
                    }
                }
            }
            return internalClient;
        }

        /**
         * Constructor for a new Factory that runs requests using a static singleton client.
         */
        public Factory() {
            this(getInternalClient());
        }

        /**
         * Constructor for a new Factory that runs requests using given client.
         */
        public Factory(OkHttpClient client) {
            this.client = client;
        }

        @Override
        public ModelLoader<GlideUrl, InputStream> build(Context context, GenericLoaderFactory factories) {
            return new OkHttpUrlLoader(client);
        }

        @Override
        public void teardown() {
            // Do nothing, this instance doesn't own the client.
        }
    }
}

public class OkHttpStreamFetcher implements DataFetcher<InputStream> {
    private final OkHttpClient client;
    private final GlideUrl url;
    private InputStream stream;
    private ResponseBody responseBody;

    public OkHttpStreamFetcher(OkHttpClient client, GlideUrl url) {
        this.client = client;
        this.url = url;
    }

    @Override
    public InputStream loadData(Priority priority) throws Exception {
        Request.Builder requestBuilder = new Request.Builder()
                .url(url.toStringUrl());

        for (Map.Entry<String, String> headerEntry : url.getHeaders().entrySet()) {
            String key = headerEntry.getKey();
            requestBuilder.addHeader(key, headerEntry.getValue());
        }

        Request request = requestBuilder.build();

        Response response = client.newCall(request).execute();
        responseBody = response.body();
        if (!response.isSuccessful()) {
            throw new IOException("Request failed with code: " + response.code());
        }

        long contentLength = responseBody.contentLength();
        stream = ContentLengthInputStream.obtain(responseBody.byteStream(), contentLength);
        return stream;
    }

    @Override
    public void cleanup() {
        if (stream != null) {
            try {
                stream.close();
            } catch (IOException e) {
                // Ignored
            }
        }
        if (responseBody != null) {
            responseBody.close();
        }
    }

    @Override
    public String getId() {
        return url.getCacheKey();
    }

    @Override
    public void cancel() {

    }
}
7.2、使用ModelLoader自定义数据源

如果需要根据不同的要求请求不同尺寸不同质量的图片,这时我们就可以使用自定义数据源。

a)定义处理URL接口

public interface IDataModel {
    String buildDataModelUrl(int width, int height);
}

b)实现不同的处理URL接口

public class JpgDataModel implements IDataModel {

    private String dataModelUrl;

    public JpgDataModel(String dataModelUrl) {
        this.dataModelUrl = dataModelUrl;
    }

    @Override
    public String buildDataModelUrl(int width, int height) {
        //http://78re52.com1.z0.glb.clouddn.com/resource/gogopher.jpg?imageView2/1/w/200/h/200/format/jpg
        return String.format("%s?imageView2/1/w/%d/h/%d/format/jpg", dataModelUrl, width, height);
    }
}

c)实现ModelLoader

public class MyDataLoader extends BaseGlideUrlLoader<IDataModel> {

    public MyDataLoader(Context context) {
        super(context);
    }

    public MyDataLoader(ModelLoader<GlideUrl, InputStream> urlLoader) {
        super(urlLoader, null);
    }

    @Override
    protected String getUrl(IDataModel model, int width, int height) {
        return model.buildDataModelUrl(width, height);
    }

    public static class Factory implements ModelLoaderFactory<IDataModel, InputStream> {

        @Override
        public ModelLoader<IDataModel, InputStream> build(Context context, GenericLoaderFactory factories) {
            return new MyDataLoader(factories.buildModelLoader(GlideUrl.class, InputStream.class));
        }

        @Override
        public void teardown() {
        }
    }
}

d)根据不同的要求采用不同的策略加载图片

//加载jpg图片
Glide.with(this).using(new MyDataLoader(this)).load(new JpgDataModel(imageUrl)).into(imageView);

e)如何跳过.using()

public class MyGlideModule implements GlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {

    }

    @Override
    public void registerComponents(Context context, Glide glide) {
        glide.register(IDataModel.class, InputStream.class, new MyDataLoader.Factory());
    }
}

//加载jpg图片
Glide.with(this).load(new JpgDataModel(imageUrl)).into(imageView);

八、特点

使用简单;
可配置度及自适应程度高;
支持常见图片格式如jpg、png、gif、webp等;
支持多种数据源如网络、本地、资源、Uri等;
高效缓存策略(支持Memory和Disk图片缓存,默认Bitmap格式采用RGB_565内存使用至少减少一半);
生命周期集成(根据Context/Activity/Fragment/FragmentActivity生命周期自动管理请求);
高效处理Bitmap(使用BitmapPool使Bitmap复用,主动调用recycle回收需要回收的Bitmap,减小系统回收压力)。

九、优点

多样化媒体加载,支持Gif、WebP、Video及缩略图以等类型;

生命周期集成,提供多种方式与生命周期绑定,可以更好的让加载图片请求的生命周期动态管理起来;

高效的缓存策略

  • 支持Memory和Disk图片缓存;
  • 缓存相应大小的图片尺寸(Picasso只会缓存原始尺寸图片,而Glide会根据你ImageView的大小来缓存相应大小的图片尺寸,因此Glide会比Picasso加载的速度要快);
  • 内存开销小(Picasso默认的是ARGB_8888格式,而Glide默认的Bitmap格式是RGB_565格式,这个内存开销大约可以减小一半)。

十、缺点

使用方法复杂:由于Glide功能强大,所以使用的方法非常多,其源码也相对的复杂;
包较大:Glide(v3.7.0)的大小约465kb。

十一、使用场景

需要更多的内容表现形式(如Gif、缩略图等);
更高的性能要求(缓存、加载速度等)。

十二、特别说明

1.ImageView的setTag问题

问题描述:如果使用Glide的into(imageView)为ImageView设置图片的同时使用ImageView的setTag(final Object tag)方法,将会导致java.lang.IllegalArgumentException: You must not call setTag() on a view Glide is targeting异常。因为Glide的ViewTarget中通过view.setTag(tag)和view.getTag()标记请求的,由于Android 4.0之前Tag存储在静态map里,如果Glide使用setTag(int key, final Object tag)方法标记请求则可能会导致内存泄露,所以Glide默认使用view.setTag(tag)标记请求,你就不能重复调用了。

解决办法:如果你需要为ImageView设置Tag,必须使用setTag(int key, final Object tag)及getTag(int key)方法,其中key必须是合法的资源id以确保key
的唯一性,典型做法就是在资源文件中声明type="id"的item资源。

2.placeholder()导致的图片变形问题

问题描述:使用.placeholder()方法在某些情况下会导致图片显示的时候出现图片变形的情况。这是因为Glide默认开启的crossFade动画导致的TransitionDrawable绘制异常,具体描述可以查看https://github.com/bumptech/glide/issues/363。根本原因就是你的placeholder图片和你要加载显示的图片宽高比不一样,而Android的
TransitionDrawable无法很好地处理不同宽高比的过渡问题,这是Android也是Glide的Bug。

解决办法:使用.dontAnimate()方法禁用过渡动画,或者使用animate()方法自己写动画,再或者自己修复TransitionDrawable的问题。

3.ImageView的资源回收问题

问题描述:默认情况下Glide会根据with()使用的Activity或Fragment的生命周期自动调整资源请求以及资源回收。但是如果有很占内存的Fragment或Activity不销毁而仅仅是隐藏视图,那么这些图片资源就没办法及时回收,即使是GC的时候。

解决办法:可以考虑使用WeakReference,如:

final WeakReference<ImageView> imageViewWeakReference = new WeakReference<>(imageView);
ImageView target = imageViewWeakReference.get();
if (target != null) {
    Glide.with(context).load(uri).into(target);
}

4.由于Bitmap复用导致的在某些设备上图片错乱的问题

问题描述: Glide默认使用BitmapPool的方式对应用中用到的Bitmap进行复用,以减少频繁的内存申请和内存回收,而且默认使用的Bitmap模式为RGB565以减少内存开销。但在某些设备上(通常在Galaxy系列5.X设备上很容易复现)某些情况下会出现图片加载错乱的问题,具体详见https://github.com/bumptech/glide/issues/601。原因初步确定是OpenGL纹理渲染异常。

解决办法:GlideModule使用PREFER_ARGB_8888(Glide4.X已经默认使用该模式了),虽然内存占用比RGB565更多一点,但可以更好地处理有透明度Bitmap的复用问
题,或者禁用Bitmap复用setBitmapPool(new BitmapPoolAdapter())来修复这个问题(不推荐这种处理方式)。

5.异步线程完成后加载图片的崩溃问题

问题描述:通常情况下异步线程会被约束在Activity生命周期内,所以异步线程完成后使用Glide加载图片是没有问题的。但如果你的异步线程在Activity销毁时没
有取消掉,那么异步线程完成后Glide就无法为一个已销毁的Activity加载图片资源,抛出的异常如下(在with()方法中就进行判断并抛出异常):

java.lang.IllegalArgumentException: You cannot start a load for a destroyed activity
    at com.bumptech.glide.manager.RequestManagerRetriever.assertNotDestroyed(RequestManagerRetriever.java:134)
    at com.bumptech.glide.manager.RequestManagerRetriever.get(RequestManagerRetriever.java:102)
    at com.bumptech.glide.Glide.with(Glide.java:653)
    at com.frank.glidedemo.TestActivity.onGetDataCompleted(TestActivity.java:23)
    at com.frank.glidedemo.TestActivity.access$000(TestActivity.java:10)
    at com.frank.glidedemo.TestActivity$BackgroundTask.onPostExecute(TestActivity.java:46)
    at com.frank.glidedemo.TestActivity$BackgroundTask.onPostExecute(TestActivity.java:28)
    at android.os.AsyncTask.finish(AsyncTask.java:632)
    at android.os.AsyncTask.access$600(AsyncTask.java:177)
    at android.os.AsyncTask$InternalHandler.handleMessage(AsyncTask.java:645)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:157)
    at android.app.ActivityThread.main(ActivityThread.java:5356)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:515)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1265)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1081)
    at dalvik.system.NativeStart.main(Native Method)

解决办法:正确管理BackgroundThreads(异步线程),当Activity停止或销毁时,停止所有相关的异步线程及后续的UI操作,或者加载前使用isFinishing()
或isDestroyed()进行限制(不建议这种处理方式)。

7.3 Android中的缓存策略

当程序第一次从网络上加载图片后,将其缓存在存储设备中,下次使用这张图片的时候就不用再从网络从获取了。很多时候为了提高应用的用户体验,往往还会把图片在内存中再缓存一份,因为从内存中加载图片比存储设备中快。一般情况会把图片存一份到内存中,一份到存储设备中,如果内存中没找到就去存储设备中找,还没有找到就从网络上下载。

缓存策略包含缓存的添加、获取和删除操作。不管是内存还是存储设备,缓存大小都是有限制的。如何删除旧的缓存并添加新的缓存,就对应缓存算法。

目前常用的一种缓存算法是LRU(Least Recently Used),最近最少使用算法。核心思想: 当缓存存满时, 会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种: LruCache和DiskLruCache。LruCache用于实现内存缓存, DiskLruCache则充当了存储设备缓存

1、LruCache

LruCache是一个泛型类, 它内部采用了一个LinkedHashMap以强引用的方式存储外界的缓存对象, 其提供了get和put方法来完成缓存的获取和添加的操作。当缓存满了时,LruCache会移除较早使用的缓存对象, 然后在添加新的缓存对象。LruCache是线程安全的。

强引用: 直接的对象引用
软引用: 当一个对象只有软引用存在时, 系统内存不足时此对象会被gc回收
弱引用: 当一个对象只有弱引用存在时, 对象会随下一次gc时被回收

LruCache 典型初始化过程:

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight() / 1024;
    }
};

只需要提供缓存的总容量大小(一般为进程可用内存的1/8)并重写 sizeOf 方法即可。sizeOf方法作用是计算缓存对象的大小。这里大小的单位需要和总容量的单位(这里是kb)一致,因此除以1024。一些特殊情况下,需要重写LruCache的entryRemoved方法,LruCache移除旧缓存时会调用entryRemoved方法,因此可以在entryRemoved中完成一些资源回收工作(如果需要的话)。

还有获取和添加方法,都比较简单:

  • get(K key) V
  • remove(K key) V

从Android 3.1开始,LruCache成为Android源码的一部分。

2、DiskLruCache

DiskLruCache用于实现磁盘缓存,DiskLruCache得到了Android官方文档推荐,但它不属于Android SDK的一部分。

2.1、DiskLruCache的创建

DiskLruCache并不能通过构造方法来创建, 他提供了open()方法用于创建自身, 如下所示:

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

  • File directory:表示磁盘缓存在文件系统中的存储路径。可以选择SD卡上的缓存目录, 具体是指/sdcard/Andriod/data/package_name/cache目录,也可以选择data目录下. 或者其他地方。 这里给出的建议:如果应用卸载后就希望删除缓存文件的话,那么就选择SD卡上的缓存目录, 如果希望保留缓存数据那就应该选择SD卡上的其他目录。
  • appVersion: 表示应用的版本号,一般设为1即可。当版本号发生改变的时候DiskLruCache会清空之前所有的缓存文件, 在实际开发中这个实用性不大。
  • valueCount: 一般设为1。
  • maxSize: 表示缓存的总大小。当缓存大小超出这个设定值后,会清除一些缓存而保证总大小不大于这个设定值。
    //初始化DiskLruCache,包括一些参数的设置
    public void initDiskLruCache() {
        //配置固定参数
        // 缓存空间大小
        private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
        //下载图片时的缓存大小
        private static final long IO_BUFFER_SIZE = 1024 * 8;
        // 缓存空间索引,用于Editor和Snapshot,设置成0表示Entry下面的第一个文件
        private static final int DISK_CACHE_INDEX = 0;

        //设置缓存目录
        File diskLruCache = getDiskCacheDir(mContext, "bitmap");
        if (!diskLruCache.exists())
            diskLruCache.mkdirs();
        //创建DiskLruCache对象,当然是在空间足够的情况下
        if (getUsableSpace(diskLruCache) > DISK_CACHE_SIZE) {
            try {
                mDiskLruCache = DiskLruCache.open(diskLruCache,
                        getAppVersion(mContext), 1, DISK_CACHE_SIZE);
                mIsDiskLruCache = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //上面的初始化过程总共用了3个方法
    //设置缓存目录
    public File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment
                .getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    // 获取可用的存储大小
    @TargetApi(VERSION_CODES.GINGERBREAD)
    private long getUsableSpace(File path) {
        if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD)
            return path.getUsableSpace();
        final StatFs stats = new StatFs(path.getPath());
        return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
    }

    //获取应用版本号,注意不同的版本号会清空缓存
    public int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(
                    context.getPackageName(), 0);
            return info.versionCode;
        } catch (NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    
2.2、DiskLruCache的缓存添加

DiskLruCache的缓存添加的操作是通过Editor完成的, Editor表示一个缓存对象的编辑对象.

如果还是缓存图片为例子, 每一张图片都通过图片的url为key, 这里由于url可能会有特殊字符所以采用url的md5值作为key. 根据这个key就可以通过edit()来获取Editor对象, 如果这个缓存对象正在被编辑, 那么edit()就会返回null. 即DiskLruCache不允许同时编辑一个缓存对象.

当用.edit(key)获得了Editor对象之后. 通过editor.newOutputStream(0)就可以得到一个文件输出流. 由于之前open()方法设置了一个节点只能有一个数据. 所以在获得输出流的时候传入常量0即可.

有了文件输出流, 可以当网络下载图片时, 图片就可以通过这个文件输出流写入到文件系统上.最后,要通过Editor中commit()来提交写操作, 如果下载中发生异常, 那么使用Editor中abort()来回退整个操作.

2.3、DiskLruCache的缓存查找

和缓存的添加过程类似, 缓存查找过程也需要将url转换成key, 然后通过DiskLruCache#get()方法可以得到一个Snapshot对象, 接着在通过Snapshot对象即可得到缓存的文件输入流, 有了文件输入流, 自然就可以得到Bitmap对象. 为了避免加载图片出现OOM所以采用压缩的方式. 在前面对BitmapFactory.Options的使用说明了. 但是这中方法对FileInputStream的缩放存在问题. 原因是FileInputStream是一种有序的文件流, 而两次decodeStream调用会影响文件的位置属性, 这样在第二次decodeStream的时候得到的会是null. 针对这一个问题, 可以通过文件流来得到它所对应的文件描述符, 然后通过BitmapFactory.decodeFileDescription()来加载一张缩放后的图片.

/**
     * 磁盘缓存的读取
     * @param url
     * @param reqWidth
     * @param reqHeight
     * @return
 */
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException
{
    if(Looper.myLooper() == Looper.getMainLooper())
        Log.w(TAG, "it's not recommented load bitmap from UI Thread");
    if(mDiskLruCache == null)
        return null;

    Bitmap bitmap = null;
    String key = hashKeyForDisk(url);
    Snapshot snapshot = mDiskLruCache.get(key);
    if(snapshot != null)
    {
        FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
        FileDescriptor fd = fileInputStream.getFD();
        bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fd, reqWidth, reqHeight);

        if(bitmap != null)
            addBitmapToMemoryCache(key, bitmap);

    }
    return bitmap;      
}

3、ImageLoader的实现

一个好的ImageLoader应该具备以下几点:

  • 图片的压缩
  • 网络拉取
  • 内存缓存
  • 磁盘缓存
  • 图片的同步加载
  • 图片的异步加载

异步加载过程:

  • bindBitmap先尝试从内存缓存读取图片,如果没有会在线程池中调用loadBitmap方法。获取成功将图片封装为LoadResult对象通过mMainHandler向UI线程发送消息。选择线程池和Handler来提供并发能力和异步能力。
  • 为了解决View复用导致的列表错位问题,在给ImageView设置图片之前都会检查它的url有没有发生改变,如果改变就不再给它设置图片。

推荐阅读更多精彩内容