前言
Fresco是一个强大的图片加载组件,使用它之后你不需要再去关心图片的加载和显示这些繁琐的事情,支持Android2.3~(这已经可以说是版本通用了),官方地址:https://www.fresco-cn.org/docs/
Image Pipeline
Fresco 中设计有一个叫做 Image Pipeline 的模块。它负责从网络,从本地文件系统,本地资源加载图片。为了最大限度节省空间和CPU时间,它含有3级缓存设计(2级内存,1级磁盘)。
Drawees
Fresco 中设计有一个叫做 Drawees 模块,它会在图片加载完成前显示占位图,加载成功后自动替换为目标图片。当图片不再显示在屏幕上时,它会及时地释放内存和空间占用。
特性
动图加载
加载Gif图和WebP动图在任何一个Android开发者眼里看来都是一件非常头疼的事情。每一帧都是一张很大的Bitmap
,每一个动画都有很多帧。Fresco让你没有这些烦恼,它处理好每一帧并管理好你的内存。
既然Fresco这么棒咱们就开始按照官方教导来弄一下吧
引入Fresco
这里告诉你如何在项目中引入 Fresco.
使用 Android Studio 或者其他 Gradle 构建的项目
编辑 build.gradle
文件:
1 2 3 4 | dependencies { // 其他依赖 compile 'com.facebook.fresco:fresco:0.12.0 ' //新版本引入为 implementation 'com.facebook.fresco:fresco:1.9.0' } |
下面的依赖需要根据需求添加:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | dependencies { // 在 API < 14 上的机器支持 WebP 时,需要添加 compile 'com.facebook.fresco:animated-base-support:0.12.0' // 支持 GIF 动图,需要添加 compile 'com.facebook.fresco:animated-gif:0.12.0' // 支持 WebP (静态图+动图),需要添加 compile 'com.facebook.fresco:animated-webp:0.12.0' compile 'com.facebook.fresco:webpsupport:0.12.0' // 仅支持 WebP 静态图,需要添加 compile 'com.facebook.fresco:webpsupport:0.12.0' } |
Eclipse ADT
下载 zip 文件.
解压后,你会看到一个目录:frescolib,注意这个目录。
- 从菜单 “文件(File)”,选择导入(Import)
- 展开 Android, 选择 “Existing Android Code into Workspace”, 下一步。
- 浏览,选中刚才解压的的文件中的 frescolib 目录。
- 这5个项目应该被添加到工程:
drawee
,fbcore
,fresco
,imagepipeline
,imagepipeline-base
。请确认这5个项目一定是被选中的。点击完成。其他的项目参考之前 Gradle的额外依赖介绍。 - 右键,项目,选择属性,然后选择 Android。
- 点击右下角的 Add 按钮,选择 fresco,点击 OK,再点击 OK。
现在,fresco 就导入到项目中了,你可以开始编译了。如果编译不通过,可以尝试清理资源,或者重启 Eclipse。
如果你想在网络层使用 OkHttp,请看这里.
如果 support-v4 包重复了,删掉 frescolib/imagepipeline/libs 下的即可。
强烈建议尽早使用 Android Studio,Android Studio确实是比Eclipse要好用很多。
开始使用 Fresco
如果你仅仅是想简单下载一张网络图片,在下载完成之前,显示一张占位图,那么简单使用 SimpleDraweeView 即可。
在加载图片之前,你必须初始化Fresco
类。你只需要调用Fresco.initialize
一次即可完成初始化,在 Application
里面做这件事再适合不过了(如下面的代码),注意多次的调用初始化是无意义的。
1 2 3 4 5 6 7 8 | [MyApplication.java] public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); Fresco.initialize(this); } } |
做完上面的工作后,你需要在 AndroidManifest.xml
中指定你的 Application 类。为了下载网络图片,请确认你声明了网络请求的权限。
1 2 3 4 5 6 7 8 9 10 11 12 13 | <manifest ... > <uses-permission android:name="android.permission.INTERNET" /> <application ... android:label="@string/app_name" android:name=".MyApplication" > ... </application> ... </manifest> |
在xml布局文件中, 加入命名空间:
1 2 3 4 5 6 | <!-- 其他元素--> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:fresco="http://schemas.android.com/apk/res-auto" android:layout_height="match_parent" android:layout_width="match_parent"> |
加入SimpleDraweeView
:
1 2 3 4 5 6 | <com.facebook.drawee.view.SimpleDraweeView android:id="@+id/my_image_view" android:layout_width="130dp" android:layout_height="130dp" fresco:placeholderImage="@drawable/my_drawable" /> |
开始加载图片:
1 2 3 | Uri uri = Uri.parse("https://raw.githubusercontent.com/facebook/fresco/gh-pages/static/logo.png"); SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view); draweeView.setImageURI(uri); |
剩下的,Fresco会替你完成:
- 显示占位图直到加载完成;
- 下载图片;
- 缓存图片;
- 图片不再显示时,从内存中移除;
等等等等。
关键概念
Drawees
Drawees 负责图片的呈现。它由三个元素组成,有点像MVC模式。
DraweeView
继承于 View, 负责图片的显示。
一般情况下,使用 SimpleDraweeView
即可。 你可以在 XML 或者在 Java 代码中使用它,通过 setImageUri
给它设置一个 URI 来使用,这里有简单的入门教学:开始使用
你可以使用 XML属性来达到各式各样的效果。
DraweeHierarchy
DraweeHierarchy 用于组织和维护最终绘制和呈现的 Drawable 对象,相当于MVC中的M。
你可以通过它来在Java代码中自定义图片的展示,具体的请参考这里: 在Java代码中自定义显示效果
DraweeController
DraweeController
负责和 image loader 交互( Fresco 中默认为 image pipeline, 当然你也可以指定别的),可以创建一个这个类的实例,来实现对所要显示的图片做更多的控制。
如果你还需要对Uri加载到的图片做一些额外的处理,那么你会需要这个类的。
DraweeControllerBuilder
DraweeControllers
由 DraweeControllerBuilder 采用 Builder 模式创建,创建之后,不可修改。具体参见: 使用ControllerBuilder。
Listeners
使用 ControllerListener
的一个场景就是设置一个 Listener监听图片的下载。
The Image Pipeline
Fresco 的 Image Pipeline 负责图片的获取和管理。图片可以来自远程服务器,本地文件,或者Content Provider,本地资源。压缩后的文件缓存在本地存储中,Bitmap数据缓存在内存中。
在5.0系统以下,Image Pipeline 使用 pinned purgeables 将Bitmap数据避开Java堆内存,存在ashmem中。这要求图片不使用时,要显式地释放内存。
SimpleDraweeView
自动处理了这个释放过程,所以没有特殊情况,尽量使用SimpleDraweeView,在特殊的场合,如果有需要,也可以直接控制Image Pipeline。
支持的URI
Fresco 支持许多URI格式。
特别注意:Fresco 不支持 相对路径的URI. 所有的 URI 都必须是绝对路径,并且带上该 URI 的 scheme。
如下:
类型 | SCHEME | 示例 |
---|---|---|
远程图片 | http://, https:// | HttpURLConnection 或者参考 使用其他网络加载方案 |
本地文件 | file:// | FileInputStream |
Content provider | content:// | ContentResolver |
asset目录下的资源 | asset:// | AssetManager |
res目录下的资源 | res:// | Resources.openRawResource |
Uri中指定图片数据 | data:mime/type;base64, | 数据类型必须符合 rfc2397规定 (仅支持 UTF-8) |
res 示例:
1 | Uri uri = Uri.parse("res://包名(实际可以是任何字符串甚至留空)/" + R.drawable.ic_launcher); |
注意,只有图片资源才能使用在Image pipeline中,比如(PNG)。其他资源类型,比如字符串,或者XML Drawable在Image pipeline中没有意义。所以加载的资源不支持这些类型。
像ShapeDrawable
这样声明在XML中的drawable可能引起困惑。注意到这毕竟不是图片。如果想把这样的drawable作为图像显示,那么把这个drawable设置为占位图,然后把URI设置为null
。
遇到的问题
刚开始在使用的时候就使用了wrap_content发现不会出图片,看了下讲解搞懂了
主要的原因是,Drawee永远会在getIntrinsicHeight/getIntrinsicWidth中返回-1。
这么做的原因是 Drawee 不像ImageView一样。它同一时刻可能会显示多个元素。比如在从占位图渐变到目标图时,两张图会有同时显示的时候。再比如可能有多张目标图片(低清晰度、高清晰度两张)。如果这些图像都是不同的尺寸,那么很难定义”intrinsic”尺寸。
如果我们要先用占位图的尺寸,等加载完成后再使用真实图的尺寸,那么图片很可能显示错误。它可能会被根据占位图的尺寸来缩放、裁剪。唯一防止这种事情的方式就只有在图片加载完成后强制触发一次layout。这样的话不仅会影响性能,而且会让应用的界面突变,很影响用户体验!如果用户正在读一篇文章,然后在图片加载完成后整篇文章突然向下移动,这是非常不好的。
所以你必须指定尺寸或者用match_parent
来布局。
你如果从服务端请求图片,服务端可以做到返回图片尺寸。然后你拿到之后通过setLayoutParams 来给View设置宽高。
当然如果你必须要使用wrap_content
,那么你可以参考StackOverflow上的一个回答。但是我们以后会移除这个功能,Ugly things should look ugly。
通过Logcat来判断原因
在加载图片时会出现各种各样的原因导致加载失败。 在使用Fresco的时候,最直接的方式就是查看 image pipeline 打出的VERBOSE
级别日志。
启动日志
默认情况下Fresco是关闭日志输出的,你可以配置image pipeline让它启动.
1 2 3 4 5 6 7 8 | Set<RequestListener> requestListeners = new HashSet<>(); requestListeners.add(new RequestLoggingListener()); ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context) // other setters .setRequestListeners(requestListeners) .build(); Fresco.initialize(context, config); FLog.setMinimumLoggingLevel(FLog.VERBOSE); |
查看日志
你可以通过下面这条shell命令来查看Fresco日志:
1 | adb logcat -v threadtime | grep -iE 'LoggingListener|AbstractDraweeController|BufferedDiskCache' |
它的输出为如下格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 08-12 09:11:14.791 6690 6690 V unknown:AbstractDraweeController: controller 28ebe0eb 0 -> 1: initialize 08-12 09:11:14.791 6690 6690 V unknown:AbstractDraweeController: controller 28ebe0eb 1: onDetach 08-12 09:11:14.791 6690 6690 V unknown:AbstractDraweeController: controller 28ebe0eb 1: setHierarchy: null 08-12 09:11:14.791 6690 6690 V unknown:AbstractDraweeController: controller 28ebe0eb 1: setHierarchy: com.facebook.drawee.generic.GenericDraweeHierarchy@2bb88e4 08-12 09:11:14.791 6690 6690 V unknown:AbstractDraweeController: controller 28ebe0eb 1: onAttach: request needs submit 08-12 09:11:14.791 6690 6690 V unknown:PipelineDraweeController: controller 28ebe0eb: getDataSource 08-12 09:11:14.791 6690 6690 V unknown:RequestLoggingListener: time 11201791: onRequestSubmit: {requestId: 1, callerContext: null, isPrefetch: false} 08-12 09:11:14.792 6690 6690 V unknown:RequestLoggingListener: time 11201791: onProducerStart: {requestId: 1, producer: BitmapMemoryCacheGetProducer} 08-12 09:11:14.792 6690 6690 V unknown:RequestLoggingListener: time 11201792: onProducerFinishWithSuccess: {requestId: 1, producer: BitmapMemoryCacheGetProducer, elapsedTime: 1 ms, extraMap: {cached_value_found=false}} 08-12 09:11:14.792 6690 6690 V unknown:RequestLoggingListener: time 11201792: onProducerStart: {requestId: 1, producer: BackgroundThreadHandoffProducer} 08-12 09:11:14.792 6690 6690 V unknown:AbstractDraweeController: controller 28ebe0eb 1: submitRequest: dataSource: 36e95857 08-12 09:11:14.792 6690 6734 V unknown:RequestLoggingListener: time 11201792: onProducerFinishWithSuccess: {requestId: 1, producer: BackgroundThreadHandoffProducer, elapsedTime: 0 ms, extraMap: null} 08-12 09:11:14.792 6690 6734 V unknown:RequestLoggingListener: time 11201792: onProducerStart: {requestId: 1, producer: BitmapMemoryCacheProducer} 08-12 09:11:14.792 6690 6734 V unknown:RequestLoggingListener: time 11201792: onProducerFinishWithSuccess: {requestId: 1, producer: BitmapMemoryCacheProducer, elapsedTime: 0 ms, extraMap: {cached_value_found=false}} 08-12 09:11:14.792 6690 6734 V unknown:RequestLoggingListener: time 11201792: onProducerStart: {requestId: 1, producer: EncodedMemoryCacheProducer} 08-12 09:11:14.792 6690 6734 V unknown:RequestLoggingListener: time 11201792: onProducerFinishWithSuccess: {requestId: 1, producer: EncodedMemoryCacheProducer, elapsedTime: 0 ms, extraMap: {cached_value_found=false}} 08-12 09:11:14.792 6690 6734 V unknown:RequestLoggingListener: time 11201792: onProducerStart: {requestId: 1, producer: DiskCacheProducer} 08-12 09:11:14.792 6690 6735 V unknown:BufferedDiskCache: Did not find image for http://www.example.com/image.jpg in staging area 08-12 09:11:14.793 6690 6735 V unknown:BufferedDiskCache: Disk cache read for http://www.example.com/image.jpg 08-12 09:11:14.793 6690 6735 V unknown:BufferedDiskCache: Disk cache miss for http://www.example.com/image.jpg 08-12 09:11:14.793 6690 6735 V unknown:RequestLoggingListener: time 11201793: onProducerFinishWithSuccess: {requestId: 1, producer: DiskCacheProducer, elapsedTime: 1 ms, extraMap: {cached_value_found=false}} 08-12 09:11:14.793 6690 6735 V unknown:RequestLoggingListener: time 11201793: onProducerStart: {requestId: 1, producer: NetworkFetchProducer} 08-12 09:11:15.161 6690 7358 V unknown:RequestLoggingListener: time 11202161: onProducerFinishWithSuccess: {requestId: 1, producer: NetworkFetchProducer, elapsedTime: 368 ms, extraMap: null} 08-12 09:11:15.162 6690 6742 V unknown:BufferedDiskCache: About to write to disk-cache for key http://www.example.com/image.jpg 08-12 09:11:15.162 6690 6734 V unknown:RequestLoggingListener: time 11202162: onProducerStart: {requestId: 1, producer: DecodeProducer} 08-12 09:11:15.163 6690 6742 V unknown:BufferedDiskCache: Successful disk-cache write for key http://www.example.com/image.jpg 08-12 09:11:15.169 6690 6734 V unknown:RequestLoggingListener: time 11202169: onProducerFinishWithSuccess: {requestId: 1, producer: DecodeProducer, elapsedTime: 7 ms, extraMap: {hasGoodQuality=true, queueTime=0, bitmapSize=600x400, isFinal=true}} 08-12 09:11:15.169 6690 6734 V unknown:RequestLoggingListener: time 11202169: onRequestSuccess: {requestId: 1, elapsedTime: 378 ms} 08-12 09:11:15.184 6690 6690 V unknown:AbstractDraweeController: controller 28ebe0eb 1: set_final_result @ onNewResult: image: CloseableReference 2fd41bb0 |
在这个示例中,我们发现名为28ebe0eb
的 DraweeView 向名为36e95857
的 DataSource 进行了图像请求。首先,图片没有在内存缓存中找到,也没有在磁盘缓存中找到,最后去网络上下载图片。下载成功后,图片被解码,之后请求结束。最后数据源通知 controller 图片就绪,显示图片(set_final_result
)。
内存管理
解压后的图片,即Android中的Bitmap
,占用大量的内存。大的内存占用势必引发更加频繁的GC。在5.0以下,GC将会显著地引发界面卡顿。
在5.0以下系统,Fresco将图片放到一个特别的内存区域。当然,在图片不显示的时候,占用的内存会自动被释放。这会使得APP更加流畅,减少因图片内存占用而引发的OOM。
Fresco 在低端机器上表现一样出色,你再也不用因图片内存占用而思前想后。
图片加载
Fresco的Image Pipeline允许你用很多种方式来自定义图片加载过程,比如:
- 为同一个图片指定不同的远程路径,或者使用已经存在本地缓存中的图片
- 先显示一个低清晰度的图片,等高清图下载完之后再显示高清图
- 加载完成回调通知
- 对于本地图,如有EXIF缩略图,在大图加载完成之前,可先显示缩略图
- 缩放或者旋转图片
- 对已下载的图片再次处理
- 支持WebP解码,即使在早先对WebP支持不完善的Android系统上也能正常使用!
图片绘制
Fresco 的 Drawees 设计,带来一些有用的特性:
- 自定义居中焦点
- 圆角图,当然圆圈也行
- 下载失败之后,点击重现下载
- 自定义占位图,自定义overlay, 或者进度条
- 指定用户按压时的overlay
图片的渐进式呈现
渐进式的JPEG图片格式已经流行数年了,渐进式图片格式先呈现大致的图片轮廓,然后随着图片下载的继续,呈现逐渐清晰的图片,这对于移动设备,尤其是慢网络有极大的利好,可带来更好的用户体验。
Android 本身的图片库不支持此格式,但是Fresco支持。使用时,和往常一样,仅仅需要提供一个图片的URI即可,剩下的事情,Fresco会处理。
自定义网络加载
Image pipeline 默认使用HttpURLConnection。应用可以根据自己需求使用不同的网络库。
OkHttp
OkHttp 是一个流行的开源网络请求库。Image pipeline有一个使用OkHttp替换掉了Android默认的网络请求的补充。
如果需要使用OkHttp, 不要使用这个下载页面的gradle依赖配置,应该使用下面的依赖配置
For OkHttp2:
1 2 3 4 | dependencies { // your project's other dependencies compile "com.facebook.fresco:imagepipeline-okhttp:0.12.0+" } |
For OkHttp3:
1 2 3 4 | dependencies { // your project's other dependencies compile "com.facebook.fresco:imagepipeline-okhttp3:0.12.0+" } |
Eclipse 中使用 OkHttp
Eclipse 用户需要依赖frescolib
目录下的imagepipeline-okhttp
或 imagepipeline-okhttp3
。 参考在Eclipse中使用Fresco.
配置 image pipeline
配置Image pipeline这时也有一些不同,不再使用ImagePipelineConfig.newBuilder
,而是使用OkHttpImagePipelineConfigFactory
:
1 2 3 4 5 6 7 8 | Context context; OkHttpClient okHttpClient; // build on your own ImagePipelineConfig config = OkHttpImagePipelineConfigFactory .newBuilder(context, okHttpClient) . // other setters . // setNetworkFetcher is already called for you .build(); Fresco.initialize(context, config); |
处理 Session 和 Cookie
你传给OkHttpClient
需要处理服务器的安全校验工作(可以通过Interceptor处理)。参考这个bug 来处理自定义网络库可能发生的 Cookie 相关的问题。
使用自定的网络层
为了完全控制网络层的行为,你可以自定义网络层。继承NetworkFetchProducer, 这个类包含了网络通信。
你也可以选择性地继承FetchState, 这个类是请求时的数据结构描述。
默认的 OkHttp 3
可以作为一个参考. 源码在这 its source code..
在配置Image pipeline时,把producer传递给Image pipeline。
1 2 3 4 5 | ImagePipelineConfig config = ImagePipelineConfig.newBuilder() .setNetworkFetcher(myNetworkFetcher); . // other setters .build(); Fresco.initialize(context, config); |
Fresco的体积可能会导致你的apk体积增大,所以可以在发布的时候使用混淆,使用混淆要注意
# Keep our interfaces so they can be used by other ProGuard rules.
# See http://sourceforge.net/p/proguard/bugs/466/
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.common.internal.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.common.internal.DoNotStrip *;
}
# Keep native methods
-keepclassmembers class * {
native <methods>;
}
-dontwarn okio.**
-dontwarn com.squareup.okhttp.**
-dontwarn okhttp3.**
-dontwarn javax.annotation.**
-dontwarn com.android.volley.toolbox.**
-dontwarn com.facebook.infer.**
按需打包
Fresco 大部分的代码是由Java写的,但是里面也有很多C++的代码。C++代码必须根据Android 设备的CPU类型(通常称为”ABIs”)进行编译。目前Fresco支持五种 ABI:
armeabiv-v7a
: 第7代及以上的 ARM 处理器。2011年15月以后的生产的大部分Android设备都使用它。arm64-v8a
: 第8代、64位ARM处理器,很少设备,三星 Galaxy S6是其中之一。armeabi
: 第5代、第6代的ARM处理器,早期的手机用的比较多。x86
: 平板、模拟器用得比较多。x86_64
: 64位的平板。
Fresco 下载下来之后已经包含了在这五种.so
文件,你可以根据不同平台打出不同的App,由此来缩减包体积。
如果你的应用不支持 Android 2.3 (Gingerbread),你可以不需要 armeabi
类的ABI.
Android Studio / Gradle
编辑 build.gradle
文件:
1 2 3 4 5 6 7 8 9 10 11 | android { // rest of your app's logic splits { abi { enable true reset() include 'x86', 'x86_64', 'arm64-v8a', 'armeabi-v7a', 'armeabi' universalApk false } } } |
参考Android Gradle 文档来获取更多信息。
Eclipse
默认情况下,Eclipse会产生一个含有所有ABI的app。将他们拆分比较困难(相比于Gradle)。
你需要下载multi-APK zip file而不是我们之前提供的标准ZIP包,而且你需要替换你的项目!
- 根据Android官方指示,你可以将项目拆分成多个项目。你可以使用相同的
AndroidManifest.xml
。 - 不同ABI需求(flavor)的项目,需要依赖于不同的
fresco-<flavor>
. (如果你使用OkHttp,你需要对应地引入imagepipeline-okhttp-<flavor>
)