Fresco介绍: 一个全新的Android图片加载库

原文:https://code.facebook.com/posts/366199913563917/introducing-fresco-a-new-image-library-for-android/

在Facebook的Android app上面快速和高效的显示图片十分重要,我们这几年在高效存储图片上遇到很多问题。图片很大,但是设备的内存很小。每一个像素占用4byte数据 ---红,绿,蓝和透明色。如果手机的屏幕是480x800像素,一个全屏的图片会占用1.5M内存。手机只有很小的内存,并且Android设备会把有效的内存分给多个App。在一些设备上面,Facebook app只有很小的16M可用内存---一张图就会占用10%内存!

你的App 内存不足会发生什么呢?会Crash掉。我们打算通过开发一个库(Fresco)去解决这个问题 --- 它会去管理图片以及它占

用的内存。Crash可以滚蛋了。

内存的分区

为了理解Facebook在这个问题上面做了什么,我们需要理解Android上面不同的内存堆的概念。

Java heap是最严格的,每个应用都被手机厂商限制的一块内存区。所有使用Java语言的new操作创建的对象都会放在那里。这是一块相对安全的内存区域。内存是垃圾回收的,所以当App用完内存后,系统会自动回收掉。

不幸的是,进程的垃圾回收机制正式我们的问题所在。为了开垦更多的可用内存,Android在垃圾回收的时候,必须要挂起这个应用,这是App被挂起或者短暂短暂停留的最普遍的原因,让用户在用App的时候感到用户体验不好,他们在滚动或者按下按钮的时候---只能看到应用在响应前莫名其妙的停留一段时间。

比较起来,本地heap是在C++的new操作创建的内存区。这里会有更多的可用内存。这块内存是由设备的物理内存限制。这里没有垃圾回收机制并且没有任何需要显示的地方。然而,C++程序需要负责释放每一byte它创建的内存,否则会造成内存泄漏,最终会crash。

Android还有一块内存区,叫做匿名共享内存区。这个操作更像是本地heap,但是会带来额外的系统调用。Android会取消锁定内存,而不是释放它。这是一块延时释放的内存区;这块内存必须在系统十分缺乏内存的时候,才会释放掉。如果Android "锁定"这块内存时,旧的数据依然不会被释放。

可清理的Bitmap

匿名共享内存并不能被Android应用直接访问,但是有少量意外情况,图片则是其中一个。当你创建一个已经解压的图片,被称为Bitmap,Android API允许你像这样指定一个可清理的Bitmap:

BitmapFactory.Options = new BitmapFactory.Options();
options.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);

可清理的位图是在共享内存区。但是,垃圾回收器不会自动的清理他们。当绘图系统正在渲染这张图片的时候,Android系统库会"锁定"这块内存,完成后“解锁”。内存被解锁后,仍然随时可能会被回收回收掉。如果一张解锁的图片再次被绘制,系统会再快速解码一次。

这看起来像是一个完美的解决方案,但是问题在于快速解码是在UI thread里做的,解码是一种CPU密集型操作,UI会在解码成功之前短暂停顿。就因为这个原因,Google现在不建议使用这个功能。他们现在建议用另外一个标志位: inBitmap。但是这个flag在3.0之前并没有。即使这样,除非所有App的图片大小都是一样,否则也没啥用。这个并不是Facebook的使用场景。在Android4.4,这个限制被移除了,但是我们需要一个解决方案,能够让任何人都能够用上Facebook,包括那些使用Android2.3的人。

来享用我们的蛋糕吧

我们发现一个方案能让我们拥有快的UI响应和快的内存。如果我们不在UI thread锁定内存,并且确保它不会被解锁,我们就能够把图片保存在匿名共享内存区里面又不影响UI。真幸运能够拥有它。Android DNK有一个叫AndroidBitmap_lockPixels的函数来做这个事。这个函数搬来是原本是用来接着unlockPixels调用,用来重新“解锁”内存。

我们意识到我们的突破点来了。如果我们调用lockPixels,而不调用unlockPixels,我们就会创建一个安全的远离Java heap并且不会减慢UI thread的的图片。少量的C++代码就能实现。

像C++一样写Java代码

从蜘蛛侠里,我们知道“巨大的责任感迸发出更强的力量”。锁定的purgeable Bitmap不会有Java内存回收,也没有匿名共享内存清理机制来清理,为了防止内存泄漏,我们只能靠自己。

在C++,通常的解决方案是创建智能指针类来实现引用计数功能。把C++的语言特性发挥到极致---拷贝构造函数,运算符重写,deterministic析构函数。但是Java并没有这些特性,垃圾回收机制会处理一切。所以,我们不得不在Java中实现C++风格的方法。

我们创建了两个类来做这个,一个叫做SharedReference. 它有两个方法:addReference, deleteReference。调用者必须要在他们使用底层对象或超出返回的时候调用。一旦引用计数归0,资源清理(比如bitmap.recycle)就会发生。

是的,很明显,让java开发者用这些方法很容易出错。Java是选择用来避免这些的语言。所以,基于SharedReference的上层,我们创建了CloseableReference。它不止是实现了Closeable接口,还实现了Cloneable接口。构造函数和clone()方法会调用addReference(),close()方法会调用deleteReference()。所以,Java开发者只需要以下两点简单的原则:

1、分配CloseableReference到新对象的时候,调用clone()。

2、在超出返回之前,调用close()。通常是在finally块里面。

这些规则有效的防止了内存泄漏,并且能够让我们更加简单的在Java程序里面使用native的内存,比如Facebook for Android和Messager for Android.

它不止是加载器--管道

在移动设备上面加载图片,会有很多的步骤:
Fresco介绍: 一个全新的Android图片加载库_第1张图片
几个牛逼的开源库就是会做这些步骤--- PicassoUniversal Image LoaderGlide, 和Volley。不一一举例了。他们都对Android发展做出了贡献。我们相信我们的库会在几个重要的方面上面走更远。
考虑到pipeline和加载器本身是不同的,每一个步骤需要是独立的,输入一些参数,产生输出。它应该能够更加并行的做一些操作,而其他部分串行。一些只是在特定条件下执行,执行的线程需要有特殊的需求。此外,如果我们想要看到逐行显示的图片,会更加的复杂。很多人在很差的网络条件下使用Facebook。我们想要这些用户能够尽快的看到图片,而且是在图片没有下载完成前,就能够看到。

不用担心,爱上流吧

在传统的Java异步代码里,是通过类似于Future的机制来执行。代码会被提交带其他的线程去执行,类似于Future的对象会被检查是否结果已经准备好了。然而,嘉定我们只要一个结果,当处理逐行化的图片时,我们希望出现的是整体的一个连续的结果序列。
我们的方案是Future的版本,叫做DataSource。它提供订阅方法,调用者必须传递一个数据订阅器和执行器。这个数据订阅器接受来自DataSource的中间执行和结果通知,并且提供一个简单的方法来区分它们。因为我们需呀经常处理需要明确调用close的对象,DataSource本身就是一个Closeable的对象。
在这些场景背后,每一个框里的都被我们使用叫做Producer/Consumer的新框架实现啦。这里,我们通过ReactiveX取得了灵感。我们的系统有和RxJava相似的接口,但是更加适应手机。
这个接口保持了简单原则。Producer有一个简单的方法:produceResults(),使用一个消费者对象。相反的,消费者也会有一个onNewResult()方法。
我们使用这样的一个系统去把生产者和消费者联系到一起。假设我们有一个消费者,功能是用来转换类型I到类型O.代码就会像这样:
public class OutputProducer implements Producer {

  private final Producer mInputProducer;

  public OutputProducer(Producer inputProducer) {
    this.mInputProducer = inputProducer;
  }

  public void produceResults(Consumer outputConsumer, ProducerContext context) {
    Consumer inputConsumer = new InputConsumer(outputConsumer);
    mInputProducer.produceResults(inputConsumer, context);
  }

  private static class InputConsumer implements Consumer {
    private final Consumer mOutputConsumer;

    public InputConsumer(Consumer outputConsumer) {
      mOutputConsumer = outputConsumer;
    }

    public void onNewResult(I newResult, boolean isLast) {
      O output = doActualWork(newResult);
      mOutputConsumer.onNewResult(output, isLast);      
    }
  }
}
这让我们把复杂的步骤联系到一起,并且保持了逻辑独立。

动画 —从1到多

Stickers是深受人们喜爱的存放在Facebook上面的gif和WebP的动画。支持他们会有一些新的挑战。一个动画并不是一个bitmap,而是一连串的bitmap,它们的每一帧都需要被解码,存放在内存中,并且显示出来。加载一个比较大的动画,把每一帧都存放在内存中是不合理的。
我们建立了AnimatedDrawable用来渲染动画,有两个格式解析器:Gif和WebP。AnimatedDrawable继承了标准的Android Animated接口,所以调用者可以开始或者结束这个动画。为了优化内存的使用,如果图片比较小,我们会缓存帧到内存里面。如果太大,我们会快速解码。这个是可以被调用者调整的。
两个格式解析器都是用C++代码实现的。我们拷贝已编码数据和解析过的元数据,比如图片宽高,还会引用计数数据,可以让Java端多个Drawable使用一张独立的WebP图片。

让我如何爱你,让我Drawee这些方法

当图片正在从网上下载的时候,我们想要显示一个占位符,如果下载失败,我们想要显示失败的图片。当图片已经下载好了,我们想要做一个快速的淡入动画。我们缩放这些图片,或者甚至显示一个矩阵,用硬件加速器渲染他们。我们不是通常都从中间缩放图片,焦点可能是在其他地方。另外,我们

有时候也想要显示有圆角甚至是圆形的图片,这些所有的操作,都需要是快且平滑的。

我们以前的实现都是用的View对象---当绘画的时候用ImageView交换tuxiang占位符,这个方式会很慢,因为改变View会强制Android去更新整个Layout,用户滚动的时候是不想要发生这个的。更好的方式是使用Android Drawable对象,它能够快速的交换图像占位符。

所以,我们发明了Drawee。这是一个类似于MVC架构的用来显示图像的框架。Model叫做DraweeHierarchy。用来实现层叠式的图像绘制,实现底层的图像成像,层叠,淡入或者缩放功能。

DraweeControllers连接到图像的pipeline。或者是链接到任意的ImageLoader。主要用来操作后台的图像处理。它会去接收pipeLine的事件,并且决定怎么处理这些事件。它们会去控制DraweeHierarchy到底显示什么,比如占位符、错误符号、或下好的图片。

DraweeViews只有几个功能,但是这些功能都是决定性的。他们主要监听Android的系统消息,决定图像是否继续显示在屏幕上面。当离开屏幕的时候,DraweeView会通知DraweeControllers回收图像的资源,避免内存泄漏。此外,DraweeControllers还会通知image pipeline在图片不需要显示的时候,取消下载的请求。因此,像Facebook这样滑动大量的图片列表的情况下,并不会中断网络。

综上所述,显示图片的繁杂的工作没那么困难了,在代码里调用DraweeView,指定一个URI,指定一些其他可选参数,其他事情都是自动化的了。开发者不用再担心图像内存管理和图片显示了。所有的东西都会在类库里面做好了。

Fresco

已经创造了这么强有力的用来管理和展示图片的工具集,我们想要共享到Android社区里面去,我们很高兴的宣布,这个项目现在已经开源了!

Fresco是已经流传了几个世纪的绘画方式,我们很荣幸很多伟大的艺术家用过这种方式,包括意大利的拉斐尔,斯里兰卡的锡吉里亚古宫等艺术家。我们达不到那样的水平。但是我们希望Androidapp开发者们能够享受到使用类库带来的乐趣。

你可能感兴趣的:(Android移动开发)