Android Picasso 显示模块深度剖析(六)

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。如果需要淡入动画(即 fadetrue),会创建一个 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 实现类中,我们重写了 onBitmapLoadedonBitmapFailedonPrepareLoad 方法,分别处理图片加载成功、加载失败和开始加载的情况。在这些方法中,我们将相应的图片或信息显示在传入的 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);
    }
}

BitmapHunterrun 方法中,当图片解码完成后,会根据解码结果调用 dispatcherdispatchCompletedispatchFailed 方法,将结果传递给显示模块。

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 将消息发送到主线程进行处理。在 HandlerhandleMessage 方法中,根据消息类型调用 BitmapHuntercompleteerror 方法,从而触发显示模块的显示逻辑或错误处理逻辑。

六、图片尺寸适配与缩放

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 方法并指定目标 ImageViewTarget 时,会创建一个 Request 对象,该对象包含了图片的 URI、目标尺寸、占位图、错误图等信息。同时,会创建一个 ImageViewActionTargetAction 对象,用于处理图片的显示操作。

7.2 显示占位图

在图片开始加载时,ImageViewActionTargetActiononPrepareLoad 方法会被调用,根据占位图的资源 ID 或 Drawable 对象,将占位图显示在 ImageViewTarget 上。

7.3 图片解码

BitmapHunter 会根据 Request 对象中的信息,从网络或本地缓存中获取图片数据,并进行解码操作。解码完成后,会将结果传递给 Dispatcher

7.4 显示解码后的 Bitmap

Dispatcher 接收到解码结果后,会将消息发送到主线程。在主线程中,BitmapHuntercomplete 方法会被调用,ImageViewActionTargetActioncomplete 方法会根据是否需要淡入动画,将解码后的 Bitmap 显示在 ImageViewTarget 上。

7.5 错误处理

如果图片加载失败,Dispatcher 会调用 BitmapHuntererror 方法,ImageViewActionTargetActionerror 方法会根据错误图的资源 ID 或 Drawable 对象,将错误图显示在 ImageViewTarget 上。

八、显示模块的优化策略

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,避免重复的解码操作,提高图片加载的速度。在 BitmapHunterhunt 方法中,会首先检查缓存中是否存在请求的 Bitmap,如果存在则直接返回。
java 复制代码
// BitmapHunter 类中的 hunt 方法,检查缓存
private Bitmap hunt() throws IOException {
    Bitmap bitmap = cache.get(data.key);
    if (bitmap != null) {
        stats.dispatchCacheHit();
        return bitmap;
    }
    // 缓存中不存在,进行解码操作
    // ...
}

8.3 视觉优化

  • 淡入动画 :通过设置淡入动画,使图片的显示更加平滑,提升用户体验。在 ImageViewActioncomplete 方法中,如果需要淡入动画,会创建一个 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 正确、高效地显示在界面上。它通过 ImageViewActionTarget 接口实现了图片的显示逻辑,与 BitmapHunterDispatcher 紧密协作,完成了图片加载状态的管理。同时,该模块采用了多种优化策略,如内存优化、性能优化和视觉优化,以提高图片显示的质量和用户体验。与其他图片显示库相比,Picasso 具有 API 简洁、易于使用的特点,适用于对图片显示功能要求不高的场景。

10.2 展望

未来,Android Picasso 的显示模块可以在以下几个方面进行改进和扩展:

  • 支持更多的视图类型 :除了 ImageView,可以考虑支持更多的视图类型,如 SurfaceViewTextureView 等,以满足不同场景下的图片显示需求。
  • 增强视觉效果:提供更多的视觉效果,如旋转、翻转、模糊等,让图片的显示更加丰富多样。
  • 优化动画效果:进一步优化淡入动画等效果,使其更加流畅和自然,提升用户体验。
  • 集成更多的图片处理功能:结合更多的图片处理算法,如图片压缩、图片修复等,提供更全面的图片处理能力。
  • 提高性能和兼容性:不断优化性能,提高在不同 Android 版本和设备上的兼容性,确保图片显示的稳定性和可靠性。

总之,Android Picasso 的显示模块是一个功能强大、灵活可扩展的模块,通过不断的改进和优化,将为 Android 开发者提供更好的图片显示解决方案。

相关推荐
teacher伟大光荣且正确5 小时前
Qt Creator 配置 Android 编译环境
android·开发语言·qt
飞猿_SIR7 小时前
Android Exoplayer 实现多个音视频文件混合播放以及音轨切换
android·音视频
HumoChen998 小时前
GZip+Base64压缩字符串在ios上解压报错问题解决(安卓、PC模拟器正常)
android·小程序·uniapp·base64·gzip
沙振宇11 小时前
【HarmonyOS】ArkTS开发应用的横竖屏切换
android·华为·harmonyos
橙子1991101613 小时前
Kotlin 中的作用域函数
android·开发语言·kotlin
zimoyin13 小时前
Kotlin 懒初始化值
android·开发语言·kotlin
chenyuhao202413 小时前
链表的面试题4之合并有序链表
数据结构·链表·面试·c#
枣伊吕波14 小时前
第六节第二部分:抽象类的应用-模板方法设计模式
android·java·设计模式
萧然CS14 小时前
使用ADB命令操作Android的apk/aab包
android·adb
PgSheep17 小时前
深入理解 JVM:StackOverFlow、OOM 与 GC overhead limit exceeded 的本质剖析及 Stack 与 Heap 的差异
jvm·面试