Fresco 其实是我司长期以来默认使用的图片加载框架,这个情况在 4 月份我在做新项目的技术选型的时候被打破了,那时我针对 Glide vs Fresco 的性能做了对比,在加载 GIF 的情况下,Glide + GifDrawable 三方库在内存和 CPU 上要优于 Fresco,所以领导就拍板决定在新版项目里使用 Glide, 但这个情况在后面发生了一些变化。
背景
我在最开始开发的时候其实是想过在Android 4.4上使用 Fresco,Android 5.0及以上使用 Glide,在代码层面也做了图片加载框架的封装,只不过领导认为太过麻烦给否决了。
后期在准对机顶盒和投影仪这些设备做版本开发时,发现内存使用没有很高就会出现内存波动,再加上我对 Fresco 持保留态度,知道 Fresco 在加载图片的时候会有效的利用匿名内存,会在一定程度上提高内存的使用率,所以就决定继续完善之前的图片加载框架的封装。
代码封装
因为 Fresco 是使用SimpleDraweeView这个自定义 View 来加载图片的,所以为了能够同时兼容 ImageView,我们需要先封装一个自定义 ViewGroup来同时承载ImageView或者 Fresco 的SimpleDraweeView。
首先使用一个接口来承担对外的共同操作,这样可以对模块外隐藏内部实现细节。
1 2 3 4
| public interface IMarketView {
... }
|
然后依据不同场景来实现不同的自定义ViewGroup,这里拿其中一个来举例:
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| public class MarketIconView extends FrameLayout implements IMarketView {
...
public MarketIconView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initAttrs(context, attrs); }
...
private void initAttrs(Context context, AttributeSet attrs) { ... if (ImageLoaderUtil.shouldUseGlideEngine()) { addView(buildShapeableImageView(), new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } else { addView(buildFrescoImageView(), new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } ... }
private ImageView buildShapeableImageView() { ShapeableImageView normalImageView = new ShapeableImageView(getContext()); normalImageView.setShapeAppearanceModel(normalImageView.getShapeAppearanceModel() .toBuilder().setAllCorners(CornerFamily.ROUNDED, mCornerSize) .build()); normalImageView.setStrokeColor(ColorStateList.valueOf(Color.TRANSPARENT)); normalImageView.setScaleType(ImageView.ScaleType.FIT_XY); return normalImageView; }
private SimpleDraweeView buildFrescoImageView() { SimpleDraweeView frescoImageView = new SimpleDraweeView(getContext()); frescoImageView.setScaleType(ImageView.ScaleType.FIT_XY); RoundingParams roundingParams = RoundingParams.fromCornersRadius(mCornerSize); frescoImageView.setHierarchy(new GenericDraweeHierarchyBuilder(getResources()) .setRoundingParams(roundingParams) .setActualImageScaleType(ScalingUtils.ScaleType.FIT_XY) .setFadeDuration(0) .build()); return frescoImageView; } }
|
然后就是修改之前定义的ImageLoader接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public interface ImageLoader {
void init(@NonNull Context context);
void loadImageWithOptions(@NonNull IMarketView viewParent, @NonNull String url, LoadOptions options);
void loadImageWithOptions(Fragment fragment, @NonNull IMarketView viewParent, @NonNull String url, LoadOptions options);
void loadImageWithOptions(Fragment fragment, @NonNull IMarketView viewParent, @NonNull String url, LoadOptions options, OnImageLoadListener loadListener);
void clearImageLoad(Fragment fragment, @NonNull IMarketView viewParent);
void resumeRequests(Fragment fragment);
void pauseRequests(Fragment fragment);
void onTrimMemory(Context context, int level);
void clearMemoryCache(Context context);
void preloadImage(@NonNull String url); }
|
这里使用了LoadOptions来封装一些加载图片时的配置,这样在不同的场景下,可以对LoadOptions进行配置,从而达到不同的效果,这里就不再赘述了。
最后再把 xml 里的<ImageView>换成IMarketView对应的实现即可。
Glide 实现
用 Glide 实ImageLoader的部分代码
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| public final class GlideImageLoader implements ImageLoader {
public static final String TAG = "GlideLoader";
public GlideImageLoader() {
}
@Override public void loadImageWithOptions(@NonNull IMarketView viewParent, @NonNull String url, LoadOptions options) { if (isGifUrl(url)) { loadGif(viewParent, url); return; } ImageView view = viewParent.getImageViewChild(); if (view == null) return; Context context = view.getContext().getApplicationContext(); Glide.with(context).clear(view); Glide.with(context) .load(url) .apply(GlideOptionsUtil.convertOptions(options)) .into(view); }
@Override public void loadImageWithOptions(Fragment fragment, @NonNull IMarketView viewParent, @NonNull String url, LoadOptions options) { if (!fragment.isAdded() || fragment.isDetached()) return; if (isGifUrl(url)) { loadGif(fragment, viewParent, url, null); return; } ImageView view = viewParent.getImageViewChild(); if (view == null) return; Glide.with(fragment).clear(view); Glide.with(fragment) .load(url) .apply(GlideOptionsUtil.convertOptions(options)) .into(view); }
@Override public void loadImageWithOptions(Fragment fragment, @NonNull IMarketView viewParent, @NonNull String url, LoadOptions options, OnImageLoadListener loadListener) { if (!fragment.isAdded() || fragment.isDetached()) return; if (isGifUrl(url)) { loadGif(fragment, viewParent, url, loadListener); return; } ImageView view = viewParent.getImageViewChild(); if (view == null) return; Glide.with(fragment).clear(view); Glide.with(fragment) .load(url) .apply(GlideOptionsUtil.convertOptions(options)) .listener(new NormalRequestListener(loadListener)) .into(view); }
...
private boolean isGifUrl(String url) { return url.toLowerCase().endsWith(".gif"); }
private void loadGif(@NonNull IMarketView viewParent, @NonNull String url) { ImageView view = viewParent.getImageViewChild(); if (view == null) return; Glide.with(view.getContext()).clear(view); RequestOptions newOption = GlideOptionsUtil.gifOptions(); RequestBuilder<GifDrawable> builder = Glide.with(view.getContext()) .asGif() .load(url) .apply(newOption); builder = builder.listener(new NormalGifRequestListener(null)); builder.into(view); } }
|
跟之前代码相比,只需要在viewParent对象中获取到 ImageView 对象,把之前的GlideOptions替换成LoadOptions即可,代码改动并不大。
Fresco 实现
用 Fresco 实ImageLoader的部分代码
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| public class FrescoImageLoader implements ImageLoader {
public static final String TAG = "FrescoLoader";
public FrescoImageLoader() { }
@Override public void init(@NonNull Context context) { if (!Fresco.hasBeenInitialized()) { ImagePipelineConfig config = FrescoOptionUtil.applyCommonConfig(context); Fresco.initialize(context, config); } this.mContext = context; }
@Override public void loadImageWithOptions(@NonNull IMarketView viewParent, @NonNull String url, LoadOptions options) { loadImageWithOptions(null, viewParent, url, options); }
@Override public void loadImageWithOptions(Fragment fragment, @NonNull IMarketView viewParent, @NonNull String url, LoadOptions options) { loadImageWithOptions(fragment, viewParent, url, options, null); }
@Override public void loadImageWithOptions(Fragment fragment, @NonNull IMarketView viewParent, @NonNull String url, LoadOptions options, OnImageLoadListener loadListener) { SimpleDraweeView view = viewParent.getFrescoDraweeView(); if (view == null) return; boolean isGifImage = isGifUrl(url); viewParent.setCornerEnable(isGifImage); if (isGifImage) { loadGifWithOptions(viewParent, url, loadListener); return; } if (options.placeholderResId != -1 || options.errorResId != -1) { GenericDraweeHierarchy oldHierarchy = view.getHierarchy(); GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(mContext.getResources()) .setPlaceholderImage(options.placeholderResId) .setFailureImage(options.errorResId) .setRoundingParams(oldHierarchy.getRoundingParams()) .setActualImageScaleType(ScalingUtils.ScaleType.FIT_XY) .setFadeDuration(0) .build(); view.setHierarchy(hierarchy); } DraweeController controller = Fresco.newDraweeControllerBuilder() .setUri(url) .setControllerListener(new NormalControllerListener(loadListener)) .setOldController(view.getController()) .setAutoPlayAnimations(false) .build(); view.setController(controller); }
...
private boolean isGifUrl(String url) { return url.toLowerCase().endsWith(".gif"); }
private void loadGifWithOptions(IMarketView viewParent, String url, OnImageLoadListener loadListener) { SimpleDraweeView view = viewParent.getFrescoDraweeView(); if (view == null) return; String uniqueTag = viewParent.getImageUniqueTag(); ... DraweeController controller = Fresco.newDraweeControllerBuilder() .setUri(url) .setControllerListener(newControllerListener) .setOldController(view.getController()) .setAutoPlayAnimations(false) .build(); view.setController(controller); } }
|
因为Fresco使用的是自定义的 View,所以这里的 Fragment 并没有使用到
总结
其实这样封装是带来了一些便利性,但是也为每一个要加载图片的 View 套上一层 FrameLayout,这样就会有一定的性能损耗,但是考虑到后期可能会在图片的上显示像加载进度等一些额外 View,到时候也是要进行这样的封装。