精华内容
下载资源
问答
  • 本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,...现在Android上的图片加载框架非常成熟,从最早的老牌图片加载框架UniversalImageLoader,到后来Google推出的Volley,再到后

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/53759439

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每天都有文章更新。

    现在Android上的图片加载框架非常成熟,从最早的老牌图片加载框架UniversalImageLoader,到后来Google推出的Volley,再到后来的新兴军Glide和Picasso,当然还有Facebook的Fresco。每一个都非常稳定,功能也都十分强大。但是它们的使用场景基本都是重合的,也就是说我们基本只需要选择其中一个来进行学习和使用就足够了,每一个框架都尝试去掌握的话则有些浪费时间。

    在这几个框架当中,我对Volley和Glide研究得比较深入,对UniversalImageLoader、Picasso和Fresco都只是有一些基本的了解。从易用性上来讲,Glide和Picasso应该都是完胜其他框架的,这两个框架都实在是太简单好用了,大多数情况下加载图片都是一行代码就能解决的,而UniversalImageLoader和Fresco则在这方面略逊一些。

    那么再拿Glide和Picasso对比呢,首先这两个框架的用法非常相似,但其实它们各有特色。Picasso比Glide更加简洁和轻量,Glide比Picasso功能更为丰富。之前已经有人对这两个框架进行过全方面的对比,大家如果想了解更多的话可以去参考一下 这篇文章

    总之,没有最好的框架,只有最适合自己的框架。经过多方面对比之后,我还是决定选择了Glide来进行研究,并且这也是Google官方推荐的图片加载框架。

    说实话,关于Glide的文章我已经筹备了好久,去年这个时候本来就打算要写了,但是一直都没有动笔。因为去年我的大部分时间都放在了写《第二行代码》上面,只能用碎片时间来写写博客,但是Glide的难度远超出了我用碎片时间所能掌握的难度。当然,这里我说的是对它的源码进行解析的难度,不是使用上的难度,Glide的用法是很简单的。所以,我觉得去年我写不好Glide这个题材的文章,也就一直拖到了今年。

    而现在,我花费了大量的精力去研究Glide的源码和各种用法,相信现在已经可以将它非常好地掌握了,因此我准备将我掌握的这些知识整理成一个新的系列,帮忙大家更好地学习Glide。这个Glide系列大概会有8篇左右文章,预计花半年时间写完,将会包括Glide的基本用法、源码解析、高级用法、功能扩展等内容,可能会是目前互联网上最详尽的Glide教程。

    那么本篇文章是这个系列的第一篇文章,我们先来了解一下Glide的基本用法吧。

    开始

    Glide是一款由Bump Technologies开发的图片加载框架,使得我们可以在Android平台上以极度简单的方式加载和展示图片。

    目前,Glide最新的稳定版本是3.7.0,虽然4.0已经推出RC版了,但是暂时问题还比较多。因此,我们这个系列的博客都会使用Glide 3.7.0版本来进行讲解,这个版本的Glide相当成熟和稳定。

    要想使用Glide,首先需要将这个库引入到我们的项目当中。新建一个GlideTest项目,然后在app/build.gradle文件当中添加如下依赖:

    dependencies {
        compile 'com.github.bumptech.glide:glide:3.7.0'
    }

    如果你还在使用Eclipse,可以点击 这里 下载Glide的jar包。

    另外,Glide中需要用到网络功能,因此你还得在AndroidManifest.xml中声明一下网络权限才行:

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

    就是这么简单,然后我们就可以自由地使用Glide中的任意功能了。

    加载图片

    现在我们就来尝试一下如何使用Glide来加载图片吧。比如这是必应上一张首页美图的地址:

    http://cn.bing.com/az/hprichbg/rb/Dongdaemun_ZH-CN10736487148_1920x1080.jpg

    然后我们想要在程序当中去加载这张图片。

    那么首先打开项目的布局文件,在布局当中加入一个Button和一个ImageView,如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Load Image"
            android:onClick="loadImage"
            />
    
        <ImageView
            android:id="@+id/image_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    </LinearLayout>

    为了让用户点击Button的时候能够将刚才的图片显示在ImageView上,我们需要修改MainActivity中的代码,如下所示:

    public class MainActivity extends AppCompatActivity {
    
        ImageView imageView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            imageView = (ImageView) findViewById(R.id.image_view);
        }
    
        public void loadImage(View view) {
            String url = "http://cn.bing.com/az/hprichbg/rb/Dongdaemun_ZH-CN10736487148_1920x1080.jpg";
            Glide.with(this).load(url).into(imageView);
        }
    
    }

    没错,就是这么简单。现在我们来运行一下程序,效果如下图所示:

    可以看到,一张网络上的图片已经被成功下载,并且展示到ImageView上了。

    而我们到底做了什么?实际上核心的代码就只有这一行而已:

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

    千万不要小看这一行代码,实际上仅仅就这一行代码,你已经可以做非常非常多的事情了,包括加载网络上的图片、加载手机本地的图片、加载应用资源中的图片等等。

    下面我们就来详细解析一下这行代码。

    首先,调用Glide.with()方法用于创建一个加载图片的实例。with()方法可以接收Context、Activity或者Fragment类型的参数。也就是说我们选择的范围非常广,不管是在Activity还是Fragment中调用with()方法,都可以直接传this。那如果调用的地方既不在Activity中也不在Fragment中呢?也没关系,我们可以获取当前应用程序的ApplicationContext,传入到with()方法当中。注意with()方法中传入的实例会决定Glide加载图片的生命周期,如果传入的是Activity或者Fragment的实例,那么当这个Activity或Fragment被销毁的时候,图片加载也会停止。如果传入的是ApplicationContext,那么只有当应用程序被杀掉的时候,图片加载才会停止。

    接下来看一下load()方法,这个方法用于指定待加载的图片资源。Glide支持加载各种各样的图片资源,包括网络图片、本地图片、应用资源、二进制流、Uri对象等等。因此load()方法也有很多个方法重载,除了我们刚才使用的加载一个字符串网址之外,你还可以这样使用load()方法:

    // 加载本地图片
    File file = new File(getExternalCacheDir() + "/image.jpg");
    Glide.with(this).load(file).into(imageView);
    
    // 加载应用资源
    int resource = R.drawable.image;
    Glide.with(this).load(resource).into(imageView);
    
    // 加载二进制流
    byte[] image = getImageBytes();
    Glide.with(this).load(image).into(imageView);
    
    // 加载Uri对象
    Uri imageUri = getImageUri();
    Glide.with(this).load(imageUri).into(imageView);

    最后看一下into()方法,这个方法就很简单了,我们希望让图片显示在哪个ImageView上,把这个ImageView的实例传进去就可以了。当然,into()方法不仅仅是只能接收ImageView类型的参数,还支持很多更丰富的用法,不过那个属于高级技巧,我们会在后面的文章当中学习。

    那么回顾一下Glide最基本的使用方式,其实就是关键的三步走:先with(),再load(),最后into()。熟记这三步,你就已经入门Glide了。

    占位图

    现在我们来学一些Glide的扩展内容。其实刚才所学的三步走就是Glide最核心的东西,而我们后面所要学习的所有东西都是在这个三步走的基础上不断进行扩展而已。

    观察刚才加载网络图片的效果,你会发现,点击了Load Image按钮之后,要稍微等一会图片才会显示出来。这其实很容易理解,因为从网络上下载图片本来就是需要时间的。那么我们有没有办法再优化一下用户体验呢?当然可以,Glide提供了各种各样非常丰富的API支持,其中就包括了占位图功能。

    顾名思义,占位图就是指在图片的加载过程中,我们先显示一张临时的图片,等图片加载出来了再替换成要加载的图片。

    下面我们就来学习一下Glide占位图功能的使用方法,首先我事先准备好了一张loading.jpg图片,用来作为占位图显示。然后修改Glide加载部分的代码,如下所示:

    Glide.with(this)
         .load(url)
         .placeholder(R.drawable.loading)
         .into(imageView);

    没错,就是这么简单。我们只是在刚才的三步走之间插入了一个placeholder()方法,然后将占位图片的资源id传入到这个方法中即可。另外,这个占位图的用法其实也演示了Glide当中绝大多数API的用法,其实就是在load()和into()方法之间串接任意想添加的功能就可以了。

    不过如果你现在重新运行一下代码并点击Load Image,很可能是根本看不到占位图效果的。因为Glide有非常强大的缓存机制,我们刚才加载那张必应美图的时候Glide自动就已经将它缓存下来了,下次加载的时候将会直接从缓存中读取,不会再去网络下载了,因而加载的速度非常快,所以占位图可能根本来不及显示。

    因此这里我们还需要稍微做一点修改,来让占位图能有机会显示出来,修改代码如下所示:

    Glide.with(this)
         .load(url)
         .placeholder(R.drawable.loading)
         .diskCacheStrategy(DiskCacheStrategy.NONE)
         .into(imageView);

    可以看到,这里串接了一个diskCacheStrategy()方法,并传入DiskCacheStrategy.NONE参数,这样就可以禁用掉Glide的缓存功能。

    关于Glide缓存方面的内容我们将会在后面的文章进行详细的讲解,这里只是为了测试占位图功能而加的一个额外配置,暂时你只需要知道禁用缓存必须这么写就可以了。

    现在重新运行一下代码,效果如下图所示:

    可以看到,当点击Load Image按钮之后会立即显示一张占位图,然后等真正的图片加载完成之后会将占位图替换掉。

    当然,这只是占位图的一种,除了这种加载占位图之外,还有一种异常占位图。异常占位图就是指,如果因为某些异常情况导致图片加载失败,比如说手机网络信号不好,这个时候就显示这张异常占位图。

    异常占位图的用法相信你已经可以猜到了,首先准备一张error.jpg图片,然后修改Glide加载部分的代码,如下所示:

    Glide.with(this)
         .load(url)
         .placeholder(R.drawable.loading)
         .error(R.drawable.error)
         .diskCacheStrategy(DiskCacheStrategy.NONE)
         .into(imageView);

    很简单,这里又串接了一个error()方法就可以指定异常占位图了。

    现在你可以将图片的url地址修改成一个不存在的图片地址,或者干脆直接将手机的网络给关了,然后重新运行程序,效果如下图所示:

    这样我们就把Glide提供的占位图功能都掌握了。

    指定图片格式

    我们还需要再了解一下Glide另外一个强大的功能,那就是Glide是支持加载GIF图片的。这一点确实非常牛逼,因为相比之下Jake Warton曾经明确表示过,Picasso是不会支持加载GIF图片的。

    而使用Glide加载GIF图并不需要编写什么额外的代码,Glide内部会自动判断图片格式。比如这是一张GIF图片的URL地址:

    http://p1.pstatp.com/large/166200019850062839d3

    我们只需要将刚才那段加载图片代码中的URL地址替换成上面的地址就可以了,现在重新运行一下代码,效果如下图所示:

    也就是说,不管我们传入的是一张普通图片,还是一张GIF图片,Glide都会自动进行判断,并且可以正确地把它解析并展示出来。

    但是如果我想指定图片的格式该怎么办呢?就比如说,我希望加载的这张图必须是一张静态图片,我不需要Glide自动帮我判断它到底是静图还是GIF图。

    想实现这个功能仍然非常简单,我们只需要再串接一个新的方法就可以了,如下所示:

    Glide.with(this)
         .load(url)
         .asBitmap()
         .placeholder(R.drawable.loading)
         .error(R.drawable.error)
         .diskCacheStrategy(DiskCacheStrategy.NONE)
         .into(imageView);

    可以看到,这里在load()方法的后面加入了一个asBitmap()方法,这个方法的意思就是说这里只允许加载静态图片,不需要Glide去帮我们自动进行图片格式的判断了。

    现在重新运行一下程序,效果如下图所示:

    由于调用了asBitmap()方法,现在GIF图就无法正常播放了,而是会在界面上显示第一帧的图片。

    那么类似地,既然我们能强制指定加载静态图片,就也能强制指定加载动态图片。比如说我们想要实现必须加载动态图片的功能,就可以这样写:

    Glide.with(this)
         .load(url)
         .asGif()
         .placeholder(R.drawable.loading)
         .error(R.drawable.error)
         .diskCacheStrategy(DiskCacheStrategy.NONE)
         .into(imageView);

    这里调用了asGif()方法替代了asBitmap()方法,很好理解,相信不用我多做什么解释了。

    那么既然指定了只允许加载动态图片,如果我们传入了一张静态图片的URL地址又会怎么样呢?试一下就知道了,将图片的URL地址改成刚才的必应美图,然后重新运行代码,效果如下图所示。

    没错,如果指定了只能加载动态图片,而传入的图片却是一张静图的话,那么结果自然就只有加载失败喽。

    指定图片大小

    实际上,使用Glide在绝大多数情况下我们都是不需要指定图片大小的。

    在学习本节内容之前,你可能还需要先了解一个概念,就是我们平时在加载图片的时候很容易会造成内存浪费。什么叫内存浪费呢?比如说一张图片的尺寸是1000*1000像素,但是我们界面上的ImageView可能只有200*200像素,这个时候如果你不对图片进行任何压缩就直接读取到内存中,这就属于内存浪费了,因为程序中根本就用不到这么高像素的图片。

    关于图片压缩这方面,我之前也翻译过Android官方的一篇文章,感兴趣的朋友可以去阅读一下 Android高效加载大图、多图解决方案,有效避免程序OOM

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

    当然,Glide也并没有使用什么神奇的魔法,它内部的实现原理其实就是上面那篇文章当中介绍的技术,因此掌握了最基本的实现原理,你也可以自己实现一套这样的图片压缩机制。

    也正是因为Glide是如此的智能,所以刚才在开始的时候我就说了,在绝大多数情况下我们都是不需要指定图片大小的,因为Glide会自动根据ImageView的大小来决定图片的大小。

    不过,如果你真的有这样的需求,必须给图片指定一个固定的大小,Glide仍然是支持这个功能的。修改Glide加载部分的代码,如下所示:

    Glide.with(this)
         .load(url)
         .placeholder(R.drawable.loading)
         .error(R.drawable.error)
         .diskCacheStrategy(DiskCacheStrategy.NONE)
         .override(100, 100)
         .into(imageView);

    仍然非常简单,这里使用override()方法指定了一个图片的尺寸,也就是说,Glide现在只会将图片加载成100*100像素的尺寸,而不会管你的ImageView的大小是多少了。

    好了,今天是我们这个Glide系列的第一篇文章,写了这么多内容已经算是挺不错的了。现在你已经了解了Glide的基本用法,当然也是一些最常用的用法。下一篇文章当中,我们会尝试去分析Glide的源码,研究一下在这些基本用法的背后,Glide到底执行了什么神奇的操作,能够使得我们加载图片变得这么简单?感兴趣的朋友请继续阅读 Android图片加载框架最全解析(二),从源码的角度理解Glide的执行流程

    关注我的技术公众号,每天都有优质技术文章推送。关注我的娱乐公众号,工作、学习累了的时候放松一下自己。

    微信扫一扫下方二维码即可关注:

    20160507110203928         20161011100137978

    展开全文
  • Android图片加载框架

    千次阅读 2016-08-17 20:16:51
    开发一个简洁而实用的Android图片加载缓存框架,并在内存占用与加载图片所需时间这两个方面与主流图片加载框架之一Universal Image Loader做出比较,来帮助我们量化这个框架的性能。通过开发这个框架,我们可以...

    这篇文章主要和大家一起动手编写Android图片加载框架,从内部原理到具体实现来详细介绍如何开发一个简洁而实用的Android图片加载缓存框架,感兴趣的小伙伴们可以参考一下

    开发一个简洁而实用的Android图片加载缓存框架,并在内存占用与加载图片所需时间这两个方面与主流图片加载框架之一Universal Image Loader做出比较,来帮助我们量化这个框架的性能。通过开发这个框架,我们可以进一步深入了解Android中的Bitmap操作、LruCache、LruDiskCache,让我们以后与Bitmap打交道能够更加得心应手。若对Bitmap的大小计算及inSampleSize计算还不太熟悉,请参考这里:高效加载Bitmap。由于个人水平有限,叙述中必然存在不准确或是不清晰的地方,希望大家能够指出,谢谢大家。

    一、图片加载框架需求描述

        在着手进行实际开发工作之前,我们先来明确以下我们的需求。通常来说,一个实用的图片加载框架应该具备以下2个功能:

    图片的加载:包括从不同来源(网络、文件系统、内存等),支持同步及异步方式,支持对图片的压缩等等;
    图片的缓存:包括内存缓存和磁盘缓存。
        下面我们来具体描述下这些需求。

    1. 图片的加载

    (1)同步加载与异步加载
    我们先来简单的复习下同步与异步的概念:

    同步:发出了一个“调用”后,需要等到该调用返回才能继续执行;
    异步:发出了一个“调用”后,无需等待该调用返回就能继续执行。
        同步加载就是我们发出加载图片这个调用后,直到完成加载我们才继续干别的活,否则就一直等着;异步加载也就是发出加载图片这个调用后我们可以直接去干别的活。

    (2)从不同的来源加载
        我们的应用有时候需要从网络上加载图片,有时候需要从磁盘加载,有时候又希望从内存中直接获取。因此一个合格的图片加载框架应该支持从不同的来源来加载一个图片。对于网络上的图片,我们可以使用HttpURLConnection来下载并解析;对于磁盘中的图片,我们可以使用BitmapFactory的decodeFile方法;对于内存中的Bitmap,我们直接就可以获取。

    (3)图片的压缩
        关于对图片的压缩,主要的工作是计算出inSampleSize,剩下的细节在下面实现部分我们会介绍。 

    2. 图片的缓存

        缓存功能对于一个图片加载框架来说是十分必要的,因为从网络上加载图片既耗时耗电又费流量。通常我们希望把已经加载过的图片缓存在内存或磁盘中,这样当我们再次需要加载相同的图片时可以直接从内存缓存或磁盘缓存中获取。

    (1)内存缓存
        访问内存的速度要比访问磁盘快得多,因此我们倾向于把更加常用的图片直接缓存在内存中,这样加载速度更快,内存缓存的不足在于由于内存空间有限,能够缓存的图片也比较少。我们可以选择使用SDK提供的LruCache类来实现内存缓存,这个类使用了LRU算法来管理缓存对象,LRU算法即Least Recently Used(最近最少使用),它的主要思想是当缓存空间已满时,移除最近最少使用的缓存对象。关于LruCache类的具体使用我们下面会进行详细介绍。

    (2)磁盘缓存
        磁盘缓存的优势在于能够缓存的图片数量比较多,不足就是磁盘IO的速度比较慢。磁盘缓存我们可以用DiskLruCache来实现,这个类不属于Android SDK,文末给出的本文示例代码的地址,其中包含了DiskLruCache。

        DisLruCache同样使用了LRU算法来管理缓存,关于它的具体使用我们会在后文进行介绍。 

    二、缓存类使用介绍
    1. LruCache的使用

        首先我们来看一下LruCache类的定义:

    ?
    publicclassLruCache<K, V> {
      privatefinalLinkedHashMap<K, V> map;
     
      ...
     
      publicLruCache(intmaxSize) {
        if(maxSize <= 0) {
          thrownewIllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = newLinkedHashMap<K, V>(0,0.75f,true);
      }
      ...
    }

        由以上代码我们可以知道,LruCache是个泛型类,它的内部使用一个LinkedHashMap来管理缓存对象。

    (1)初始化LruCache
    初始化LruCache的惯用代码如下所示:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    //获取当前进程的可用内存(单位KB)
    intmaxMemory = (int) (Runtime.getRuntime().maxMemory() /1024);
    intmemoryCacheSize = maxMemory / 8;
    mMemoryCache = newLruCache<String, Bitmap>(memoryCacheSize) {
      @Override
      protectedintsizeOf(String key, Bitmap bitmap) {
        returnbitmap.getByteCount() / 1024;
      }
    }; 

    在以上代码中,我们创建了一个LruCache实例,并指定它的maxSize为当前进程可用内存的1/8。我们使用String作为key,value自然是Bitmap。第6行到第8行我们重写了sizeOf方法,这个方法被LruCache用来计算一个缓存对象的大小。我们使用了getByteCount方法返回Bitmap对象以字节为单位的方法,又除以了1024,转换为KB为单位的大小,以达到与cacheSize的单位统一。

    (2)获取缓存对象
       LruCache类通过get方法来获取缓存对象,get方法的源码如下:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    publicfinalV get(K key) {
        if(key == null) {
          thrownewNullPointerException("key == null");
        }
     
        V mapValue;
        synchronized(this) {
          mapValue = map.get(key);
          if(mapValue != null) {
            hitCount++;
            returnmapValue;
          }
          missCount++;
        }
     
        /*
         * Attempt to create a value. This may take a long time, and the map
         * may be different when create() returns. If a conflicting value was
         * added to the map while create() was working, we leave that value in
         * the map and release the created value.
         */
     
        V createdValue = create(key);
        if(createdValue == null) {
          returnnull;
        }
     
        synchronized(this) {
          createCount++;
          mapValue = map.put(key, createdValue);
     
          if(mapValue != null) {
            // There was a conflict so undo that last put
            map.put(key, mapValue);
          }else{
            size += safeSizeOf(key, createdValue);
          }
        }
     
        if(mapValue != null) {
          entryRemoved(false, key, createdValue, mapValue);
          returnmapValue;
        }else{
          trimToSize(maxSize);
          returncreatedValue;
        }
      }

        通过以上代码我们了解到,首先会尝试根据key获取相应value(第8行),若不存在则会新建一个key-value对,并将它放入到LinkedHashMap中。从get方法的实现我们可以看到,它用synchronized关键字作了同步,因此这个方法是线程安全的。实际上,LruCache类对所有可能涉及并发数据访问的方法都作了同步。

    (3)添加缓存对象
        在添加缓存对象之前,我们先得确定用什么作为被缓存的Bitmap对象的key,一种很直接的做法便是使用Bitmap的URL作为key,然而由于URL中存在一些特殊字符,所以可能会产生一些问题。基于以上原因,我们可以考虑使用URL的md5值作为key,这能够很好的保证不同的url具有不同的key,而且相同的url得到的key相同。我们自定义一个getKeyFromUrl方法来通过URI获取key,该方法的代码如下:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    privateString getKeyFromUrl(String url) {
      String key;
      try{
        MessageDigest messageDigest = MessageDigest.getInstance("MD5");
        messageDigest.update(url.getBytes());
        byte[] m = messageDigest.digest();
        returngetString(m);
      }catch(NoSuchAlgorithmException e) {
        key = String.valueOf(url.hashCode());
      }
      returnkey;
    }
    privatestaticString getString(byte[] b){
      StringBuffer sb = newStringBuffer();
      for(inti = 0; i < b.length; i ++){
        sb.append(b[i]);
      }
      returnsb.toString();
    }

        得到了key后,我们可以使用put方法向LruCache内部的LinkedHashMap中添加缓存对象,这个方法的源码如下:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    publicfinalV put(K key, V value) {
        if(key == null|| value == null) {
          thrownewNullPointerException("key == null || value == null");
        }
     
        V previous;
        synchronized(this) {
          putCount++;
          size += safeSizeOf(key, value);
          previous = map.put(key, value);
          if(previous != null) {
            size -= safeSizeOf(key, previous);
          }
        }
     
        if(previous != null) {
          entryRemoved(false, key, previous, value);
        }
     
        trimToSize(maxSize);
        returnprevious;
    }

        从以上代码我们可以看到这个方法确实也作了同步,它将新的key-value对放入LinkedHashMap后会返回相应key原来对应的value。 

    (4)删除缓存对象
        我们可以通过remove方法来删除缓存对象,这个方法的源码如下:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    publicfinalV remove(K key) {
        if(key == null) {
          thrownewNullPointerException("key == null");
        }
     
        V previous;
        synchronized(this) {
          previous = map.remove(key);
          if(previous != null) {
            size -= safeSizeOf(key, previous);
          }
        }
     
        if(previous != null) {
          entryRemoved(false, key, previous, null);
        }
     
        returnprevious;
    }

        这个方法会从LinkedHashMap中移除指定key对应的value并返回这个value,我们可以看到它的内部还调用了entryRemoved方法,如果有需要的话,我们可以重写entryRemoved方法来做一些资源回收的工作。   

     2. DiskLruCache的使用

    (1)初始化DiskLruCache
        通过查看DiskLruCache的源码我们可以发现,DiskLruCache就存在如下一个私有构造方法:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    privateDiskLruCache(File directory, intappVersion,intvalueCount,longmaxSize) {
        this.directory = directory;
        this.appVersion = appVersion;
        this.journalFile = newFile(directory, JOURNAL_FILE);
        this.journalFileTmp = newFile(directory, JOURNAL_FILE_TMP);
        this.valueCount = valueCount;
        this.maxSize = maxSize;
    }
     

        因此我们不能直接调用构造方法来创建DiskLruCache的实例。实际上DiskLruCache为我们提供了open静态方法来创建一个DiskLruCache实例,我们来看一下这个方法的实现:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    publicstaticDiskLruCache open(File directory, intappVersion,intvalueCount,longmaxSize)
          throwsIOException {
        if(maxSize <= 0) {
          thrownewIllegalArgumentException("maxSize <= 0");
        }
        if(valueCount <= 0) {
          thrownewIllegalArgumentException("valueCount <= 0");
        }
      
        // prefer to pick up where we left off
        DiskLruCache cache = newDiskLruCache(directory, appVersion, valueCount, maxSize);
        if(cache.journalFile.exists()) {
          try{
            cache.readJournal();
            cache.processJournal();
            cache.journalWriter = newBufferedWriter(newFileWriter(cache.journalFile,true),
                IO_BUFFER_SIZE);
            returncache;
          }catch(IOException journalIsCorrupt) {
    //        System.logW("DiskLruCache " + directory + " is corrupt: "
    //            + journalIsCorrupt.getMessage() + ", removing");
            cache.delete();
          }
        }
      
        // create a new empty cache
        directory.mkdirs();
        cache = newDiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
        returncache;
    }

         从以上代码中我们可以看到,open方法内部调用了DiskLruCache的构造方法,并传入了我们传入open方法的4个参数,这4个参数的含义分别如下:

    directory:代表缓存文件在文件系统的存储路径;
    appVersion:代表应用版本号,通常设为1即可;
    valueCount:代表LinkedHashMap中每个节点上的缓存对象数目,通常设为1即可;
    maxSize:代表了缓存的总大小,若缓存对象的总大小超过了maxSize,DiskLruCache会自动删去最近最少使用的一些缓存对象。
        以下代码展示了初始化DiskLruCache的惯用代码:

    ?
    1
    2
    3
    4
    5
    File diskCacheDir= getAppCacheDir(mContext, "images");
    if(!diskCacheDir.exists()) {
      diskCacheDir.mkdirs();
    }
    mDiskLruCache = DiskLruCache.open(diskCacheDir, 1,1, DISK_CACHE_SIZE);

        以上代码中的getAppCacheDir是我们自定义的用来获取磁盘缓存目录的方法,它的定义如下:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    publicstaticFile getAppCacheDir(Context context, String dirName) {
      String cacheDirString;
      if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
        cacheDirString = context.getExternalCacheDir().getPath();
      }else{
        cacheDirString = context.getCacheDir().getPath();
      }
      returnnewFile(cacheDirString + File.separator + dirName);
    }

    接下来我们介绍如何添加、获取和删除缓存对象。 

    (2)添加缓存对象
        先通过以上介绍的getKeyFromUrl获取Bitmap对象对应的key,接下来我们就可以把这个Bitmap存入磁盘缓存中了。我们通过Editor来向DiskLruCache添加缓存对象。首先我们要通过edit方法获取一个Editor对象:

    String key = getKeyFromUrl(url);
    DiskLruCache.Editor editor = mDiskLruCache.edit(key); 

    获取到Editor对象后,通过调用Editor对象的newOutputStream我们就可以获取key对应的Bitmap的输出流,需要注意的是,若我们想通过edit方法获取的那个缓存对象正在被“编辑”,那么edit方法会返回null。相关的代码如下:

    ?
    1
    2
    3
    if(editor != null) {
      OutputStream outputStream = editor.newOutputStream(0);//参数为索引,由于我们创建时指定一个节点只有一个缓存对象,所以传入0即可
    }

        获取了输出流后,我们就可以向这个输出流中写入图片数据,成功写入后调用commit方法即可,若写入失败则调用abort方法进行回退。相关的代码如下:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    //getStream为我们自定义的方法,它通过URL获取输入流并写入outputStream,具体实现后文会给出
    if(getStreamFromUrl(url, outputStream)) {
      editor.commit();
    }else{
      //返回false表示写入outputStream未成功,因此调用abort方法回退整个操作
      editor.abort();
    }
    mDiskLruCache.flush();//将内存中的操作记录同步到日志文件中

        下面我们来看一下getStream方法的实现,这个方法实现很直接简单,就是创建一个HttpURLConnection,然后获取InputStream再写入outputStream,为了提高效率,使用了包装流。该方法的代码如下:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    publicbooleangetStreamFromUrl(String urlString, OutputStream outputStream) {
      HttpURLConnection urlCOnnection = null;
      BufferedInputStream bis = null;
      BufferedOutputStream bos = null;
       
      try{
        finalURL url = newURL(urlString);
        urlConnection = (HttpURLConnection) url.openConnection();
        bis = newBufferedInputStream(urlConnection.getInputStream(), BUF_SIZE);
         
        intbyteRead;
        while((byteRead = bis.read()) != -1) {
          bos.write(byteRead);
        }
        returntrue;
      }catch(IOException e) {
        e.printStackTrace();
      }finally{
        if(urlConnection != null) {
          urlConnection.disconnect();
        }
        //HttpUtils为一个自定义工具类
        HttpUtils.close(bis);
        HttpUtils.close(bos);
      }
      returnfalse;
    }

         经过以上的步骤,我们已经成功地将图片写入了文件系统。 

    (3)获取缓存对象
        我们使用DiskLruCache的get方法从中获取缓存对象,这个方法的大致源码如下:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    publicsynchronizedSnapshot get(String key) throwsIOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if(entry == null) {
          returnnull;
        }
      
        if(!entry.readable) {
          returnnull;
        }
      
        /*
         * Open all streams eagerly to guarantee that we see a single published
         * snapshot. If we opened streams lazily then the streams could come
         * from different edits.
         */
        InputStream[] ins = newInputStream[valueCount];19    ...
        returnnewSnapshot(key, entry.sequenceNumber, ins);
     }

        我们可以看到,这个方法最终返回了一个Snapshot对象,并以我们要获取的缓存对象的key作为构造参数之一。Snapshot是DiskLruCache的内部类,它包含一个getInputStream方法,通过这个方法可以获取相应缓存对象的输入流,得到了这个输入流,我们就可以进一步获取到Bitmap对象了。在获取缓存的Bitmap时,我们通常都要对它进行一些预处理,主要就是通过设置inSampleSize来适当的缩放图片,以防止出现OOM。我们之前已经介绍过如何高效加载Bitmap,在那篇文章里我们的图片来源于Resources。尽管现在我们的图片来源是流对象,但是计算inSampleSize的方法是一样的,只不过我们不再使用decodeResource方法而是使用decodeFileDescriptor方法。

    相关的代码如下:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Bitmap bitmap = null;
    String key = getKeyFromUrl(url);
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
    if(snapShot != null) {
      FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(0);//参数表示索引,同之前的newOutputStream一样
      FileDescriptor fileDescriptor = fileInputStream.getFD();
      bitmap = decodeSampledBitmapFromFD(fileDescriptor, dstWidth, dstHeight);
      if(bitmap != null) {
        addBitmapToMemoryCache(key, bitmap);
      }
    }
     

          第7行我们调用了decodeSampledBitmapFromFD来从fileInputStream的文件描述符中解析出Bitmap,decodeSampledBitmapFromFD方法的定义如下:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    publicBitmap decodeSampledBitmapFromFD(FileDescriptor fd, intdstWidth,intdstHeight) {
      finalBitmapFactory.Options options = newBitmapFactory.Options();
      options.inJustDecodeBounds = true;
      BitmapFactory.decodeFileDescriptor(fd,null, options);
      //calInSampleSize方法的实现请见“Android开发之高效加载Bitmap”这篇博文
      options.inSampleSize = calInSampleSize(options, dstWidth, dstHeight);
      options.inJustDecodeBounds = false;
      returnBitmapFactory.decodeFileDescriptor(fd,null, options);
    }

         第9行我们调用了addBitmapToMemoryCache方法把获取到的Bitmap加入到内存缓存中,关于这一方法的具体实现下文会进行介绍。

    三、图片加载框架的具体实现
    1. 图片的加载

    (1)同步加载
        同步加载的相关代码需要在工作者线程中执行,因为其中涉及到对网络的访问,并且可能是耗时操作。同步加载的大致步骤如下:首先尝试从内存缓存中加载Bitmap,若不存在再从磁盘缓存中加载,若不存在则从网络中获取并添加到磁盘缓存中。同步加载的代码如下:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    publicBitmap loadBitmap(String url, intdstWidth,intdstHeight) {
      Bitmap bitmap = loadFromMemory(url);
      if(bitmap != null) {
        returnbitmap;
      }
      //内存缓存中不存在相应图片
      try{
        bitmap = loadFromDisk(url, dstWidth, dstHeight);
        if(bitmap != null) {
          returnbitmap;
        }
        //磁盘缓存中也不存在相应图片
        bitmap = loadFromNet(url, dstWidth, dstHeight);
      }catch(IOException e) {
        e.printStackTrace();
      }
     
      returnbitmap;
    }

        loadBitmapFromNet方法的功能是从网络上获取指定url的图片,并根据给定的dstWidth和dstHeight对它进行缩放,返回缩放后的图片。loadBitmapFromDisk方法则是从磁盘缓存中获取并缩放,而后返回缩放后的图片。关于这两个方法的实现在下面“图片的缓存”部分我们会具体介绍。下面我们先来看看异步加载图片的实现。 

    (2)异步加载
        异步加载图片在实际开发中更经常被使用,通常我们希望图片加载框架帮我们去加载图片,我们接着干别的活,等到图片加载好了,图片加载框架会负责将它显示在我们给定的ImageView中。我们可以使用线程池去执行异步加载任务,加载好后通过Handler来更新UI(将图片显示在ImageView中)。相关代码如下所示:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    publicvoiddisplayImage(String url, ImageView imageView, intdstWidth,intwidthHeight) {
      imageView.setTag(IMG_URL, url);
      Bitmap bitmap = loadFromMemory(url);
      if(bitmap != null) {
        imageView.setImageBitmap(bitmap);
        return;
      }
       
      Runnable loadBitmapTask = newRunnable() {
        @Override
        publicvoidrun() {
          Bitmap bitmap = loadBitmap(url, dstWidth, dstHeigth);
          if(bitmap != null) {
            //Result是我们自定义的类,封装了返回的Bitmap以及它的URL和作为它的容器的ImageView
            Result result = newResult(bitmap, url, imageView);
            //mMainHandler为主线程中创建的Handler
            Message msg = mMainHandler.obtainMessage(MESSAGE_SEND_RESULT, result);
            msg.sendToTarget();
           }
        }
      };
      threadPoolExecutor.execute(loadBitmapTask);
    }

        从以上代码我们可以看到,异步加载与同步加载之间的区别在于,异步加载把耗时任务放入了线程池中执行。同步加载需要我们创建一个线程并在新线程中执行loadBitmap方法,使用异步加载我们只需传入url、imageView等参数,图片加载框架负责使用线程池在后台执行图片加载任务,加载成功后会通过发送消息给主线程来实现把Bitmap显示在ImageView中。我们来简单的解释下obtainMessage这个方法,我们传入了两个参数,第一个参数代表消息的what属性,这时个int值,相当于我们给消息定的一个标识,来区分不同的消息;第二个参数代表消息的obj属性,表示我们附带的一个数据对象,就好比我们发email时带的附件。obtainMessage用于从内部的消息池中获取一个消息,就像线程池对线程的复用一样,通过这个方法获取校区更加高效。获取了消息并设置好它的what、obj后,我们在第18行调用sendToTarget方法来发送消息。

    下面我们来看看mMainHandler和threadPoolExecutor的创建代码: