Android Picasso 显示模块深度剖析
本人掘金号,欢迎点击关注:掘金号地址
本人公众号,欢迎点击关注:公众号地址
一、引言
在 Android 开发领域,图片的高效显示是提升用户体验的关键环节之一。Picasso 作为一款广受欢迎的图片加载库,以其简洁的 API 和出色的性能在开发者群体中得到了广泛应用。其中,显示模块负责将解码后的图片正确、高效地展示在 Android 界面上,是整个图片加载流程中的重要一环。本文将深入剖析 Picasso 的显示模块,从源码层面详细解读其工作原理和实现细节。
二、显示模块概述
2.1 模块功能
Picasso 的显示模块主要承担将解码后的 Bitmap
对象显示到 ImageView
或其他可显示视图上的任务。它需要处理图片的尺寸适配、缩放、裁剪等操作,确保图片能够以最佳的效果展示在界面上。同时,该模块还需要处理图片加载过程中的状态管理,如加载中、加载成功、加载失败等状态的显示。
2.2 主要类和接口
在显示模块中,有几个核心的类和接口起着关键作用:
- ImageViewAction :负责将
Bitmap
显示到ImageView
上的具体操作类。 - Target:一个接口,用于定义图片加载完成后的回调处理逻辑,开发者可以通过实现该接口来自定义图片显示的方式。
- BitmapHunter:虽然主要负责图片的解码任务,但在解码完成后会将结果传递给显示模块,与显示模块有紧密的交互。
三、ImageViewAction 类分析
3.1 类的定义和基本属性
java
// ImageViewAction 类继承自 Action<ImageView>,用于处理将 Bitmap 显示到 ImageView 的操作
class ImageViewAction extends Action<ImageView> {
// 用于标记是否需要在图片加载完成后设置动画效果
private final boolean noFade;
// 用于存储占位图的资源 ID
private final int placeholderResId;
// 用于存储占位图的 Drawable 对象
private final Drawable placeholderDrawable;
// 用于存储错误图的资源 ID
private final int errorResId;
// 用于存储错误图的 Drawable 对象
private final Drawable errorDrawable;
// 构造函数,初始化各种属性
ImageViewAction(Picasso picasso, ImageView target, Request data, int placeholderResId,
Drawable placeholderDrawable, int errorResId, Drawable errorDrawable,
String key, Object tag, boolean noFade) {
// 调用父类的构造函数,传入 Picasso 实例、目标 ImageView、请求数据、键和标签
super(picasso, target, data, key, tag);
this.noFade = noFade;
this.placeholderResId = placeholderResId;
this.placeholderDrawable = placeholderDrawable;
this.errorResId = errorResId;
this.errorDrawable = errorDrawable;
}
}
ImageViewAction
类继承自 Action<ImageView>
,它封装了将 Bitmap
显示到 ImageView
上所需的各种信息,包括占位图、错误图以及是否需要动画效果等。
3.2 显示占位图
java
// 显示占位图的方法
@Override
void onPrepareLoad(Drawable placeHolderDrawable) {
ImageView target = getTarget();
if (target == null) {
return;
}
// 如果有占位图的 Drawable 对象,直接设置给 ImageView
if (placeholderDrawable != null) {
target.setImageDrawable(placeholderDrawable);
} else if (placeholderResId != 0) {
// 如果有占位图的资源 ID,通过资源 ID 获取 Drawable 并设置给 ImageView
target.setImageResource(placeholderResId);
} else {
// 如果没有占位图,将 ImageView 的图片设置为 null
target.setImageDrawable(null);
}
}
onPrepareLoad
方法在图片开始加载时被调用,用于显示占位图。它会根据占位图的资源 ID 或 Drawable
对象,将占位图显示在 ImageView
上。如果没有占位图,则将 ImageView
的图片设置为 null
。
3.3 显示解码后的 Bitmap
java
// 显示解码后的 Bitmap 的方法
@Override
void complete(Bitmap result, Picasso.LoadedFrom from) {
if (result == null) {
throw new AssertionError(
String.format("Attempted to complete action with no result!\n%s", this));
}
ImageView target = getTarget();
if (target == null) {
return;
}
Context context = picasso.context;
boolean fade = picasso.fadeDuration > 0 && from != Picasso.LoadedFrom.MEMORY &&!noFade;
if (fade) {
// 如果需要淡入动画,设置淡入动画并显示 Bitmap
TransitionDrawable transition =
new TransitionDrawable(new Drawable[]{
getPlaceholderDrawable(context),
new BitmapDrawable(context.getResources(), result)
});
target.setImageDrawable(transition);
transition.startTransition(picasso.fadeDuration);
} else {
// 如果不需要淡入动画,直接显示 Bitmap
target.setImageBitmap(result);
}
// 通知 Picasso 该操作已完成
picasso.cancelTag(target);
}
complete
方法在图片解码完成后被调用,用于显示解码后的 Bitmap
。如果需要淡入动画(即 fade
为 true
),会创建一个 TransitionDrawable
对象,实现从占位图到最终图片的淡入效果;如果不需要淡入动画,则直接将 Bitmap
设置给 ImageView
。最后,通知 Picasso 该操作已完成。
3.4 显示错误图
java
// 显示错误图的方法
@Override
void error(Exception e) {
ImageView target = getTarget();
if (target == null) {
return;
}
// 如果有错误图的 Drawable 对象,直接设置给 ImageView
if (errorDrawable != null) {
target.setImageDrawable(errorDrawable);
} else if (errorResId != 0) {
// 如果有错误图的资源 ID,通过资源 ID 获取 Drawable 并设置给 ImageView
target.setImageResource(errorResId);
} else {
// 如果没有错误图,将 ImageView 的图片设置为 null
target.setImageDrawable(null);
}
}
error
方法在图片加载失败时被调用,用于显示错误图。它会根据错误图的资源 ID 或 Drawable
对象,将错误图显示在 ImageView
上。如果没有错误图,则将 ImageView
的图片设置为 null
。
四、Target 接口分析
4.1 接口定义
java
// Target 接口定义了图片加载完成后的回调处理逻辑
public interface Target {
// 当图片加载成功时调用,传入解码后的 Bitmap 和图片来源
void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from);
// 当图片加载失败时调用,传入错误信息的 Drawable
void onBitmapFailed(Exception e, Drawable errorDrawable);
// 当图片开始加载时调用,传入占位图的 Drawable
void onPrepareLoad(Drawable placeHolderDrawable);
}
Target
接口定义了三个方法,分别用于处理图片加载成功、加载失败和开始加载的情况。开发者可以通过实现该接口来自定义图片显示的方式。
4.2 自定义 Target 示例
java
// 自定义的 Target 实现类
public class CustomTarget implements Target {
private ImageView imageView;
// 构造函数,传入要显示图片的 ImageView
public CustomTarget(ImageView imageView) {
this.imageView = imageView;
}
// 当图片加载成功时调用
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
if (imageView != null) {
// 将解码后的 Bitmap 设置给 ImageView
imageView.setImageBitmap(bitmap);
}
}
// 当图片加载失败时调用
@Override
public void onBitmapFailed(Exception e, Drawable errorDrawable) {
if (imageView != null) {
// 将错误图设置给 ImageView
imageView.setImageDrawable(errorDrawable);
}
}
// 当图片开始加载时调用
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
if (imageView != null) {
// 将占位图设置给 ImageView
imageView.setImageDrawable(placeHolderDrawable);
}
}
}
在这个自定义的 Target
实现类中,我们重写了 onBitmapLoaded
、onBitmapFailed
和 onPrepareLoad
方法,分别处理图片加载成功、加载失败和开始加载的情况。在这些方法中,我们将相应的图片或信息显示在传入的 ImageView
上。
五、BitmapHunter 与显示模块的交互
5.1 解码完成后的回调
java
// BitmapHunter 类中的部分代码
@Override
public void run() {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
try {
Bitmap bitmap = hunt();
if (bitmap != null) {
// 解码成功,调用 dispatcher 的 dispatchComplete 方法通知显示模块
dispatcher.dispatchComplete(this);
} else {
// 解码失败,调用 dispatcher 的 dispatchFailed 方法通知显示模块
dispatcher.dispatchFailed(this);
}
} catch (Exception e) {
// 发生异常,调用 dispatcher 的 dispatchFailed 方法通知显示模块
dispatcher.dispatchFailed(this);
}
}
在 BitmapHunter
的 run
方法中,当图片解码完成后,会根据解码结果调用 dispatcher
的 dispatchComplete
或 dispatchFailed
方法,将结果传递给显示模块。
5.2 Dispatcher 中的处理
java
// Dispatcher 类中的 dispatchComplete 方法
void dispatchComplete(BitmapHunter hunter) {
handler.sendMessage(handler.obtainMessage(COMPLETE, hunter));
}
// Dispatcher 类中的 dispatchFailed 方法
void dispatchFailed(BitmapHunter hunter) {
handler.sendMessage(handler.obtainMessage(FAILED, hunter));
}
// Dispatcher 类中的 Handler 处理逻辑
private final Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case COMPLETE: {
BitmapHunter hunter = (BitmapHunter) msg.obj;
// 调用 hunter 的 complete 方法,触发显示模块的显示逻辑
hunter.complete();
break;
}
case FAILED: {
BitmapHunter hunter = (BitmapHunter) msg.obj;
// 调用 hunter 的 error 方法,触发显示模块的错误处理逻辑
hunter.error();
break;
}
}
}
};
Dispatcher
类负责接收 BitmapHunter
的解码结果,并通过 Handler
将消息发送到主线程进行处理。在 Handler
的 handleMessage
方法中,根据消息类型调用 BitmapHunter
的 complete
或 error
方法,从而触发显示模块的显示逻辑或错误处理逻辑。
六、图片尺寸适配与缩放
6.1 计算目标尺寸
java
// Request 类中的部分代码,用于计算目标尺寸
private static int getTargetDimen(int viewDimen, int fallback, int requested) {
if (requested > 0) {
return requested;
}
if (viewDimen > 0) {
return viewDimen;
}
return fallback;
}
// 在计算宽度和高度时调用
int targetWidth = getTargetDimen(targetWidth, targetWidthFallback, data.targetWidth);
int targetHeight = getTargetDimen(targetHeight, targetHeightFallback, data.targetHeight);
在 Request
类中,getTargetDimen
方法用于计算目标尺寸。它会优先使用请求中指定的尺寸,如果请求中没有指定,则使用视图的实际尺寸,如果视图尺寸也不可用,则使用默认的备用尺寸。
6.2 缩放处理
java
// BitmapFactory.Options 中的采样率计算部分
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 原始图片的高度
final int height = options.outHeight;
// 原始图片的宽度
final int width = options.outWidth;
// 初始采样率为 1
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
// 计算高度和宽度的缩放比例
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// 循环计算采样率,直到满足目标尺寸要求
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
// 在解码 Bitmap 时设置采样率
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calculateInSampleSize(options, targetWidth, targetHeight);
Bitmap bitmap = BitmapFactory.decodeStream(is, null, options);
在解码 Bitmap
时,会根据目标尺寸计算采样率。calculateInSampleSize
方法会根据原始图片的尺寸和目标尺寸,计算出合适的采样率,从而实现图片的缩放。在解码时,将计算得到的采样率设置到 BitmapFactory.Options
中,以减少内存占用并实现尺寸适配。
七、显示模块的工作流程
7.1 请求发起
当开发者调用 Picasso 的 load
方法并指定目标 ImageView
或 Target
时,会创建一个 Request
对象,该对象包含了图片的 URI、目标尺寸、占位图、错误图等信息。同时,会创建一个 ImageViewAction
或 TargetAction
对象,用于处理图片的显示操作。
7.2 显示占位图
在图片开始加载时,ImageViewAction
或 TargetAction
的 onPrepareLoad
方法会被调用,根据占位图的资源 ID 或 Drawable
对象,将占位图显示在 ImageView
或 Target
上。
7.3 图片解码
BitmapHunter
会根据 Request
对象中的信息,从网络或本地缓存中获取图片数据,并进行解码操作。解码完成后,会将结果传递给 Dispatcher
。
7.4 显示解码后的 Bitmap
Dispatcher
接收到解码结果后,会将消息发送到主线程。在主线程中,BitmapHunter
的 complete
方法会被调用,ImageViewAction
或 TargetAction
的 complete
方法会根据是否需要淡入动画,将解码后的 Bitmap
显示在 ImageView
或 Target
上。
7.5 错误处理
如果图片加载失败,Dispatcher
会调用 BitmapHunter
的 error
方法,ImageViewAction
或 TargetAction
的 error
方法会根据错误图的资源 ID 或 Drawable
对象,将错误图显示在 ImageView
或 Target
上。
八、显示模块的优化策略
8.1 内存优化
- 采样率调整 :通过合理计算采样率,减少解码后的
Bitmap
尺寸,从而降低内存占用。如前面提到的calculateInSampleSize
方法,根据目标尺寸动态调整采样率。
java
// 计算采样率的方法
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 原始图片的高度
final int height = options.outHeight;
// 原始图片的宽度
final int width = options.outWidth;
// 初始采样率为 1
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
// 计算高度和宽度的缩放比例
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// 循环计算采样率,直到满足目标尺寸要求
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
- 及时回收 Bitmap :在
Bitmap
不再使用时,及时调用recycle
方法回收内存。虽然 Picasso 内部会对Bitmap
进行一定的管理,但在某些情况下,开发者也可以手动调用该方法。
java
if (bitmap != null &&!bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
}
8.2 性能优化
- 异步加载:Picasso 的显示模块采用异步加载的方式,将图片的解码和显示操作放在后台线程中进行,避免阻塞主线程,从而提高应用的响应性能。
java
// BitmapHunter 类中的 run 方法,在后台线程中执行解码操作
@Override
public void run() {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
try {
Bitmap bitmap = hunt();
if (bitmap != null) {
// 解码成功,调用 dispatcher 的 dispatchComplete 方法通知显示模块
dispatcher.dispatchComplete(this);
} else {
// 解码失败,调用 dispatcher 的 dispatchFailed 方法通知显示模块
dispatcher.dispatchFailed(this);
}
} catch (Exception e) {
// 发生异常,调用 dispatcher 的 dispatchFailed 方法通知显示模块
dispatcher.dispatchFailed(this);
}
}
- 缓存机制 :Picasso 使用内存缓存和磁盘缓存来存储已解码的
Bitmap
,避免重复的解码操作,提高图片加载的速度。在BitmapHunter
的hunt
方法中,会首先检查缓存中是否存在请求的Bitmap
,如果存在则直接返回。
java
// BitmapHunter 类中的 hunt 方法,检查缓存
private Bitmap hunt() throws IOException {
Bitmap bitmap = cache.get(data.key);
if (bitmap != null) {
stats.dispatchCacheHit();
return bitmap;
}
// 缓存中不存在,进行解码操作
// ...
}
8.3 视觉优化
- 淡入动画 :通过设置淡入动画,使图片的显示更加平滑,提升用户体验。在
ImageViewAction
的complete
方法中,如果需要淡入动画,会创建一个TransitionDrawable
对象,实现从占位图到最终图片的淡入效果。
java
// ImageViewAction 类中的 complete 方法,处理淡入动画
boolean fade = picasso.fadeDuration > 0 && from != Picasso.LoadedFrom.MEMORY &&!noFade;
if (fade) {
TransitionDrawable transition =
new TransitionDrawable(new Drawable[]{
getPlaceholderDrawable(context),
new BitmapDrawable(context.getResources(), result)
});
target.setImageDrawable(transition);
transition.startTransition(picasso.fadeDuration);
} else {
target.setImageBitmap(result);
}
- 占位图和错误图设计:合理设计占位图和错误图,使其与应用的整体风格一致,并且能够提供清晰的视觉反馈,让用户了解图片加载的状态。
九、与其他图片显示库的对比
9.1 与 Glide 的对比
9.1.1 功能特点
- Picasso:API 简洁,易于使用,专注于基本的图片加载和显示功能。提供了简单的链式调用方式,方便开发者快速实现图片显示。
- Glide:功能更加丰富,除了基本的图片加载和显示外,还支持 GIF 动画、视频帧提取等功能。提供了更多的配置选项,如内存缓存策略、磁盘缓存策略等。
9.1.2 性能表现
- Picasso:在内存管理方面相对较为保守,通过采样率调整等方式控制内存占用。但在处理大量高分辨率图片时,可能会出现内存压力较大的情况。
- Glide:具有更高效的内存管理机制,能够根据设备的内存情况自动调整缓存策略,减少内存溢出的风险。在加载大尺寸图片和 GIF 动画时,性能表现较好。
9.1.3 使用场景
- Picasso:适用于对图片显示功能要求不高,追求简洁 API 的场景。例如,一些简单的应用中只需要实现基本的图片展示功能。
- Glide:适用于对图片显示功能要求较高,需要处理复杂图片类型(如 GIF 动画)的场景。例如,社交应用、视频应用等。
9.2 与 Fresco 的对比
9.2.1 功能特点
- Picasso:使用简单,集成方便,对开发者的技术要求较低。主要关注图片的加载和显示,提供了基本的尺寸适配和缩放功能。
- Fresco:提供了强大的图片管理功能,如渐进式加载、图片缩放、图片裁剪等。使用了独立的内存管理机制,能够有效避免内存溢出问题。
9.2.2 性能表现
- Picasso:在性能方面表现稳定,但在处理大尺寸图片和复杂图片操作时,性能可能不如 Fresco。
- Fresco:在处理大尺寸图片和复杂图片操作时,性能优势明显。它的内存管理机制和图片解码算法经过优化,能够快速加载和显示图片。
9.2.3 使用场景
- Picasso:适用于小型项目或对性能要求不是特别高的场景。
- Fresco:适用于对图片质量和性能要求较高的大型项目,如电商应用、图库应用等。
十、总结与展望
10.1 总结
通过对 Android Picasso 显示模块的深入分析,我们了解到该模块是整个图片加载流程中的重要环节,负责将解码后的 Bitmap
正确、高效地显示在界面上。它通过 ImageViewAction
和 Target
接口实现了图片的显示逻辑,与 BitmapHunter
和 Dispatcher
紧密协作,完成了图片加载状态的管理。同时,该模块采用了多种优化策略,如内存优化、性能优化和视觉优化,以提高图片显示的质量和用户体验。与其他图片显示库相比,Picasso 具有 API 简洁、易于使用的特点,适用于对图片显示功能要求不高的场景。
10.2 展望
未来,Android Picasso 的显示模块可以在以下几个方面进行改进和扩展:
- 支持更多的视图类型 :除了
ImageView
,可以考虑支持更多的视图类型,如SurfaceView
、TextureView
等,以满足不同场景下的图片显示需求。 - 增强视觉效果:提供更多的视觉效果,如旋转、翻转、模糊等,让图片的显示更加丰富多样。
- 优化动画效果:进一步优化淡入动画等效果,使其更加流畅和自然,提升用户体验。
- 集成更多的图片处理功能:结合更多的图片处理算法,如图片压缩、图片修复等,提供更全面的图片处理能力。
- 提高性能和兼容性:不断优化性能,提高在不同 Android 版本和设备上的兼容性,确保图片显示的稳定性和可靠性。
总之,Android Picasso 的显示模块是一个功能强大、灵活可扩展的模块,通过不断的改进和优化,将为 Android 开发者提供更好的图片显示解决方案。