本文将基于 OpenHarmony 3.2 源码,分析 Graphic 子系统 2D 图形库中的 OpenHarmony 系统开机动画源码。

开机动画是 OpenHarmony 启动后,运行的第一个和图形渲染相关的进程,依赖相对独立便于分析,是分析图形子系统比较好的切入点。

源码仓库

本文基于 OpenHarmony 3.2 源码。

由于 OpenHarmony 尚未稳定,源代码和项目仓库结构变更速度和幅度都较大,请读者直接参照 OpenHarmony 3.2 源码阅读本文。

Graphic 子系统 2D 图形库源码

仓库结构

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 图形库。

Skia 官网

Skia 文档

下文以 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_servicebootanimation 以显示开机动画。

初始化工作

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::InitBootAnimation::PlaySound 加入任务队列。

BootAnimation::InitBootAnimation::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::InitBootAnimation::InitRsSurface 创建获取 Surface。

BootAnimation::InitBootAnimation::InitPicCoordinates 计算得出开机动画的实际显示区域,大致上就是屏幕居中的位置。

ReadZipFileSortZipFile 函数不是我们关注的重点,其主要作用是解压开机动画图片压缩包,并将其按序添加至 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::MakeFromDataSkCodec::MakeFromDataSkImage::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::DrawBootAnimation::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::OnVsyncBootAnimation::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 源码阅读本文。