赞
踩
开发者经常需要在应用中显示一些图片,例如:按钮中的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时,图像数据会被解码,变成RGB的颜色数据。 解码是一个计算量较大的任务,且需要CPU来执行。
解码出来的图片体积与图片的宽高有关,与图片原来的体积无关。
图片解码是耗时操作,如果图片非常大,建议放到子线程解码。
在上下滑动展示图片的过程中,我们会在lazyforeach的方法加载Image图片,相当于在主线程同时进行IO操作、解码等操作。这会造成内存迅速增长和CPU负载瞬间提升。 并且内存的迅速增加会触发系统的内存回收机制,尝试回收其他后台进程的内存,增加CPU的工作量。
如果系统无法提供足够的内存,则会先结束后台app进程,同时造成UI卡顿。
使用图片资源管理工具,存储不同分辨率的图片,在不同分辨率的设备使用最适合的尺寸。如果图片是网络获取,可以通过传参的类型告诉服务端,服务端根据设备类型返回最合适尺寸的图片。
详细分析见下文《图片缓存》章节。
在图像渲染时是通过一块一块渲染,因此数据是一块块地取,如果一块连续的内存数据里结尾的数据不是图像的内容,是内存里其他的数据,会影响读取效率。
块的大小和CPU cache有关,64位系统按64byte作为一块数据去读取和渲染,让图像数据对齐64byte就可以避免图形管理器再拷贝一份数据进行修补。
提前将需要的图片下载到本地,并在CPU空闲的时候解压缩。
我们发现聊天列表头像图片很小,加载很快,根据官方文档指示:在加载图片的耗时比较短的时候,通过异步加载的效果会大打折扣,建议配置 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,如果不设置,图片加载就是异步的。
· 查看 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();
}
那么 loadingCtx_
是一个什么东西呢?通过查看定义文件 image_pattern.h
发现:
RefPtr<ImageLoadingContext> loadingCtx_;
loadingCtx_
是一个 RefPtr
类型的指针。
我们也可以在源码memery
下的 referenced.h
中找到 RefPtr
的定义,由于对我们分析图片加载影响不大,简单看一下,可以得知:
RefPtr
使用引用计数管理实例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(); } } ... };
再回到之前的调用代码 loadingCtx_ = AceType::MakeRefPtr<ImageLoadingContext>(src, std::move(loadNotifier), syncLoad_);
,主要关注下使用到的 MakeRefPtr
函数,可以得知:
Referenced::MakeRefPtr
是用于创建新实例的,而这个创建的新实例是继承于 Referenced
的。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)...)
的一个参数被管理起来。
· 知道了 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()
这个方法:
(这个方法是重点,所以完整展示代码,并添加一些注释)
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() 有了一定的改进,包括在内存优化方面和运行效率方面。内存优化主要体现在使用了就地构造(直接在容器内构造对象,不用拷贝一个复制品再使用)+强制类型转换的方法来实现,在运行效率方面,由于省去了拷贝构造过程,因此也有一定的提升。
有创建就有销毁,同样我们在析构函数中也找到响应证据,如果是异步的,就会在析构函数中调用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。。。
todo
ArkUI的图片缓存策略以及我们建议的图片缓存策略:
todo
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。