Fresco学习笔记

Fresco学习笔记

Fresco是Facebook出品的一款比较新的图片库,相比 Volley 或者 Picasso 具有很多优点,这篇文章主要是记录如何学习使用Fresco。 内容主要来自: http://fresco-cn.org/docs/getting-started.html


 

配置和使用

引用

Gradle添加lib

dependencies { compile 'com.facebook.fresco:fresco:0.5.0+'}

使用

网络权限

 

初始化Fresco

注意,初始化Fresco的代码必须位于setContentView(ResourceId)代码调用之前。

Fresco.initialize(context);

xml布局文件命名空间

 
 

使用SimpleDraweeView

 

需要注意的是,Fresco的layout_width/height必须要设置,但不能使用wrap_content来根据图片自动调整布局大小,但根据layout_weight属性均分的时候,可以使用wrap_content属性。

加载图片

Uri uri = Uri.parse("https://raw.githubusercontent.com/facebook/fresco/gh-pages/static/fresco-logo.png");
SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view);
draweeView.setImageURI(uri);

剩余的工作,Fresco完成

  • 显示占位图直到加载完成
  • 下载图片
  • 缓存图片
  • 图片不再显示时,从内存中移除

几个概念

Drawees

Drawees负责图片的显示、包括几个组件,组织上类似MVC架构。

DraweeView

继承自View,负责图片的显示。(需要注意的是,Fresco并没有使用自定义View,而使用了View来显示图片) 一般情况下,使用SimpleDraweeView即可。

DraweeHierarchy

DraweeHierarchy用于组织和维护Draweeable对象,相比于MVC中的Module,需要在Java代码中自定义图片的显示,可通过该类实现。

DraweeController

DraweeController负责和image loader交互(默认情况下使用Fresco的image pipeline),可创建一个该类的实例,用于实现对图片的更多的控制。

DraweeControllerBuilder

DraweeControllers由DraweeControllerBuilder采用Builder构建者模式创建,immutable。

Liseners

builder的作用之一就是,当图片从服务器下载时,在某些状态时刻设置需要执行的代码。

Image Pipeline

Fraesco使用image pipeline来处理图片获取的过程,包括从网络、本地文件、content provider、本地资源中获取,Image pipeline在本地磁盘中保存了一份压缩的图片缓存,在RAM中一份未压缩的图片。 Image pipeline使用特定的技术 pinned purgeables 来在Java heap之外保存图片,这也要求在图片使用完成之后要关闭 close 图片。 SimpleDraweeView自动处理这一过程,大部分情况下可以满足我们的使用需求。

支持的URI

Fresco 不支持 相对路径的 URI,因此所有的URI在使用的时候指定绝对路径,包括scheme。 Fresco支持的URI包括:

Type Scheme Fetch method used
File on network http://, https:// HttpURLConnection or network layer
File on device file:// FileInputStream
Content Provider content:// ContentResolver
Asset in app asset:// AssetManager
Resource in app res:// Resource.openRawResource

注意:image pipeline仅仅支持图片资源,比如PNG图片格式,并未支持其他的比如String或者XML Drawable等。其中可能困惑的就是使用 XML 声明的Drawable,比如ShapeDraable,需要注意的是,ShapeDrawable也并非图片。如果必须要在Fresco中使用XML Drawable,可以将XML drawable指定为 placeholder,并指定uri为 null.

Drawee

在XML中使用Drawees

xml中支持的可配置属性

 

wrap_content

需要注意的是,必须为Drawees指定layout_width和layout_height,但Drawees不支持wrap_content属性。原因是:占位图和所下载的图像可能大小并不一致,如果不一致,那么下载完成之后,若为warp_content,那么View将会重新layout,改变大小和位置,导致界面跳跃。(其他的Picasso和Volley是怎么处理的?是默认使用placeholder的大小吗?)

固定宽高比

如果需要设置固定的宽和高的比例,比如4:3,那么则在XML中,指定width,height设定为warp_content。在代码中指定比例,如下:

 
mSimpleDraweeView.setAspectRatio(1.33f);

Java中使用Drawee

更改图片大小

最简单的方法是,setImageURI即可。

mSimpleDraweeView.setImageURI(uri);

如果需要更加复杂的效果,可使用controller builder.

自定义Hierarchy

在一般情况下,XML中的配置参数可满足对hierarchy参数设置的需求,但有的时候,用户可能有更多的需求:

首先创建一个hierarchy builder的实例,然后构建hierarchy并传递给Drawee。

List  backgroundsList;
List  overlaysList;
// 创建Hierarchy Builder
GenericDraweeHierarchyBuilder builder =
    new GenericDraweeHierarchyBuilder(getResources());
// 创建Hierarchy
GenericDraweeHierarchy hierarchy = builder
    .setFadeDuration(300)
    .setPlaceholderImage(new MyCustomDrawable())
    .setBackgrounds(backgroundList)
    .setOverlays(overlaysList)
    .build();
// 设置给DraweeView
mSimpleDraweeView.setHierarchy(hierarchy);

需要注意的是,对于同一个View,不要调用超过1次的setHierarchy方法,即使该view已经被回收也不可以。 Hierarchy的创建是 耗时 的,因此要注意重复利用而不是反复创建。 需要更改显示的图片可通过调用setController或者setImageURI来实现。

修改Hierarchy的配置

Hierarchy中的配置可在运行期间进行多次修改,调用下列代码获取Hierarchy

GenericDraweeHierarchy hierarchy = mSimpleDraweeView.getHierarchy();
修改placeholder
hierarchy.setPlaceholderImage(R.drawable.placeholderId);
Drawable drawable; 
// create your drawable
hierarchy.setPlaceholderImage(drawable);
修改图片

修改图片缩放方式

hierarchy.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_INSIDE);

如果设置的缩放方式为 focusCrop , 那么需要设置焦点:

hierarchy.setActualImageFocusPoint(point);

给图片添加Color Filer

ColorFilter filter;
// create your filter
hierarchy.setActualImageColorFilter(filter);
设置Rounding
RoundingParams roundingParams = hierarchy.getRoundingParams();
roundingParams.setCornersRadius(10);
hierarchy.setRoundingParams(roundingParams);

Drawee Components

定义

Drawees中,除了真实的需要显示的图片之外,其他的属性均可通过XML进行配置,在XML中配置的属性,必须为Android drawable或者color资源。 这些属性也可通过GenericDraweeHierarchyBuilder在程序中进行配置,如果在程序中配置,那么配置的资源必须为Drawable的子类。 有些属性可以在程序运行中实时配置,这些拥有可配置的属性都在GenericDraweeHierarchy类中拥有对应的方法。

Actual

Actual即对应的需要显示的图片,使用URI指定,可为网络资源、本地文件、本地资源或者content provider。 Actual为controller的属性而不是hierarchy的属性。因此在一个Drawee中配置的Actual不会出现在其他的Drawee中。 可使用setImageURI来设置图片或者通过设置controller来设置。 除了sclale类型之外,hierarchy还公开了设置Acutal图片的其他的方法:

  • focus point,当设置scale type为focuesCrop时有效
  • color filter
  • 默认的scale type为:centerCrop

Placeholder

当Drawee出现在界面的时候,会首先显示 placeholder ,当调用 setControlelr 或者 setImageURI 设置Drawee显示的图像之后, placeholder 仍会显示直至图片加载完成。如果加载的图片为渐进式JPEG(progressive JPEG),可设置JPEG的清晰度阈值,当清晰度满足条件之后,则placeholder不再显示。

  • XML属性:placeholderImage
  • Hierarchy builder方法:setPlaceholderImage
  • Hierarchy方法:setPlaceholderImage
  • 默认值:transparent ColorDrawable
  • 默认sclale方式:centerInside

Failure

加载失败的时候,显示Failure

  • XML属性:failureImage
  • Hierarchy builder方法:setFailureImage
  • 默认值:*placeholder image*
  • 默认sclale方式:centerInside

Retry

当图片下载失败,且对应的controller设置 enable tap-to-retry 属性之后。 必须在 build your own Controller 中设置tap-to-retry属性: setTapToRetryEnabled(true) image piple会在用户点击之后重试,最高可重试4次。

  • XML属性:retryImage
  • Hierarchy builder方法:setRetryImage
  • 默认值:*placeholder image*
  • 默认sclale方式:centerInside

Progress Bar

如果设置该属性,则会在Drawee之上显示一层overlay,直至最终的图片加载完成。

  • XML属性:progressBarImage
  • Hierarchy builder方法:setProgressBarImage
  • 默认值:*placeholder image*
  • 默认sclale方式:centerInside

Backgrounds

Background会首先绘制,在hierarchy其余层次之下。 在XML中仅仅可以指定一个Background,但是在Java代码中可以指定多个,这种情况下,则会将第一绘制在地下,然后绘制剩下的Background。 Backgound images不支持scale-types,并且自动缩放至Drawee的尺寸。

  • XML属性:backgroundImage
  • Hierarchy builder方法:setBackground、setBackgrounds
  • 默认值:None
  • 默认sclale方式:N/A

Overlays

Overlays会最后绘制,在hierarchy其余层次之上。 在XML中仅仅可以指定一个Overlay,但是在Java代码中可以指定多个,这种情况下,则会将第一绘制在地下,然后绘制剩下的Overlay。 Overlay images不支持scale-types,并且自动缩放至Drawee的尺寸。

  • XML属性:overlayImage
  • Hierarchy builder方法:setOverlay、setOverlays
  • 默认值:None
  • 默认sclale方式:N/A

Pressed State Overlay

如果指定Pressed State Overlay,那么在用户点击Drawee区域之后,则会显示对应的Overlay。比如,Drawee在显示一个button图片,overlay可以用于在用户点击之后改变button的颜色。 不支持scale-types。

  • XML属性:pressedStateOverlayImage
  • Hierarchy builder方法:setPressedStateOverlay
  • 默认值:None
  • 默认sclale方式:N/A

Progress Bars

在应用程序中,最简单的使用progress bar的方式是在构建hierarchy的时候使用ProgressBarDrawable。

.setProgressBarImage(new ProgressBarDrawable())

通过该操作可在Drawee下面显示一个黑色蓝角的progress bar。

定义自己的progress bar

如果我们需要定义自己的progress bar,需要 注意 的是 为了当加载的时候,progress能够动态变化,需要覆写Drawable.onLevelChange方法

class CustomProgressBar extends Drawable {
   @Override
   protected void onLevelChange(int level) {
     // level is on a scale of 0-10,000
     // where 10,000 means fully downloaded

     // your app's logic to change the drawable's
     // appearance here based on progress
   }
}

Scaling

可用缩放类型

ScaleType Explanation
center Center the image in the view, but perform no scaling.
centerCrop Scales the image so that both dimensions will be greater than or equal to the corresponding dimension of the parent.
One of width or height will fit exactly.
The image is centered within parent’s bounds.
focusCrop Same as centerCrop, but based around a caller-specified focus point instead of the center.
centerInside Downscales the image so that it fits entirely inside the parent.
Unlike fitCenter, no upscaling will be performed.
Aspect ratio is preserved.
The image is centered within parent’s bounds.
fitCenter Scales the image so that it fits entirely inside the parent.
One of width or height will fit exactly.
Aspect ratio is preserved.
The image is centered within the parent’s bounds.
fitStart Scales the image so that it fits entirely inside the parent.
One of width or height will fit exactly.
Aspect ratio is preserved.
The image is aligned to the top-left corner of the parent.
fitEnd Scales the image so that it fits entirely inside the parent.
One of width or height will fit exactly.
Aspect ratio is preserved.
The image is aligned to the bottom-right corner of the parent.
fitXY Scales width and height independently, so that the image matches the parent exactly.
Aspect ratio is not preserved.
none Used for Android’s tile mode.

以上的ScleType与Android中提供的Scale Type类似,但不同的是Fresco中不提供 matrix,然而Fresco中提供 focusCrop以实现更好的效果。

如何设置Scale Type

Actual, placeholder, retry和failure图片均可在XML中配置,使用方法为:fresco:actualImageScaleType,我们同样也可以在GenericDraweeHierachyBuilder类中使用代码设置。 即使在hierarchy设置之后,我们仍然可以通过GenericDraweeHierarchy来配置。 然而, 不要使用 android:scaleType属性,同样 不要使用 .setScaleType方法,这两种方式对Drawees没有效果。

focusCrop

Android和Fresco均提供 centerCrop缩放类型,该类型很常用,但也会存在使用场景的尴尬时刻,比如图片的左上角为头像,则无法通过 centerCrop来获取。 通过设置focus point,我们可以设置获取图片的某些部分,比如我们设置focus point为图片的边上,比如(0.5f, 0f),Fresco可以保证,无论怎样扩展,该focus point均可以显示。 Focus points使用相对坐标系统,(0f, 0f)为左上角,(1f, 1f)为右下角。 如果focus point设置为(0.5f, 0.5f),那么则等于 centerCrop。 为了使用focus points,必须在XML中首先设置正确的scale type。

fresco:actualImageScaleType="focusCrop"

在Java代码中,必须程序 显式 设置正确的focus point。

PointF focusPoint;
// your app populates the focus point
mSimpleDraweeView
    .getHierarchy()
    .setActualImageFocusPoint(focusPoint);

Rounded Corners and Circles Edit on GitHub

在很多时候,APP需要图片有圆角或者是园。Drawee支持各种各样的变化,但不会引入由于复制bitmaps而引入的额外的内存开销。

图片可以以两种方式制作成圆角

  • circle,圆形,设置roundAsCircle为true
  • 如果图片是个直角,需要圆角,那么同时需要设置roundedCornerRadius。 图片是直角的情况下,支持为4个角分别设置4个不同的圆弧半径,但这种情况下只能在Java中设置而不是XML中设置。

怎么设置

图片可以通过两种方式来设置圆弧

  • BITMAP_ONLY:使用一个shader来绘制图片以及圆角。这是默认的生成圆角的办法。这种方法在针对actual image和placeholder的时候有效。针对其他的,比如failure和retry的图片,不会生成圆角。此外,这种生成圆角的方式不支持动画效果。
  • OVERLAY_COLOR :通过生成一个solid color的overlay来生成圆角,由调用者指定。在这种情况下,Drawee的背景颜色应该设置为固定的,且与solid color同样。可在XML中设置 roundWithOverlayColor或者在Java代码中调用setOverlayColr方法来设置这种效果。 ###XML 通过在XML中配置此类属性,可以将设置传递给RoundingParams。
 

Java代码

当创建hierarchy的时候,我们可以传递一个RoundingParams的实例给GenericDraweeHierarchyBuilder:

RoundingParams roundingParams = RoundingParams.fromCornersRadius(7f);
// alternatively use fromCornersRadii or asCircle
roundingParams.setOverlayColor(R.color.green);
genericDraweeHierarchyBuilder
    .setRoundingParams(roundingParams);

我们也可以在代码运行的时候修改大部分的rounding参数:

RoundingParams roundingParams = RoundingParams.fromCornersRadius(5f);
roundingParams.setBorder(R.color.red, 1.0);
roundingParams.setRoundAsCircle(true);
mSimpleDraweeView.getHierarchy().setRoundingParams(roundingParams);

注意

在使用BITMAP_ONLY(默认)情况下,有几种使用上的限制:

  • 并非所有的图片都会圆角处理,只有actual image和placeholder会被处理。当前Fresco的代码在修改支持对background进行处理。
  • 只有图片可转化为BitmapDrawable或者ColorDrawable可以被rounded处理。对NinePathDrawable、ShapeDrawable以及其他类似的Drawable不会被处理(无论是XML或者Java编程方式)
  • 动画不会被rounded。
  • 由于Android中BitmapShader的限制,如果image的大小无法覆盖住整个View,那么不会说不绘制任何东西,而是edges会被不断重复。一种变通的方法是设置不同的scale类型,比如centerCrop来保证整个View被覆盖住。

OVERLAY_COLOR模式并没有上述限制,但由于其模仿实现rounded的思路是通过在image之上覆盖一层图层的方法实现,因此仅仅在Drawee的背景设置为固定的相同的颜色的时候,具有较好的效果。

Drawee原本拥有CLIPPING模式,但已经取消。

最后,这些问题都可通过设孩子一个临时bitmap的方式解决,但这会引入额外的性能负担,并不推荐。 因此,在Android平台上,没有一个完美的解决方案来实现绘制圆角。

使用ControllerBuilder

SimaleDraweeView可以通过两种方法指定图片,最简单的方式为setImageURI。 如果希望对Drawee拥有更加详细的控制,可以使用DraweeController。

构建DraweeController

将image request传递给一个PipelineDraweeControllerBuilder,并为Controller设置更加详尽的设置。

ControllerListener listener = new BaseControllerListener() {...}

DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setUri(uri)
    .setTapToRetryEnabled(true)
    .setOldController(mSimpleDraweeView.getController())
    .setControllerListener(listener)
    .build();

mSimpleDraweeView.setController(controller);

注意 我们应当记住,当构建一个新的controller的时候,应当调用setOldController以避免不必要的内存开销。

自定义ImageRequest

在一些比较高级的应用场合,我们可能需要向pipeline传递一个ImageRequest,而不仅仅是一个URI。其中一个例子就是使用postprocessor。

Uri uri;
Postprocessor myPostprocessor = new Postprocessor() { ... }
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setPostprocessor(myPostprocessor)
    .build();

DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setImageRequest(request)
    .setOldController(mSimpleDraweeView.getController())
    // other setters as you need
    .build();

Progressive JPEGs 渐进式JPEGs

Fresco支持从网络上获取Progressive JPEGs。 渐进式JPEGs仅仅在从网络上获取的时候支持。本地的图片在一次解码中全部完成,因此并不需要这种需求。

构建image request

如果需要Fresco支持渐进式JPEG,那么必须在配置image request的时候明确指定渲染此类型的image。

Uri uri;
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setProgressiveRenderingEnabled(true)
    .build();
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setImageRequest(request)
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);

Fresco会考虑在以后的版本中,通过setImageURI支持渐进式JPEGs。

动画图像

Fresco支持 GIF 动画和 WebP 格式的动画。 Fresco 在Android 2.3+ 版本上支持 WebP 以及 WebP extended 格式的图片,而此类格式在Android原生版本中并未支持。

自动播放动画

如果希望当图片出现在屏幕上的时候立刻开始绘制,而当图片不显示的时候停止播放动画,可通过对image request进行配置实现:

Uri uri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setUri(uri)
    .setAutoPlayAnimations(true)
    . // other setters
    .build();
mSimpleDraweeView.setController(controller);

手动控制播放

如果希望手动控制播放,那么需要监听图片加载的过程。

ControllerListener controllerListener = new BaseControllerListener () {
    @Override
    public void onFinalImageSet(
        String id,
        @Nullable ImageInfo imageInfo,
        @Nullable Animatable anim) {
    if (anim != null) {
      // app-specific logic to enable animation starting
      anim.start();
    }
};

Uri uri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setUri(uri)
    .setControllerListener(controllerListener)
    // other setters
    .build();
mSimpleDraweeView.setController(controller);

此外,controller暴露Animatable的接口。如果non-null,我们可通过该接口直接控制动画播放。

Animatable animatable = mSimpleDraweeView.getController().getAnimatable();
if (animatable != null) {
  animatable.start();
  // later
  animatable.stop();
}

局限性

动画Animations不支持postprocessors功能。

Requesting Multiple Images (Multi-URI)

这种方法要求我们实现自己的 image request , 主要有以下几种使用场景:

从低分辨率-->高分辨率

有的情况下,高分辨率的图片其体积较大,这个时候直接下载的话,就会导致图片很长时间无法显示,在这种情况下,可以先设置一个低分辨率的图片,然后当高分辨率的图片下载完成之后显示。

Uri lowResUri, highResUri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setLowResImageRequest(ImageRequest.fromUri(lowResUri))
    .setImageRequest(ImageRequest.fromUri(highResUri))
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);

使用缩略预览图

本选项仅供本地URI使用,且仅供JPEG图像使用 如果JPEG图像在其EXIF元信息中保存了缩略图,那么image pioeline可以先返回该缩略图。那么我们的Drawee则会先显示缩略预览图,当完整的图片加载完成并成功解码之后,会更改为显示完整的图像。

Uri uri;
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setLocalThumbnailPreviewsEnabled(true)
    .build();

DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setImageRequest(request)
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);

加载第一张下载成功的图片

一般时候,一张图片仅仅对应一个URI,但有的场景也是例外的。

比如,我们可能拥有一张从Camera获取的已经上传过的图像。原始的图像直接上传的话,体积实在太大,因此需要首先将该图像压缩。在这种场景下,我们就可以先使用压缩的图像(local-downscaled-uri),如果加载失败之后,则加载Camera原始图像(local-original-uri),即使失败了,那么则尝试加载网络上的图像(network-uploaded-uri)。

正常情况下,image pipeline会首先从memory cache中搜索图像,然后是disk cache,再然后才是network或者其他的资源。我们可以使得pipeline首先搜索memory cache中所有的图像,而不是一次一次依次操作,只有在没有找到任何图片的时候,再从disk cache中进行搜索。如果仍然没有,则会继续发出其他的请求。

实现起来也很简单,仅仅需要创建一个image request的数组,然后传递给controller的builder。

Uri uri1, uri2;
ImageRequest request = ImageRequest.fromUri(uri1);
ImageRequest request2 = ImageRequest.fromUri(uri2);
ImageRequest[] requests = { request1, request2 };

DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setFirstAvailableImageRequests(requests)
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);

以上的两个请求中,仅仅有一个图像可以得到显示,主要取决于哪张图片最先加载成功,无论是memory、disk或者network。pipeline会假设requests数组的顺序为用户所期待的搜索顺序。

指定自定义DataSource Supplier

为了更加灵活的使用,我们可以在构建Drawee controller的时候,为Drawee controller指定一个自定义的DataSource Supplier。我们可以自定义一个DataSource Supplier或者是组合一些已经有的实现。可以参考 FirstAvailableDataSourceSupplier和InscreasingQualityDataSourceSupplier作为例子。参考AbastractDraweeControllerBuilder来参考怎么组合不同的DataSource Supplier。

监听Downloads Events

原因 当图片下载完成的时候,我们可能需要执行一些特殊的操作:比如,显示其他的图片,显示标题等等;或者说当网络故障,提示用户等等。

图片的加载是异步进行的,所以需要额外的操作来监听DraweeController发布的events事件,其原理就是controller listener。

注意 此处不允许对图片本上进行修改,如果需要对图片本身进行修改,使用Postprocessor即可。

使用

使用很简单,只要实现ControllerListener接口即可,我们建议集成BaseControllerListener来实现。

ControllerListener controllerListener = new BaseControllerListener () {
    @Override
    public void onFinalImageSet(
        String id,
        @Nullable ImageInfo imageInfo,
        @Nullable Animatable anim) {
      if (imageInfo == null) {
        return;
      }
      QualityInfo qualityInfo = imageInfo.getQualityInfo();
      FLog.d("Final image received! " + 
          "Size %d x %d",
          "Quality level %d, good enough: %s, full quality: %s",
          imageInfo.getWidth(),
          imageInfo.getHeight(),
          qualityInfo.getQuality(),
          qualityInfo.isOfGoodEnoughQuality(),
          qualityInfo.isOfFullQuality());
    }

    @Override 
    public void onIntermediateImageSet(String id, @Nullable ImageInfo imageInfo) {
      FLog.d("Intermediate image received");
    }

    @Override
    public void onFailure(String id, Throwable throwable) {
      FLog.e(getClass(), throwable, "Error loading %s", id)
    }
};

Uri uri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setControllerListener(controllerListener)
    .setUri(uri);
    // other setters
    .build();
mSimpleDraweeView.setController(controller);

onFinalImageSet or onFailure 对于所有的image加载工作都会起作用。

对于渐进式JPEG,如果允许渐进显示,那么每次解码之后,都会调用onIntermediateImageSet方法。

Resizing and Rotating

调整大小、旋转是图像显示中经常使用的功能,要实现功能,需要手工直接指定image request。

Resizing Images

Resizing VS Scaling

  • Resizing:是一种软件操作,在pipeline中执行,返回一个完整的拥有不同尺寸的新的图像。
  • Scaling:是一种绘制操作,一般采用硬件加速。图像本身大小不变。

Resize和Scaling如何选择? Resizing并不经常使用,Scaling经常使用,即使在resizing中,也会使用Scaling。

Resizing在使用上有几个局限性:

  • Resizing不会返回比原始图像更大的image,Resizing操作只会使得图像更小。
  • 当前仅仅JPEG支持resized。
  • 对于图像的Resize的尺寸控制,仅仅有个大约的数字。图像无法调制大小至非常精确的尺寸,这就意味着,图像即使经过Resized,那么在显示的时候,也需要缩放才能使用View大小。
  • 仅仅支持一下的Resizing尺寸: N/8 with 1 <= N <= 8.
  • Resize调整大小是纯软件操作,相比硬件加速的scaling,速度要慢很多。R

相比之下,Scaling没有以上的局限。Scaling使用Android内置的缩放算法。在Android4.0以上的设备上可以使用GPU硬件加速。在大多数情况下,这也是最快以及最有效的将图片调整至合适尺寸的方法。为一的缺点是,如果图片的尺寸要远远大于界面的话,内存就浪费掉了。

那为什么还要使用Resizing呢?这是一种权衡。我们仅仅在图像的大小远大约View的大小,可以通过Resizing来节约内存的使用。一个具体的例子比如说,我们需要在1280*800(大约1MP)的分辨率上显示相机拍摄的一个8MP的相片。那么这个8MP的图像在ARGB排列的时候大约占据32MB的内存,如果将尺寸Resized,那么可以最低占用4MB的内存。

如果图像是从网络上获取的,在resizing之前,应当考虑能否从网络中获取对应大小尺寸的图像。如果服务器可以返回一个较小尺寸的图像,不要尝试获取8MP的图像。在开发中,要考虑使用者的数据流量消耗。除此之外,获取较小尺寸的图像,还可以节省内存存储空间和CPU时间。如果服务器无法返回一个合适较小尺寸的图片,或者正在使用本地图库,那么应该考虑resizing。除了这些情况之外,我们应当首先考虑使用Rescaling。如果需要Scale,仅仅需要设定SimpleDraweeView的layout_width/layout_height即可。

Resizing

Resizing不会修改原先的文件,而是对内存中的图像进行解码。当前的Fresco仅仅支持JPEG调整尺寸。为ImageRequest传递ResizeOptions即可。

Uri uri = "file:///mnt/sdcard/MyApp/myfile.jpg";
int width = 50, height = 50;
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setResizeOptions(new ResizeOptions(width, height))
    .build();
PipelineDraweeController controller = Fresco.newDraweeControllerBuilder()
    .setOldController(mDraweeView.getController())
    .setImageRequest(request)
    .build();
mSimpleDraweeView.setController(controller);

Auto-rotation

自动旋转

ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setAutoRotateEnabled(true)
    .build();
// as above

对图像进行编辑

原因

有些情况下,需要对从服务器或者本地获取的图片进行修改,最好的方法是使用Postprocessor,其中组好的实现方式是集成BasePostprocessor类。 例子:在图像上添加红色网:

Uri uri;
Postprocessor redMeshPostprocessor = new BasePostprocessor() { 
  @Override
  public String getName() {
    return "redMeshPostprocessor";
  }

  @Override
  public void process(Bitmap bitmap) {
    for (int x = 0; x  < bitmap.getWidth(); x+=2) {
      for (int y = 0; y  < bitmap.getHeight(); y+=2) {
        bitmap.setPixel(x, y, Color.RED);
      }
    }
  }
}

ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setPostprocessor(redMeshPostprocessor)
    .build();

PipelineDraweeController controller = (PipelineDraweeController) 
    Fresco.newDraweeControllerBuilder()
    .setImageRequest(request)
    .setOldController(mSimpleDraweeView.getController())
    // other setters as you need
    .build();
mSimpleDraweeView.setController(controller);

需要明确

  • 图像进入postprocessor得到修改,但需要注意的是,cache中的图像不会受到此处postprocessor的影响。在Android4.x及以下版本,修改后的图像与原图像一起保存在Java堆之外。
  • 为了每次都能够展现同样的编辑效果,使用的时候必须每次都指定postprocessor。对于不同请求的同一个图像,可以使用不同的postprocessor。
  • 动态图像不支持postprocessor

复制图像

可能有些原因导致无法针对原图像直接编辑,如果这种情况下,BasePostprocessor拥有另外的方法签名已解决这个问题,此处例子展示了如何将bitmap做水平翻转。

@Override
public void process(Bitmap destBitmap, Bitmap sourceBitmap) {
  for (int x = 0; x  < destBitmap.getWidth(); x++) {
    for (int y = 0; y  < destBitmap.getHeight(); y++) {
      destBitmap.setPixel(destBitmap.getWidth() - x, y, sourceBitmap.getPixel(x, y));
    }
  }
}

原图像与目标图像大小完全一样。 注意:

  • 不要修改元sourceBitmap,在以后的版本中,如果将sourceBitmap进行修改将会抛出异常。
  • 不要保存对任一个bitmap对象的引用,此处的bitmap对象均由pipeline管理,destBitmap生命周期与Drawee或者DataSource同样。

复制图像,且图像尺寸不一致

如果postprocessed图像需要一个不一样尺寸的图像,此处有第三个方法签名:使用PlatformBitmapFacotry类来安全的创建一个保存在Java heap之外的自定义尺寸的Bitmap。 此处例子展示如何创建一个1/4尺寸的图像:

@Override
public CloseableReference  process(
    Bitmap sourceBitmap,
    PlatformBitmapFactory bitmapFactory) {
  CloseableReference  bitmapRef = bitmapFactory.createBitmap(
      sourceBitmap.getWidth() / 2,
      sourceBitmap.getHeight() / 2);
  try {
    Bitmap destBitmap = bitmapRef.get();
     for (int x = 0; x  < destBitmap.getWidth(); x+=2) {
       for (int y = 0; y  < destBitmap.getHeight(); y+=2) {
         destBitmap.setPixel(x, y, sourceBitmap.getPixel(x, y));
       }
     }
     return CloseableReference.cloneOrNull(bitmapRef);
  } finally {
    CloseableReference.closeSafely(bitmapRef);
  } 
}

注意,必须遵守closeable reference的使用规范。 不要使用 Android的Btimap.createBitmap的方法,此方法创建的图像保存在Java heap当中。

Which to override?

Do not override more than one of the three process methods. Doing so can produce unpredictable results.

Caching postprocessed images

可以将postprocess处理的图像进行保存,为了实现这个效果,自定义的postprocessor必须实现getPostprocessorCacheKey方法并且返回一个not null的值。 为了使得cache命中,在以后的request中的postprocessor必须为同样的类,并且返回同样的cache key。如果不是,那么将会覆盖之前生成的cache entry。

public class OperationPostprocessor extends BasePostprocessor {
  private int myParameter;

  public OperationPostprocessor(int param) {
    myParameter = param;
  }

  public void process(Bitmap bitmap) { 
    doSomething(myParameter);
  }

  public CacheKey getPostprocessorCacheKey() {
    return new MyCacheKey(myParameter);
  }
}

如果希望cache始终命中,则设置getPostprocessorCacheKey方法返回固定常量,如果不想使cache命中,则返回null即可。

Repeated Postprocessors

可能需要对同一张图片重复处理多次。这种情况下,仅仅需要集成BaseRepeatedPostprocessor即可。 此处的例子允许对图片上的网格在任意时间变换颜色。

public class MeshPostprocessor extends BaseRepeatedPostprocessor { 
  private int mColor = Color.TRANSPARENT;

  public void setColor(int color) {
    mColor = color;
    update();
  }

  @Override
  public String getName() {
    return "meshPostprocessor";
  }

  @Override
  public void process(Bitmap bitmap) {
    for (int x = 0; x  < bitmap.getWidth(); x+=2) {
      for (int y = 0; y  < bitmap.getHeight(); y+=2) {
        bitmap.setPixel(x, y, mColor);
      }
    }
  }
}
MeshPostprocessor meshPostprocessor = new MeshPostprocessor();

/// setPostprocessor as in above example

meshPostprocessor.setColor(Color.RED);
meshPostprocessor.setColor(Color.BLUE);

需要注意的是,每一个image request仍然需要设置Postprocessor。

Image Requests

如果简单的通过Image URI设置图像,那么使用ImageRequest.fromURI方法即可,但如果需要更多的设置,则需要ImageRequestBuilder;

Uri uri;

ImageDecodeOptions decodeOptions = ImageDecodeOptions.newBuilder()
    .setBackgroundColor(Color.GREEN)
    .build();

ImageRequest request = ImageRequestBuilder
    .newBuilderWithSource(uri)
    .setAutoRotateEnabled(true)
    .setLocalThumbnailPreviewsEnabled(true)
    .setLowestPermittedRequestLevel(RequestLevel.FULL_FETCH)
    .setProgressiveRenderingEnabled(false)
    .setResizeOptions(new ResizeOptions(width, height))
    .build();

Fields in ImageRequest

Filed Description
uri the only mandatory field. See Supported URIs.
autoRotateEnabled whether to enable auto-rotation.
progressiveEnabled whether to enable progressive loading.
postprocessor component to postprocess the decoded image.
resizeOptions desired width and height. Use with caution. See Resizing.

Lowest Permitted Request Level

image pipeline根据确定的顺序查找图像

  • Check the bitmap cache. This is nearly instant. If found, return.
  • Check the encoded memory cache. If found, decode the image and return.
  • Check the "disk" (local storage) cache. If found, load from disk, decode, and return.
  • Go to the original file on network or local file. Download, resize and/or rotate if requested, decode, and return. For network images in particular, this will be the slowest by a long shot.

setLowestPermittedRequestLevel方法允许设置pipeline可以走多远,其数值为:

  • BITMAP_MEMORY_CACHE
  • ENCODED_MEMORY_CACHE
  • DISK_CACHE
  • FULL_FETCH

自定义View

DraweeHolders

总有时候DraweeViews无法满足我们的需求。比如我们可能需要在Image所在的View上添加额外的内容,我们可能需要在一个View中显示多个图片。Fresco提供了各种各样的类用于功能扩展:

  • DraweeHolder -> 单个图片
  • MultiDraweeHolder -> 多个图片

自定义View需要做的事情

Android绘制View中的组件,且这些时间仅仅在Android系统内部调用,DraweeViews使用这些时间可以更好的管理内存,提高内存的高效使用。但我们使用holders的时候,必须手动实现其中的部分功能。

处理attach/detach事件

如果不进行该操作,我们的自定义holder可能会出现内存泄露

当Android界面不再绘制图片的时候,图片也就没有在内存中保存的意义。Drawees通过监听detach事件来及时的释放内存。当Android重新绘制的时候,这些图片也会自动恢复以供显示。 这些操作在DraweeView中是自动处理的,但在自定义View中需要我们手动处理4个系统调用,并传递给DraweeHolder,下面为例子: ``` DraweeHolder mDraweeHolder;

@Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); mDraweeHolder.onDetach(); }

@Override public void onStartTemporaryDetach() { super.onStartTemporaryDetach(); mDraweeHolder.onDetach(); }

@Override public void onAttachedToWindow() { super.onAttachedToWindow(); mDraweeHolder.onAttach(); }

@Override public void onFinishTemporaryDetach() { super.onFinishTemporaryDetach(); mDraweeHolder.onAttach(); } ```

处理touch events

如果我们在Drawee中允许tap-to-retry功能,那么需要监听屏幕的触摸事件。

@Override
public boolean onTouchEvent(MotionEvent event) {
  return mDraweeHolder.onTouchEvent(event) || super.onTouchEvent(event);
}

定制onDraw

我们必须调用以下代码Drawable drawable = mDraweeHolder.getTopLevelDrawable(); drawable.setBounds(...);否则Drawee不会显示在界面上。

  • Do not downcast this Drawable.
  • Do not translate it.

其他需要做的工作

设置Drawable.Callback

// When a holder is set to the view for the first time,
// don't forget to set the callback to its top-level drawable:
mDraweeHolder = ...
mDraweeHolder.getTopLevelDrawable().setCallback(this);

// In case the old holder is no longer needed,
// don't forget to clear the callback from its top-level drawable:
mDraweeHolder.getTopLevelDrawable().setCallback(null);
mDraweeHolder = ...
Override verifyDrawable:
@Override
protected boolean verifyDrawable(Drawable who) {
  if (who == mDraweeHolder.getTopLevelDrawable()) {
    return true;
  }
  // other logic for other Drawables in your view, if any
}

Make sure invalidateDrawable invalidates the region occupied by your Drawee.

Constructing a DraweeHolder

组织构造函数

推荐构建函数如下安排:

  • 覆写View的全部3个构造函数
  • 每个构造函数中调用父类的构造方法,然后调用私有的init方法
  • 所有的初始化操作放在 init 方法中完成 这样做的初衷是:不要在一个构造方法中调用其他的构造方法。同时保证了无论调用哪个构造方法,其初始化顺序都是正确的。我们的holder在init方法中创建。

创建Holder

如果可能,总是在自定义View创建之后立刻创建Drawee。hierarchy的创建开销较大,因此最好仅仅操作一次。

class CustomView extends View {
  DraweeHolder  mDraweeHolder;

  // constructors following above pattern

  private void init() {
    GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources());
      .set...
      .set...
      .build();
    mDraweeHolder = DraweeHolder.create(hierarchy, context);
  }
}

设置图片

使用controller build实现,但要调用holder的setController方法而不是View中的方法。

DraweeController controller = Fresco.newControllerBuilder()
    .setUri(uri)
    .setOldController(mDraweeHolder.getController())
    .build();
mDraweeHolder.setController(controller);

MultiDraweeHolder

MultiDraweeHolder  mMultiDraweeHolder;

private void init() {
  GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources());
    .set...
    .build();
  mMultiDraweeHolder = new MultiDraweeHolder ();
  mMultiDraweeHolder.add(new DraweeHolder (hierarchy, context));
  // repeat for more hierarchies
}

其他与DraweeHolder类似。

一些陷阱

Don't use ScrollViews

如果想要实现滑动一列图片的功能,要使用RecyclerView, ListView, or GridView.这些可以复用View,因此Fresco可以知道什么时候Drawee显示了,什么时候不显示了,但ScrollView无法实现,ScrollView会始终保持图像直至Fragment或者Activity销毁,因此如果使用ScrollView,那么程序更有可能遇到OOM的问题。

Don't downcast

It is tempting to downcast objects returns by Fresco classes into actual objects that appear to give you greater control. At best, this will result in fragile code that gets broken next release; at worst, it will lead to very subtle bugs.

Don't use getTopLevelDrawable

DraweeHierarchy.getTopLevelDrawable() should only be used by DraweeViews. Client code should almost never interact with it.

The sole exception is custom views. Even there, the top-level drawable should never be downcast. We may change the actual type of the drawable in future releases.

Don't re-use DraweeHierarchies

Never call DraweeView.setHierarchy with the same argument on two different views. Hierarchies are made up of Drawables, and Drawables on Android cannot be shared among multiple views.

Don't use Drawables in more than one DraweeHierarchy

This is for the same reason as the above. Drawables cannot be shared in multiple views.

You are completely free, of course, to use the same resourceID in multiple hierarchies and views. Android will create a separate instance of each Drawable for each view.

Don't set images directly on a DraweeView

Currently DraweeView is a subclass of Android's ImageView. This has various methods to set an image (such as setImageBitmap, setImageDrawable)

If you set an image directly, you will completely lose your DraweeHierarchy, and will not get any results from the image pipeline.

Don't use ImageView attributes or methods with DraweeView

Any XML attribute or method of ImageView not found in View will not work on a DraweeView. Typical cases are scaleType, src, etc. Don't use those. DraweeView has its own counterparts as explained in the other sections of this documentation. Any ImageView attrribute or method will be removed in the upcoming release, so please don't use those.

Image Pipeline

Introduction

流水线主要进行以下的操作: 1. 检查bitmap cache,如果存在,则返回。 2. 切换至其他的线程。 3. 检查memory cache,如果存在,则对图像解码、变换、返回,并保存至bitmap cache。 4. 检查disk cache。如果存在,则对图像解码、变换、返回,并保存至memory cache 和 bitmap cache。 5. 检查网络或者其他位置的资源,如果存在,则对图像解码、变换、返回,并保存至disk cache、memory cache 和 bitmap cache。

Fresco Image Pipeline

上图中,'disk' cache绘制在encode memory cache中来保持流程图正解。

流水线支持从本地文件、网络中获取。支持PNG、GIF、WebP、JPEG文件。

旧设备上支持WebP

Android在3.0之后才支持WebP,Extended WebP在Android 4.1.2之后才支持。如果设备不支持WebP,image pipeline会将其转换为JPEG,这样就可以在Android2.3以上的系统中均可使用WebP。

配置Image Pipeline

大部分程序可通过 Fresco.initialize(context)来简单配置Fresco。

如果程序需要更加自定义的配置,可使用ImagePipelineConfig类。

ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context)
    .setBitmapMemoryCacheParamsSupplier(bitmapCacheParamsSupplier)
    .setCacheKeyFactory(cacheKeyFactory)
    .setEncodedMemoryCacheParamsSupplier(encodedCacheParamsSupplier)
    .setExecutorSupplier(executorSupplier)
    .setImageCacheStatsTracker(imageCacheStatsTracker)
    .setMainDiskCacheConfig(mainDiskCacheConfig)
    .setMemoryTrimmableRegistry(memoryTrimmableRegistry) 
    .setNetworkFetchProducer(networkFetchProducer)
    .setPoolFactory(poolFactory)
    .setProgressiveJpegConfig(progressiveJpegConfig)
    .setRequestListeners(requestListeners)
    .setSmallImageDiskCacheConfig(smallImageDiskCacheConfig)
    .build();
Fresco.initialize(context, config);

理解Suppliers

在上述参数配置中,很多都配置对应的Supplier的实例,而不是对应的实例。这样看起来可能会麻烦些,但会带来更多的收益,我们可以在程序运行中动态切换Fresco的行为。比如Memory caches,可以每5分钟检查其Supplier。 如果不需要动态调整这些参数,那么可设置Supplier每次返回都一个实例。

Supplier  xSupplier = new Supplier () {
  public X get() {
    return new X(xparam1, xparam2...);
  }
);
// when creating image pipeline
.setXSupplier(xSupplier);

线程池

默认情况下,Image pipeline使用3个线程池。

  • 3个线程用于下载;
  • 2个线程用于所有的disk操作:本地文件读取、disk cache相关
  • 2个线程用于CPU敏感操作:decodes、transforms、后台操作

可通过设置自己的network layer来自定义网络的行为。 对于其他的修改,可通过设置ExecutorSupplier完成。

使用MemoryTrimmableRegistry

如果我们的程序监听系统内存事件,那么可以将这些事件传递给Fresco以优化memory cache; 最简单的方法是覆写 Avtivity.onTrimMemory。也可以使用ComponmentCallbacks2的子类实现。 我们应当使用MemoryTrimableRegistry。该类用于保存MemoryTrimmable的实例-Fresco的cache也在其中。当接收到系统内存事件的时候,依次调用每个trimmable的MemoryTrimable方法。

配置memory caches

bitmap cache以及解码memory cache使用MemoryCacheParams中的Supplier配置。

配置disk cache

使用构造者模式创建DiskCacheConfig实例:

DiskCacheConfig diskCacheConfig = DiskCacheConfig.newBuilder()
   .set....
   .set....
   .build()

// when building ImagePipelineConfig
.setMainDiskCacheConfig(diskCacheConfig)

追踪Cache统计信息

If you want to keep track of metrics like the cache hit rate, you can implement the ImageCacheStatsTracker class. This provides callbacks for every cache event that you can use to keep your own statistics.

Caching

总共三种Cache

Bitmap cache

bitmap cache保存Android Bitmap对象。这里面的对象都已经解码完成,准备用于展示或者用于postprocess。

在Android 4.x或者更低的版本中,bitmap的cache数据放在ashmen heap,而不是Java heap中。这意味着这些图像并不需要运行额外的GC,避免降低app的运行速度。

Android 5.0中已经相比之前的版本对内存管理进行了很好的优化,所以将bitmap的cache数据放在Java heap中是合适的。

当app切换到背景之后,bitmap cache则会被清空。

Encoded memory cache

此处Cache保存了原始的压缩的图像格式。此处的图像在展示之前必须首先解码才行。

如果需要其他的变换,比如resizing、rotating或者转码,那么这些操作在decode解码之前进行。

当app切换到背景之后,encoded memory cache也会被清空。

Disk cache

Disk cache即保存在local storage中的cache。

与Encoded Memory Cache一样,Disk cache中保存的也是压缩的图像,与encoded memory cache一样,图像在展示之前也必须解码。

与Bitmap cache/Encoded memory cache不同,在app切换到背景之后,Disk cache中的数据并不会被清空。用户可以在Android Setting界面中将这部分数据清除。

从cache中删除数据

可以使用ImagePipeline中的方法从cache中清除单独的cache数据。

ImagePipeline imagePipeline = Fresco.getImagePipeline();
Uri uri;
imagePipeline.evictFromMemoryCache(uri);
imagePipeline.evictFromDiskCache(uri);

// combines above two lines
imagePipeline.evictFromCache(uri);

evictFromDiskCache(Uri) 默认情况下假设使用的是默认的cache key factory。如果自定义了Cache Key,那么则需要调用 evictFromDiskCache(ImageRequest)方法。

清空cache

ImagePipeline imagePipeline = Fresco.getImagePipeline();
imagePipeline.clearMemoryCaches();
imagePipeline.clearDiskCaches();

// combines above two lines
imagePipeline.clearCaches();

使用一个disk cache还是两个?

大多数应用程序仅仅使用1个disk cache。但是在某些使用场景中,我们需要将尺寸的图片放在一个单独的cache中以避免由于cache中存放了过多的较大的尺寸的图片而迅速将cache填满,进而导致这小较小的图片从cache中被清楚。

为了实现这种思路,则在配置image pipeline中调用setMainDiskCacheConfigsetSmallImageDiskCacheConfig方法即可。

怎么定义什么叫small,小的图片?我们的程序自己定义,我们在构造一个image request的时候,设置其ImageType:

ImageRequest request = ImageRequest.newBuilderWithSourceUri(uri)
    .setImageType(ImageType.SMALL)

如果我们仅仅需要1个cache,那么我们应该避免调用setSmallImageDiskCacheConfig。在这种情况下,pipeline会默认使用同一个cache,设置的ImageType并不会起作用。

调整caches - Trimming the caches

当配置image pipeline的时候,我们可以设置每一个cache的最大空间。但是在某些场景下,我们仍然希望降低cache所占的空间。

Fresco的cache实现了DiskTrimmable或者MemoryTrimmable接口。这些接口中有钩子可以使得我们在app中压缩cache。

然后我们的程序则可以调用DiskTrimmableRegistry和MemoryTrimmableRegistry接口。

These objects must keep a list of trimmables. They must use app-specific logic to determine when memory or disk space must be preserved. They then notify the trimmable objects to carry out their trims.

直接使用Image Pipeline

在大多数情况下,Image Pipeline不需要单独使用,大多数的APP上使用Drawee来处理与Fresco的交互即可。

由于内存使用的原因,在Fresco上直接使用image pipeline是比较困难的。Drawee可以自动追踪我们的图片是否需要在内存中存储。当不需要的时候,Fresco可以将图片自动转存,当需要的时候图片可以自动将图片重新加载到内存中。如果我们需要直接使用image pipeline,则需要手动处理这些逻辑。

image pipeline中返回的实例都是ClosableReference的包装类。Drawee在处理完成的时候会调用.close()方法。如果我们不是直接使用Drawee,那么也需要这样处理。

Java的GC会在图片out of scope的时候自动回收内存,但这样回收的太迟,而且GC的代价较高,而且对于较大的实例会导致较大的性能损失,尤其是对于Android 4.x即以下的版本中,Android没有单独的内存用于存储Bitmap,这会特别突出。

调用pipeline

我们必须构建一个image request,然后将这个image request传递给ImagePipeline:

ImagePipeline imagePipeline = Fresco.getImagePipeline();
DataSource > 
    dataSource = imagePipeline.fetchDecodedImage(imageRequest);

参考DataSource来看怎么从其中获取数据。

跳过解码

如果不需要解码,而是直接保持其中压缩的格式,那么仅仅使用fetchEncodedImage即可:

DataSource > 
    dataSource = imagePipeline.fetchEncodedImage(imageRequest);

直接使用bitmap cache中的数据

与其他操作不同,bitmap cache的查找工作是直接在UI线程中执行的,如果Bitmap在内存中,那么可以直接获取使用:

DataSource > dataSource =
    imagePipeline.fetchImageFromBitmapCache(imageRequest);
try {
  CloseableReference  imageReference = dataSource.getResult();
  if (imageReference != null) {
    try {
      CloseableImage image = imageReference.get();
      // do something with the image
    } finally {
      CloseableReference.closeSafely(imageReference);
    }
  }
} finally {
  dataSource.close();
}

注意,不要避免finally块中的调用dataSource.close()。

Prefetching

对图片进行预取可以降低用户等待的时间,进而优化用户体验,但需要注意的是,这也是一种trade-off。预取图片会增加用户数据流量的使用,增加CPU和内存的使用。因此,对于大多数APP来说,不建议开启图片的预取。

尽管如此,image pipeline允许我们将图片提前预取至disk或者bitmap cache。这两种方案都会增加网络数据的使用,但是区别在于disk cache并不会对图片进行解码,因此其CPU的占用率更低。

预取至disk cache:

imagePipeline.prefetchToDiskCache(imageRequest);

预取至bitmap cache中:

imagePipeline.prefetchToBitmapCache(imageRequest);

DataSources和DataSubscribers

DataSource类似于Java的Future,用于异步返回计算结果。DataSource与Future不同的是,DataSource可以返回单个命令对于的一系列运算结果,而不仅仅是一个。

当提交了一个image request之后,image pipeline会返回一个data source。为了从其中获取运算结果,因此需要使用DataSourceSubScriber。

I just want a bitmap...

如果我们向image request提交的请求仅仅decoded image - Android Bitmap,我们可以充分利用BaseBitmapDataSubscriber:

dataSource.subscribe(new BaseBitmapDataSubscriber() {
    @Override
    public void onNewResultImpl(@Nullable Bitmap bitmap) {
       // You can use the bitmap in only limited ways
      // No need to do any cleanup.
    }

    @Override
    public void onFailureImpl(DataSource dataSource) {
      // No cleanup required here.
    }
  },
  executor);

使用起来很简单,但有几个使用上需要注意的地方: * 对于animated images 动画图像无法使用subscriber * 不能将bitmap变量赋值给onNewResultImpl方法之外的任何变量。原因是,当subscriber执行完成之后,image pipeline则会复用bitmap并且释放内存。如果在其后绘制图片,则会导致应用程序崩溃并抛出IllegalStateException。 * 将Bimap传递给Android通知或者remote view是安全的。如果Android需要使用Bitmap并传递给系统调用,Fresco会将Bitmap数据在ashmem中复制一份数据。所以Fresco在这种情况下可以自动清理内存。

通用解决方案

如果需要使用bitmap,那么不能直接使用Bitmap,可以通过cloaseable reference和BaseDataSubscriber:

DataSubscriber dataSubscriber =
    new BaseDataSubscriber >() {
  @Override
  public void onNewResultImpl(
      DataSource > dataSource) {

    if (!dataSource.isFinished()) {
      FLog.v("Not yet finished - this is just another progressive scan.");
    }  

    CloseableReference  imageReference = dataSource.getResult();
    if (imageReference != null) {
      try {
        CloseableImage image = imageReference.get();
        // do something with the image
      } finally {
        imageReference.close();
      }
    }
  }
  @Override
  public void onFailureImpl(DataSource dataSource) {
    Throwable throwable = dataSource.getFailureCause();
    // handle failure
  }
};

dataSource.subscribe(dataSubscriber, executor);

If you want to deviate from the example above and assign the CloseableReference to another variable somewhere else, you can. Just be sure to follow the rules.

Closeable References

大多数的应用程序进需要使用Drawee即可,而且不需要担心Drawee关闭的问题。

由于Java是支持垃圾回收的编程语言,因此大多数开发者都是任意创建对象,并且理所当然的认为这些对象最后都会从内存中释放。

但实际上,在Android 5.0以上,这种情况才算得上理想,因此在之前的版本中,这种方法并不适合处理Bitmaps。在Android 5.0以前的版本中,Bitmap会占用APP共享内存中的一大部分,由于Bitmap的存在,会大大增加系统GC的频率,降低程序的执行效果。

Bitmap的问题,使得开发者更加思念C++以及其智能指针,比如Boost。

Fresco的解决方案是CloseableReference类。为了保证能够正确的使用CloseableReference,必须遵守以下的规则:

  1. 调用者拥有CloseableReference的引用。 比如,此处我们创建了一个CloseableReference引用,但由于我们将其传递你给caller,因此该CloseableReference引用的所有权移交给该caller:
CloseableReference  foo() {
  Val val;
  return CloseableReference.of(val);
}
  1. 引用的所有者在使用完CloseableReference引用之后,必须调用close方法释放该引用。

例子中,我们创建了一个CloseableReference引用,但是没有将其给传递给调用者,因此我们必须手动关闭该引用。

void gee() {
  CloseableReference  ref = foo();
  try {
    haa(ref);
  } finally {
    ref.close();
  }
}

一般而言,finally块最适合完成该操作。

  1. 其他的,除了CloseableReference引用的所有者之外的,其他代码不能关闭引用。 例子中,我们从参数中获取到一个CloseableReference引用。但caller仍然是其所有者,因此我们不能调用对应的close方法。
void haa(CloseableReference  ref) {
  Log.println("Haa: " + ref.get());
}
  1. 在赋值之前,要记得调用clone方法复制CloseableReference引用。 如果需要保持CloseableReference引用,需要调用clone方法:
class MyClass {
  CloseableReference  myValRef;

  void mmm(CloseableReference  ref) {
    myValRef = ref.clone();
  };
  // caller can now safely close its copy as we made our own clone.

  void close() {
    CloseableReference.closeSafely(myValRef);
  }
}
// Now the caller of MyClass must close it!

如果在内部类中使用该CloseableReference引用:

void haa(CloseableReference  ref) {
  final CloseableReference  refClone = ref.clone();
  executor.submit(new Runnable() {
    public void run() {
      try {
        Log.println("Haa Async: " + refClone.get());
      } finally {
        refClone.close();
      }
    }
  });
  // caller can now safely close its copy as we made our own clone.
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注

A robot?