Fresco: android上的图像处理库

Fresco: android上的图像处理库

fresco是一种流行数百年绘画技术。从意大利文艺复兴时期的啦菲尔到斯里兰卡锡吉里亚古宫的艺术家们,无不在fresco这种艺术形式的造诣为后人所敬仰。fresco的开发者,不奢望能像这些大师们一样享有如此之高的荣誉,只企盼这个image library 能为越来越多的android开发者所用采用,并喜欢用它,就像开发者们那么热衷于开发它一样。(扯淡结束)

  如何快速,高效地显示图片在android平台上是非常重要的。目前所有的app几乎都离不开图片,这也似乎预示着图片的加载性能和速率至关重要。接下来我们分析图片加载慢的主要瓶颈是什么。如果一张img是采用rgba四个通道存储的,那么一个像素就会占用4个字节。假设一副img的size是480x800,那么这个图片大概就需要占用1.5M的内存。然而,不同的手机给我们的应用留的内存是少之又少(例如有的手机只给了16M),随之而来的就是内存溢出,程序崩溃。这些都是用户不可接受的。fresco可以自己管理图片以及内存,避免了内存溢出。
  
  在真正学习fresco之前,我们首先学习一下android都有哪些内存区域(本人也是刚开始接触android的开发,但是从基础学习还是很必要的。方法论(抽象)告诉我们在NB的东西只要有方法(实例)做指导就能做好,装一下13大笑),所以学fresco也不例外。

简单的说有三种内存:

  • Java heap
  • Native heap
  • ashmem

      Java heap 实际上设备厂商或者说手机厂商预先设定好的,所有的内存申请都是通过Java的new操作符申请的。这个区域是相对比较安全的,他是由GC(garbage collection)管理的。也就是这个看似安全的管家给我们的应用带来的很多问题。在这里我就不一一例举了,因为我也不知道。但是,我只说一点,就是这个垃圾回收机制,是要在整个应用完全停止的的情况下,才开始执行,这也就是我们在使用app的常常遇见的一个现象–屏幕卡顿,按了半天没反应的原因。
      与Java heap不同的是,native heap.它是用C++的new操作符来申请的,他是没有限制的。当然这个没限制是相对Java heap来讲的。理论上讲,理想情况下你的物理内存有多大,你就能new出来多大的空间。不过,这里的内存申请和释放都需要自己实现,相信这对C++工程师不是什么难事。
      android的另一个内存区域是ashmem,就是传说种匿名内存共享。它有点像native heap,在使用ashmem的时候会需要额外的调用两个api,一个是pin,一个是unpin.当使用unpin的时候,内存不是立即就会被释放的,只有在内存紧张的时候才会被释放。当调用unpin后,再次调用pin的话,如果,之前的内存没有被释放,那么原始数据是不会被清除的。

Bitmap的可清除性:

  除了极个别的情况,很少有android应用程序会直接操作ashmem.不过,bitmap就是其中的一个.当我我们创建一个bitmap的时候,也就是一个解码后的image.我们可以通过一下android的API去使该bitmap可被清除

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

可清除的bitmap是在ashmem heap中,所以,它是不会被GC回收的。当绘画系统要渲染这副image的时候,那Android系统库就会把它"pin”住,当渲染结束后,就会”unpin”.”unpin”后的内存随时有可能会被系统回收。当一个unpin后的图片需要再次被渲染的时候,那么该image会在传输过程种,一边解码,一边传输。这看起来不错,但是,这个过程是放生在UI线程种,而且,解码的过程是很消耗cpu资源的,所以这个过程往往会造成UI的卡顿。因此,Google也不建议使用这种方式。他们现在建议使用另一个标志位inBitmap.然后,它还有几个令人用的不爽的地方就是,在android3.0之前是没有这个特性的,在Android4.4之前呢,又必须要求所有的image尺寸一致(这不扯淡吗,怎么可能,至少大多数应用是不可能)。从Android4.4之后才好用,然而,使用我的应用的人不能都保证是4.4版本之后的。所以,这个方法也是行不通啊。

有便宜就得占哦:

  我们找到个方法,既可以让UI不卡顿,也让内存操作更快。如果我们在UI线程之外提前就能把内存pin上,并且保证之后不unpin.那么我们app就不会再有UI卡顿这么不爽的用户体验了。庆幸的是,我们能做到~ 在Android的Native Development Kit(NDK)中恰好有个函数能做到。这就是:AndroidBitmap_lockPixels。这个api原本是要和unlockPixels成对使用的。但是,facebook这帮人发现即使只调用lockPixels而不掉用unlockPixels也没事,因为它们又不会打扰JavaHeap,并且能安全的存在。也不会降低UI线程的速度。就仅仅几行代码就OK了。(这么做有点流氓啊。。。)

身在曹营(Java),心在汉(C++):

  用c++的思想去写Java代码。蜘蛛侠告诉我们:"能力有多大,责任就有多大"。Pinned的可清除image,既没有GC的回收机制,也没有像ashmem那样内置的回收机制,所以,内存回收,防止内存溢出的重任就落在了我们自己的头上了。
  在C++中,通常的解决办法,就是用个smart pointer 类去实现引用计数。这些都是利用了C++语言的特性,如构造函数,赋值操作符,以及析够函数。但是,在GC大行其道的Java中,这些语法所带来的便利是不存在的,所以,我们必须找到某种办法去实现C++的这种方式。
  我们用两个类去实现上述的要求。一个就是SharedReference.它有两个方法,一个是addReference,另一个就是deleteReference.每当它被传到某个作用域或者出了某个作用域,这两个方法必须被调用。一旦引用计数为0,那么该资源随即就被销毁(例如:Bitmap.recycle).
  显然,让一个Java工程师去调用这些方法会大幅度提高出错率,因为,这种机制本来就是Java有意要避免的。我们用的另一个类就是CloseableReference,实际上SharedReference就是CloseableReferene的一个成员变量(从源码可以看出来)。它不仅实现了Java种的Closeable接口,也实现了Cloneable接口。CloseableReference的构造函数和clone方法会调用addReference(),close()方法会调用deleteReference().
  至此,Java工程师只需要做两个简单的事:

  1. 一个新的对象用CloseableReference的clone()方法去赋值。
  2. 在出作用域之前调用close()方法,该方法通常是在finally块中调用的。

fresco不是一个独立加载器,它更是一个管道:

在一个移动设备中,image的显示涉及很多步骤,下面的图能说明一切。Fresco: android上的图像处理库_第1张图片

很多优秀的开源库也实现了上述的流程图–Picasso,Universal Image Loader,Glide 和Volley等等,所有的这些都对Android的开发作出了卓越的贡献。FaceBook的工程师认为他们在某些方面做的更好。
  把上述每一个步骤当作是pipeline的一个部分比把它们当作一个整体去加载一个图片更具有意义。把上述的每一步进可能的与其他步骤独立,只要一个输入,和一些其他的参数就能得到相应的输出。这样,我们就能让一些操作可以并行执行,一部分操作顺序执行。一些操作,只能在指定条件下去执行,然后有些需求又依赖于他们所在的线程执行结果。当考虑到渐进加载图片的时候,那么情况就变得更复杂了。有些人经常在网络很慢的情况使用app,facebook的工程师就想让用会尽可能快的看到他们的图片,即使是在图片还没有完全下载下来的时候。

你也别担心,用流水操作就OK了~

  上面的问题,似乎挺棘手,不过还是有办法解决的。流水操作闪亮登场。(吹NB开始)本人之前做过两年异构计算。深知优化之道。提升系统性能的方法,无外乎并行计算(SIMD就是其中的一个实现方式,例如Neon和目前比较流行的并行计算语言opencl,cuda主要就是这种模式),线程级并行(这不用多说了,典型的就是android应用中UI线程与非UI线程的并行),以及多级流水实现不同操作上的时间隐藏,从而提高性能。
  
  言归正传,在Java种,异步的代码是通过像Future这样的机制实现的。代码被提交给另一个线程去执行,然后有个像Future这样的的对象去检查结果出来了没有,如果要是在返回结果只有一个的情况下,这个模式确定挺实用。不过,在处理图片渐进加载的时候,似乎就不那么灵光了,因为,我们等待的返回是一系列连续的结果。Facebook的工程师的解决办法就是----弄个加强版的"Future"叫做DataSource。它提供一个subscribe方法,调用这个方法的时候必须要传两个参数,一个是DataSubscriber另一个是Executor.其中,DataSubscriber接受来自DataSource的结果通知,同时他也提供一个很简单的方法来判断这个结果是中间结果,还是最终结果。因为,我们处理的对象,经常是需要关闭的,所以,DataSource本身也是Closeable的。
  
  在这种背景下,上面的流程图的每一个模块都是采用生产/消费者模式实现的。他们的接口都是非常简单的,Producer只有一个方法produceResults(Consumer,*),这个方法有个Consumer参数, Consumer也有个方法onNewResult.
  我们用一个系统把这些Producer都串联起来,假设Producer的工作就是把类型I转变成类型O,那么就像下面的

例子:

public class OutputProducer<I, O> implements Producer<O> {

  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<I> {
    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);      
    }
  }
}

这个方法,让我们把一系列复杂的东西联系在了一起,同时又保持了他们逻辑上的独立性。

动画:从一到多

  Stickers是以GIF和WebP的形式存储的动画。这些动画很受欢迎。在页面显示这些动画将会带来新的挑战,因为一个动画不单单是一个Bitmap,而是一组Bitmap.每一个Bitmap都需要解码,存储以及显示。对于大尺寸的动画,要是想把每一帧都存放在内存中有点不现实。
  我们创建了一个AnimatedDrawable和两个后台去支持它去渲染GIF和WebP.AnimatedDrawable实现了Andorid标准的Animatable接口,所以调用者可以随时的启动和停止动画。内存优化采用的方法是,当图像很小的时候,缓存所有帧,当图像很大的时候,将在传输过程中解码。这些操作对于调用者来说都是可控的。两个后台支持都是采用C++语言实现的。我们保存了,未解码的数据以及数据信息的副本。我们引用计数数据,这使得在Java端的多个Drawables可以同时访问同一个WebP图像。

Fresco怎么用:

  如果是单单使用层面的需求,那么可以看看facebook的官方使用手册,介绍的虽然不那么详细,但是够用了。fresco的源码在这里,感兴趣的话,可以去看看,但是建议把上面内容仔细看完在去研究源码,这样会对你的理解有很大帮助。
  我最开始研究Fresco是为了图片的渐进加载效果。不过遗憾的是目前fresco的源码是有bug的,没有实现渐进加载的效果,直到0.5.0版本后,渐进加载才好用。所以,有需求的同学别走弯路。还有,在实现渐进加载的时候,不仅需要的程序支持,更重要的是服务器上的jpeg图片也支持,这里简单说一下,jpeg图片有两种存储模式,一个是baseline,另一个是progressive.我们需要的就是progressive这种。如何转化成progressive,详细情况请参照这里。
  最后,我自己也实现了一个渐进加载的小例子https://github.com/sminger1202/progressiveLoading,感兴趣的同学就看看。

本文根据英文博客并加入了一些自己的理解,和实现的例子构成,本人知识有限,时间仓促,不免有观点不对的地方,请批评指正~~~ 

你可能感兴趣的:(图片加载)