1 关键字

Router;页面跳转;

2 问题描述

OpenHarmony版本:3.2 Release

问题现象:应用内跳转时延537ms,低于达标值(450ms)10%以上。

测试步骤:

  1. 设备进入主界面并打开设置保持亮屏

  2. 使用慢动作相机打开八倍速慢动作开始记录手指点击wlan到wlan界面加载完成

  3. 将拍摄好的视频通过数据线上传到PC端

  4. 在PC端打开avidemux2工具

  5. 通过单帧播放记录记录起点帧数和终点帧数

  6. 通过时延公式(终点帧数减起点帧数)乘以1000除以240计算出响应时延

  7. 重复步骤1~6测试5次计算平均值,如出现偏差较大结果,则当次测试结果作废

预期结果:应用内跳转时延平均值为450ms

实际结果:应用内跳转时延平均值为537ms

3 问题原因

3.1 正常机制

应用内跳转时延平均值为450ms以内

3.2 异常机制

应用内跳转时延平均值为537ms

4 解决方案

经过分析,发现引起该问题的原因为页面跳转默认动画时间太长,导致性能指标不达标,因此可以修改页面跳转默认动画时长。

  • 代码路径:foundation/arkui/ace_engine/frameworks/core/components_ng/render/adapter/rosen_render_context.cpp

bool RosenRenderContext::TriggerPageTransition(PageTransitionType type, const std::function<void()>& onFinish)
{
    ......
    if (transition) {
       ......
    } else {
       effect = GetDefaultPageTransition(type);
       // const int32_t pageTransitionDuration = 300;
       const int32_t pageTransitionDuration = 0; //修改动画默认时长
       option.SetCurve(Curves::LINEAR);
       option.SetDuration(pageTransitionDuration);
    }
    ......
    return true;
}

5 定位过程

通过 avidemux2 工具逐帧分析拍摄的页面跳转视频:当手指抬起后,下一个页面冲右侧开始显现,随后页面整体开始像左侧滑动,等待页面全部显示,大概三百多毫秒。通过分析视频现象,可以猜测由于页面切换动画时间过长,导致页面跳转的性能不达标。

通过 hdc 工具抓取页面跳转的 trace 文件,通过 Chrome 浏览器的 tracing插件(chrome://tracing/) 分析生成的trace文件。

  • trace 抓取命令

hdc_std shell bytrace -t 5 --overwrite ability ace app ark binder disk distributeddatamgr dsoftbus freq graphic gresource i2c idle irq mdfs memory memreclaim misc msdp multimodalinput notification ohos pagecache regulators rpc sched sensors sync window workq zaudio zcamera zimage zmedia > D:\mytrace.ftrace

手指抬起到下一个页面完全显示耗时大概464.77ms。

下一个页面初始化到第一帧RSRenderThread绘制大概135.23ms。

 

经过这个上述2个trace 片段的对比,第一帧显示到界面完全显示大概有320ms的时差。这个过程,RSRenderThread和render_service在不停的绘制和合成帧,通过视频逐帧播放对比,可以判定该时差为动画的切换时长。

 

上面步骤已经确定了引起该问题的原因,于是开始根据日志追踪代码调用链,从而解决该问题。前端页面跳转是用的 ohos.router 包下的 push 方法,在页面跳转时,过滤日志router。

# hilog | grep router
01-01 04:13:19.456  1669  1669 I C03900/Ace: [jsi_router_module.cpp(PagePush)-(0)] PagePush Start
01-01 04:13:19.456  1669  1669 I C03900/Ace: [page_router_manager.cpp(LoadPage)-(0)] PageRouterManager LoadPage[8]: pages/wifi.
01-01 04:13:19.504  1669  1669 I C03900/Ace: [page_router_manager.cpp(LoadPage)-(0)] PageRouterManager LoadPage[8]: pages/wifi. success

于是进入 page_router_manager.cpp 文件的 LoadPage 方法,开始跟踪路由跳转的方法调用链,LoadPage 调用的同文件的 OnPageReady 方法,OnPageReady 方法中获取 PipelineContext,通过 PipelineContext 获取 stageManager 对象,调用了 stage_manager.cpp 文件中的 PushPage 方法。

文件路径:foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/ng/page_router_manager.cpp

void PageRouterManager::LoadPage(int32_t pageId, const RouterPageInfo& target, const std::string& params,
    bool /*isRestore*/, bool needHideLast, bool needTransition,
    const std::function<void(const std::string&, int32_t)>& errorCallback)
{
   ......
   //739
    if (!OnPageReady(pageNode, needHideLast, needTransition)) {
       ......
       return;
    }
   ......
}
bool PageRouterManager::OnPageReady(
    const RefPtr<FrameNode>& pageNode, bool needHideLast, bool needTransition, bool isCardRouter, int64_t cardId)
{
    ......
    auto context = DynamicCast<NG::PipelineContext>(pipeline);
    auto stageManager = context ? context->GetStageManager() : nullptr;
    if (stageManager) {
        return stageManager->PushPage(pageNode, needHideLast, needTransition);
    }
    LOGE("fail to push page due to stage manager is nullptr");
    return false;
}

stage_manager.cpp 文件中的 PushPage 方法会根据传入的 needTransition 参数,判断是否需要执行页面切换的动画。进而进一步调用同cpp 文件的 StartTransition 和 FirePageTransition 方法。

文件路径:foundation/arkui/ace_engine/frameworks/core/components_ng/pattern/stage/stage_manager.cpp

bool StageManager::PushPage(const RefPtr<FrameNode>& node, bool needHideLast, bool needTransition)
{
    ......
    if (needTransition) {
        pipeline->FlushPipelineImmediately();
    }
   ......
    if (needTransition) {
        pipeline->AddAfterLayoutTask([weakStage = WeakClaim(this), weakIn = WeakPtr<FrameNode>(node),
                                               weakOut = WeakPtr<FrameNode>(outPageNode)]() {
            auto stage = weakStage.Upgrade();
            CHECK_NULL_VOID(stage);
            auto inPageNode = weakIn.Upgrade();
            auto outPageNode = weakOut.Upgrade();
            stage->StartTransition(outPageNode, inPageNode, RouteType::PUSH);
        });
    }
    ......
    return true;
}
void StageManager::StartTransition(const RefPtr<FrameNode>& srcPage, const RefPtr<FrameNode>& destPage, RouteType type)
{
    ......
    if (type == RouteType::PUSH) { // 页面加载
        FirePageTransition(srcPage, PageTransitionType::EXIT_PUSH);
        FirePageTransition(destPage, PageTransitionType::ENTER_PUSH);
    } else if (type == RouteType::POP) { // 页面退出
        FirePageTransition(srcPage, PageTransitionType::EXIT_POP);
        FirePageTransition(destPage, PageTransitionType::ENTER_POP);
    }
}

FirePageTransition 方法中会获取页面自己的 PagePattern 对象,进而调用 page_pattern.cpp 文件中的 TriggerPageTransition 方法。

void FirePageTransition(const RefPtr<FrameNode>& page, PageTransitionType transitionType)
{
    CHECK_NULL_VOID(page);
    auto pagePattern = page->GetPattern<PagePattern>();
    ......
    if (transitionType == PageTransitionType::EXIT_PUSH || transitionType == PageTransitionType::EXIT_POP) {
        ......
        return;
    }
    pagePattern->TriggerPageTransition(......); // 方法调用
}

TriggerPageTransition 方法获取当前页面的 renderContext 对象,进而调用rosen_render_context.cpp 文件中的 TriggerPageTransition 方法。

bool PagePattern::TriggerPageTransition(PageTransitionType type, const std::function<void()>& onFinish)
{
    auto host = GetHost();
    CHECK_NULL_RETURN(host, false);
    auto renderContext = host->GetRenderContext();
    ...... // 调用的 else 分支的 TriggerPageTransition
    return renderContext->TriggerPageTransition(type, wrappedOnFinish);
}

文件路径:foundation/arkui/ace_engine/frameworks/core/components_ng/pattern/stage/page_pattern.cpp

最后,rosen_render_context.cpp 文件中 TriggerPageTransition 会执行对应的页面启动动画和退出动画。

bool RosenRenderContext::TriggerPageTransition(PageTransitionType type, const std::function<void()>& onFinish)
{
    bool transitionIn = true;
    if (type == PageTransitionType::ENTER_PUSH || type == PageTransitionType::ENTER_POP) {
        transitionIn = true;
    } else if (type == PageTransitionType::EXIT_PUSH || type == PageTransitionType::EXIT_POP) {
        transitionIn = false;
    } else {
        LOGW("unexpected transition type");
        return false;
    }
    CHECK_NULL_RETURN(rsNode_, false);
    auto host = GetHost();
    CHECK_NULL_RETURN(host, false);
    auto pattern = host->GetPattern<PagePattern>();
    CHECK_NULL_RETURN(pattern, false);
    auto transition = pattern->FindPageTransitionEffect(type);
    RefPtr<PageTransitionEffect> effect;
    AnimationOption option;
    if (transition) {
        // js 页面通过页
        effect = GetPageTransitionEffect(transition);
        option.SetCurve(transition->GetCurve());
        option.SetDuration(transition->GetDuration());
        option.SetDelay(transition->GetDelay());
    } else {
        // 默认动画
        effect = GetDefaultPageTransition(type);
        const int32_t pageTransitionDuration = 300; // 默认动画时长
        option.SetCurve(Curves::LINEAR);
        option.SetDuration(pageTransitionDuration);
    }
    const auto& scaleOptions = effect->GetScaleEffect();
    const auto& translateOptions = effect->GetTranslateEffect();
    UpdateTransformCenter(DimensionOffset(scaleOptions->centerX, scaleOptions->centerY));
    // 进入动画
    if (transitionIn) {
        UpdateTransformScale(VectorF(scaleOptions->xScale, scaleOptions->yScale));
        UpdateTransformTranslate(translateOptions.value());
        UpdateOpacity(effect->GetOpacityEffect().value());
        AnimationUtils::OpenImplicitAnimation(option, option.GetCurve(), onFinish);
        UpdateTransformScale(VectorF(1.0f, 1.0f));
        UpdateTransformTranslate({ 0.0f, 0.0f, 0.0f });
        UpdateOpacity(1.0);
        AnimationUtils::CloseImplicitAnimation();
        return true;
    }
    // 退出动画
    UpdateTransformScale(VectorF(1.0f, 1.0f));
    UpdateTransformTranslate({ 0.0f, 0.0f, 0.0f });
    UpdateOpacity(1.0);
    AnimationUtils::OpenImplicitAnimation(option, option.GetCurve(), onFinish);
    UpdateTransformScale(VectorF(scaleOptions->xScale, scaleOptions->yScale));
    UpdateTransformTranslate(translateOptions.value());
    UpdateOpacity(effect->GetOpacityEffect().value());
    AnimationUtils::CloseImplicitAnimation();
    return true;
}

文件路径:foundation/arkui/ace_engine/frameworks/core/components_ng/render/adapter/rosen_render_context.cpp

通过上述代码,可以看出,当前端JS页面设置了动画,会根据设置的动画属性运行页面加载的动画,否者会走默认的动画。默认的动画是一个时长为 300ms 的线性动画。

针对页面跳转时延太长,可以修改对应的动画时长,来减少页面加载的动画时间。

将页面加载的默认动画设置为0后,页面跳转的trace如下:

 

通过上面的trace,手指抬起到页面全部显示,页面的加载时长只有150ms。通过修改默认加载的动画时长,多次拍摄视频取平均值,页面跳转的平均时延只有284ms,低于450ms数据指标,对比之前的537ms,时间减少了45%。

6 知识分享

两个页面间发生跳转,一个页面消失,另一个页面出现,这时可以配置各自页面的页面转场参数实现自定义的页面转场效果。页面转场效果写在pageTransition函数中,通过PageTransitionEnter和PageTransitionExit指定页面进入和退出的动画效果。

pageTransition() {
    PageTransitionEnter({ duration: 500, curve: Curve.Linear }).slide(SlideEffect.Bottom)
    PageTransitionExit({ duration: 500, curve: Curve.Ease }).slide(SlideEffect.Bottom)
  }
Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐