鸿蒙系列-如何更好地使用 ArkUI 的 Image 组件?

news2024/12/23 13:57:26

如何使用好 ArkUI 的 Image 组件?

开发者经常需要在应用中显示一些图片,例如:按钮中的logo、网络图片、本地图片等。在应用中显示图片需要使用 Image 组件实现,Image支持多种图片格式,包括png、jpg、bmp、svg和gif,具体用法请参考Image组件。

ArkUI 的 Image组件类比SwiftUI中的Image,也就是UIKit中的UIImageView。

本文主要对 Image 如何展示图像做一些解读,然后对 Image组件一些特殊属性做分析,进而帮助开发者理解设置前后的效果,以及我们会尝试寻找何时使用这个属性最佳,进而给开发者提出参考方案。

一、整理潜在的优化点:

Image组件将图片显示到屏幕上分为三步:加载、解码、渲染:

在这里插入图片描述

一般情况下,我们直接调用组件操作的是加载这个步骤。因此,从渲染过程分析,考虑以下几个方面可能可以提升性能。

· 异步下载图片。

一般情况下,图片加载流程会异步进行,以避免阻塞主线程,影响UI交互。但是特定情况下,图片刷新时会出现闪烁,这时可以使用syncLoad属性,使图片同步加载,从而避免出现闪烁。

· 将image解码放到子线程。

以Image组件为例。当其显示在屏幕上时,需要Image作为数据源。 Image持有的数据是没有解码的压缩数据,能节省较多的内存和加快存储。 当image被赋值给Image时,图像数据会被解码,变成RGB的颜色数据。 解码是一个计算量较大的任务,且需要CPU来执行。

解码出来的图片体积与图片的宽高有关,与图片原来的体积无关。

图片解码是耗时操作,如果图片非常大,建议放到子线程解码。

在这里插入图片描述

图片解码可能会产生什么问题?

在上下滑动展示图片的过程中,我们会在lazyforeach的方法加载Image图片,相当于在主线程同时进行IO操作、解码等操作。这会造成内存迅速增长和CPU负载瞬间提升。 并且内存的迅速增加会触发系统的内存回收机制,尝试回收其他后台进程的内存,增加CPU的工作量。

如果系统无法提供足够的内存,则会先结束后台app进程,同时造成UI卡顿。

· 使用合适尺寸的图片,减少为了适配屏幕产生的bitmap计算。

使用图片资源管理工具,存储不同分辨率的图片,在不同分辨率的设备使用最适合的尺寸。如果图片是网络获取,可以通过传参的类型告诉服务端,服务端根据设备类型返回最合适尺寸的图片。

· 使用缓存 (内存缓存和磁盘缓存)

详细分析见下文《图片缓存》章节。

· 直接存储压缩后的图片,避免下次从使用缓存的时候再次解压缩。

· 减少内存级别的拷贝。(采用字节对齐)

在图像渲染时是通过一块一块渲染,因此数据是一块块地取,如果一块连续的内存数据里结尾的数据不是图像的内容,是内存里其他的数据,会影响读取效率。
块的大小和CPU cache有关,64位系统按64byte作为一块数据去读取和渲染,让图像数据对齐64byte就可以避免图形管理器再拷贝一份数据进行修补。

· 图片预下载。

提前将需要的图片下载到本地,并在CPU空闲的时候解压缩。

二、对 syncLoad 加载原理分析

我们发现聊天列表头像图片很小,加载很快,根据官方文档指示:在加载图片的耗时比较短的时候,通过异步加载的效果会大打折扣,建议配置 Image.syncLoad属性。

Image($r('app.media.icon'))
  .syncLoad(true)

这个修改虽然很简单,按字面意思就是设置 同步/异步 加载,但我们想弄清楚的是什么时候使用这个特性才是合理的?

为了彻底搞明白,我们尝试阅读实现代码,从代码上看设置了这个值对组件有什么影响。

分析结果

结论先行,通过下面的分析我们可以得知,设置.syncLoad(true)这个值,产生的影响是:创建图片时是否创建一个异步任务,是否使用互斥锁。

而我们知道,创建异步任务和使用互斥锁也是有开销的,进而会影响内存和性能。

在这里插入图片描述

分析过程

· 由于我们使用的是Image组件,Image组件属于ArkUI,所以我们找到ArkUI的代码仓下载代码:https://gitee.com/openharmony/arkui_ace_engine

· 打开下载后的工程,找到对应Image的目录:

在这里插入图片描述

设置初始值

· 查看 image_pattern.h中属性的定义,其中省略了无关的代码:

namespace OHOS::Ace::NG {

class ACE_EXPORT ImagePattern : public Pattern, public SelectionHost {
    DECLARE_ACE_TYPE(ImagePattern, Pattern, SelectionHost);

public:
    ...

private:
    ...

    bool syncLoad_ = false;
    bool isShow_ = true;

    ACE_DISALLOW_COPY_AND_MOVE(ImagePattern);
};

} // namespace OHOS::Ace::NG

#endif // FOUNDATION_ACE_FRAMEWORKS_CORE_COMPONENTS_NG_PATTERNS_IMAGE_IMAGE_PATTERN_H

通过 bool syncLoad_ = false; 我们知道了syncLoad的默认属性是false,如果不设置,图片加载就是异步的。

LoadImageDataIfNeed()

· 查看 image_pattern.cpp中的实现,其中省略了无关的代码:

void ImagePattern::ToJsonValue(std::unique_ptr<JsonValue>& json) const
{
    ...
    json->Put("syncLoad", syncLoad_ ? "true" : "false");
    ...
}

ToJsonValue这个方法通过将sync属性转换成json值,和我们目的无关,不需要细看。

找到另一个实现方法 LoadImageDataIfNeed 和图片加载强相关,我们粗略看一下整段代码:

void ImagePattern::LoadImageDataIfNeed()
{
	// 获得图片布局属性
    auto imageLayoutProperty = GetLayoutProperty<ImageLayoutProperty>();
    CHECK_NULL_VOID(imageLayoutProperty);
    // 获得图片绘制属性
    auto imageRenderProperty = GetPaintProperty<ImageRenderProperty>();
    CHECK_NULL_VOID(imageRenderProperty);
    auto src = imageLayoutProperty->GetImageSourceInfo().value_or(ImageSourceInfo(""));
    UpdateInternalResource(src);

    if (!loadingCtx_ || loadingCtx_->GetSourceInfo() != src) {
        LoadNotifier loadNotifier(CreateDataReadyCallback(), CreateLoadSuccessCallback(), CreateLoadFailCallback());

        loadingCtx_ = AceType::MakeRefPtr<ImageLoadingContext>(src, std::move(loadNotifier), syncLoad_);
        LOGI("start loading image %{public}s", src.ToString().c_str());
        loadingCtx_->LoadImageData();
    }
    if (loadingCtx_->NeedAlt() && imageLayoutProperty->GetAlt()) {
        auto altImageSourceInfo = imageLayoutProperty->GetAlt().value_or(ImageSourceInfo(""));
        LoadNotifier altLoadNotifier(CreateDataReadyCallbackForAlt(), CreateLoadSuccessCallbackForAlt(), nullptr);
        if (!altLoadingCtx_ || altLoadingCtx_->GetSourceInfo() != altImageSourceInfo ||
            (altLoadingCtx_ && altImageSourceInfo.IsSvg())) {
            altLoadingCtx_ = AceType::MakeRefPtr<ImageLoadingContext>(altImageSourceInfo, std::move(altLoadNotifier));
            altLoadingCtx_->LoadImageData();
        }
    }
}

其中重点部分:如果 loadingCtx_ 不存在 或者 loadingCtx_ 的图片地址和当前不一致时就会创建一个 RefPtr

	// 判断条件:如果 loadingCtx_ 不存在 或者 loadingCtx_ 的图片地址和当前不一致时
    if (!loadingCtx_ || loadingCtx_->GetSourceInfo() != src) {
   	//  创建一个 loadingCtx_, syncLoad_ 是其中一个属性
        loadingCtx_ = AceType::MakeRefPtr<ImageLoadingContext>(src, std::move(loadNotifier), syncLoad_);
        loadingCtx_->LoadImageData();
    }

RefPtr

那么 loadingCtx_ 是一个什么东西呢?通过查看定义文件 image_pattern.h发现:

    RefPtr<ImageLoadingContext> loadingCtx_;

loadingCtx_ 是一个 RefPtr 类型的指针。

我们也可以在源码memery 下的 referenced.h 中找到 RefPtr 的定义,由于对我们分析图片加载影响不大,简单看一下,可以得知:

  1. RefPtr 使用引用计数管理实例
  2. 由于在一些场景需要隐式转换,所以在构造函数中移除了 explicit(explicit指定构造函数或转换函数 (C++11起)为显式, 即它不能用于隐式转换和复制初始化)
template<class T>
class RefPtr final {
	public:
	...
    private:
    ...
    explicit RefPtr(T* rawPtr, bool forceIncRef = true) : rawPtr_(rawPtr)
    {
        if (rawPtr_ != nullptr && forceIncRef) {
            // Increase strong reference count for holding instance.
            rawPtr_->IncRefCount();
        }
    }
	...
};

AceType::MakeRefPtr

再回到之前的调用代码 loadingCtx_ = AceType::MakeRefPtr<ImageLoadingContext>(src, std::move(loadNotifier), syncLoad_);,主要关注下使用到的 MakeRefPtr 函数,可以得知:

  1. Referenced::MakeRefPtr 是用于创建新实例的,而这个创建的新实例是继承于 Referenced的。
  2. 使用 RefPtr 管理指针。
template<class T, class... Args>
static RefPtr<T> MakeRefPtr(Args&&... args)
{
    return Claim(new T(std::forward<Args>(args)...));
}

我们发现 MakeRefPtr 这个函数核心是调用 Claim函数,所以我们需要找到 Claim函数:

template<class T>
static RefPtr<T> Claim(T* rawPtr)
{
    if (MemoryMonitor::IsEnable()) {
        MemoryMonitor::GetInstance().Update(rawPtr, static_cast<Referenced*>(rawPtr));
    }
    return RefPtr<T>(rawPtr);
}

通过代码可以得知,Claim通过内存监控管理器用 原始指针构建 RefPtr,而 syncLoad_ 是作为一个 std::forward<Args>(args)...)的一个参数被管理起来。

ImageLoadingContext()

· 知道了 syncLoad_ 是怎么被管理的之后,我们再看 syncLoad_ 怎么用就更容易理解了。
通过之前的指针类型定义 RefPtr<ImageLoadingContext> loadingCtx_;,我们可以找到 ImageLoadingContext 这个类,在cpp实现中找到了这个方法 OnDataLoading()

void ImageLoadingContext::OnDataLoading()
{
    if (auto obj = ImageProvider::QueryImageObjectFromCache(src_); obj) {
        DataReadyCallback(obj);
        return;
    }
    ImageProvider::CreateImageObject(src_, WeakClaim(this), syncLoad_);
}

可以发现是CreateImageObject() 这个方法创建了图片对象,并且使用了 syncLoad_ 这个参数作为创建时的初始值参数。所以我们再次在 image_provider.cpp这个文件中找到 CreateImageObject() 这个方法:

这个方法是重点,所以完整展示代码,并添加一些注释

CreateImageObject()

void ImageProvider::CreateImageObject(const ImageSourceInfo& src, const WeakPtr<ImageLoadingContext>& ctx, bool sync)
{
    if (!RegisterTask(src.GetKey(), ctx)) {
        // 如果任务已经在跑了,直接返回
        return;
    }
    if (sync) {
    	// 如果是同步的,直接调用helper类创建
        CreateImageObjHelper(src, true);
    } else {
    	// 如果是异步的,使用了一个互斥锁
        std::scoped_lock<std::mutex> lock(taskMtx_);
        // 创建一个可取消的任务
        CancelableCallback<void()> task;
        // 以src作为唯一键值绑定任务
        task.Reset([src] { ImageProvider::CreateImageObjHelper(src); });
        tasks_[src.GetKey()].bgTask_ = task;
        // 放到后台去执行任务
        ImageUtils::PostToBg(task);
    }
}

image_utils.cpp

void ImageUtils::PostToBg(std::function<void()>&& task)
{
    CHECK_NULL_VOID(task);
    ImageUtils::PostTask(std::move(task), TaskExecutor::TaskType::BACKGROUND, "BACKGROUND");
}
void ImageUtils::PostTask(
    std::function<void()>&& task, TaskExecutor::TaskType taskType, const char* taskTypeName)
{
    auto taskExecutor = Container::CurrentTaskExecutor();
    if (!taskExecutor) {
        LOGE("taskExecutor is null when try post task to %{public}s", taskTypeName);
        return;
    }
    taskExecutor->PostTask(
        [task, id = Container::CurrentId()] {
            ContainerScope scope(id);
            CHECK_NULL_VOID(task);
            task();
        },
        taskType);
}
/**
 * Post a task to the specified thread.
 *
 * @param task Task which need execution.
 * @param type FrontendType of task, used to specify the thread.
 * @return Returns 'true' whether task has been post successfully.
 */
bool PostTask(Task&& task, TaskType type) const
{
    return PostDelayedTask(std::move(task), type, 0);
}

mock_image_utils.cpp

void ImageUtils::PostToBg(std::function<void()>&& task)
{
    // mock bg thread pool
    if (g_threads.size() > MAX_THREADS) {
        return;
    }
    g_threads.emplace_back(std::thread(task));
}

emplace_back() 函数在原理上比 push_back() 有了一定的改进,包括在内存优化方面和运行效率方面。内存优化主要体现在使用了就地构造(直接在容器内构造对象,不用拷贝一个复制品再使用)+强制类型转换的方法来实现,在运行效率方面,由于省去了拷贝构造过程,因此也有一定的提升。

~ImageLoadingContext()

有创建就有销毁,同样我们在析构函数中也找到响应证据,如果是异步的,就会在析构函数中调用CancelTask 取消任务:

ImageLoadingContext::~ImageLoadingContext()
{
    // 取消后台任务
    if (!syncLoad_) {
        auto state = stateManager_->GetCurrentState();
        if (state == ImageLoadingState::DATA_LOADING) {
            // 取消 CreateImgObj 任务
            ImageProvider::CancelTask(src_.GetKey(), WeakClaim(this));
        } else if (state == ImageLoadingState::MAKE_CANVAS_IMAGE) {
            // 取消 MakeCanvasImage 任务
            if (InstanceOf<StaticImageObject>(imageObj_)) {
                ImageProvider::CancelTask(canvasKey_, WeakClaim(this));
            }
        }
    }
}

总结:综上分析,我们知道了设置了.syncLoad(true)这个值后,创建图片时就不会创建一个异步任务,而我们知道,创建异步任务和互斥锁也是有开销的,会影响内存和性能,所以是否使用这个属性取决于 空间和时间 的取舍,关键在于这个阈值是在哪里。为了找出多大的图片使用 syncLoad 更好,我们做了如下测试:

对比测试

todo。。。

三、objectFit、autoResize 属性对 Image 组件性能的影响

todo

四、图片缓存

ArkUI的图片缓存策略以及我们建议的图片缓存策略:
todo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/954753.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实现公网远程访问:Windows本地快速搭建SFTP文件服务器并配置端口映射

文章目录 1. 搭建SFTP服务器1.1 下载 freesshd服务器软件1.3 启动SFTP服务1.4 添加用户1.5 保存所有配置 2 安装SFTP客户端FileZilla测试2.1 配置一个本地SFTP站点2.2 内网连接测试成功 3 使用cpolar内网穿透3.1 创建SFTP隧道3.2 查看在线隧道列表 4. 使用SFTP客户端&#xff0…

镜头翻转大师:视频剪辑高手的魔法技巧

在数字媒体时代&#xff0c;视频制作已成为各种规模的组织和个人的必备技能。无论是小型家庭活动还是大型企业项目&#xff0c;都需要通过视频来展示成果、传播信息&#xff0c;或是仅仅为了分享生活的美好瞬间。然而&#xff0c;视频制作并非易事&#xff0c;其中最困难的步骤…

气传导蓝牙耳机排行榜,值得大家选择的气传导耳机推荐!

​随着科技的不断进步&#xff0c;气传导耳机已经成为了市场上备受瞩目的产品之一。相比传统耳机&#xff0c;气传导耳机的音质表现更加出色&#xff0c;同时还具有更好的佩戴感受。下面跟着我脚步来&#xff0c;推荐几款值得大家选择的气传导耳机&#xff0c;看看哪款更好&…

【错误记录】exe4j 打包程序无法设置 jar 包依赖的问题 ( 将源码 和 依赖库打包到同一个 jar 包中 )

文章目录 一、问题描述二、解决方案 一、问题描述 在 【错误记录】IntelliJ IDEA 导出可执行 jar 包执行报错 ( java.lang.ClassNotFoundException | 打包时没有选择依赖库 ) 博客中遇到 java.lang.ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriverat j…

[LeetCode周赛复盘] 第 360 场周赛20230827

[LeetCode周赛复盘] 第 360 场周赛20230827 一、本周周赛总结2833. 距离原点最远的点1. 题目描述2. 思路分析3. 代码实现 2834. 找出美丽数组的最小和2. 思路分析3. 代码实现 2835. 使子序列的和等于目标的最少操作次数1. 题目描述2. 思路分析3. 代码实现 2836. 在传球游戏中最…

Leetcode328 奇偶链表

思路&#xff1a;分别处理奇偶&#xff0c;保存奇偶的第一个和最后一个节点&#xff0c;注意最后链接的时候需要把偶数的next去掉再拼接不然就成环了 class Solution:def oddEvenList(self, head: ListNode) -> ListNode:if not head or not head.next or not head.next.ne…

数据结构与算法-递归回溯分治

引入思考&#xff1a; 1.微信分销系统中有一个返利&#xff0c;大家应该都知道&#xff0c;比如B是A的下线&#xff0c;C是B的下线&#xff0c;那么在分钱返利的时候A可以分B&#xff0c;C的钱&#xff0c;这时候我们是不是就要分别找B,C的最后上级。这个问题我们一般怎么来解决…

数据通信——传输层TCP(可靠传输机制的滑动窗口)

引言 之前提到过拥塞问题&#xff0c;如果大量数据疯狂涌入&#xff0c;接收端无法及时处理就会导致数据丢包&#xff0c;从而使得通信受到干扰。之前的连续ARQ如果不加以节制&#xff0c;疯狂发送报文&#xff0c;接收端无法及时返回ACK就会导致网络瘫痪。 滑动窗口机制协议 这…

Linux服务器部署JavaWeb后端项目

适用于&#xff1a;MVVM前后台分离开发、部署、域名配置 前端&#xff1a;Vue 后端&#xff1a;Spring Boot 这篇文章只讲后端部署&#xff0c;前端部署戳这里 目录 Step1&#xff1a;服务器上搭建后端所需环境1、更新服务器软件包2、安装JDK83、安装MySQL4、登录MySQL5、修…

一百六十六、MySQL——systemctl stop mysqld无法停止MySQL8.0服务

一、目的 为了解决MySQL的中文乱码问题&#xff0c;需要对/etc/my.cnf文件进行参数配置。 而对/etc/my.cnf文件进行配置参数之前&#xff0c;需要先停止MySQL服务 二、错误命令语句示范 &#xff08;一&#xff09;错误的命令语句 # systemctl stop mysqld &#xff08;…

SQL分组后取topN

准备数据 create table SC( SId varchar(10) comment "学生ID", CId varchar(10) comment "课程ID", score decimal(18,1) comment "课程成绩");insert into SC values(01 , 01 , 80); insert into SC values(01 , 02 , 90); insert into SC va…

用最少数量的箭引爆气球【贪心算法】

用最少数量的箭引爆气球 有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points &#xff0c;其中points[i] [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。 一支弓箭可以沿着 x 轴从不同点 完全垂直 地…

Debian12 Gnome环境下的办公软件安装

一、禁用Wayland&#xff0c;启用xorg 当前Debian12 默认采用Wayland来支持gnome环境&#xff0c;但是许多软件无法在该系统下显示&#xff0c;例如&#xff1a;openoffice&#xff0c;yozo-office&#xff0c;weixin&#xff0c;fcitx。所以要在gdm3的配置文件中&#xff0c;…

港交所MMDH行情协议

目录 一、交易时间 二、MMDH与OMD的差异 三、MMDH消息类型 四、MMDH的市场快照数据 内地市场数据枢纽-证券市场(OMD-MMDH) 港交所OMD-C对接笔记 - skylerjiang - 博客园 (cnblogs.com) 一、交易时间 图 1 港交所交易时间段 图 2 消息序列 二、MMDH与OMD的差异 图 3 标准…

Leetcode213 打劫家舍2

思路&#xff1a;既然头尾不能同时取&#xff0c;那就分别算只取头或者只取尾&#xff0c;不考虑特殊情况的话是一个简单的动态规划 class Solution:def rob(self, nums: list[int]) -> int:if len(nums) < 3:return max(nums)max_sum [nums[0], max(nums[1], nums[0])…

IC芯片老化测试以及方案详解

芯片老化试验是一种对芯片进行长时间运行和负载测试的方法&#xff0c;以模拟芯片在实际使用中的老化情况。 1. 目的&#xff1a;芯片老化试验的目的是评估芯片在长时间使用和负载情况下的可靠性和性能稳定性&#xff0c;以确定其寿命和可靠性指标。 2. 测试方案设计&#xff1…

软件测试/测试开发丨Python 内置库 正则表达式

点此获取更多相关资料 本文为霍格沃兹测试开发学社学员学习笔记分享 原文链接&#xff1a;https://ceshiren.com/t/topic/27058 python 内置库 正则表达式 目录 正则表达式使用re模块实现正则表达式操作 正则表达式 正则表达式就是记录文本规则的代码可以查找操作符合某些复…

大屏开发,浏览器的可视区域和设备的分辨率

在线屏幕检测 - 显示器检测 - 显示器坏点检测工具

趣解装饰者模式之《我想吃煎饼果子了》

〇、小故事 话说最近早起没时间做早饭&#xff0c;并且早上上班的地铁口不远处就有一处非常火爆的煎饼摊&#xff0c;所以我就经常去那边吃煎饼&#xff0c;一个“基础版”煎饼是7块钱&#xff0c;向煎饼中加一颗鸡蛋是1元钱&#xff0c;加一根火腿肠是3元钱&#xff0c;加鸡柳…

【算法日志】动态规划刷题:不相邻选择类问题(day40)

算法随想录刷题60Day 目录 前言 打家劫舍1 (数组) 打家劫舍2&#xff08;环形数组&#xff09; 打家劫舍3&#xff08;二叉树&#xff09; 前言 今天主要讨论不相邻选择类问题&#xff0c;会在不同数据结构题型的下探讨该类问题的解法。 打家劫舍1 (数组) 本题只需要讨论当…