最近写了一个单机版的商城项目『HacppleStore』,涉及到了商品展示等内容,需要加载大量图片,因为我是使用原生 Android 进行开发,而不是像现实中大多数商城一样使用 H5 加载,所以图片的加载与存储就会在本地进行。

另外,我还希望能够对图片的操作更加简便,并且最好能对图片进行一定的压缩处理,以提高性能。

根据从『第一行代码——Android』学来的经验,我首先就想到了『Glide』。

Glide

『Glide』是一个被 Google 所推荐的图片加载库,由 bumptech(Bump Technologies)开发。这个库被广泛运用在 Google 的开源项目中,包括 2014 年的 Google I/O 大会上发布的官方 App。

提到『Glide』就不得不提提『Picasso』了,『Glide』是 Google 员工的开源项目(这也解释了为什么 Google 如此推荐它),是基于『Picasso』的一个图片加载框架。

那『Picasso』又是啥?

还记得在『一次实战爱上「OkHttp」』中提到的 Square 吗?除了『OkHttp』以外,Square 还开发了许多实用的开源框架,Square 出品必属精品,『Picasso』就是其一。

敢于冠上毕加索之名,相信也不会太差,主要特点就是使用简单,扩展性强,支持各种来源的图片,包括网络、Resources、Assets、Files、Content Providers 等。内部集成了『OkHttp』的网络框架,所以如果项目中使用了 Square 公司的其他框架,那么就更推荐使用『Picasso』了。

由于『Glide』在『Picasso』基础上进行的二次开发的,所以两者有近 90% 的相似度,而其优势也是显而易见的。

当然,『Picasso』我目前仅仅简单了解,并未深入学习,由于『Glide』的易用性我一直使用的都是『Glide』,所以如果以后闲下来有机会深探『Picasso』的时候可以单独唠一唠。

『Glide』的代码风格与『Picasso』非常相似,增加了更多的功能,非常重要的就是支持 GIF,当然它的包会大一些。

Glide 是滑行的意思,可以看出这个库的主旨就在于让图片加载变的流畅。它不仅可以用于加载本地图片,还可以加载网络图片、GIF 图片、甚至是本地视频。我只能用超级强大这个词来形容它了。

吹完『Glide』的优点,就直接介绍用法吧。

第一件事依然是添加依赖:

dependencies {
    implementation 'com.github.bumptech.glide:glide:(insert latest version)'
}

『Glide』的用法非常简单,使用简明的流式语法 API,它允许你在大部分情况下只需一行代码就能轻松实现复杂的图片加载功能:

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

这就是『Glide』一个完整的请求至少需要的三个参数,with() 中传入一个 ContextActivityFragment,然后调用 load() 方法去加载图片,可以是一个 URL 地址,也可以是一个本地路径,或者是一个资源 ID,最后调用 into() 方法将图片设置到具体某一个 ImageView 中就可以了。

在『HacppleStore』的开发中,我惊讶的发现,『Glide』还支持对二进制图片的加载,这就使我们的开发更加方便了。

在『一次实战爱上「LitePal」』一文中,我把图片存储在了『SQLite』中,当读取出来加载时,也经历了不少麻烦的操作。

一开始我是使用原生的写法,将二进制图片转为 Bitmap 再进行加载。

ImageView goodsImageView = (ImageView) findViewById(R.id.goods_image_view);
byte[] goodsImg;
List<Goods> goods = LitePal.where("name = ?", goodsName).find(Goods.class);
for (Goods good: goods) {
    goodsImg.setImageBitmap(BitmapFactory.decodeByteArray(good.getImage(), 0, good.getImage().length));
}

而后来查阅文档后发现,使用『Glide』不用进行转换就可以一步到位。

ImageView goodsImageView = (ImageView) findViewById(R.id.goods_image_view);
byte[] goodsImg;
List<Goods> goods = LitePal.where("name = ?", goodsName).find(Goods.class);
for (Goods good: goods) {
    goodsImg = good.getImage();
    Glide.with(this).load(goodsImg).into(goodsImageView);
}

当然,『Glide』也是可以加载 Bitmap 的,只不过在这个场景下,由于其强大的 API,所以我并不需要做多余的操作。

当然,除了加载图片方便这个原因,『Glide』还有一个特点让我坚持使用,因为我使用的很多图片像素都非常高,如果不进行压缩就直接展示的话,很容易就会引起内存溢出。而使用『Glide』就完全不需要担心这回事,因为『Glide』在内部做了许多非常复杂的逻辑操作,其中就包括了图片压缩,我们只需安心按照它的标准用法去加载图片即可。

『Glide』默认的 Bitmap 格式是 RGB_565,值得一提的是,『Picasso』用的是 ARGB_8888,所以总体来说,『Glide』加载的图片在质量上不如『Picasso』。

Picasso 和 Glide 默认加载格式下图片对比

但是由于手机屏幕尺寸的限制,用户往往很难察觉出这种差别,所以『Glide』的优势就展现出来了。

你以为我说的优势是对用户眼睛的欺骗?并不是。

『Glide』加载的 RGB_565 格式的图片仅仅消耗『Picasso』ARGB_8888 格式图片一半的内存:

默认加载格式下 Picasso 和 Glide 消耗内存对比

当然,由于条件不一样,所以这样对比也并不太妥当。但是『Glide』同样支持把图片的格式转换为 ARGB_8888,那就公平的来比一次吧。

修改是全局修改,即整个项目使用『Glide』都会自动转换为 ARGB_8888

首先自定义一个全局的 GlideModule

public class GlideConfiguration implements GlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        // Apply options to the builder here.
        builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
    }
    @Override
    public void registerComponents(Context context, Glide glide) {
        // register ModelLoaders here.
    }
}

然后在「AndroidManifest.xml」中配置:.

<meta-data  
    android:name="packagename.MyGlideModule "  
    android:value="GlideModule"/>

之后就可以在项目中按照标准方式使用了。

这种方法优点是一次设置,全局使用,十分方便,但是缺点也十分明显,所有加载的图片都使用高质量会大大增加内存的消耗,有些得不偿失。

当然还有第二种方法,我打算放到下文再讲。

按照第一种方法设置成功后可以看下对比图:

Picasso 和 Glide 相同加载格式下图片对比

图片质量是一模一样的。

再看看两者之间的内存消耗:

相同加载格式下 Picasso 和 Glide 消耗内存对比

虽然用的图片格式一样,并且『Glide』加载的几乎是先前的两倍内存,但是『Picasso』消耗的内存仍然远大于『Glide』。

其原因在于两者的加载方式不同,上方图片原尺寸是 1920×1080 像素,加载到 768×432 像素的 ImageView 中,『Picasso』使用的方法是加载了完整尺寸的图片进入内存,当绘图的时候,让 GPU 即时的恢复到所需要的尺寸,然而『Glide』则加载精确的 ImageView 尺寸进入内存,所以避免了内存损耗。

『Glide』还可以取消图片的加载:

Glide.with(context).clear(imageView);

不过该方法并不常使用,因为图片会跟随 Context 的生命周期消亡而消亡的。

接下来可以谈谈『Glide』其他的一些用法了。

先说前文提到的修改图片质量。

『Glide』使用注解处理器生成一个流式 API,其目的一是为了更好地扩展自定义选项,其二是为了方便打包常用选项组。

使用 Generated API 仅仅需要两步。第一步,依然是添加依赖:

dependencies {
    annotationProcessor 'com.github.bumptech.glide:compiler:(insert latest version)'
}

第二步,我们需要创建一个类继承 AppGlideModule,并为该类添加 @GlideModule 注解。

import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;

@GlideModule
public class ExampleAppGlideModule extends AppGlideModule {}

AppGlideModule 虽然是抽象类,却可以不用重写任何方法。但必须使用 @GlideModule 注解标记该类,否则没法顺利的生成 GlideApp的API,因为该 Module 将不会被『Glide』发现。

第一次添加 AppGlideModule 或者对 AppGlideModule 做了某些修改时,我们需要重新构建项目重新生成 API。如果『Android Studio』没法自动完成构建,可以使用「Make Project」手动重新构建。

生成 API 的默认名为 GlideApp,其包名与所在的 Module 的包名相同,而基本用法也于之前相同,只是将 Glide 替换成了 GlideApp,就可以使用了。

比如上文中的加载图片可以写成:

GlideApp.with(context).load(url).into(imageView);

而修改图片质量则写成:

GlideApp.with(context).load(url).format(PREFER_ARGB_8888).into(imageView);.

使用这个方法可以单独在一个加载项中设置想要的图片质量,而不用去全局设定,可以大大的减少内存占用。

另外,『Glide』允许用户使用这种方法指定三种不同类型的占位符,分别在三种不同场景使用。

其中,Placeholder 是当请求正在执行时被展示的 Drawable。当请求成功完成时,Placeholder 会被请求到的资源替换。如果被请求的资源是从内存中加载出来的,那么 Placeholder 可能根本不会被显示。如果请求失败并且没有设置 Error,则 Placeholder 将被持续展示。类似地,如果请求的 url/modelnull,并且 ErrorFallback 都没有设置,那么 Placeholder 也会继续显示。

Error 在请求永久性失败时展示。Error 同样也在请求的 url/modelnull,且并没有设置 Fallback 时展示。

Fallback 在请求的 url/modelnull 时展示。设计 Fallback 的主要目的是允许用户指示 null 是否为可接受的正常情况。例如,一个 null 的个人资料 URL 可能暗示这个用户没有设置头像,因此应该使用默认头像。然而,null 也可能表明这个元数据根本就是不合法的,或者取不到。默认情况下『Glide』将 null 作为错误处理,所以可以接受 null 的应用应当显式地设置一个 Fallback

使用方法如下:

GlideApp.with(context)
    .load(url)
    .placeholder(R.drawable.placeholder)
    .error(R.drawable.error)
    .fallback(R.drawable.fallback)
    .into(imageView);

『Glide』还为我们提供了过渡效果,允许定义如何从占位符到新加载的图片,或从缩略图到全尺寸图像过渡。

由于版本的升级,『Glide』将不会默认应用交叉淡入或任何其他的过渡效果,每个请求必须手动应用过渡。使用方法如下:

Glide.with(context)
    .load(url)
    .transition(DrawableTransitionOptions.withCrossFade())
    .into(imageView);

TransitionOptions 用于给一个特定的请求指定过渡,而不同的资源类型能决定使用什么类型的过渡选项。BitmapDrawable 可以对应使用使用 BitmapTransitionOptionsDrawableTransitionOptions 来指定类型特定的过渡动画。对于 BitmapDrawable 之外的资源类型,可以使用 GenericTransitionOptions

『Glide』提供了变换功能,在获取到请求的图片之后,能对图片进行一些处理,例如:裁剪、模糊等;而它的强大在于可以自定义,这样一来不仅能处理 Bitmap,同样可以用于处理 GIF 动画和自定义资源类型。

使用方法如下:

Glide.with(context)
    .load(url)
    .apply(fitCenterTransform())
    .into(imageView);

如果使用 Generated API,那么这些变换方法已经被内联了,所以使用起来甚至更为轻松:

GlideApp.with(context)
    .load(url)
    .fitCenter()
    .into(imageView);

不过我个人更倾向于在编写布局文件的时候就设置 ImageViewScaleType 属性,这样能使界面和逻辑更好的区分开来。

同时,我们还可以对图片的大小进行修改:

GlideApp.with(context)
    .load(url)
    .override(width, height)     // 单位是 px
    .into(imageView);

前文提到,加载 GIF 图是『Glide』比较特别的功能之一,而且使用起来同样十分简单:

Glide.with(context)
    .load(gifUrl)
    .placeholder(R.drawable.placeholder)
    .error(R.drawable.error)
    .into(imageView);

如果希望加载 GIF 时只加载 GIF 的第一帧,把 GIF 当作普通图片一样加载,那么只需要做如下修改即可:

Glide.with(context)
    .load(gifUrl)
    .asBitmap()
    .error(R.drawable.error)
    .into(imageView);

如果希望加载的只是 GIF 图,如果不是 GIF 就显示错误图片,则:

Glide.with(context)
    .load(gifUrl)
    .asGif()
    .error(R.drawable.error)
    .into(imageView);

上面还提到了缩略图,再来看看缩略图的加载方法:

Glide.with(context)
    .load(url)
    .thumbnail(0.1f)
    .into(imageView);

这里传入一个浮点数,它代表尺寸的倍数。比如我传了 0.1f 作为参数,那么『Glide』则会显示原图大小的 10%。如果原图的尺寸是 1000×1000 像素,那么缩略图就是 100×100 像素。由于缩略图会比 ImageView 要小许多,所以要确保一个准确的缩放比例。

最后再说一个非常强大但却使用得比较少的功能,就是上方提到的加载视频,因为作为一个图片加载库可以加载视频的确是很强大,只不过很多人都不会用它来加载视频。

Glide.with(context)
    .load(Uri.fromFile(new File(filePath)))
    .into(view);

『Glide』只能加载本地视频,网络视频或其他途径获得的视频是无效的。

据开发者统计,『Glide』提供了两千多个方法,相信数量还在增加,所以,就不一一介绍了,上方提到的方法应该算是比较经典的,如果有需要还是靠官方文档吧。

Method Count

更值得一提的是,『Glide』有中文文档