- 原文链接:Introducing Fresco: A new image library for Android
- 译者 : ZhaoKaiQiang
- 校订者: Chaossss
- 校订者: bboyfeiyu
- 校订者: BillionWang
- 状态 : 完成
在Android装备上面,快速高效的显示图片是极其重要的。过去的几年里,我们在如何高效的存储图象这方面遇到了很多问题。图片太大,但是手机的内存却很小。每个像素的R、G、B和alpha通道总共要占用4byte的空间。如果手机的屏幕是480*800,那末1张屏幕大小的图片就要占用1.5M的内存。手机的内存通常很小,特别是Android装备还要给各个利用分配内存。在某些装备上,分给Facebook App的内存仅唯一16MB。1张图片就要占据其内存的10分之1。
当你的App内存溢出会产生甚么呢?它固然会崩溃!我们开发了1个库来解决这个问题,我们叫它Fresco。它可以管理使用到的图片和内存,从此App不再崩溃。
为了理解Facebook到底做了甚么工作,在此之前我们需要了解在Android可使用的堆内存之间的区分。Android中每一个App的Java堆内存大小都是被严格的限制的。每一个对象都是使用Java的new在堆内存实例化,这是内存中相对安全的1块区域。内存有垃圾回收机制,所以当App不在使用内存的时候,系统就会自动把这块内存回收。
不幸的是,内存进行垃圾回收的进程正是问题所在。当内存进行垃圾回收时,内存不单单进行了垃圾回收,还把 Android 利用完全终止了。这也是用户在使用 App 时最多见的卡顿或短暂假死的缘由之1。这会让正在使用 App 的用户非常愁闷,然后他们可能会烦躁地滑动屏幕或点击按钮,但 App 唯1的响应就是:在 App 恢复正常之前,要求用户耐心等待
相比之下,Native堆是由C++程序的new进行分配的。在Native堆里面有更多可用内存,App只被装备的物理可用内存限制,而且没有垃圾回收机制或其他东西拖后腿。但是c++程序员必须自己回收所分配的每块内存,否则就会造成内存泄漏,终究致使程序崩溃。
Android有另外1种内存区域,叫做Ashmem。它操作起来更像Native堆,但是也有额外的系统调用。Android 在操作 Ashmem 堆时,会把该堆中存有数据的内存区域从 Ashmem 堆中抽取出来,而不是把它释放掉,这是1种弱内存释放模式;被抽取出来的这部份内存只有当系统真正需要更多的内存时(系统内存不够用)才会被释放。当 Android 把被抽取出来的这部份内寄存回 Ashmem 堆,只要被抽取的内存空间没有被释放,之前的数据就会恢复到相应的位置。
Ashmem不能被Java利用直接处理,但是也有1些例外,图片就是其中之1。当你创建1张没有经过紧缩的Bitmap的时候,Android的API允许你指定是不是是可清除的。
BitmapFactory.Options = new BitmapFactory.Options();
options.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);
经过上面的代码处理后,可清除的Bitmap会驻留在 Ashmem 堆中。不管产生甚么,垃圾回收器都不会自动回收这些 Bitmap。当 Android 绘制系统在渲染这些图片,Android 的系统库就会把这些 Bitmap 从 Ashmem 堆中抽取出来,而当渲染结束后,这些 Bitmap 又会被放回到原来的位置。如果1个被抽取的图片需要再绘制1次,系统仅仅需要把它再解码1次,这个操作非常迅速。
这听起来像1个完善的解决方案,但是问题是Bitmap解码的操作是运行在UI线程的。Bitmap解码是非常消耗CPU资源的,当消耗过大时会引发UI阻塞。由于这个缘由,所以Google不推荐使用这个特性。现在它们推荐使用另外1个特性――inBitmap。但是这个特性直到Android3.0以后才被支持。即便是这样,这个特性也不是非常有用,除非 App 里的所有图片大小都相同,这对Fackbook来讲明显是不适用的。1直到4.4版本,这个限制才被移除。但我们需要的是能够运行在 Android 2.3 - 最新版本中的通用解决方案。
对上面提到的“解码操作导致 UI 假死”的问题,我们找到了1种同时使 UI 显示和内存管理都表现良好的解决方法。如果我们在 UI 线程进行渲染之前把被抽取的内存区域放回到原来的位置,并确保它不再会被抽取,那我们就能够把这些图片放在 Ashmem 里,同时不会出现 UI 假死的问题。荣幸的是,Android 的 NDK 中有1个函数可以完善地实现这个需求,名字叫做 AndroidBitmap_lockPixels。这个函数最初的目的就是:在调用 unlockPixels 再次抽取内存区域后被履行。
当我们意想到我们没有必要这样做的时候,我们获得了突破。如果我们只调用lockPixels而不调用对应的unlockPixels,那末我们就能够在Java的堆内存里面创建1个内存安全的图象,并且不会致使UI线程加载缓慢。只需要几行c++代码,我们就完善的解决了这个问题。
就像《蜘蛛侠》里面说的:“能力越强,责任越大。”可清除的 Bitmap 既不会被垃圾回收器回收,也不会被 Ashmem 内置的清除机制处理,这使得使用它们可能会造成内存泄漏。所以我们只能靠自己啦。
在c++中,通常的解决方案是建立智能指针类,实现援用计数。这些需要利用到c++的语言特性――拷贝构造函数、赋值操作符和肯定的析构函数。这类语法在Java当中不存在,由于垃圾回收器能够处理这1切。所以我们必须以某种方式在Java中实现C++的这些保证机制。
我们创建了两个类去完成这件事。其中1个叫做“SharedReference”,它有addReference和deleteReference两个方法,调用者调用时必须采取基类对象或让它在范围以外。1旦援用计数器归零,资源处理(Bitmap.recycle)就会产生。
但是,很明显,让Java开发者去调用这些方法是很容易出错的。Java语言就是为了不做这样的事情的!所以SharedReference之上,我们构建了CloseableReference类。它不但实现了Java的Closeable接口,而且也实现了Cloneable接口。它的构造器和clone()方法会调用addReference(),而close()方法会调用deleteReference()。所以Java开发者需要遵照下面两条简单的的规则:
这些规则可以有效地避免内存泄漏,并让我们在像Fackbook的Android客户端这类大型的Java程序中享受Native内存管理和通讯。
在移动装备上显示图片需要很多的步骤:
几个优秀的开源库都是依照这个顺序履行的,比如 Picasso,Universal Image Loader,Glide和 Volley等等。上面这些开源库为Android的发展做出了非常重要的贡献。我们相信Fresco在几个重要方面会表现的更好。
我们的不同的地方在于把上面的这些步骤看做是管道,而不单单是加载器。每个步骤和其他方面应当是尽量独立的,把数据和参数传递进去,然后产生1个输出,就这么简单。它应当可以做1些操作,不论是并行还是串行。1些操作只能在特性条件下才能履行。1些有特殊要求的在线程上履行。除此以外,当我们斟酌改进图象的时候,所有的图片就会变得非常复杂。很多人在低网速情况下使用Facebook,我们想要这些人能够尽快的看到图片,乃至常常是在图片没有完全下载完之前。
在Java中,异步代码历来都是通过Future机制来履行的。在另外的线程里面代码被提交履行,然后1个类似Future的对象可以检查履行的结果是否是已完成了。但是,这只在假定只有1种结果的情况下行得通。在处理渐进的图象的时候,我们希望可以完全而且连续的显示结果。
我们的解决方式是定义1个更广义的Future版本,叫做DataSource。它提供了1个定阅方法,调用者必须传入1个DataSubscriber和Executor。DataSubscriber可以从DataSource获得到处理中和处理终了的结果,并且提供了很简单的方法来辨别。由于我们需要非常频繁的处理这些对象,所以必须有1个明确的close调用,荣幸的是,DataSource本身就是Closeable。
在后台,每个箱子上面都实现了1个叫做“生产者/消费者”的新框架。在这个问题是,我们是从ReactiveX获得的灵感。我们的系统具有和RxJava类似的接口,但是更加合适移动装备,并且有内置的对Closeables的支持。
保持简单的接口。Producer只有1个叫做produceResults的方法,这个方法需要1个Consumer对象。反过来,Consumer有1个onNewResult方法。
我们使用像这样的系统把Producer联系起来。假定我们有1个producer的工作是把类型I转化为类型O,那末它看起来应当是这个模样:
public class OutputProducer<I, O> implements Producer<O> {
private final Producer<I> mInputProducer;
public OutputProducer(Producer<I> inputProducer) {
this.mInputProducer = inputProducer;
}
public void produceResults(Consumer<O> outputConsumer, ProducerContext context) {
Consumer<I> inputConsumer = new InputConsumer(outputConsumer);
mInputProducer.produceResults(inputConsumer, context);
}
private static class InputConsumer implements Consumer<I> {
private final Consumer<O> mOutputConsumer;
public InputConsumer(Consumer<O> outputConsumer) {
mOutputConsumer = outputConsumer;
}
public void onNewResult(I newResult, boolean isLast) {
O output = doActualWork(newResult);
mOutputConsumer.onNewResult(output, isLast);
}
}
}
这可使我们把非常复杂的步骤串起来,同时也能够保持他们逻辑的独立性。
使用Facebook的人都非常喜欢Stickers,由于它可以以动画情势存储GIF和Web格式。如果支持这些格式,就需要面临新的挑战。由于每个动画都是由不止1张图片组成的,你需要解码每张图片,存储在内存里,然后显示出来。对大1点的动画,把每帧图片放在内存是不可行的。
我们建立了AnimatedDrawable,1个强大的可以显现动画的Drawable,同时支持GIF和WebP格式。AnimatedDrawable实现标准的Android Animatable接口,所以调用者可以随便的启动或停止动画。为了优化内存使用,如果图片足够小的时候,我们就在内存里面缓存这些图片,但是如果太大,我们可以迅速的解码这些图片。这些行动调用者是完全可控的。
所有的后台都用c++代码实现。我们保持1份解码数据和元数据解析,如宽度和高度。我们援用技术数据,它允许多个Java真个Drawables同时访问1个WebP图象。
当1张图片从网络上下载下来以后,我们想显示1张占位图。如果下载失败了,我们就会显示1个毛病标志。当图片加载完以后,我们有1个渐变动画。通过使用硬件加速,我们可以按比例放缩,或是矩阵变换成我们想要的大小然后渲染。我们不总是依照图片的中心进行放缩,那末我们可以自己定义放缩的聚焦点。有些时候,我们想显示圆角乃至是圆形的图片。所有的这些操作都应当是迅速而平滑的。
我们之前的实现是使用Android的View对象――时机到了,可使用ImageView替换出占位的View。这个操作是非常慢的。改变View会让Android强迫刷新全部布局,当用户滑动的时候,这绝对不是你想看到的效果。比较明智的做法是使用Android的Drawables,它可以迅速的被替换。
所以我们创建了Drawee。这是1个像MVC架构的图片显示框架。该模型被称为DraweeHierarchy。它被实现为Drawables的1个层,对底层的图象而言,每个曾都有特定的功能――成像、层叠、渐变或是放缩。
DraweeControllers通过管道的方式连接到图象上――或是其他的图片加载库――并且处理后台的图片操作。他们从管道接收事件并决定如何处理他们。他们控制DraweeHierarchy实际上的操作――不管是占位图片,毛病条件或是完成的图片。
DraweeViews 的功能不多,但都是相当重要的。他们监听Android的View不再显示在屏幕上的系统事件。当图片离开屏幕的时候,DraweeView可以告知DraweeController关闭使用的图象资源。这可以免内存泄漏。另外,如果它已不在屏幕范围内的话,控制器会告知图片管道取消网络要求。因此,像Fackbook那样转动1长串的图片的时候,不会频繁的网络要求。
通过这些努力,显示图片的辛苦操作1去不复返了。调用代码只需要实例化1个DraweeView,然后指定1个URI和其他可选的参数就能够了。剩下的1切都会自动完成。开发人员不需要担心管理图象内存,或更新图象流。Fresco为他们把1切都做了。
完成这个图象显示和操作复杂的工具库以后,我们想要把它分享到Android开发者社区。我们很高兴的宣布,从今天起,这个项目已作为开源代码了!
壁画是绘画技术,几个世纪以来1直遭到世界各地人们的欢迎。我们许多伟大的艺术家使用这类名字,从意大利文艺复兴时期的大师拉斐尔到壁画艺术家斯里兰卡。我们其实不是伪装到达这个伟大的水平,我们真的希望Android开发者能像我们当初享受创建这个开源库的进程1样,非常享受的使用它。
Fresco中文文档
下一篇 js中使用正则表达式