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 开发者提供更好的图片显示解决方案。

相关推荐
zone773910 分钟前
002:RAG 入门-LangChain 读取文本
后端·算法·面试
青青家的小灰灰11 分钟前
从入门到精通:Vue3 ref vs reactive 最佳实践与底层原理
前端·vue.js·面试
over6971 小时前
从 URL 输入到页面展示:一次完整的 Web 导航之旅
前端·面试·架构
飞哥的AI笔记1 小时前
为什么 OpenClaw 在实时推送场景下选择拥抱 WebSocket?
面试
SuperEugene1 小时前
Vue状态管理扫盲篇:状态管理中的常见坑 | 循环依赖、状态污染与调试技巧
前端·vue.js·面试
Kapaseker3 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴3 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
哈里谢顿3 小时前
0304面试kimi总结归纳版
面试
哈里谢顿3 小时前
0304面试千问总结归纳版
面试
恋猫de小郭13 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter