Szhangbiao's blog

记录一些让自己可以回忆的东西

0%

图片加载框架封装(Fresco和Glide)

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,到时候也是要进行这样的封装。