android 图片加载库(4)- Fresco
上一篇说完 Glide 之后,我们现在来说下 Fresco,从使用感受上来看,使用 Fresco 比 Glide 要简便不少,但是 Fresco 需要使用单独的图片控件,系统原生的就不行了,代码侵入性太强。从性能上来说 Fresco 很强大,尤其内存性能强大,但是 Fresco 包太 5M,Glide 的包才 900K
Fresco 支持很多功能:
- 占位图
- 每种占位图都支持独立的缩放模式
- 进度图 :样式是提供一张图,然后旋转,可以指定旋转一周的时间
- 重试 :重试的支持很棒,可以设置重试图,重试4次失败,显示失败图
- 淡入淡出动画:这里支持的很好,没 Glide 这么多事
- 支持圆角,圆形显示
- 支持边框
- 支持遮蔽图层 : 叠加图
- 支持打底图层:就是背景图
- 支持按压时的显示指定图片
- 支持渐进式显示:不过需要 JPEG 图片按照渐进式编码才行
- 支持 GIF, 静态动态 WebP 的显示
- 支持自动调整图片方向,按照当前的屏幕方向
- 支持预加载,可以指定预加载到 File 还是 memory ,这对于我们展示大图的列表,尤其是像电商的详情页,漫画来说是有尤为重要的。
- 支持自定义的 overlay 图层,这样我们可以很方便的加标签上去
- 支持进度条,但是 Fresco 的默认实现很一般,只是在图片那地步的一个蓝色的进度条,要实现更多必须自定义 drawable
Fresco 的不足:
- 不支持 wrap_content ,必须显示的指定 view 的长宽
- 不会按照 view 的尺寸,动态调整图片宽高尺寸
- res 资源只支持真正的图片资源,xml 的不行。若需要的话,可以设置为占位图,加载资源设为 null 即可
- 支持多图请求,相当于 Glide 的缩略图,但是相比 Glide 的缩略图设置要差很多,多图请求不是并发,是先后顺序,不能设置原图为缩略图然后指定尺寸和缩略倍数
- 使用特有的 view 来显示图片,侵入性强
- 在 4.X 版本不兼容 SVG 矢量图,即便是按照 SVG 4.X 版本兼容写,把 SVG 图片包裹到 seletor 里面一样也是不行。
学习资料
Fresco 从15年出现开始,已经有很多优秀的学习资料了,跟着这些学习资料走就是最好的选择,当然不要忘了看完入门再去看看光放文档,中文的:Fresco官方中文文档
入门3部曲:
-
详细图解Fresco的使用
讲解 Fresco 的所有 xml 配置的,很基础,强烈建议看 -
Java代码实现XML效果
用 java 代码实现上面 xml 设置项 -
Java代码实现圆形圆角效果
用 java 代码实现圆形,圆角
深入学习:
简单使用
上面我们看完入门文档后,这里我还是要记录下 Fresco 常用的使用和各种设置的。
引入依赖
implementation 'com.facebook.fresco:fresco:1.8.1'
若想支持 webp,gif 还需要添加相应的支持库
compile 'com.facebook.fresco:fresco:1.5.0'
//加载gif动图需添加此库
compile 'com.facebook.fresco:animated-gif:1.5.0'
//加载webp动图需添加此库
compile 'com.facebook.fresco:animated-webp:1.5.0'
//支持webp需添加此库
compile 'com.facebook.fresco:webpsupport:1.5.0'
//网络实现层使用okhttp3需添加此库
compile 'com.facebook.fresco:imagepipeline-okhttp3:1.5.0'
全局初始化
Fresco.initialize(this);
加载图片
image.setImageURI( "xxx" );
重试代码
DraweeController controller = Fresco.newDraweeControllerBuilder()
//加载的图片URI地址
.setUri("xxx")
//设置点击重试是否开启
.setTapToRetryEnabled(true)
//设置旧的Controller
.setOldController(image.getController())
//构建
.build();
image.setController(controller);
xml 使用
这里简单写一下,全部属性去看下面的属性列表
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_net"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="10dp"
app:actualImageScaleType="focusCrop"
app:fadeDuration="300"
app:failureImage="@drawable/icon_error"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:placeholderImage="@drawable/icon_place"
app:placeholderImageScaleType="centerCrop"
app:progressBarAutoRotateInterval="5000"
app:progressBarImage="@drawable/icon_process"
app:retryImage="@drawable/icon_retry"
app:roundingBorderWidth="3dp"
app:roundingBorderColor="@color/colorPrimary"
app:roundAsCircle="true"/>
全部属性列表
XML属性 | 意义 |
---|---|
fadeDuration | 淡入淡出动画持续时间(单位:毫秒ms) |
actualImageScaleType | 实际图像的缩放类型 |
placeholderImage | 占位图 |
placeholderImageScaleType | 占位图的缩放类型 |
progressBarImage | 进度图 |
progressBarImageScaleType | 进度图的缩放类型 |
progressBarAutoRotateInterval | 进度图自动旋转间隔时间(单位:毫秒ms) |
failureImage | 失败图 |
failureImageScaleType | 失败图的缩放类型 |
retryImage | 重试图 |
retryImageScaleType | 重试图的缩放类型 |
backgroundImage | 背景图 |
overlayImage | 叠加图 |
pressedStateOverlayImage | 按压状态下所显示的叠加图 |
roundAsCircle | 设置为圆形图 |
roundedCornerRadius | 圆角半径 |
roundTopLeft | 左上角是否为圆角 |
roundTopRight | 右上角是否为圆角 |
roundBottomLeft | 左下角是否为圆角 |
roundBottomRight | 右下角是否为圆角 |
roundingBorderWidth | 圆形或者圆角图边框的宽度 |
roundingBorderColor | 圆形或者圆角图边框的颜色 |
roundWithOverlayColor | 圆形或者圆角图底下的叠加颜色(只能设置颜色) |
viewAspectRatio | 控件宽高比 |
缩放类型 focusCrop = centerCrop,这个是 Fresco 特有的,可以指定居中点的位置
缓存策略
Fresco 的图片缓存是非常值得我们去好好探寻一番的,Fresco 在 5.0 以下的内存性能是要比 Glide 强很多的,但是 5.0 之后就趋同了,都是用 BitmapFactory.Options.inBitmap 这个优化方案了,这毕竟是 google 官方的优化嘛,5.0 之前 google 做的太烂了,大家各自有不同的优化可以理解,但是到 5.0 之后 Google 的 inBitmap 这个 Bitmap 在堆内存的缓存池的优化做的还是不错的,所以大家又回到 google 的方案了。
Fresco 的网络缓存,磁盘缓存每什么说的,重点在 Bitmap 的内存缓存上
- Glide 的方案:
缓存 bitmap 原图和变换处理过后的 bitmap。 - Fresco 的方案:
2个缓存池,一个缓存 decode 解码钱的图片资源,一个缓存 bitmap 位图。
这2大框架,相比较起来,理论上还是 Fresco 要节省一些内存的,图片没 decode 解码就不是bitmap 对象,内存占用小很多,但是劣势是 CPU 工作量大一些,得益于 Fresco 优秀的线程池设计,这点不是问题,Fresco 可是维护了3个干不同类型活的线程池,具体的可以去看官方文档。但是实际上 Fresco 不支持根据 view 的大小动态调节图片尺寸,在实际使用中要是不优化这块的话 Fresco 的内存占用比 Glide 要多一点点的。这块优化内容下面我会说的,不要急。
下面是我在学习 Fresco 内存优化中的一些收获,内容比较杂,大家看看,也许能消除大家一些疑惑。
android 中内存,优化的一些知识看这里:
-
android app性能优化大汇总(内存性能优化)
非常建议大家静下心来看看读一读这篇文章,虽说年头有些长了,但是对于很多基础问题还是有很清晰的描述的。
根据 android 的内存特性( android 对于 bitmap 的优化 ),Fresco 的内存管理是分上下2部分的,分割点就在于 5.0 系统对于 bitmap 的优化
- 5.0 之前:
android 5.0 之前,Fresco 把 Bitmap缓存放到 ashmem(匿名共享内存),ashmem 的内存空间在 app 进程虚拟机之外,不占用 app 虚拟机的内存简直是太爽啦。不仅这样,GC 对于Bitmap对象的创建和释放是有优化的,更少的GC会使你的APP运行得更加流畅。早先 ashmem 是通过 BitmapFactory.Optinons.inPurgeable = true 这个标签来标记的,但是这个 inPurgeable 标签有个很大的问题,就是在复用 ashmem 中的 Bitmap 空间时,对 Bitmap 的 decode 解码的工作是在 UI 线程执行的,可能会卡掉帧。随意 google 有出了一个新的标记 inBitmap ,但是这个标记直到 API 19 时才完善,很不给力,所以 Fresco 对于 5.0 一下系统的 ashmem 使用另起炉灶,自己实现,效果不错。这里面有很对学问呢,Ashmem 的内容可以看这2篇:- Android系统匿名共享内存Ashmem
-
那两年炼就的Android内功修养
里面是老罗的总结,ashmem 的内容得往后翻翻才能找到
- 5.0 之后:
5.0 之后系统 Fresco 就没有使用 ashmem 了,而是跟着 google 自身对于 Bitmap 的优化走了。5.0 之后 Bitmap 缓存直接位于 Java 的 heap 上,Bitmap 会复用堆内存的 Bitmap 对象,理论上 Java 的 heap 上只有当前屏幕正在显示的 Bitmap,不过据说这种优化策略效果也就那么回事(其实还是之前的 inBitmap)。 - Glide与Fresco缓存机制对比可以看下面的文章:
官方文档的描述:
Snip20180303_4.png
所以呢,Fresco 的内存缓存管理做的还是不错的,View 在离开显示区域时会触动回收bitmap 资源的,这点对比 Glide 监听页面生命周期回收资源的方式,我觉得要好上不少,这种方式才是紧密贴合 5.0 之后 Google 对 bitmap 的优化思路的,java heap 堆内存层面的高度复用。
线程池
Fresco 的线程池是必须要说一下的,下面内容来自官方文档:
Image pipeline 默认有3个线程池:
- 3个线程用于网络下载
- 2个线程用于磁盘操作: 本地文件的读取,磁盘缓存操作。
- 2个线程用于CPU相关的操作: 解码,转换,以及后处理等后台操作。
渐进式显示
什么是渐进式显示,之前我也不知道,但是看到效果后就知道了,原来一直在我们身边
顺序式
渐进式
顺序式我们不说,这里我们来说说渐进式,Fresco 支持渐进式,渐进式除了需要 Fresco 库的支持外,最总要的图片是要按照渐进式编码做的,这点网上有很多,大伙搜索一下,我看到的是 渐进式需要 SRGB 编码。
我们可以在 Fresco 初始化时操作的 ImagePipelineConfig 类中,设置自定义的渐进式解码类,也可以使用默认的 SimpleProgressiveJpegConfig
ProgressiveJpegConfig pjpegConfig = new ProgressiveJpegConfig() {
@Override
public int getNextScanNumberToDecode(int scanNumber) {
return scanNumber + 2;
}
public QualityInfo getQualityInfo(int scanNumber) {
boolean isGoodEnough = (scanNumber >= 5);
return ImmutableQualityInfo.of(scanNumber, isGoodEnough, false);
}
}
ImagePipelineConfig config = ImagePipelineConfig.newBuilder()
.setProgressiveJpegConfig(pjpeg)
.build();
文档提示我们,必须显式地在加载时设置,才允许渐进式JPEG图片加载
Uri uri;
ImageRequest request = ImageRequestBuilder
.newBuilderWithSource(uri)
.setProgressiveRenderingEnabled(true)
.build();
PipelineDraweeController controller = Fresco.newControllerBuilder()
.setImageRequest(requests)
.setOldController(mSimpleDraweeView.getController())
.build();
mSimpleDraweeView.setController(controller);
资料来源:
渐进式编码资料:
Fresco API 深入使用
自古以来,看一个开源库,无非以下几步:
- 一看:核心性能。一个库毕竟是拿来用的,性能才是第一位的
- 二看:API 易用度。性能是比上下的,API 易用度是比同水平的,库好用不好用,也是我们选择一个库非常重要的点。性能+易用度 - 决定我们用不用这个库
- 三看: 框架结构。通过官方文档一般就可以把框架结构看个大概
- 四看:核心算法。从第三看开始,就是去深入体会,学习这个库了,不了解框架结构,你怎么找出核心算法呢。
- 五看:源码。做到这一步,并且源码都能看懂的都至少是高级开发了,一般资深开发就都会仔细看源码了。看源码也是一大学问啊。
只有知道了 Fresco 的功能代码结构,我们才好继续深入的使用 Fresco 这个库,一个优秀的库必然是有很多可以深入使用研究的功能和操作的。这里我分析不了源码,没那么高水平,通过官方文档,简单写下功能分层的几个核心类。自己的水平还是差很多啊,要不然至少框架结构的 UML 类图至少是能画出来的。
Fresco 核心功能对象:
-
ImagePipeline
负责完成加载图片,变换图片操作,我们可见叫他管道 -
ImagePipelineConfig
用来设置全局配置,在初始化 Fresco 时添加 - ImageRequest
图片请求对象,封装有所有请求的参数 - DataSource
数据源,本质就是一个 Observer 被观察者对象,我们可以在其身上观察者 - DraweeHierarchy
图层管理器,管理 SimpleDraweeView 这个 UI 控件的所有显示设置 - DraweeController
Fresco 的流程控制器,可以接受多个请求对象以实现缩略图加载
全局配置
以下列出了所有可配置的选项,当然了一般我们是不会配置这么多选项的
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context)
.setBitmapMemoryCacheParamsSupplier(bitmapCacheParamsSupplier)
.setCacheKeyFactory(cacheKeyFactory)
.setDownsampleEnabled(true)
.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);
DraweeHierarchy
同一个 DraweeHierarchy 不能设置给多个 view, 也就是说 DraweeHierarchy 对象是我们不要去复用他
GenericDraweeHierarchy draweeHierarchy = new GenericDraweeHierarchyBuilder(getResources())
.setFadeDuration(300)
.setPlaceholderImage(R.drawable.icon_place)
.setFailureImage(R.drawable.icon_error)
.setRetryImage(R.drawable.icon_retry)
.build();
mSimpleDraweeView.setHierarchy(draweeHierarchy );
DraweeController
这个不多说了,API 很简单了,每什么特别需要说的点
ImageRequest imageRequest = ImageRequest.fromUri(uri);
imageRequest.setAutoRotateEnabled(true)
DraweeController controller = Fresco.newDraweeControllerBuilder()
.setUri(uri)
.setImageRequest(request)
.setAutoPlayAnimations(true)
.setTapToRetryEnabled(true)
.setOldController(mSimpleDraweeView.getController())
.setControllerListener(listener)
.build();
mSimpleDraweeView.setController(controller);
缩略图
Fresco 的缩略图支持比 Glide 要差不少,本质是请求2个图片地址
Uri lowResUri, highResUri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
.setLowResImageRequest(ImageRequest.fromUri(lowResUri))
.setImageRequest(ImageRequest.fromUri(highResUri))
.setOldController(mSimpleDraweeView.getController())
.build();
mSimpleDraweeView.setController(controller);
ImagePipeline 管道的直接使用
ImagePipeline 管道是 Fresco 的 核心,从数据的加载,换粗你的管理,到图片的变化处理都是由 ImagePipeline 来管理的。Fresco 支持我们直接获取 ImagePipeline 对象,那么我们就可以使用 ImagePipeline 来做很多事了。
- 判断一个资源是否在缓存中
// 获取管道对象
ImagePipeline imagePipeline = Fresco.getImagePipeline();
Uri uri = Uri.parse("");
// 判断是否在内存中有缓存
boolean inBitmapMemoryCache = imagePipeline.isInBitmapMemoryCache(uri);
可以判断内存,磁盘缓存
可以看到,不光内存缓存,我们还可以判断磁盘是不是缓存过了,而且可以接受多种类型的数据 - Uri ,ImageRequest ,而且考虑到访问磁盘是 IO 操作,提供了同步和异步方法。
- 删除某个 URI 的缓存
ImagePipeline imagePipeline = Fresco.getImagePipeline();
Uri uri;
imagePipeline.evictFromMemoryCache(uri);
imagePipeline.evictFromDiskCache(uri);
// combines above two lines
imagePipeline.evictFromCache(uri);
当然你要是清楚全部缓存也是可以的,但是官方不推荐这么做
ImagePipeline imagePipeline = Fresco.getImagePipeline();
imagePipeline.clearMemoryCaches();
imagePipeline.clearDiskCaches();
// combines above two lines
imagePipeline.clearCaches();
- 预加载
预加载这东西具体怎么用,在哪用就得看各位看官的需求了,我举个例子,漫画 app 这样的需要显示特大图片的,应该预加载下一页的内容。
预加载到磁盘缓存:
imagePipeline.prefetchToDiskCache(imageRequest, callerContext);
预加载到内存缓存:
imagePipeline.prefetchToBitmapCache(imageRequest, callerContext);
取消预加载:
DataSource<Void> prefetchDataSource = imagePipeline.prefetchTo...;
prefetchDataSource.close();
- 获取 bitmap 缓存对象
DataSource<CloseableReference<CloseableImage>> dataSource = imagePipeline.fetchDecodedImage(imageRequest, this);
dataSource.subscribe(new BaseBitmapDataSubscriber() {
@Override
protected void onNewResultImpl(@Nullable Bitmap bitmap) {
// 处理显示
}
@Override
protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
// 失败
}
}, UiThreadImmediateExecutorService.getInstance());
Fresco 并不能直接返回 Bitmap 对象,只能给我们一个数据源,让我们去注册一个观察者去处理 Bitmap 对象,Bitmap 对象的生命周期不能超出这个方法,这个方法结束后这个 Bitmap 对象就会被回收掉。
dataSource.subscribe 方法的第二个参数是 Fresco 的进程调度器:
- UI 进程
UiThreadImmediateExecutorService.getInstance()
- 当前进程
CallerThreadExecutor.getInstance();
- 获取 File 缓存对象
Boolean result = imagePipeline.isInDiskCache(imageRequest).getResult();
if (result) {
CacheKey cacheKey = DefaultCacheKeyFactory.getInstance().getEncodedCacheKey(imageRequest, this);
BinaryResource resource = ImagePipelineFactory.getInstance().getMainFileCache().getResource(cacheKey);
File file = ((FileBinaryResource) resource).getFile();
}
0.jpeg
Fresco 非侵入式设计
Fresco 要求我们使用独有的 SimpleDraweeView ,对于我们的工程来说,替换 imageview 为 SimpleDraweeView 是一个大工程。使用一个新的库,但是会大量修改原有的代码这就是侵入行,不能使用 imageview 对我们来说,很多时候都是非常不便的,SimpleDraweeView 毕竟不是 imageview 这种 google 原生的view 组件,一些 android 系统对原生图片组件的优化和功能,SimpleDraweeView 是无法支持的。那么有什么办法解决吗,目前还没有太好的方案,我看了很多文章,现在有3种思路:
-
SimpleDraweeView + imageview 切换型
这种方式性能不好,控件对象对多不少,有其是在列表中,而且无法兼容动画。
参考项目:项目重构之路——Fresco非侵入式替换Glide -
自定义 view ,使用 Fresco 的数据层
数据层是可以拿出来的,但是 SimpleDraweeView 可是控制了 bitmap 的回收,缓存,整个视图层功能实现的,这些东西官方也是不建议自己去做的,非要自己去做的话也可以,不过不推荐普通开发者去尝试,容易给自己挖坑
参考项目:一种使用 Fresco 非侵入式加载图片的方式 -
还是自定义 view,但是继承 SimpleDraweeView
这个方式中,这个自定义 view 里面只封装 load 功能,其实不动。
Fresco 图片自适应显示
Fresco 的 SimpleDraweeView 不能使用 warpContent ,这是很让人沮丧的一点,在显示大图,长图时这是一个麻烦。目前有2种处理手法:
- 加监听,图片下载后根据原图长宽比和 view 的宽来计算 view 的高是多少,一般布局中,显示图片的 view 的宽我们是能够确定的
- 服务器返回图片宽高,然后根据这个数据来设置(主推)
这2中方法还是主推第二种,服务器返回给我们目标图片的宽高,我们计算之后再去设置 imageview 的宽高,这样我们在 imageview 设置图片之前走一次 layout 比 imageview 设置图片之后再去走一次 layout ,在性能上要好不少,这是没有 bitmap 来参与,布局,绘制要快很多。这点大家使用淘宝时应该有体会,有的商品详情里面图很大,很长,这时淘宝会加载一个和目标图片登场的 item 进来。
手段一:加载图片获取宽高后,去修改 view 高度
给 simpleDraweeView 的 DraweeController 添加一个 ControllerListener 监听器。原文地址:Fresco进阶 图片错位 宽高自适应
/**
* 通过imageWidth 的宽度,自动适应高度
* * @param simpleDraweeView view
* * @param imagePath Uri
* * @param imageWidth width
*/
public static void setControllerListener(final SimpleDraweeView simpleDraweeView, String imagePath, final int imageWidth) {
final ViewGroup.LayoutParams layoutParams = simpleDraweeView.getLayoutParams();
ControllerListener controllerListener = new BaseControllerListener<ImageInfo>() {
@Override
public void onFinalImageSet(String id, @Nullable ImageInfo imageInfo, @Nullable Animatable anim) {
if (imageInfo == null) {
return;
}
int height = imageInfo.getHeight();
int width = imageInfo.getWidth();
layoutParams.width = imageWidth;
layoutParams.height = (int) ((float) (imageWidth * height) / (float) width);
simpleDraweeView.setLayoutParams(layoutParams);
}
@Override
public void onIntermediateImageSet(String id, @Nullable ImageInfo imageInfo) {
Log.d("TAG", "Intermediate image received");
}
@Override
public void onFailure(String id, Throwable throwable) {
throwable.printStackTrace();
}
};
DraweeController controller = Fresco.newDraweeControllerBuilder().setControllerListener(controllerListener).setUri(Uri.parse(imagePath)).build();
simpleDraweeView.setController(controller);
}
手段二:从服务器获取图片宽高,再修改 view 高度
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);
推荐这种手段,现在各大图片云可以支持返回图片宽高,所以这种做法现在成本也不是很高了,而且还是 Fresco 官方推荐的做法。官方文档:wrap_content的限制
优化加载大图
这部分内容是承接上面 Fresco 图片自适应显示 来说的,上面的部分侧重如何确定加载大图时 view 的宽高,这部分侧重我们如何在加载大图时如何优化显示性能,减少内容消耗,这2部分其实是一块内容
首先最好的办法是我们先加载小兔,缩录图,在显示图片详情的单独页面时再去显示原始的大图。七牛云挺不错,在图片请求中可以带上需要的宽和高
但是要注意,不管服务器能不能返回缩略图,所存储的原图都不应该太大,有时图片太大,甚至都无法下载下来(报504之类的错误)
那么若是只能拿到原图或大图,fresco怎么优化显示,Fresco 默认提供了3种缩小图片的策略:
- Scaling
画布操作,通常是由硬件加速的。图片实际大小保持不变,它只不过在绘制时被放大或缩小.使用时需要配置缩放类型fresco:actualImageScaleType,具体类型名与Imageview的ScaleType几乎一样. - Resizing
是一种软件执行的管道操作。它返回一张新的,尺寸不同的图片,也就是说直接改变bitmap的大小,可惜是单独使用时,只支持jpg,当然,结合Downsampling使用时,可以支持除gif以为的所有常见图片,包括webp. - Downsampling
同样是软件实现的管道操作。它不是创建一张新的图片,而是在解码时改变图片的大小。 同样是软件实现的管道操作。它不是创建一张新的图片,而是在解码时改变图片的大小。类似于android中的BitmapFactory在decodefile时的inSampleSize,都是指定一个采样率,默认是关闭的,如果开启,那么需要结合Resizing来使用.
综上,要缩小内存占用,以及减少cpu计算量,减少卡顿,应该是Downsampling结合Resizing来使用.其中Downsampling是在Fresco初始化时开启,而Resizing则是通过构建ImageRequest时通过制定宽高来实现,所以可以定制每一张或每一类图片的宽高. 示例代码如下:
初始化 Fresco
设置图片加载请求数据
还有一种思路,就是学 Glide 呗,每次请求我们按照 view 的 size rezise 呗
public void setImageSrc(final SimpleDraweeView draweeView, Uri uri, int width, int height) {
ImageRequestBuilder builder = ImageRequestBuilder.newBuilderWithSource(uri);
if(width > 0 && height > 0){
builder.setResizeOptions(new ResizeOptions(width, height));
}
ImageRequest request = builder.build();
PipelineDraweeController controller = (PipelineDraweeController) Fresco.newDraweeControllerBuilder()
.setImageRequest(request)
.setTapToRetryEnabled(true)
.setOldController(draweeView.getController())
.build();
draweeView.setController(controller);
高斯模糊
高斯模糊是app里设置一些背景效果 常用到的手段.在fresco中,可以通过postprocessor来实现,也可以自己拿到bitmap后将bitmap模糊化后设置到ImageView或SimpleDraweeView(这个不建议,会消除掉SimpleDraweeView的层级结构,变成单纯的ImageView)上.
推荐开源库: BlurPostprocessor
列表滚动时的优化
在列表视图滚动时,不加载图片,等滚动停止后再开始加载图片,提升列表视图的滚动流畅度。
// 需要暂停网络请求时调用
public static void pause(){
Fresco.getImagePipeline().pause();
}
// 需要恢复网络请求时调用
public static void resume(){
Fresco.getImagePipeline().resume();
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
switch (newState) {
case RecyclerView.SCROLL_STATE_IDLE://停止滑动
if (Fresco.getImagePipeline().isPaused())
Fresco.getImagePipeline().resume();
break;
case RecyclerView.SCROLL_STATE_DRAGGING:
if (preScrollState == RecyclerView.SCROLL_STATE_SETTLING) {
//触摸滑动不需要加载
Fresco.getImagePipeline().pause();
} else {
//触摸滑动需要加载
if (Fresco.getImagePipeline().isPaused())
Fresco.getImagePipeline().resume();
}
break;
case RecyclerView.SCROLL_STATE_SETTLING://惯性滑动
Fresco.getImagePipeline().pause();
break;
}
preScrollState = newState;
}
自定义进度图
Fresco 支持自定义进度图,我们需要自己实现一个 Progress 的 Drawable ,核心就是 onLevelChange 这个方法,下面贴一个自定义的进度图
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import com.facebook.drawee.drawable.DrawableUtils;
public class ImageLoadingDrawable extends Drawable {
private Paint mRingBackgroundPaint;
private int mRingBackgroundColor;
// 画圆环的画笔
private Paint mRingPaint;
// 圆环颜色
private int mRingColor;
// 半径
private float mRadius;
// 圆环半径
private float mRingRadius;
// 圆环宽度
private float mStrokeWidth;
// 圆心x坐标
private int mXCenter;
// 圆心y坐标
private int mYCenter;
// 总进度
private int mTotalProgress = 10000;
// 当前进度
private int mProgress;
public ImageLoadingDrawable(){
initAttrs();
}
private void initAttrs() {
mRadius = 160;
mStrokeWidth = 4;
mRingBackgroundColor = 0xFFadadad;
mRingColor = 0xFF0EB6D2;
mRingRadius = mRadius + mStrokeWidth / 2;
initVariable();
}
private void initVariable() {
mRingBackgroundPaint = new Paint();
mRingBackgroundPaint.setAntiAlias(true);
mRingBackgroundPaint.setColor(mRingBackgroundColor);
mRingBackgroundPaint.setStyle(Paint.Style.STROKE);
mRingBackgroundPaint.setStrokeWidth(mStrokeWidth);
mRingPaint = new Paint();
mRingPaint.setAntiAlias(true);
mRingPaint.setColor(mRingColor);
mRingPaint.setStyle(Paint.Style.STROKE);
mRingPaint.setStrokeWidth(mStrokeWidth);
}
@Override
public void draw(Canvas canvas) {
drawBar(canvas,mTotalProgress,mRingBackgroundPaint);
drawBar(canvas,mProgress,mRingPaint);
}
private void drawBar(Canvas canvas, int level, Paint paint) {
if (level > 0 ) {
Rect bound= getBounds();
mXCenter = bound.centerX();
mYCenter = bound.centerY();
RectF oval = new RectF();
oval.left = (mXCenter - mRingRadius);
oval.top = (mYCenter - mRingRadius);
oval.right = mRingRadius * 2 + (mXCenter - mRingRadius);
oval.bottom = mRingRadius * 2 + (mYCenter - mRingRadius);
canvas.drawArc(oval, -90, ((float) level / mTotalProgress) * 360, false, paint); //
}
}
@Override
protected boolean onLevelChange(int level) {
mProgress = level;
if(level > 0 && level < 10000) {
invalidateSelf();
return true;
}else {
return false;
}
}
@Override
public void setAlpha(int alpha) {
mRingPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
mRingPaint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return DrawableUtils.getOpacityFromColor(this.mRingPaint.getColor());
}
}
使用的话就是设置进 DraweetHierarchy 图层管理器中
image.getHierarchy().setProgressBarImage(new ImageLoadingDrawable());
Fresco 开源扩展库
-
drawee-text-view
bilibili开源的借助fresco加载图片的 图文混排的 textView -
BlurPostprocessor
Fresco 的开源后处理库,包含圆角,圆形,高斯模糊等 -
PhotoDraweeView
手势操作,放大,缩小,双击放大 -
subsampling-scale-image-view
它采用的是BitmapRegionDecoder 子编码的方式,分段加载显示超长图,拒绝OOM,而且支持支持支持:双击放大,单击返回,手动放大等,目前只能加载本地,可以下下来用缓存
PhotoDraweeView 手势库说明
这个库有一点得说一下,demo 里的 PhotoDraweeView 宽高给 match 之后就可以了,但我写的时候,发现必须在图片加载之后把图片的宽高更新近去 PhotoDraweeView 才能正常支持手势
String uri = "res:///" + R.drawable.panda1;
PipelineDraweeControllerBuilder builder = Fresco.newDraweeControllerBuilder();
builder.setUri(uri);
builder.setOldController(photoDraweeView.getController());
builder.setControllerListener(new BaseControllerListener<ImageInfo>() {
@Override
public void onFinalImageSet(String id, @Nullable ImageInfo imageInfo, @Nullable Animatable animatable) {
photoDraweeView.update(imageInfo.getWidth(), imageInfo.getHeight());
}
});
photoDraweeView.setController(builder.build());
后来看别人的实现,发现 PhotoDraweeView 有专门设置资源的方法,使用 Fresco 的 control 不好用了。
photoDraweeView.setPhotoUri(Uri.parse(uri));
subsampling-scale-image-view 超长图加载库
这个库我每试出来库描述的那种按页加载的情况,不过看了 heap 堆内存的使用量,的确是要少不少的,加载大图的确比较合适。我测试的时候,1080*11776 的图是一起出来的,我下滑的时候不会再去请求数据了,只是使用 子编码优化超大图的内存占用量,大概可以优化到原图的60%。这个控件有一点不好的是只能加载 file 文件,需要我们自己去监听一下下载完成后,手动获取 disk 磁盘缓存
public File getFile(Uri uri, ImagePipeline imagePipeline) {
ImageRequest imageRequest = ImageRequest.fromUri(uri);
CacheKey encodedCacheKey = imagePipeline.getCacheKeyFactory().getEncodedCacheKey(imageRequest, this);
BinaryResource resource = ImagePipelineFactory.getInstance()
.getMainFileCache().getResource(encodedCacheKey);
return ((FileBinaryResource) resource).getFile();
}
ImagePipeline pipeline = Fresco.getImagePipeline();
if (pipeline.isInDiskCacheSync(uri)) {
photoDraweeView.setImage(ImageSource.uri(getFile(uri, pipeline).getAbsolutePath()));
} else {
ImageRequest imageRequest = ImageRequest.fromUri(uri);
DataSource<CloseableReference<CloseableImage>> dataSource = pipeline.fetchDecodedImage(imageRequest, this);
dataSource.subscribe(new BaseBitmapDataSubscriber() {
@Override
protected void onNewResultImpl(@Nullable Bitmap bitmap) {
if (pipeline.isInDiskCacheSync(uri)) {
photoDraweeView.setImage(ImageSource.uri(getFile(uri, pipeline).getAbsolutePath()));
}
}
@Override
protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
}
}, UiThreadImmediateExecutorService.getInstance());
}