美文网首页Android 基础知识Android技术知识Android知识
项目重构之路——Fresco非侵入式替换Glide

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

作者: 拉丁吴 | 来源:发表于2017-07-04 21:54 被阅读1507次

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

    前言

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

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

    替换Glide遇到的难题

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

    所以我们需要考虑的是在项目迁移到Fresco上的时候,到底是想办法沿用ImageView来加载图片,还是想办法使用SimpleDraweeView来加载图片,如果是前者,那么我们应该怎么做?假如是后者,要怎样才能保证Fresco的代码不会大量入侵到业务代码中,同时又能兼容原来项目的接口呢?

    Tips : 对于Fresco自己的图片加载容器DraweeView,目前暂时继承自ImageView,但是官方表示未来会直接继承自View,我们开发时常常使用DraweeView的子类SimpleDraweeView来加载图片。

    解决的思路

    方案一:沿用ImageView加载

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

      在我看官方文档的时候,暗喜了良久。因为如果可以这么做的话,那么这就是最简单的直接的迁移到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了)
    • 结论:

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

    方案三 : 动态替换(使用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 !

    相关文章

      网友评论

      • 明朗__:如果需要ImageView 做动画效果 那就呵呵了
        拉丁吴: @明朗__ 嗯,webp动图比gif,帧动画都好一些
        明朗__:@拉丁吴 WebP动图是什么鬼 直播送礼动画那种吗 我以前是通过服务器发送的bitmap数组 还有每个bitmap显示的位置 和时间 客户端接收到后就缓存本地 不过webp确实可以节省传输空间和内存空间 但是在本地解析显示的时候却是非常耗内存的 反正各有取舍
        拉丁吴:@明朗__ 最近正在研究如何在这个基础上做过渡动画,确实有些难度
      • Android开发哥:如果addView之前把原来的ImageView移除掉会不会优化UI渲染性能?
        Android开发哥:@拉丁吴 好像是 尴尬
        拉丁吴: @August1996 这是方案3的思路啊,我说了,最后卡在recycleview的缓存问题里面
      • a0249b32a0b5:看到了那个for循环 总感觉并不是好的方案。。
        拉丁吴:父容器里面包含的childview基本不会太多,如果多了,就应该考虑使用列表了,for循环执行不了几次,这种数量级根本不用担心代码的执行效率。你看过View的绘制的相关代码的话,你会发现,都是ViewParent去遍历ChildView....

      本文标题:项目重构之路——Fresco非侵入式替换Glide

      本文链接:https://www.haomeiwen.com/subject/pxtfhxtx.html