Glide源码学习(流程分析)

本文基于Glide3.7源码分析。

前言:Glide是最常用的图片加载框架,它最大的优点是帮我们实现了图片的缓存管理,而且它不会直接将图片的完整尺寸全部加载到内存中,而是用多少加载多少。Glide会自动判断ImageView的大小,然后只将这么大的图片像素加载到内存当中,帮助我们节省内存开支。

从Glide的基础使用分析源码
  Glide.with(this).load(url).into(imageView);
  • with():返回值为RequestManager。如果在Glide.with()方法中传入的是一个Application对象,那么不需要做什么特殊的处理,它自动就是和应用程序的生命周期是同步的,如果应用程序关闭的话,Glide的加载也会同时终止。传入非Application参数的情况,不管你在Glide.with()方法中传入的是Activity、FragmentActivity、v4包下的Fragment、还是app包下的Fragment,最终的流程都是一样的,那就是会向当前的Activity当中添加一个隐藏的Fragment,之所以这样做,是因为Glide并没有办法知道Activity的生命周期,于是Glide就使用了添加隐藏Fragment的这种小技巧,因为Fragment的生命周期和Activity是同步的,如果Activity被销毁了,Fragment是可以监听到的,这样Glide就可以捕获这个事件并停止图片加载了。

    具体逻辑看下图:


    注意:如果我们是在非主线程当中使用的Glide,那么不管你是传入的Activity还是Fragment,都会被强制当成Application来处理。

  • load():返回值为DrawableTypeRequest。主要是loadGeneric()方法,方法里分别调用了buildStreamModelLoader()方法和buildFileDescriptorModelLoader()方法来获得ModelLoader对象。ModelLoader对象是用于加载图片的。在这一步就是初始化一些加载图片的对象,并且封装Request对象,request对象封装的是用户对加载图片的配置,例如placeholder()方法、error()方法。

  • into(): 返回值为Target。首先是通过buildRequest构造一个GenericRequest,将刚刚load()返回的DrawableTypeRequest里面的用户对加载图片的配置封装进去,这个Request是用来发出加载图片请求的;然后是调用runRequest方法,来执行这个Request。执行这个Request时,首先是判断是否显示error和placeholder的图片,然后判断用户是否设置overide,如果设置了,则直接走onSizeReady(),否则先对图片进行宽高进行计算,再走onSizeReady()。
    onSizeReady方法中,主要是调用了Engine中的load()方法,该方法中关键代码如下

        EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
        DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
                transcoder, diskCacheProvider, diskCacheStrategy, priority);
        EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
        jobs.put(key, engineJob);
        engineJob.addCallback(cb);
        engineJob.start(runnable);

这里构建了一个EngineJob,它的主要作用就是用来开启线程的,为后面的异步加载图片做准备。创建了一个EngineRunnable对象,并且调用了EngineJob的start()方法来运行EngineRunnable对象,这实际上就是让EngineRunnable的run()方法在子线程当中执行了。EngineRunnable的run()方法里主要是调用了decode()方法,如下:

private Resource<?> decode() throws Exception {
    if (isDecodingFromCache()) {
        return decodeFromCache();
    } else {
        return decodeFromSource();
    }
}

我们看decodeFromSource:

private Resource<?> decodeFromSource() throws Exception {
    return decodeJob.decodeFromSource();
}

接着看DecodeJob中的decodeFromSource:

class DecodeJob<A, T, Z> {

    ...

    public Resource<Z> decodeFromSource() throws Exception {
        Resource<T> decoded = decodeSource();
        return transformEncodeAndTranscode(decoded);
    }

    private Resource<T> decodeSource() throws Exception {
        Resource<T> decoded = null;
        try {
            long startTime = LogTime.getLogTime();
            final A data = fetcher.loadData(priority);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Fetched data", startTime);
            }
            if (isCancelled) {
                return null;
            }
            decoded = decodeFromSourceData(data);
        } finally {
            fetcher.cleanup();
        }
        return decoded;
    }

    ...
}

这是先是调用fetcher.loadData(priority),即HttpUrlFetcher的loadData方法,在这里发起了网络请求,得到一个InputStream,然后用ImageVideoWrapper包一层返回回去。
然后调用decoded = decodeFromSourceData(data);这个方法里面执行的其实就是从服务器返回的流当中读取数据。注意到的是,在读取数据前,会先从流中读取2个字节的数据,来判断这张图是GIF图还是普通的静图,然后调用不同的方法读取数据。读取数据的逻辑在Downsampler的decode()方法中。

到这里

public Resource<Z> decodeFromSource() throws Exception {
        Resource<T> decoded = decodeSource();
        return transformEncodeAndTranscode(decoded);
    }

第一个方法就分析完了,接着是transformEncodeAndTranscode(decoded)方法,这个方法主要是进行转码的工作。到此为止,图片的加载工作其实已经完成了,剩下的就是把图片设置到ImageView。我们再回到Engine的run方法

@Override
public void run() {
    if (isCancelled) {
        return;
    }
    Exception exception = null;
    Resource<?> resource = null;
    try {
        resource = decode();
    } catch (Exception e) {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "Exception decoding", e);
        }
        exception = e;
    }
    if (isCancelled) {
        if (resource != null) {
            resource.recycle();
        }
        return;
    }
    if (resource == null) {
        onLoadFailed(exception);
    } else {
        onLoadComplete(resource);
    }
}

将图片显示到ImageView就是通过onLoadComplete(resource)来实现的。大致的流程就是EngineJob中发送一个Handler消息,这个时候就切换到主线程中,然后通过回调的方式,最终回调到了GenericRequest的onResourceReady()方法当中(GenericRequest实现了ResourceCallback的接口),最后调用Target的onResourceReady()。

到此整个流程就结束了。

---------------------------------------这是分割线------------------------------------------
Glide的缓存
Glide的缓存由内存缓存和磁盘缓存构成,默认是开启的,磁盘缓存有四种,默认是缓存转换过的图片,即RESULT模式。
Glide的内存缓存由LruResouceCache和弱引用的HashMap两部分构成,取缓存的时候首先从LruResouceCache取,如果取不到,再从弱引用中取。其实这个弱引用的HashMap是存放当前正在使用的图片的,可以保证正在使用的图片不被Lru给回收了。Glide内存缓存还使用了acquire这个变量来计数,代表图片被引用的次数。
Glide的磁盘缓存使用的则是自己实现的DiskLruCache。

---------------------------------------这是分割线------------------------------------------
Glide的回调与监听

  1. Glide之自定义Target功能

into()方法还有一个接收Target参数的重载即使我们传入的参数是ImageView,Glide也会在内部自动构建一个Target对象。而如果我们能够掌握自定义Target技术的话,就可以更加随心所欲地控制Glide的回调了。

如果我们要进行自定义的话,通常只需要在两种Target的基础上去自定义就可以了,一种是SimpleTarget,一种是ViewTarget

首先来看SimpleTarget,顾名思义,它是一种极为简单的Target,我们使用它可以将Glide加载出来的图片对象获取到,而不是像之前那样只能将图片在ImageView上显示出来。

SimpleTarget<GlideDrawable> simpleTarget = new SimpleTarget<GlideDrawable>() {
    @Override
    public void onResourceReady(GlideDrawable resource, GlideAnimation glideAnimation) {
        imageView.setImageDrawable(resource);
    }
};

public void loadImage(View view) {
    String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
    Glide.with(this)
         .load(url)
         .into(simpleTarget);
}

接下来我们学习一下ViewTarget的用法

public class MyLayout extends LinearLayout {

    private ViewTarget<MyLayout, GlideDrawable> viewTarget;

    public MyLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        viewTarget = new ViewTarget<MyLayout, GlideDrawable>(this) {
            @Override
            public void onResourceReady(GlideDrawable resource, GlideAnimation glideAnimation) {
                MyLayout myLayout = getView();
                myLayout.setImageAsBackground(resource);
            }
        };
    }

    public ViewTarget<MyLayout, GlideDrawable> getTarget() {
        return viewTarget;
    }

    public void setImageAsBackground(GlideDrawable resource) {
        setBackground(resource);
    }

}

在MyLayout的构造函数中,我们创建了一个ViewTarget的实例,并将Mylayout当前的实例this传了进去。ViewTarget中需要指定两个泛型,一个是View的类型,一个图片的类型(GlideDrawable或Bitmap)。然后在onResourceReady()方法中,我们就可以通过getView()方法获取到MyLayout的实例,并调用它的任意接口了

  1. preload()方法

如果我希望提前对图片进行一个预加载,等真正需要加载图片的时候就直接从缓存中读取,不想再等待慢长的网络加载时间了,那就可以使用该方法。
preload()方法的用法也非常简单,直接使用它来替换into()方法即可,如下所示:

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

preload()方法有两个方法重载,一个不带参数,表示将会加载图片的原始尺寸,另一个可以通过参数指定加载图片的宽和高。

需要注意的是,我们如果使用了preload()方法,最好要将diskCacheStrategy的缓存策略指定成DiskCacheStrategy.SOURCE。因为preload()方法默认是预加载的原始图片大小

  1. downloadOnly()方法
    顾名思义,downloadOnly()方法表示只会下载图片,而不会对图片进行加载。当图片下载完成之后,我们可以得到图片的存储路径,以便后续进行操作。它有两个方法重载,一个接收图片的宽度和高度,另一个接收一个泛型对象,如下所示:
downloadOnly(int width, int height)
downloadOnly(Y target)

其中downloadOnly(int width, int height)是用于在子线程中下载图片的,而downloadOnly(Y target)是用于在主线程中下载图片的。

先来看downloadOnly(int width, int height)的用法。当调用了downloadOnly(int width, int height)方法后会立即返回一个FutureTarget对象,然后Glide会在后台开始下载图片文件。接下来我们调用FutureTarget的get()方法就可以去获取下载好的图片文件了,如果此时图片还没有下载完,那么get()方法就会阻塞住,一直等到图片下载完成才会有值返回。

public void downloadImage(View view) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
                final Context context = getApplicationContext();
                FutureTarget<File> target = Glide.with(context)
                                                 .load(url)
                                                 .downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
                final File imageFile = target.get();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(context, imageFile.getPath(), Toast.LENGTH_LONG).show();
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}

下面解释downloadOnly(Y target)方法的用法,实现一个最简单的DownloadImageTarget,注意Target接口的泛型必须指定成File对象,这是downloadOnly(Y target)方法要求的,代码如下所示:

public class DownloadImageTarget implements Target<File> {

    private static final String TAG = "DownloadImageTarget";

    @Override
    public void onStart() {
    }

    @Override
    public void onStop() {
    }

    @Override
    public void onDestroy() {
    }

    @Override
    public void onLoadStarted(Drawable placeholder) {
    }

    @Override
    public void onLoadFailed(Exception e, Drawable errorDrawable) {
    }

    @Override
    public void onResourceReady(File resource, GlideAnimation<? super File> glideAnimation) {
        Log.d(TAG, resource.getPath());
    }

    @Override
    public void onLoadCleared(Drawable placeholder) {
    }

    @Override
    public void getSize(SizeReadyCallback cb) {
        cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
    }

    @Override
    public void setRequest(Request request) {
    }

    @Override
    public Request getRequest() {
        return null;
    }
}

其中只有两个方法是必须实现的,一个是getSize()方法,一个是onResourceReady()方法,Glide在开始加载图片之前会先计算图片的大小,然后回调到onSizeReady()方法当中,之后才会开始执行图片加载。而这里,计算图片大小的任务就交给我们了。只不过这是一个最简单的Target实现,我在getSize()方法中就直接回调了Target.SIZE_ORIGINAL,表示图片的原始尺寸。然后onResourceReady()方法我们就非常熟悉了,图片下载完成之后就会回调到这里。然后这样使用:

   Glide.with(this)
            .load(url)
            .downloadOnly(new DownloadImageTarget());
  1. listener()方法
    它可以用来监听Glide加载图片的状态。最基本的用法如下
public void loadImage(View view) {
    String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
    Glide.with(this)
            .load(url)
            .listener(new RequestListener<String, GlideDrawable>() {
                @Override
                public boolean onException(Exception e, String model, Target<GlideDrawable> target,
                    boolean isFirstResource) {
                    return false;
                }

                @Override
                public boolean onResourceReady(GlideDrawable resource, String model,
                    Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
                    return false;
                }
            })
            .into(imageView);
}

nResourceReady()方法和onException()方法都有一个布尔值的返回值,返回false就表示这个事件没有被处理,还会继续向下传递,返回true就表示这个事件已经被处理掉了,从而不会再继续向下传递。举个简单点的例子,如果我们在RequestListener的onResourceReady()方法中返回了true,那么就不会再回调Target的onResourceReady()方法了。
---------------------------------------这是分割线------------------------------------------

Glide的图片变化功能

注意:当ImageView的scaleType是CENTER_CROP、FIT_CENTER、FIT_START或FIT_END时,Glide会自动添加一个图片变换操作

dontTransform(),表示让Glide在加载图片的过程中不进行图片变换

Glide.with(this)
     .load(url)
     .dontTransform()
     .into(imageView);

但是使用dontTransform()方法存在着一个问题,就是调用这个方法之后,所有的图片变换操作就全部失效了,如果我有一些图片变换操作是必须要执行的该怎么办呢?这种情况下我们只需要借助override()方法强制将图片尺寸指定成原始大小就可以了

Glide内置了两种图片变换操作,我们可以直接拿来使用,一个是CenterCrop,一个是FitCenter,他们其实都是借助transform()方法来是实现的

centerCrop()方法还可以配合override()方法来实现更加丰富的效果

Glide.with(this)
     .load(url)
     .override(500, 500)
     .centerCrop()
     .into(imageView);

自定义图片变换功能其实比较简单,下面看看实现对图片进行圆形化变换的功能

public class CircleCrop extends BitmapTransformation {

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

    public CircleCrop(BitmapPool bitmapPool) {
        super(bitmapPool);
    }

    @Override
    public String getId() {
        return "com.example.glidetest.CircleCrop";
    }

    @Override
    protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
        int diameter = Math.min(toTransform.getWidth(), toTransform.getHeight());

        final Bitmap toReuse = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888);
        final Bitmap result;
        if (toReuse != null) {
            result = toReuse;
        } else {
            result = Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888);
        }

        int dx = (toTransform.getWidth() - diameter) / 2;
        int dy = (toTransform.getHeight() - diameter) / 2;
        Canvas canvas = new Canvas(result);
        Paint paint = new Paint();
        BitmapShader shader = new BitmapShader(toTransform, BitmapShader.TileMode.CLAMP, 
                                            BitmapShader.TileMode.CLAMP);
        if (dx != 0 || dy != 0) {
            Matrix matrix = new Matrix();
            matrix.setTranslate(-dx, -dy);
            shader.setLocalMatrix(matrix);
        }
        paint.setShader(shader);
        paint.setAntiAlias(true);
        float radius = diameter / 2f;
        canvas.drawCircle(radius, radius, radius, paint);

        if (toReuse != null && !pool.put(toReuse)) {
            toReuse.recycle();
        }
        return result;
    }
}

glide-transformations这个库实现了很多通用的图片变换效果,如裁剪变换、颜色变换、模糊变换等等,使得我们可以非常轻松地进行各种各样的图片变换。glide-transformations的项目主页地址是 https://github.com/wasabeef/glide-transformations

---------------------------------------这是分割线------------------------------------------

Glide的自定义模块功能

首先需要定义一个我们自己的模块类,并让它实现GlideModule接口,如下所示:

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

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

两个方法分别就是用来更改Glide和配置以及替换Glide组件的,然后在AndroidManifest.xml文件当中加入如下配置:

<application>

        <meta-data
            android:name="com.example.glidetest.MyGlideModule"
            android:value="GlideModule" />

        ...

</application>

其中name为自定义模块类的路径,value为固定值,只能是"GlideModule"。

如果想要更改Glide的默认配置,只需要在applyOptions()方法中提前将Glide的配置项进行初始化就可以了,有以下的配置项:

setMemoryCache() 
用于配置Glide的内存缓存策略,默认配置是LruResourceCache。

setBitmapPool() 
用于配置Glide的Bitmap缓存池,默认配置是LruBitmapPool。

setDiskCache() 
用于配置Glide的硬盘缓存策略,默认配置是InternalCacheDiskCacheFactory,默认硬盘缓存大小是250M。

setDiskCacheService() 
用于配置Glide读取缓存中图片的异步执行器,默认配置是FifoPriorityThreadPoolExecutor,也就是先入先出原则。

setResizeService() 
用于配置Glide读取非缓存中图片的异步执行器,默认配置也是FifoPriorityThreadPoolExecutor。

setDecodeFormat() 
用于配置Glide加载图片的解码模式,默认配置是RGB_565。

以上是更改Glide的配置,下面说说替换Glide的组件。
Glide网络通讯默认使用的是HttpURLConnection,如果我们希望修改默认的网络通讯为OkHttp,那么可以这样做:

public class MyGlideModule implements GlideModule {

    ...

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

}

使用

  glide.register(GlideUrl.class, InputStream.class, new OkHttpGlideUrlLoader.Factory());

来注册组件,register()方法中使用Map类型来存储已注册的组件,因此我们这里重新注册了一遍GlideUrl.class类型的组件,就把原来的组件给替换掉了,原来的组件注册如下:

register(GlideUrl.class, InputStream.class, new HttpUrlGlideUrlLoader.Factory());

这句代码就表示,我们可以使用Glide.with(context).load(new GlideUrl("url...")).into(imageView)的方式来加载图片,而HttpUrlGlideUrlLoader.Factory则是要负责处理具体的网络通讯逻辑。这里之所以不是替换String.class类型的组件是因为,load(String)方法只是Glide给我们提供一种简易的API封装而已,它的底层仍然还是调用的GlideUrl组件,因此我们在替换组件的时候只需要直接替换最底层的。

上面方法中的第三个参数OkHttpGlideUrlLoader需要我们自己定义。

替换Glide的默认网络通讯组件有更简便的方法:

dependencies {
    compile 'com.squareup.okhttp3:okhttp:3.9.0'
    compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar'
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容