在Facebook的Android客户端上快速高效的显示图片是非常重要的。然而多年来,我们遇到了很多如何高效存储图片的问题。图片太大,而设备太小。一个像素点就占据了4个字节数据(分别代表R G B和alpha)。如果在一个480*800尺寸的手机屏幕上,一张单独的全屏图片就会占据1.5MB的内存空间。通常手机的内存都非常小,而这些内存被多种多样的app划分占用。在一些设备上,Facebook app虽然只有16MB,但是仅仅一个图片就占用了1/10的空间。
当你的app用完你的内存时会发生什么呢?会崩溃。我们着手通过创建一个类(Fresco)来解决这个问题,它会管理好图片和内存。崩溃走开!
内存区域
为了了解Facebook所做的工作,我们必须理解应用于Android中的各种堆内存。
Java heap能严格的限制每个应用,它由设备制作商设置的。所有由Java语言的new操作符创建的对象都会来到这里。这里是一个相对安全的内存区域。内存总会被回收的,所以当app已经用完内存时,系统将会自动回收它。
不幸的是,内存回收阶段出现了问题。为了回收内存,Android必须停止app的运行,然后运行垃圾回收器。这是你正在使用中的app出现停止或变慢的原因之一。这令人沮丧,或许用户正尝试滚动或者按一个button,结果app没有响应,只有莫名的等待。
相反,native heap是被C++ new操作符使用的一个堆。在这里有大量的可用内存。app被限制在设备的可用物理内存中,这里不存在垃圾回收,app也不会变慢。然而,我们需要在C++程序中释放内存空间,否则他们会内存溢出,app最终会崩溃。
Android中还有另外一块内存区域,叫ashmem。这很像是本地heap,但是需要额外的系统调用。Android能够“unpin”内存而不是释放他们。这是一种懒释放,只有在系统真正需要更多内存空间的时候才会释放。当Android再次指向(pin)内存时,旧的数据还会在那里,只要没有被释放的话。
可清除的位图
ashmem不是直接的访问java应用,因为存在一些异常,图像就是其中一个。当你创建一个解码的(未压缩的)图像时,比如一个bitmap,Android API会允许你指定这个图像为“可清除的”:
BitmapFactory.Options = new BitmapFactory.Options();
options.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);
这些可清除图像存在于ashmem中,然而,垃圾回收器并不会自动的回收他们。当绘制系统渲染图像时,Android的系统库会标记这块内存,当渲染完毕后,便不再标记它。没有被标记的内存随时都能被系统回收。如果一个没有被标记的图像需要重新绘制,系统会重新解码图像。
这听起来像是一个完美的解决方案,但问题是快速解码图像发生在UI线程。解码是一个CPU密集型操作,执行时UI会变慢。为此,Google反对使用特征,他们推荐使用一种不同的flag:inBitmap。然而这种flag在Android3.0才出现。直到现在,这种flag也不常用,除非app里面所有图像的尺寸相同,这完全不符合Facebook的情况。这种限制直到Android4.4才移除。然而我们需要一个能让所有Facebook用户满意的方案,包括使用Android2.3的那些用户。
拥有蛋糕并吃掉它
我们发现一种能够兼顾快速UI和快速内存的解决方案。如果我们提前标记内存,在非UI线程,并且保证不被除掉标记,然后我们能够保证图片存在于ashmem中而不会导致UI变慢。运气好的话,NDK中有一个函数能准确的做到,叫“AndroidBitmap_lockPixels”。这个函数会在“unlockPixels”调用之后被调用,再次去掉内存标记。
当我们意识到我们不必那样做时,我们取得了突破。如果我们没有匹配“unlockPixels”而调用“lockPixels”,我们创建了一种安全存在于Java heap之外的图像,还不会拖慢UI线程。几行C++代码即可办到的事情。
用Java写代码,但用C++思考
正如我们从蜘蛛侠上所学到的,“巨大的力量来源于重大的责任”。标记的可清除图像既不是垃圾回收也不是ashmem中的内置清除工具来防止内存泄漏。我们相信我们自己。
在C++中,通常的办法是创建漂亮的指针类来实现引用计数,这些利用C++中的一些工具,如copy constructors, assignment operators, and deterministic destructors。这些动态特性不存在于Java中,在Java中,垃圾回收器会处理一切。所以我们必须在Java中找到实现C++风格的方法。
我们利用两个类来实现之,一个是SharedReference,它有两个方法,addReference和deleteReference。每当他们获取底层对象或超出作用域时调用者必须调用他们。一旦引用计数器归零时,资源清理(如Bitmap.recycle)发生。
然而显而易见的是,对于Java开发者来说,调用这些方法及其容易出错。Java被选为一个避免出现此种情况的语言。所以在SharedReference的顶部,我们建立了CloseableReference。它不但实现了Java Closeable接口,同时也实现了Cloneable。构造器和clone()方法调用addReference(),而且close()方法调用deleteReference。所以Java开发者仅需要遵循两个简单的规则。
1.分配一个CloseableReference给一个新对象时,调用.clone()方法。
2.超出作用域之前,调用.close()方法,通常在一个finally块中。
这些规则在防止内存泄漏方面是非常有效的,同时让我们享受到本地内存管理的乐趣,比如在Facebook for Android 或 Messenger for Android这样的比较大的Java应用中。
它不只是加载器--它是管道
当在移动设备上面显示一个图像时,会有很多步骤。
有几个很棒的开源库会进行这些步骤,如Picasso,Universal Image Loader,和Volley等等,不一而足。这些都对Android发展做出了重要贡献。我们相信我们的开源库在几个重要的方面走的更远。
考虑到这些步骤作为一个管道而不是一个加载器是有区别的,每一步都应该尽量独立于其它部分,输入一些参数同时输出结果。它应尽可能并行的进行一些操作。而其他部分则串行。一些执行仅在特定条件下。他们执行的一些线程具有特殊的需求。此外,如果我们看重创新的图像,整个图片会变得更复杂。很多人是在非常慢的网络连接中使用Facebook的,我们想让这些用户能够快速的看到他们的图片,甚至是在图片真正加载完毕之前。
别担心,爱上流吧
传统上,Java中的异步代码是通过像Future这样的机制被执行的。代码上传后在另外一个线程执行,像一个Future对象能被检测到结果是否准备好。然而,假设仅有一个结果,当处理先进的图像时,我们想,会出现一个连续结果的整体序列。
我们的解决方案是Future的一个大概版本,叫做DataSource。他提供一个订阅方法,调用者必须传递一个数据订阅者和一个执行器。数据订阅者收到来自中间的或最终结果的DataSource的通知,而且提供一个简单方法来区分他们。因为我们经常处理一些需要明确调用close的对象,DataSource本身也是一个Closeable。
在幕后,上面方框内的每一个都被实现了,并且使用了一个新的框架,叫做Producer/Consumer。这里我们画出了来自ReactiveX框架的灵感。我们的系统有与RxJava相同的接口,而且更适合移动设备和支持Closeable的嵌入式。
接口很简单,Producer有一个独立的方法,是produceResults,它负责得到一个Consumer对象。相反,Consumer有一个onNewResult方法。
我们使用这样的一个系统来把生产者链接在一起。假设我们有一个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);
}
}
}
这让我们将一系列复杂的步骤链接在一起,而且仍然保持他们逻辑上是独立的。
动画--从一个到多个
Stickers,是存储在GIF中的WebP格式的动画,深受Facebook用户喜欢。但支持Stickers出现了新的挑战。一个动画并不是一幅图像而是一系列的图像的组合,其中每一张图片都要被解码,并存入内存,展示出来。存储大动画中的每个单独帧到内存是不合理的。
我们创建了AnimatedDrawable,一个具有渲染能力的类,有两个后端--一个是GIF,另一个是WebP。AnimatedDrawable实现了标准Android的Animatable接口,所以调用者能够随时开始和结束动画。为了优化内存存储,在内存足够时我们缓存了所有帧到内存中,但是帧太多时,我们会匆忙的解码。这种行为是完全可以被调用者调节的。
两个后端都是用C++代码实现,我们拷贝已编码数据和已解析的元数据,如宽和高。我们引用计数数据,这样可以在Java端让多个Drawable同时访问一个单独WebP图像。
我如何爱你?让我制定方法
当图片正在从网络被下载时,我们想显示一个占位符。如果下载失败,我们显示一个错误指示器。当图片到达时,我们做出一个快速的淡入效果动画。我们经常缩放图片,或者应用一个显示矩阵,使用硬件加速器来渲染它。而且我们不总是缩放图片--有用的焦点可能是其他地方。有时我们想显示一个圆角图片,或者圆形图片。所有这样的操作都应该是快速而平滑的。
我们以前的实现都是使用Android的View对象--当时间到时为一个ImageView交换占位符,这会非常缓慢的。切换视图会强迫Android去执行整个layout,并不是像用户滚动这样的事情。一个更明智的方法是使用Android的Drawables,它能被快速交换。
所以我们创建了Drawee。这是一个为显示图像的类似于MVC的框架。此模型被称作DraweeHierarchy。它是一个Drawables的实现层次架构,其中每个应用一个特定的函数--成像、分层、淡入、或缩放--对于底层图像来说。
DraweeControllers连接图像管道--或对于任何图像加载器--照顾到后台图像控制。他们接收来自管道的事件,并决定如何处理它们。他们控制DraweeHierarchy的实际显示--如占位符、错误符号、或完成图片。
DraweeViews仅有几个功能,但都是起决定作用的。他们监听Android视图不再显示的系统事件。当离开屏幕,DraweeView能告诉DraweeController关闭图像使用的资源。这会避免内存泄漏。另外,如果还没有出去的话,控制器将会告诉图像管道取消网络请求。因此,滚动长列表图像(如在Facebook中经常这样),并不会中止网络。
有了这些工具,显示图像就不那么困难了。调用程序只需要实例化一个DraweeView,指定一个URI,然后随意设置一些其他的参数。其他所有工作都是自动化的。开发者不必担心管理图像内存或者更新流到图像。所有事情都是靠类库实现。
已经建立了灵活的工具集来显示和操作图像,我们想分享它给Android开发者社区。我们很高兴的宣布,从今日开始,这个项目开放源代码。
fresco(湿壁画)是一种绘画技术,它已经流行了几个世纪。我们很荣幸许多伟大的艺术家都曾使用过这种形式,从意大利的文艺复兴大师如拉斐尔,到斯里兰卡的锡吉里亚古宫艺术家们。我们不自称达到那样的水平。我们希望Android app开发者们能够享受到使用类库带来的乐趣。
本文来自:https://code.facebook.com/posts/366199913563917/introducing-fresco-a-new-image-library-for-android/