项目重构之路——Fresco非侵入式替换Glide

本文GitHub项目地址 : 统一的图片加载架构html

前言

咱们目前的项目对于图片加载的需求很大,一直以来,咱们使用的是Glide做为图片加载的底层包,为了节省图片占用的空间,但愿使用webp的格式来展现动图。因为Glide不支持Animated WebP(即WebP动图) 格式,因此咱们须要把底层的Glide替换为Fresco。相信用过这两个包的同窗都知道,二者的差别仍是比较大的,想要保证在不大量修改代码,也不入侵业务代码的前提下迁移到Fresco上,是个值得思考的问题。java

因为以前已经把项目的图片加载模块重构整合了一次,把图片加载和业务代码有效分离开,所以此次迁移也算有一个较好的基础。至于整合的具体思路以及为何整合这些模块的意义,我在《封装并实现统一的图片加载架构》文章里面已经讲了不少,没看过那篇文章的同窗建议看看,由于接下来就是在这篇文章的基础上去分析如何迁移到Fresco上的问题。git

替换Glide遇到的难题

咱们都知道,Glide是使用了ImageView来加载图片的,而实际项目中还会有咱们自定义的ImageView,好比CircleImageView等,而Fresco则是经过DraweeView来加载图片的,这是一个很是严重的问题。github

因此咱们须要考虑的是在项目迁移到Fresco上的时候,究竟是想办法沿用ImageView来加载图片,仍是想办法使用SimpleDraweeView来加载图片,若是是前者,那么咱们应该怎么作?假如是后者,要怎样才能保证Fresco的代码不会大量入侵到业务代码中,同时又能兼容原来项目的接口呢?web

Tips : 对于Fresco本身的图片加载容器DraweeView,目前暂时继承自ImageView,可是官方表示将来会直接继承自View,咱们开发时经常使用DraweeView的子类SimpleDraweeView来加载图片。缓存

解决的思路

方案一:沿用ImageView加载

  • 思路
    • 寻找一种办法从Fresco中获取图片数据,而后使用ImageView来加载。而正好,Fresco有一个Image pipeline 做为图片加载的控制核心,它负责判断图片缓存,而且能从各类渠道解码,变换,返回bitmap,而后缓存起来。也就是说,咱们能够直接经过这个模块去获取缓存的bitmap,把它加载到ImageView上,这样的话,就不须要使用Fresco的DraweeView了。

  • 优势bash

    在我看官方文档的时候,暗喜了良久。由于若是能够这么作的话,那么这就是最简单的直接的迁移到Fresco上的方法。架构

  • 缺陷
    • 我发现若是使用Image Pipline去获取Bitmap来加载图片,那么也就必须放弃Fresco特有的Drawee各类效果配置。不只如此,Fresco对本身返回的bitmap的生命周期控制十分严格,以致于咱们在使用时必须十分当心,并且一旦再也不使用必须释放引用,换言之,咱们必须手动控制整个图片加载的缓存释放策略,稍有不慎就会报错。因为咱们的项目是一个直播App,许多复杂的业务都涉及到图片加载模块的使用,我没法保证能在那么多复杂得场景中控制好Bitmap的生命周期。
  • 结论
    • 这种策略看起来最简单,实际上须要付出的代价极大,基本上是没法承受的。Pass。

方案二 :直接替换(使用SimpleDraweeView)

  • 思路:
    • 既然咱们本身控制图片加载和内存回收太过吃力,就只能把这些工做交给Fresco,最直接的方法就是,把xml,Java文件中的涉及到ImageView以及自定义的ImageView的引用粘贴复制成SimpleDraweeView就好了(并且Fresco虽然有本身的DraweeView,可是也都继承自ImageView,这也会给咱们带来一些便利)。固然,须要修改的还会有不少。当第一个思路行不通的时候,这个思路能够说是很容易想到的,无非就是统一替换的问题,简单粗暴,不会有太多难题。
  • 缺陷:框架

    • 可是,能最早想到的未必是最好的方案,首先,xml,Java文件中大量出现Fresco的引用,是个比较糟糕的状况。会破坏咱们的以前对于图片加载框架的一些封装,之后再修改框架,或者替换Fresco就很被动。(想想,万一之后Glide之后支持了Webp格式,而咱们又对Fresco包的大小很不爽,那么就免不了要替换掉Fresco了)
  • 结论:ide

    • 难度很低,工做量较大,也会破坏项目的封装性。保留做为万不得已的选择

方案三 : 动态替换(使用SimpleDraweeView)

  • 思路:
    保持原有的代码不变,当须要调用图片加载接口的时候,动态的把原有的ImageView及其子类替换成Fresco的SimpleDraweeView,而后使用SimpleDraweeView去加载图片。

具体操做大概就是当调用了以下加载接口的时候,把ImageView动态替换成SimpleDraweeView,而后加载图片

void showImage(ImageLoaderOptions options);  // ImageLoaderOptions包含ImageView,Url 等等复制代码
  • 优点:
    • 这个方案很好的解决了上面的担心,既不改动原有的代码,也不将Fresco的代码大量参杂到业务中去,未来的替换或者拓展将十分轻松。
  • 缺陷:
    • 这个方案在实现上却有不少难点,好比如何动态替换,原有的ImageView中设置的click监听事件怎么转移过去?最重要的一个问题是:listview,recycleview中的ImageView(缓存)如何替换?
  • 结论:
    • 在实际测试的过程当中,我发现RecycleView中加载的ImageView会有缓存的问题,没法完成替换,所以这个方案也只能抛弃掉。

方案四 : 动态添加(使用SimpleDraweeView)

  • 思路:
    • 保持原有代码中的ImageView不变,在原有的布局关系中插入一个“如出一辙”的SimpleDraweeView,保证SimpleDraweeView在父容器中的位置正好和ImageView彻底重合,Click事件依然还保留原来的ImageView上,RecycleView的缓存问题也能够避免,这样,SimpleDraweeView负责加载显示图片,ImageView负责交互。
  • 优点:
    • 这是方案三延伸出的一个折中方案,避免了不少难题,实现起来也会很简单,并且代码的入侵度很低。
  • 缺陷:
    • 这个方案稍逊于方案三,但可行性极高。假如原项目中对ImageView的使用得当,那么几乎就无需再修改项目代码了。并且也无须担忧对象过可能是否致使内存的问题,由于真正的内存消耗的大户都是图片,ImageView这个对象自己并不大。

比较方案

上面四个方案是我所想到的全部的实现方案,基本上方案三和方案四实现后的效果是最佳的。可是方案三的难度更大,因此综合来看,方案四的性价比更好。(目前项目使用的就是方案四,效果良好,运行正常)

实践

肯定了方案,咱们能够开始实践了,结合方案四的思路,代码实现上基本上是没有什么难度了。
对于如何整合图片加载模块,请务必参考以前的文章《封装并实现统一的图片加载架构

Fresco加载模块的重点代码实现以下:

FrescoImageLoader.java

// 项目种几乎全部的图片加载都调用到了这里
    @Override
    public void showImage(@NonNull ImageLoaderOptions options) {
        showImgaeDrawee(options);
    }

    private void showImgaeDrawee(ImageLoaderOptions options) {
         // 这个View就是加载图片的ImageView
        View view=options.getViewContainer();
        SimpleDraweeView drawee=null;
        Class clazz=null;
        GenericDraweeHierarchy hierarchy=null;
        GenericDraweeHierarchyBuilder hierarchyBuilder = GenericDraweeHierarchyBuilder.newInstance(getResources());
        // 因为本身的项目中有好几种ImageView,所以须要一一判断
        if (view instanceof SquareRImageView) {
            clazz= SquareRImageView.class;
            drawee=getDraweeView(view,clazz);
            if (drawee != null) {
                drawee.setAspectRatio(1);
            }
        }else if (view instanceof CircleImageView){
            clazz= CircleImageView.class;
            // 传入
            drawee=getDraweeView(view,clazz);
            hierarchyBuilder.setFadeDuration(400).setRoundingParams(RoundingParams.asCircle());
        }else if (view instanceof SimpleDraweeView){
            drawee= (SimpleDraweeView) view;
            hierarchy=drawee.getHierarchy();
        }else if(view instanceof ImageView){
            clazz= ImageView.class;
            drawee=getDraweeView(view,clazz);
        }
        else {
            Logger.i("no type !!");
            return;
        }

        if (drawee != null) {
        // 图片地址
            Uri uri=Uri.parse(options.getUrl());
            if (options.getHolderDrawable()!=-1) {
                hierarchyBuilder.setPlaceholderImage(options.getHolderDrawable());
            }
            if (options.getErrorDrawable()!=-1) {
                hierarchyBuilder.setFailureImage(options.getErrorDrawable());
            }

            if (hierarchy == null) {
                hierarchy= hierarchyBuilder.build();

            }
            drawee.setHierarchy(hierarchy);

            PipelineDraweeControllerBuilder controllerBuilder=Fresco.newDraweeControllerBuilder().setUri(uri).setAutoPlayAnimations(true);

            ImageRequestBuilder imageRequestBuilder= ImageRequestBuilder.newBuilderWithSource(uri);
            if (options.getImageSize() != null) {
                imageRequestBuilder.setResizeOptions(new ResizeOptions(getSize(options.getImageSize().getWidth(),view), getSize(options.getImageSize().getWidth(),view)));
            }
            if (options.isBlurImage()) {
                 // 是否作高斯模糊
                imageRequestBuilder.setPostprocessor(new BlurPostprocessor(view.getContext().getApplicationContext(), 15));
            }
            ImageRequest request =imageRequestBuilder.build();
            controllerBuilder.setImageRequest(request);
            DraweeController controller=controllerBuilder.build();
            drawee.setController(controller);
        }
    }复制代码

在图片加载时,首先须要判断加载图片的容器是ImageView仍是ImageView的子类,由于这意味着对图片不一样的处理,好比CircleImageView意味着是加载一个圆图,因此咱们须要设置SimpleDraweeView为圆图等等。

// 传入加载图片的ImageView,返回一个相同位置,相同大小的SimpleDraweeView
        private SimpleDraweeView getDraweeView(View viewContainer,Class<?> classType) {
        if (viewContainer instanceof SimpleDraweeView){
            return (SimpleDraweeView) viewContainer;
        }
        SimpleDraweeView mDraweeView=null;
        if (classType.isInstance(viewContainer)){
            FrameLayout layout=new FrameLayout(viewContainer.getContext());
            if(viewContainer.getParent() instanceof FrameLayout){
                FrameLayout parent= (FrameLayout) viewContainer.getParent();
                FrameLayout.LayoutParams params= (FrameLayout.LayoutParams) viewContainer.getLayoutParams();
                // 这个方法来完成最终的添加
                mDraweeView=exchangeChilde(parent,viewContainer,params);
            }else if(viewContainer.getParent() instanceof RelativeLayout){
                RelativeLayout parent= (RelativeLayout) viewContainer.getParent();
                RelativeLayout.LayoutParams params= (RelativeLayout.LayoutParams) viewContainer.getLayoutParams();

                mDraweeView=exchangeChilde(parent,viewContainer,params);
            }else if(viewContainer.getParent() instanceof LinearLayout){
                // 当ImageView 的Parent时LinearLayout的时候,处理会有一些不一样
                LinearLayout parent= (LinearLayout) viewContainer.getParent();
                LinearLayout.LayoutParams params= (LinearLayout.LayoutParams) viewContainer.getLayoutParams();
                layout.setLayoutParams(params);
                addToViewGroup(parent,viewContainer,layout); 
                layout.addView(viewContainer);
                mDraweeView=exchangeChilde(layout,viewContainer,params);
            }else{
                //基本上能够涵盖上面一个项目中用到的布局类型了,
                //其余的类型如Tablayout等等,视实际状况而定
                ViewParent parent=viewContainer.getParent();
                Logger.i("");
            }
        }else{
            Logger.i("");
        }
        return mDraweeView;
    }

  // 该方将ImageView从原来的Parent种移除,并添加到一个FrameLayout中去
      private void addToViewGroup(ViewGroup parent,View viewOld,View viewNew){
        for (int i = 0; i < parent.getChildCount(); i++) {
            if (parent.getChildAt(i).equals(viewOld)) {
                parent.removeView(viewOld);
                parent.addView(viewNew,i);
                return;
            }
        }
    }复制代码

这里须要判断ImageView的父容器ViewGroup是那些,须要着重区分LinearLayout这个父布局,由于若是ImageView的父容器是LinearLayout,那么咱们就没法在LinearLayout中添加一个大小相同,位置和ImageView重合的SimpleDraweeView来加载图片了,所以,此时咱们须要把这个ImageView拿出来,把它和SimpleDraweeView一块儿装在FrameLayout中,而后在把FrameLayout添加到ImageView原来在LinearLayout中所处的位置。

// 紧挨着ImageView添加SimpleDraweeView到原来的ImageView的位置
    private SimpleDraweeView exchangeChilde(ViewGroup parent, View testImageView, ViewGroup.LayoutParams layoutParams) {
        SimpleDraweeView draweeview =null;
        for (int i = 0; i < parent.getChildCount(); i++) {
            if (testImageView.equals(parent.getChildAt(i))) {
                if (testImageView instanceof ImageView) {
                    ImageView img= (ImageView) testImageView;
                    img.setBackgroundDrawable(null);
                    img.setImageDrawable(null);
                }
                if (i+1 < parent.getChildCount()) {
                    View child=parent.getChildAt(i+1);
                    // 此处理应作更加仔细的判断
                    if (child instanceof SimpleDraweeView) {
                        return (SimpleDraweeView) child;
                    }
                }
                draweeview=new SimpleDraweeView(testImageView.getContext());
                draweeview.setLayoutParams(layoutParams);
                parent.addView(draweeview,i+1);
                return draweeview;
            }
        }
        return draweeview;
    }复制代码

以上基本上就是以Fresco来实现图片加载模块的核心代码了,基本能够覆盖原有的Glide的功能,而且入侵度低,无需修改原有代码,随时可替换。

勘误

暂无


项目已经上传了github,点此获取,求star! 求follow !

相关文章
相关标签/搜索