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

对于Android平台的Facebook来说,快速而高效的展示图片是很重要的。不过近年来,我们在如何高效地存储图片方面遇到了很多问题。图片所需空间很大,而设备上空间很小。每个像素占用4个字节,分别为红、绿、蓝和α透明度。如果一个手机的屏幕尺寸是480*800像素,一张全屏的图片就占用1.5MB的内存。通常手机的内存很少,并且Android设备在众多应用程序之间会平均分配自身的内存。在一些设备上,Facebook程序内存被限制在16MB,可是仅仅一张图片就占用了十分之一!

当你的应用程序运行超出内存时会发生什么?它会crash。我们打算通过创建我们称之为Fresco的库来解决这个问题它能管理图片及其所占内存Crash便随之消失了。

内存区

要了解Facebook的做法,我们需要了解Android上可用的不同内存堆。

严格来说,Java堆是一个棘手问题,每个应用程序由设备制造商所限制。所有通过Java创建出来的对象都存在于此,这是一个存储器使用相对稳定的区域。内存会被垃圾回收,所以当应用程序不再需要内存时,系统会对其自动回收。

不巧的是,垃圾回收的过程恰好是一个难题。为了做到较为彻底的垃圾回收,当系统执行垃圾回收时,Andoid必须停止应用程序。这就是在你使用应用程序的时候,遇到短暂卡顿或停滞的众多常见原因之一。这会困扰用户的使用,同时他们可能会尝试滚动屏幕或者按下按钮,却只看到应用程序在响应之前莫名其妙的等待。

相比之下,native层的堆是通过C++创建出来的。它会有更多的可用内存。应用程序仅仅被设备上可用内存所限制。然而,C++程序管理其所释放的每一个字节,就是说他们会内存泄露,并且最终崩溃。

Android有另一个内存区,叫做ashmem。它的操作很像native层的堆,但是有额外的系统调用。Android可以“取下”内存而不是释放内存。这是一种懒释放,内存只是在系统真正需要更多内存的时候才释放。当Android重新”固定”内存的时候,如果原有的数据没有被释放掉,会仍然存在。

可回收Bitmap

Ashmen不直接和Java应用程序打交道,但有少数例外的情况,图片就是其中之一。当你创建一个解码的(未被压缩的)被称为位图的图片,Android的API允许你指明这个图片是可回收的。

可回收的bitmap存在于ashmem。然而,垃圾回收器不会自动回收它们。Android的系统库在系统绘制图片的时候“固定”内存,在图片销毁的时候“取下”内存。在任何情况下,内存都可以被系统回收。如果被回收的图片需要再次绘制,系统会在运行时再次解码图片。

这似乎是一个完美的解决方案,但问题是运行时解码发生在UI线程。解码是CPU耗时的操作,与此同时UI会停滞。出于这个原因,Google目前不建议使用该功能。他们现在推荐使用一个不同的标志位,叫做inBitmap。然而,这个标志位在Android 3.0之前是不存在的。即使如此,除非应用程序中大部分图片是相同大小的,否则这是没有用的。对于Facebook来说,这种情况不可能出现。直到Android 4.4出现之后,这一限制被打破。然而我们需要一个解决方案,那就是在Android 2.3设备上Facebook用户如何使用的问题。

兼而有之

我们找到一个解决方案,无论在快速的UI还是在高效内存使用上都可以两全其美。如果我们在UI线程之后提前占用内存,并确保它不被回收掉,然后我们可以在ashmem中持有图片而不会遇到UI停滞的情况。碰巧的是,Android Native Development Kit (NDK)具有这样的函数,叫做AndroidBitmap_lockPixels。这个函数原本计划紧跟调用unlockPixels之后,用于再次回收内存。

当意识到不必要这么做的时候,我们取得了突破。即便我们调用lockPixels时没有一个匹配的unlockPixels,也可以在Java堆内存中创建一张图片并不拖慢UI线程。在完成了少量C++代码之后,我们便回家了。

用C++思维写Java代码

正如我们在蜘蛛侠中学到的,“权力越大责任就越大”。持有可回收的位图可以既不使用垃圾回收也不使用ashmem内置的回收特性从而避免内存泄露。我们确实可以自力更生。

在C++中,通常的解决办法是创建实现引用计数的智能的指针类。这些类充分利用C++语言的特性,拷贝构造函数赋值运算符和确定性析构函数。Java中没有这个语法优势,Java中垃圾回收被假定可以打理一切。因此我们终究要找到一个方法,能够在Java中实现C++风格式的保障性。

我们充分利用两个类来做到这一点。其中一个被称为SharedReference它有两个方法addReference和deleteReference调用者应该在它们获取到底层对象或者超出范围的时候调用。一旦引用计数为0,就要回收资源(比如Bitmap.recycle)。

但显而易