解读 OpenHarmony 系统开机动画源码
本文将基于 OpenHarmony 3.2 源码,分析 Graphic 子系统 2D 图形库中的 OpenHarmony 系统开机动画源码。
开机动画是 OpenHarmony 启动后,运行的第一个和图形渲染相关的进程,依赖相对独立便于分析,是分析图形子系统比较好的切入点。
源码仓库
本文基于 OpenHarmony 3.2 源码。
由于 OpenHarmony 尚未稳定,源代码和项目仓库结构变更速度和幅度都较大,请读者直接参照 OpenHarmony 3.2 源码阅读本文。
仓库结构
foundation/graphic/graphic_2d/
├── figures # Markdown 引用的图片目录
├── frameworks # 框架代码目录
│ ├── animation_server # animation_server 代码
│ ├── bootanimation # 开机动画目录
│ ├── dumper # graphic dumper 代码
│ ├── fence # fence 代码
│ ├── opengl_wrapper # opengl_wrapper
│ ├── surface # surface 代码
│ ├── surfaceimage # surfaceimage 代码
│ ├── vsync # vsync 代码
│ ├── wm # wm 代码
│ ├── wmserver # wmserver 代码
│ ├── wmservice # wmservice 代码
│ ├── wmtest # wmtest 代码
├── rosen # 框架代码目录
│ ├── build # 构建说明
│ ├── doc # doc
│ ├── include # 对外头文件代码
│ ├── modules # Graphic 子系统各模块代码
│ ├── samples # 实例代码
│ ├── test # 开发测试代码
│ ├── tools # 工具代码
├── interfaces # 图形接口存放目录
│ ├── inner_api # 内部 native 接口存放目录
│ └── kits # js/napi 外部接口存放目录
└── utils # 小部件存放目录
前导知识:构建目标
一般情况下,我们使用最基础的全量编译,也就是编译产品。但在开发过程中,修改一部分代码后,若我们每次都采用全量编译,则十分浪费时间。
好在 OpenHarmony 的编译构建工具支持部分编译,我们可以指定编译目标:
./build.sh --product-name=<PRODUCT_NAME> --build-target=<BUILD_TARGET> --ccache
OpenHarmony 的 hb
命令行工具也支持指定编译目标,请自行查阅手册。
这里简单分析一下 OpenHarmony 系统开机动画源码是如何被加入构建编译的。
我们先抛开平台和产品的差异,假设构建编译包含了 Graphic 子系统。OpenHarmony 中所有子系统的定义可以在 build/subsystem_config.json
中找到,Graphic 子系统也不例外:
{
},
"sensors": {
"path": "base/sensors",
"name": "sensors"
},
"graphic": {
"path": "foundation/graphic",
"name": "graphic"
},
"window": {
"path": "foundation/window",
"name": "window"
},
}
由此可知 Graphic 子系统位于 foundation/graphic
目录,本文的主角 OpenHarmony 系统开机动画源码就在该目录中 foundation/graphic/graphic_2d/frameworks/bootanimation
。
首先我们关注 bootanimation
目录下的 BUILD.gn
文件:
ohos_executable("bootanimation") {
install_enable = true
sources = [
"src/boot_animation.cpp",
"src/main.cpp",
"src/util.cpp",
]
…………
…………
…………
part_name = "graphic_standard"
subsystem_name = "graphic"
}
ohos_prebuilt_etc("bootanimation_pics") {
source = "data/bootpic.zip"
relative_install_dir = "init"
part_name = "graphic_standard"
subsystem_name = "graphic"
}
ohos_prebuilt_etc("bootanimation_sounds") {
source = "data/bootsound.wav"
relative_install_dir = "init"
part_name = "graphic_standard"
subsystem_name = "graphic"
}
bootanimation
编译生成二进制可执行文件,并在全量编译时将生成的文件复制到/system/bin
目录bootanimation_pics
在全量编译时将开机动画图片压缩包复制到/system/bin/init
目录bootanimation_sounds
在全量编译时将开机动画音效文件复制到/system/bin/init
目录
现在,让我们回到父目录 foundation/graphic/graphic_2d
。
查看该目录下的 BUILD.gn
文件:
group("default") {
public_deps = [
":graphic.rc",
"frameworks/dumper:gdumper",
"frameworks/dumper:gdumper.ini",
"frameworks/dumper:graphic_dumper_server",
"frameworks/vsync:vsync_server",
]
if (graphic_standard_feature_bootanimation_enable) {
public_deps += [ "frameworks/bootanimation:bootanimation" ]
public_deps += [ "frameworks/bootanimation:bootanimation_pics" ]
public_deps += [ "frameworks/bootanimation:bootanimation_sounds" ]
}
}
查看该目录下的 bundle.json
文件
{
…………
…………
…………
"build": {
"sub_component": [
"//third_party/libpng:libpng",
"//foundation/graphic/graphic_2d:default",
"//foundation/graphic/graphic_2d/interfaces/kits/napi:napi_packages",
…………
…………
…………
],
…………
…………
…………
},
…………
…………
…………
}
通过分析这两个文件,我们得知构建 OpenHarmony 系统开机动画源码所需的编译目标就是 foundation/graphic/graphic_2d:default
。
假设我们使用的产品为 vendor/hihope/rk3568
,则可以这样编译构建 OpenHarmony 系统开机动画源码:
./build.sh --product-name=rk3568 --build-target=foundation/graphic/graphic_2d:default --ccache
源码分析
接下来,本文将解读开机动画源码 foundation/graphic/graphic_2d/frameworks/bootanimation
。
OpenHarmony 的 2D 图形库底层使用的是 Google 开源的 Skia 2D 图形库。
下文以 Sk
开头的都属于该库,读者可以查阅 Skia 文档进一步了解。
启动
开机动画的启动流程定义在 foundation/graphic/graphic_2d/graphic.cfg
文件中:
{
"jobs" : [{
"name" : "init",
"cmds" : [
"chmod 666 /dev/mali0",
"chown system graphics /dev/mali0"
]
}
],
"services" : [{
"name" : "render_service",
"path" : ["/system/bin/render_service"],
"uid" : "system",
"gid" : ["system", "shell", "uhid", "root"],
"caps" : ["SYS_NICE"],
"secon" : "u:r:render_service:s0"
}, {
"name" : "bootanimation",
"path" : ["/system/bin/bootanimation"],
"once" : 1,
"uid" : "graphics",
"gid" : ["graphics", "system", "shell", "uhid", "root"],
"secon" : "u:r:bootanimation:s0"
}
]
}
OpenHarmony 分别启动了 render_service
和 bootanimation
以显示开机动画。
初始化工作
src/main.cpp
int main(int argc, const char *argv[])
{
LOGI("main enter");
WaitRenderServiceInit();
auto& dms = OHOS::Rosen::DisplayManager::GetInstance();
auto displays = dms.GetAllDisplays();
while (displays.empty()) {
LOGI("displays is empty, retry to get displays");
displays = dms.GetAllDisplays();
sleep(1);
}
BootAnimation bootAnimation;
auto runner = AppExecFwk::EventRunner::Create(false);
auto handler = std::make_shared<AppExecFwk::EventHandler>(runner);
handler->PostTask(std::bind(&BootAnimation::Init, &bootAnimation,
displays[0]->GetWidth(), displays[0]->GetHeight(), handler, runner));
handler->PostTask(std::bind(&BootAnimation::PlaySound, &bootAnimation));
runner->Run();
LOGI("main exit");
return 0;
}
WaitRenderServiceInit()
定义在 src/util.cpp
中,其作用就是等待直到 render_service
就绪后返回。
程序通过 Rosen 框架获取 DisplayManager 实例并取得当前屏幕参数,随后利用 PostTask
函数将 BootAnimation::Init
和 BootAnimation::PlaySound
加入任务队列。
BootAnimation::Init
中 BootAnimation::InitBootWindow
创建启动窗口,通过 WindowScene 调用 WindowImpl 创建 RSSurfaceNode 对象。
sptr<OHOS::Rosen::WindowOption> option = new OHOS::Rosen::WindowOption();
option->SetWindowType(OHOS::Rosen::WindowType::WINDOW_TYPE_BOOT_ANIMATION);
option->RemoveWindowFlag(OHOS::Rosen::WindowFlag::WINDOW_FLAG_NEED_AVOID);
option->SetWindowRect( {0, 0, windowWidth_, windowHeight_} );
option->SetWindowType(OHOS::Rosen::WindowType::WINDOW_TYPE_BOOT_ANIMATION);
WindowType 定义在
foundation/window/window_manager/interfaces/innerkits/wm/wm_common.h
中,这里使用的是 WINDOW_TYPE_BOOT_ANIMATION 。值得注意的是,wm_common.h
中并不是所有列出的 WindowType 都可以使用,可用的 WindowType 类型可参考foundation/window/window_manager/wmserver/include/window_zorder_policy.h
文件。option->RemoveWindowFlag(OHOS::Rosen::WindowFlag::WINDOW_FLAG_NEED_AVOID);
WindowOption 初始化时,自动添加了 WINDOW_FLAG_NEED_AVOID(见
foundation/window/window_manager/wm/src/window_option.cpp
),这一步将其移除。option->SetWindowRect( {0, 0, windowWidth_, windowHeight_} );
根据先前通过 DisplayManager 获取到的屏幕参数设置窗口区域。
BootAnimation::Init
中 BootAnimation::InitRsSurface
创建获取 Surface。
BootAnimation::Init
中 BootAnimation::InitPicCoordinates
计算得出开机动画的实际显示区域,大致上就是屏幕居中的位置。
ReadZipFile
和 SortZipFile
函数不是我们关注的重点,其主要作用是解压开机动画图片压缩包,并将其按序添加至 ImageStructVec imageVector_
。
我们需要关注的是其中的 GenImageData
函数:
bool GenImageData(const std::string& filename, std::shared_ptr<ImageStruct> imagetruct, int32_t bufferlen,
ImageStructVec& outBgImgVec)
{
if (imagetruct->memPtr.memBuffer == nullptr) {
LOGE("Json File buffer is null.");
return false;
}
auto skData = SkData::MakeFromMalloc(imagetruct->memPtr.memBuffer, bufferlen);
if (skData == nullptr) {
LOGE("skdata memory data is null. update data failed");
return false;
}
imagetruct->memPtr.setOwnerShip(skData);
auto codec = SkCodec::MakeFromData(skData);
imagetruct->fileName = filename;
imagetruct->imageData = SkImage::MakeFromEncoded(skData);
outBgImgVec.push_back(imagetruct);
return true;
}
SkCodec::MakeFromData
,SkCodec::MakeFromData
和 SkImage::MakeFromEncoded
三个函数合力将从图片文件读至内存的数据转换为 SkImage 类型数据用于后续画面渲染。
开机动画图压缩包中还有个 config.json
文件,它定义了开机动画的帧率:
{
"Remark": "FrameRate Support 30, 60 frame rate configuration",
"FrameRate": 30
}
随后,程序根据读取到的开机动画帧率,设置帧回调函数的调用速率。
OHOS::Rosen::VSyncReceiver::FrameCallback fcb = {
.userData_ = this,
.callback_ = std::bind(&BootAnimation::OnVsync, this),
};
int32_t changefreq = static_cast<int32_t>((1000.0 / freq_) / 16);
ret = receiver_->SetVSyncRate(fcb, changefreq);
每次回调都会调用 BootAnimation::OnVsync
,向任务队列添加一次 BootAnimation::Draw
任务。
void BootAnimation::OnVsync()
{
PostTask(std::bind(&BootAnimation::Draw, this));
}
渲染绘制开机动画
aa64564b feat: 增加开机音频
BootAnimation::Init
任务结束后,BootAnimation::PlaySound
通过多媒体子系统创建播放器,开始播放开机动画音效。
接下来正式开始绘制:
BootAnimation::Draw
和 BootAnimation::OnDraw
void BootAnimation::Draw()
{
if (picCurNo_ < (imgVecSize_ - 1)) {
picCurNo_ = picCurNo_ + 1;
} else {
CheckExitAnimation();
return;
}
ROSEN_TRACE_BEGIN(HITRACE_TAG_GRAPHIC_AGP, "BootAnimation::Draw RequestFrame");
auto frame = rsSurface_->RequestFrame(windowWidth_, windowHeight_);
if (frame == nullptr) {
LOGE("Draw frame is nullptr");
return;
}
ROSEN_TRACE_END(HITRACE_TAG_GRAPHIC_AGP);
framePtr_ = std::move(frame);
auto canvas = framePtr_->GetCanvas();
OnDraw(canvas, picCurNo_);
ROSEN_TRACE_BEGIN(HITRACE_TAG_GRAPHIC_AGP, "BootAnimation::Draw FlushFrame");
rsSurface_->FlushFrame(framePtr_);
ROSEN_TRACE_END(HITRACE_TAG_GRAPHIC_AGP);
}
void BootAnimation::OnDraw(SkCanvas* canvas, int32_t curNo)
{
if (canvas == nullptr) {
LOGE("OnDraw canvas is nullptr");
return;
}
if (curNo > (imgVecSize_ - 1) || curNo < 0) {
return;
}
std::shared_ptr<ImageStruct> imgstruct = imageVector_[curNo];
sk_sp<SkImage> image = imgstruct->imageData;
ROSEN_TRACE_BEGIN(HITRACE_TAG_GRAPHIC_AGP, "BootAnimation::OnDraw in drawRect");
SkPaint backPaint;
backPaint.setColor(SK_ColorBLACK);
canvas->drawRect(SkRect::MakeXYWH(0.0, 0.0, windowWidth_, windowHeight_), backPaint);
ROSEN_TRACE_END(HITRACE_TAG_GRAPHIC_AGP);
ROSEN_TRACE_BEGIN(HITRACE_TAG_GRAPHIC_AGP, "BootAnimation::OnDraw in drawImageRect");
SkPaint paint;
SkRect rect;
rect.setXYWH(pointX_, pointY_, realWidth_, realHeight_);
canvas->drawImageRect(image.get(), rect, &paint);
ROSEN_TRACE_END(HITRACE_TAG_GRAPHIC_AGP);
}
Draw
函数:auto frame = rsSurface_->RequestFrame(windowWidth_, windowHeight_);
通过初始化阶段获取到的 Surface 申请 Frame
Draw
函数:framePtr_ = std::move(frame); auto canvas = framePtr_->GetCanvas();
获取 SkCanvas 对象
OnDraw
函数:canvas->drawRect(SkRect::MakeXYWH(0.0, 0.0, windowWidth_, windowHeight_), backPaint);
windowWidth_
和windowHeight_
为当前屏幕真实大小,该函数将整块屏幕填满黑色OnDraw
函数:rect.setXYWH(pointX_, pointY_, realWidth_, realHeight_);
通过初始化阶段计算得出的开机动画实际显示区域,设置绘制区域
OnDraw
函数:canvas->drawImageRect(image.get(), rect, &paint);
将从图片文件中转换得到的 SkImage 数据渲染到设置好的开机动画实际显示区域
Draw
函数:rsSurface_->FlushFrame(framePtr_);
将渲染好的画布数据(纯黑背景 + 一张图片)刷入 Surface 的 BufferQueue 以真正显示到物理屏幕
foundation/graphic/graphic_2d/frameworks/surface/src/buffer_queue.cpp
GSError BufferQueue::FlushBuffer(uint32_t sequence, const sptr<BufferExtraData> &bedata,
const sptr<SyncFence>& fence, const BufferFlushConfig &config)
{
ScopedBytrace func(__func__);
if (!GetStatus()) {
BLOGN_FAILURE_RET(GSERROR_NO_CONSUMER);
}
auto sret = CheckFlushConfig(config);
if (sret != GSERROR_OK) {
BLOGN_FAILURE_API(CheckFlushConfig, sret);
return sret;
}
{
std::lock_guard<std::mutex> lockGuard(mutex_);
if (bufferQueueCache_.find(sequence) == bufferQueueCache_.end()) {
BLOGN_FAILURE_ID(sequence, "not found in cache");
return GSERROR_NO_ENTRY;
}
if (isShared_ == false) {
auto &state = bufferQueueCache_[sequence].state;
if (state != BUFFER_STATE_REQUESTED && state != BUFFER_STATE_ATTACHED) {
BLOGN_FAILURE_ID(sequence, "invalid state %{public}d", state);
return GSERROR_NO_ENTRY;
}
}
}
if (listener_ == nullptr && listenerClazz_ == nullptr) {
CancelBuffer(sequence, bedata);
return GSERROR_NO_CONSUMER;
}
ScopedBytrace bufferIPCSend("BufferIPCSend");
sret = DoFlushBuffer(sequence, bedata, fence, config);
if (sret != GSERROR_OK) {
return sret;
}
CountTrace(HITRACE_TAG_GRAPHIC_AGP, name_, static_cast<int32_t>(dirtyList_.size()));
if (sret == GSERROR_OK) {
if (listener_ != nullptr) {
ScopedBytrace bufferIPCSend("OnBufferAvailable");
listener_->OnBufferAvailable();
} else if (listenerClazz_ != nullptr) {
ScopedBytrace bufferIPCSend("OnBufferAvailable");
listenerClazz_->OnBufferAvailable();
}
}
BLOGND("Success Buffer seq id: %{public}d Queue id: %{public}" PRIu64 " AcquireFence:%{public}d",
sequence, uniqueId_, fence->Get());
return sret;
}
DoFlushBuffer
函数会调用 Display 驱动的 FlushCache
函数,关于 Display 驱动可以查阅 drivers/peripheral/display
目录。
开机动画结束条件
程序将根据先前设置的回调频率调用 BootAnimation::OnVsync
,BootAnimation::OnVsync
又将调用 BootAnimation::Draw
if (picCurNo_ < (imgVecSize_ - 1)) {
picCurNo_ = picCurNo_ + 1;
} else {
CheckExitAnimation();
return;
}
显而易见的,Draw
函数会进行条件判断,将图片文件压缩包中的图片逐个绘制送显。若所有图片都已渲染,则调用 CheckExitAnimation
函数,不再调用后续的 OnDraw
函数。
void BootAnimation::CheckExitAnimation()
{
LOGI("CheckExitAnimation enter");
if (!setBootEvent_) {
LOGI("CheckExitAnimation set bootevent parameter");
system::SetParameter("bootevent.bootanimation.started", "true");
setBootEvent_ = true;
}
std::string windowInit = system::GetParameter("persist.window.boot.inited", "0");
if (windowInit == "1") {
PostTask(std::bind(&AppExecFwk::EventRunner::Stop, runner_));
LOGI("CheckExitAnimation read windowInit is 1");
return;
}
}
CheckExitAnimation
函数将读取系统参数,判断当前系统是否已启动完成。
- 若系统已启动完成,则通过
PostTask(std::bind(&AppExecFwk::EventRunner::Stop, runner_));
结束开机动画进程。 - 若系统未启动完成,则继续等待下一次
OnVsync
调用。此时,屏幕将维持显示最后一帧的画面。
总结
OpenHarmony 开机动画的渲染过程是从 render_service 获取 Buffer,在 Client 端用 Buffer + Skia 创建 Canvas 进行绘制,并逐个图片 flush 到 render_service 的 Server 端,最终完成送显。
本文基于 OpenHarmony 3.2 源码。
由于 OpenHarmony 尚未稳定,源代码和项目仓库结构变更速度和幅度都较大,请读者直接参照 OpenHarmony 3.2 源码阅读本文。