Android图片库-Fresco

1,124 阅读13分钟

Fresco特征

  Fresco出产于facebook公司,也可以发生facebook公司的Android框架的图片加载库都是使用的Fresco,比如ReactNative。Fresco很明显的特点就是类似于MVC的结构,DraweeView作为View层,是图片的真正渲染层,DraweeHierarchy负责组件维护最终的Drawable对象,相当于是M,而DraweeController则相当于Controller,负责图片的数据处理,默认实现是ImagePipeline,拥有两级内存缓存和一级磁盘缓存。
  对比于另外两个图片加载库Picasso和Glide,Fresco的主要优点就是加载速度和缓存能力是最好的,而且兼容Android5.0以下,利用C++层解决OOM问题。Android系统解压后的图片使用Bitmap来保存,会占用大量内存(几Mb的图片很常见了,有些广告页长图几十Mb,别说这种情况少,有就是有,拿了东家的钱,就得解决所有问题),超大Bitmap一般在低端机上容易OOM。
  而Fresco使用了Ashmem匿名共享内存来解决这个问题。Android的内存区又叫Java Heap,由Dalvik虚拟机管理,每new一个对象都会消耗虚拟机的内存,并且Java Heap内存由GC直接管理,能够自动内容回收。除了Java Heap,还有另一个叫作Native Heap,这个内存区由C/C++申请,不受App最大可用内存限制,而只受设备物理可用内存限制。总所周知,C/C++的内存需要手动释放,因此Native Heap中的内存,需要有手动释放,否则就会内存泄漏。
  Android有一块特殊的内存区域叫Ashmem,类似于Native内存区,当系统内存不足时,会回收Ashmem区状态为unpin的对象内存块。Ashmem内存区一般应用是无法访问的,但将BitmapFactory.Options.inPurgeable设置为ture,就可创建一个Purgeable Bitmap,这样decode出来的Bitmap就在Ashmem内存中,Dalvik的GC就无法回收这些内存了。当decode这个Bitmap时,将这个Bitmap的内存给pin住,这样系统就不会回收,渲染结束时,调用unPin让系统回收,Fresco为了防止重新渲染导致在UI线程再decode而没有调用unPin,而是在Bitmap离开屏幕时才调用unPin,也就是在onDetachWindow的时候去unPin。
 emsp;Picasso库的特点是使用起来特别简单,Picasso出产于square,因此和OkHttp和Retrofit结合使用更多方便,但Picasso默认不支持磁盘缓存,需要自己实现,或者是交给OkHttp去实现。Glide是支持了加载尺寸,加载失败的处理,也支持了磁盘缓存,但使用起来可能比较麻烦。
  Fresco库的特点就是加载快,缓存更可控,并且加载逻辑与显示逻辑分离,可定制度高,但缺点也是使用较为麻烦,封装太多,而且要使用Fresco,就必须得使用Fresco提供的View而不能再使用ImageView了,因为Fresco的View层是使用的继承结构。这也是最要命的,使用了Fresco,就很难再切到其他图片库,就算要使用其他图片库,也使用其他图片库将Fresco的DraweeView再包一层,也就是使用其他图片库来实现Fresco的DraweeView的接口。

Fresco简单使用

class FrescoDemoActivity: AppCompatActivity() {

    companion object {
        const val FRESCO_IMAGE_CACHE = "fresco_image_cache"
        const val FRESCO_DISK_CACHE_SIZE = 1024 * 1024 * 500L

        const val IMAGE_URL = "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=3361338045,2067021064&fm=26&gp=0.jpg"

        fun initFresco(context: Context) {
            val builder = ImagePipelineConfig.newBuilder(context)
                .setMainDiskCacheConfig(DiskCacheConfig.newBuilder(context)
                    .setBaseDirectoryPath(context.externalCacheDir)
                    .setBaseDirectoryName(FRESCO_IMAGE_CACHE)
                    .setMaxCacheSize(FRESCO_DISK_CACHE_SIZE)
                    .build()
                )
                .setBitmapMemoryCacheParamsSupplier {
                    val MAX_HEAP_SIZE = Runtime.getRuntime().maxMemory()
                    val MAX_MEMORY_CACHE_SIZE = (MAX_HEAP_SIZE / 5).toInt()
                    MemoryCacheParams(
                        MAX_MEMORY_CACHE_SIZE, // 可用最大内存
                        Int.MAX_VALUE,  // 内存中允许的最大图片数
                        MAX_MEMORY_CACHE_SIZE, // 内存中准备清理但还未删除的总图片可用最大内存数
                        Int.MAX_VALUE, // 内存中准备清除的图片最大数量
                        Int.MAX_VALUE // 内存中单图的最大大小
                    )
                }
                .setEncodedMemoryCacheParamsSupplier {
                    val MAX_HEAP_SIZE = Runtime.getRuntime().maxMemory()
                    val MAX_MEMORY_CACHE_SIZE = (MAX_HEAP_SIZE / 5).toInt()
                    MemoryCacheParams(
                        MAX_MEMORY_CACHE_SIZE, // 可用最大内存
                        Int.MAX_VALUE,  // 内存中允许的最大图片数
                        MAX_MEMORY_CACHE_SIZE, // 内存中准备清理但还未删除的总图片可用最大内存数
                        Int.MAX_VALUE, // 内存中准备清除的图片最大数量
                        Int.MAX_VALUE // 内存中单图的最大大小
                    )
                }
            val memoryTrimmableRegistry = NoOpMemoryTrimmableRegistry.getInstance()
            memoryTrimmableRegistry.registerMemoryTrimmable(
                MemoryTrimmable {
                    val suggestedTrimRatio = it.suggestedTrimRatio
                    if (suggestedTrimRatio == MemoryTrimType.OnCloseToDalvikHeapLimit.suggestedTrimRatio
                        || suggestedTrimRatio == MemoryTrimType.OnSystemLowMemoryWhileAppInBackground.suggestedTrimRatio
                        || suggestedTrimRatio == MemoryTrimType.OnSystemLowMemoryWhileAppInForeground.suggestedTrimRatio) {
                        ImagePipelineFactory.getInstance().imagePipeline.clearMemoryCaches()
                    }
                })
            builder.setMemoryTrimmableRegistry(memoryTrimmableRegistry).isDownsampleEnabled = true
            FLog.setMinimumLoggingLevel(FLog.VERBOSE)
            val config = builder.setRequestListeners(setOf(object: RequestLoggingListener() {

            })).build()
            Fresco.initialize(context, config)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val imageView = SimpleDraweeView(this)
        imageView.setPadding(50, 50, 50, 50)
        val roundingParams = RoundingParams.fromCornersRadii(50F, 300F, 50F, 300F)
        roundingParams.setBorder(Color.RED, 2.0F)
        roundingParams.roundAsCircle = false
        imageView.hierarchy.roundingParams = roundingParams
        setContentView(imageView)
        val uri = Uri.parse(IMAGE_URL)
        val draweeController = Fresco.newDraweeControllerBuilder()
            .setImageRequest(ImageRequestBuilder.newBuilderWithSource(uri).setProgressiveRenderingEnabled(true).build()).build()
        imageView.controller = draweeController
    }

}

这个demo中使用了最爱的花火小姐姐,并将图片搞成左上右下50px圆角,右上左下300px圆角,外加2px的红色边框,图片使用了渐进加载模式。

Fresco的加载过程浅析

  首先是使用Fresco之前,都得调用Fresco.initilize进行初始化,并设置一些config。Fresco.initilize方法中,判断如果传入的ImagePipelineConfig不为null,就调用ImagePipelineFactory.initilize方法对ImagePipeline进行初始化,否则调用ImagePipelineFactory.initilize(context)进行默认初始化。而ImagePipelineFactory的默认初始化其他就是使用了ImagePipelineConfig.Builder的默认参数。调用了ImagePipelineFactory的initilize将传入的imagePipelineConfig储存起来后,再调用initilizeDrawee方法来设置对Drawee的控制,创建一个PipelineDraweeControllerBuilderSupplier对象传给SimpleDraweeView的initilize方法。SimpleDraweeView会将传入的PipelineDraweeControllerBuilderSupplier储存起来,每个SimpleDraweeView对象在调用init方法的时候,都会使用PipelineDraweeControllerBuilderSupplier get到一个PipelineDraweeController对象,利用这个PipelineDraweeController来加载真正的图片资源。
  还是看下真正的加载逻辑,首先是hierachy的设置逻辑,官方的建议是不要重复设置hierarchy,而是复用已有的,因此如果设置圆角的话,直接就对hierarchy的相关属性进行设置即可。本demo中对圆角进行了设置,首先创建了一个RoundingParams对象,将边框设置为红色且边框宽度为2像素,然后设置了roundAsCircle=false,这表示图片不使用圆形,最后设置了圆角,即设置了hierarchy的roundingParams参数。GenericDraweeHierarchy的setRoundingParms方法中调用WrappingUtils.updateOverlayColorRounding(mTopLevelDrawable, mRoundingParams)方法将圆角参数设置到顶部涂层。这里涉及到GenericDraweeHierachy的涂层问题,由于图片控件DraweeView主要任务是显示图像,而DraweeView又涉及到点位图、失败图、进度条,本身图之类的,一个View要承担多个ImageView的责任,因此GenericDraweeHierachy中就持有了不同功能的Drawable。分别有

  • mTopLevelDrawable,顶层涂层,相当于最终要显示的图像
  • mFadeDrawable,所有数据整合后存放的ArrayDrawable
  • ACTUAL_IMAGE_INDEX,加载成功展示的图片
  • PLACEHOLDER_IMAGE_INDEX,点位图的index
  • PROGRESS_BAR_IMAGE_INDEX,进度条的index
  • RETRY_IMAGE_INDEX,重试图的index
  • FAILURE_IMAGE_INDEX,失败图的index 而设置圆角的时候,是需要更新最终图的参数的,因此是使用mTopLevelDrawable。如果发现mTopLevelDrawable不是RoundedCornersDrawable类型的,就调用maybeWrapWithRoundingOverlayColor,来将mTopLevelDrawable给包装成RoundedCornersDrawable类型的,将参数设置的时候,由于这时真正的Drawable已经是RoundedCornersDrawable类型的了,实现了Rounded接口,因此可以直接将参数设置给Drawable了。在RoundedCornersDrawable中对Rounded接口的实现都是将相关的变量赋值,最后都调用了updatePath和invalidateSelf方法,updatePath是为了重新Drawable的相关线的,而invalidateSelf是由基类Drawable实现,调用了callback.invalidateDrawable方法,而这个callback实际就是DraweeView,而DraweeView继承自ImageView,因此这具invalidateDrawable就是ImageView实现的,ImageView的invalidateDrawable方法中最终都会调用invalidate方法,在View绘制系统中,这个invalidate是会触发View重绘的。
      再回到如何让一个DraweeView加载一个图片的过程中来,调用DraweeView的setImageURI方法,最后都是调到了
public void setImageURI(Uri uri, @Nullable Object callerContext) {
    DraweeController controller =
        mControllerBuilder
            .setCallerContext(callerContext)
            .setUri(uri)
            .setOldController(getController())
            .build();
    setController(controller);
  }

在这个方法中,构造了一个DraweeController,并调用setController。setController方法中主要是调用了mDraweeHolder.setController,并且将mDraweeHolder中的也就是GenericDraweeHierarchy中的mTopLevelDrawable设置成ImageView要显示的drawable。DraweeHolder的setController方法先判断是否已经有controller被attach了,如果有则先调用detachController,detachController方法中主要是调用了mController的onDetach方法,而Controller就是用这个onDetach方法去释放资源的,View在detach时也会调到这个Controller的onDetach方法。在DraweeHolder的setController的最后调用attachController方法,attachController方法中判断controller的getHierarchy是否为null,如果不为null就调用了传入的controller的onAttach方法,为null的话表示根本就不能绘制图像,那去请求资源也没啥用了。Fresco默认使用PipelineDraweeController,PipelineDraweeController继承自AbstractDraweeController,其对onAttach的实现是判断是否已经提交过请求,未提交就调用submitRequest方法。
  AbstractDraweeController的submitRequest中,首先调用getCachedImage从缓存中取,如果缓存中取到的不为null,则调用onNewResultInternal方法使用createDrawable(image: T)根据从缓存中取出的image构造出一个Drawable,其实也就是从image中取出bitmap,创建出一个BitmapDrawable。得到的Drawable调用DraweeHierarchy的setImage方法,setImage方法中调用mActrualImageWrapper的setDrawable方法,而setDrawable方法的实现是将新传入的drawable替换老的,将新的drawable的callback设置成ForwardingDrawable,ForwardingDrawable对callback的实现就是调用ImageView的相关方法。最后ForwardingDrawable在setCurrent中调用了invalidateSelf,也就是要重新绘制ImageViewb了。这样从缓存中得到了Drawable就被绘制到了ImageView上。   如果从缓存中得到了为null,就得从网络请求了,这时将进度设置为0,并显示进度条。调用getDataSource获取DataSource对象,并创建一个DataSubscriber对象,调用DataSource的subscribe方法,在UI线程中去分发数据请求结果。因此真正的请求是在getDataSource实现中,PipelineDraweeController对getDataSource的实现是调用了mDataSourceSupplier的get方法。PipelineDraweeController的mDataSourceSupplier是在init方法中进行赋值的,init方法在其initialize方法中调用,传入的DataSourceSupplier通过AbstractDraweeeControllerBuilder的obtainDataSourceSupplier方法获取。由于传入的uri已经构造成了ImageRequest对象,因此是调用getDataSourceSupplierForRequest方法获取的Supplier对象。在getDataSourceSupplierForRequest方法中,创建一个Supplier对象,其get方法的实现是调用getDataSourceForRequst方法。这个getDataSourceForRequst是在PipelineDraweeControllerBuilder中实现的,其实现直接调用了mImagePipeline的fetchDecodeImage方法。ImagePipeline的fetchDecodeImage方法使用CloseableProducerToDataSourceAdapter.create方法创建出DataSource对象,这是个静态的创建者方法,new了一个CloseableProducerToDataSourceAdapter对象,在CloseableProducerToDataSourceAdapter的父类AbstractProducerToDataSourceAdapter构造器中调用了传入的producer的produceResults方法,而这个producer是ImagePipeline的fetchDecodedImage方法中,使用mProducerSequenceFactory的getDecodedImageProducerSequence方法创建出来的。而这个getDecodedImageProducerSequence方法中是调用getBasicDecodedImageSequence来获取Producer的,getBasicDecodedImageSequence方法中对request的类型作了判断,如果我们是使用的uri,则表示要创建一个网络的Producer,也即getNetworkFetchSequence方法,这里又调用了newBitmapCacheGetToDecodedSequence,而且将getCommonNetworkFetchToEncodedMemorySequence方法的结果作为inputProducer,这里的意思是将网络请求的结果缓存起来,缓存的Producer的实现类是BitmapMemoryCacheGetProducer,而getCommonNetworkFetchToEncodedMemorySequence方法中也是封装了好几层,将inputProducer包了好几层,最底层的是mProducerFactory.newNetworkFetchProducer,也即最底层的网络请求Producer是NetworkFetchProducer。
  这里有点像职责链模式了,将网线请求的Producer包了好多层,最上面一层是BitmapMemoryCacheProducer,在其produceResults方法中,发现缓存不可用时,就调用mInputProducer.produceResults方法,最后会调用NetworkFetchProducer的produceResults方法中去。而NetworkFetchProducer的produceResults方法中,是调用了mNetworkFetcher的fetch方法,传入了一个callback对象,在onResponse回调中调用NetwrokFetchProducer的onResponse方法,在onFailure回调中调用NetworkFetchProducer的onFailure方法,在onCancellation中调用NetwrokFetchProducer的onCancellation方法。在这三个处理方法中,都是调用了fetchState.getConsumer的相应方法回调了处理结果。可以发现这个consumer也是在produceResults在各层Producer之间一层一层向下传递的,在最上一层也就是在AbstractProducerToDataSourceAdapter的构造器中,是调用createConsumer生产出的。其实现也是调用了AbstractProducerToDataSourceAdapter的相应方法。而这个方法中都是调用了RequestListener的相应方法,可以发现在这些RequestListener的参数传递过程中,也是将Listenrer包装了多层,每层都做了一些工作并且将回调调给下一层。在AbstractProducerToDataSourceAdapter的onNewResultImpl方法中,也就是请求成功的回调中调用了super.setResult方法,将请求结果传入。这个setResult方法实现中调用了notifyDataSubscribers。而notifyDataSubscribers方法中对每个Subscriber进行遍历,在executor中调用DataSubscriber的相应回调方法。在AbstractDraweeController的submitRequest方法中就实现了一个DataSubscriber,在其成功的回调onNewResultImpl方法中又调用了onNewResultInternal方法,在最开始就分析了,这个onNewResultInternal会更新ImageView展现的图像。这样就完成从setImageURI到本地缓存请求到网络请求到最后怎么回调并更新ImageView展现的图像。

DraweeView多状态视图切换原理

  Fresco使用DraweeView来处理图像的显示,而控制各种状态的图像则是由DraweeHierarchy负责的,DraweeHierarchy是个接口,只有一个getTopLevelDrawable返回一个Drawable的方法,这个方法表示要最终显示的Drawable图像。SettableDraweeHierachy也是个接口,继承自DraweeHierarchy,并添加了reset、setImage、setProgress、setFailure、setRetry、setControllerOverlay几个方法,分别为不同加载状态的图像做设置。DraweeHierarchy接口的最终实现类是GenericDraweeHierarchy,在这个类中使用了FadeDrawable来储存各个图层,当状态改变时,就将图层显示出来。GenericDraweeHierarchy的getTopLevelDrawable方法的实现是返回了mTopLevelDrawable,这个mTopLevelDrawable是个RootDrawable类型的,RootDrawable继承自ForwardingDrawable,ForwardingDrawable并不是一个真正的Drawable,而是一个代理模式的Drawable,相当于一个Wrapper。真正使用的是传入的drawable作为delegate,mTopLevelDrawable真正的功能是由mFadeDrawable提供的。
  mFadeDrawable是个FadeDrawable类型的,继承自ArrayDrawable,在GenericDraweeHierarchy中实现有resetFade、fadeOutBranchs、fadeInLayer(index: int)、fadeOutLayer(index:int),分别表示重置所有的Drawable、让所有的Drawable都隐藏,让index层的Drawable显示出来,让index位置的Drawable隐藏。以fadeInLayer为例,FadeDrawable的fadeInLayer的实现是将mIsLayerOn对应index置为true,同时fadeOutLayer是将mIsLayerOn的对应index置为false,然后调用invalidetaSelf。这个invalidetaSelf会触发Drawable重绘,会调用Drawable的draw方法。在FadeDrawable的draw方法中,调用了updateAlphas,如果mIsLayerOn相应index位置为true,则给mAlpha相应index位置的alpha值加上更新的值,如果为false,则减去更新的值,也就是说如果如果要隐藏某层的Drawable,会通过mIsLayerOn数组相应index位置的值,来让这个Drawable的透明度改变。updateAlphas只是计算了每层Drawable的透明度,而遍历所有层的Drawable并调用drawDrawableWithAlpha才是真正地改变透明度。
  因此得出结论,Fresco通过GenericDraweeHierarchy来管理所有的状态Drawable,而管理的原理是使用一个ArrayDrawable来保存所有的图层,内部维护一个mIsLayerOn的boolean数组,显示某层Drawable就将这个boolean数组的相应位置置为true,反之置为false,在draw的时候,根据这个mIsLayerOn数组,判断每层Drawable的透明度是该增加还是减少,增加就是显示出来,减小就是要隐藏。

总结

  Fresco作为一个图片库,拥有着良好的加载性能和可扩展性,采用MVC模式,将图像显示与数据加载隔离开,并将数据加载的状态与图像的显示有机的联系了起来。Fresco内置的图片扩展功能已经能够满足日常开发中的需要了,我们还是可以根据项目的真实需求做相应的定制化。但Fresco的缺点,还是那句话,如果要用Fresco,就得一条路走到黑了。