ArkUI Engine - JS View 与C++

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

引言

本篇是ArkUI Engine系列的第二篇,通过学习ViewPU与Component的关系,我们能够知道在ArkUI中写的一系列Component的具体实现,打通JS View与native C++的世界

ViewPU创建过程

ArkUI中,Component是一个个页面的表示,接下来我们以最简单的例子,为大家介绍一下ArkUI背后的秘密。我们声明了一个名为HelloArkUI的Componet,内容如下

scss 复制代码
@Component
struct HelloArkUI{
  build(){
    Row(){
      Text("文本1")
      Text("文本2")
    }
  }
}

我们在导读篇说过,一个简单的Component,最终会被编译成一个集成于ViewPU或者View的class,通过反编译,我们可以看到,HelloArkUI继承于ViewPU

scss 复制代码
class HelloArkUI extends ViewPU {
    constructor(parent, params, __localStorage, elmtId = -1) {
        super(parent, __localStorage, elmtId);
        this.setInitiallyProvidedValue(params);
    }
    setInitiallyProvidedValue(params) {
    }
    updateStateVars(params) {
    }
    purgeVariableDependenciesOnElmtId(rmElmtId) {
    }
    aboutToBeDeleted() {
        SubscriberManager.Get().delete(this.id__());
        this.aboutToBeDeletedInternal();
    }
    initialRender() {
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Row.create();
            if (!isInitialRender) {
                Row.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Text.create("文本1");
            if (!isInitialRender) {
                Text.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        创建完成Text后立即调用pop函数
        Text.pop();
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Text.create("文本2");
            if (!isInitialRender) {
                Text.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        Text.pop();
        当前Row的子组件完成之后,才会调用自身的pop函数
        Row.pop();
    }
    rerender() {
        this.updateDirtyElements();
    }
}

我们之前简单介绍过ViewPU,ViewPU中有一个抽象initialRender,用于触发组件的生成

csharp 复制代码
  protected abstract initialRender(): void;
  protected abstract rerender(): void;

HelloArkUI这个Component,是由build函数内几个基础组件组成的,它实现了initialRender函数。我们也留意到,这里面会根据里面build函数内声明的组件数量,依次执行了observeComponentCreation方法,这个方法实现如下:

typescript 复制代码
  public observeComponentCreation(compilerAssignedUpdateFunc: UpdateFunc): void {
    if (this.isDeleting_) {
      stateMgmtConsole.error(`View ${this.constructor.name} elmtId ${this.id__()} is already in process of destruction, will not execute observeComponentCreation `);
      return;
    }
    内部定义了一个更新方法
    const updateFunc = (elmtId: number, isFirstRender: boolean) => {
      stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} start ....`);
      this.currentlyRenderedElmtIdStack_.push(elmtId);
      compilerAssignedUpdateFunc(elmtId, isFirstRender);
      this.currentlyRenderedElmtIdStack_.pop();
      stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} - DONE ....`);
    }
    通过ViewStackProcessor的AllocateNewElmetIdForNextComponent为当前创建的组件赋予一个全局id
    const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent();
    // in observeComponentCreation function we do not get info about the component name, in 
    // observeComponentCreation2 we do.
    this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc });
    // add element id -> owning ViewPU
    UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this));
    try {
      调用更新方法,即上面的updateFunc,第一次创建为true
      updateFunc(elmtId, /* is first render */ true);
    } catch (error) {
      // avoid the incompatible change that move set function before updateFunc.
      this.updateFuncByElmtId.delete(elmtId);
      UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId);
      stateMgmtConsole.applicationError(`${this.debugInfo__()} has error in update func: ${(error as Error).message}`);
      throw error;
    }
  }

observeComponentCreation里面有几个细节需要关注,我们拿Row创建时的observeComponentCreation举例子,

scss 复制代码
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Row.create();
            初始化不执行这里
            if (!isInitialRender) {
                Row.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });

第一次创建的时候,isInitialRender会被赋值为true,因此当前组件的pop函数是不会在observeComponentCreation过程中被执行的,而是等当前子组件依次创建完成之后,才会调用pop函数。为什么会是这样呢?其实最大的目的就是确立了视图的层级关系与组件父子关系,Row展示的部分一定是在Text之后,我们之后会说到。

下面我们来看create函数与pop函数究竟做了什么,它的实现在哪

js view绑定

在整个engine初始化的时候,会通过JsBindFormViews方法通过绑定关系把js与C++进行绑定,读者们可以通过这个方法跟踪整个引擎初始化过程,我们这边只关注创建过程

每个控件都有自己的JSBind方法,通过这里建立起js与C++的方法映射关系

ruby 复制代码
void JSRow::JSBind(BindingTarget globalObj)
{
    JSClass<JSRow>::Declare("Row");
    MethodOptions opt = MethodOptions::NONE;
    JSClass<JSRow>::StaticMethod("create", &JSRow::Create, opt);
    JSClass<JSRow>::StaticMethod("createWithWrap", &JSRow::CreateWithWrap, opt);
    JSClass<JSRow>::StaticMethod("fillParent", &JSFlex::SetFillParent, opt);
    JSClass<JSRow>::StaticMethod("wrapContent", &JSFlex::SetWrapContent, opt);
    JSClass<JSRow>::StaticMethod("justifyContent", &JSRow::SetJustifyContent, opt);
    JSClass<JSRow>::StaticMethod("alignItems", &JSRow::SetAlignItems, opt);
    JSClass<JSRow>::StaticMethod("alignContent", &JSFlex::SetAlignContent, opt);
    JSClass<JSRow>::StaticMethod("height", &JSFlex::JsHeight, opt);
    JSClass<JSRow>::StaticMethod("width", &JSFlex::JsWidth, opt);
    JSClass<JSRow>::StaticMethod("size", &JSFlex::JsSize, opt);
    JSClass<JSRow>::StaticMethod("onTouch", &JSInteractableView::JsOnTouch);
    JSClass<JSRow>::StaticMethod("onHover", &JSInteractableView::JsOnHover);
    JSClass<JSRow>::StaticMethod("onKeyEvent", &JSInteractableView::JsOnKey);
    JSClass<JSRow>::StaticMethod("onDeleteEvent", &JSInteractableView::JsOnDelete);
    JSClass<JSRow>::StaticMethod("onClick", &JSInteractableView::JsOnClick);
    JSClass<JSRow>::StaticMethod("onAppear", &JSInteractableView::JsOnAppear);
    JSClass<JSRow>::StaticMethod("onDisAppear", &JSInteractableView::JsOnDisAppear);
    JSClass<JSRow>::StaticMethod("remoteMessage", &JSInteractableView::JsCommonRemoteMessage);
    JSClass<JSRow>::StaticMethod("pointLight", &JSViewAbstract::JsPointLight, opt);
    JSClass<JSRow>::InheritAndBind<JSContainerBase>(globalObj);

我们这里主要关心create方法,它的实现是JSRow::Create

ini 复制代码
void JSRow::Create(const JSCallbackInfo& info)
{
    std::optional<CalcDimension> space;
    if (info.Length() > 0 && info[0]->IsObject()) {
        JSRef<JSObject> obj = JSRef<JSObject>::Cast(info[0]);
        JSRef<JSVal> spaceVal = obj->GetProperty("space");
        CalcDimension value;
        if (ParseJsDimensionVp(spaceVal, value)) {
            space = value;
        } else if (Container::GreatOrEqualAPIVersion(PlatformVersion::VERSION_TEN)) {
            space = Dimension();
        }
    }
    VerticalAlignDeclaration* declaration = nullptr;
    if (info.Length() > 0 && info[0]->IsObject()) {
        JSRef<JSObject> obj = JSRef<JSObject>::Cast(info[0]);
        JSRef<JSVal> useAlign = obj->GetProperty("useAlign");
        if (useAlign->IsObject()) {
            declaration = JSRef<JSObject>::Cast(useAlign)->Unwrap<VerticalAlignDeclaration>();
        }
    }

    RowModel::GetInstance()->Create(space, declaration, "");
}

最终的实现是RowModel的Create方法,这里传入了两个参数,当前的空间大小space,与对齐方向。在RowModel里面,我们终于看到了第一个熟悉的概念,RowComponent!

Component建立

在model的Create函数中,会通过MakeRefPtr建立一个对应类型的Component,相当于new出来一个对象

rust 复制代码
void RowModelImpl::Create(const std::optional<Dimension>& space, AlignDeclaration* declaration, const std::string& tag)
{
    std::list<RefPtr<Component>> children;
    RefPtr<RowComponent> rowComponent =
        AceType::MakeRefPtr<OHOS::Ace::RowComponent>(FlexAlign::FLEX_START, FlexAlign::CENTER, children);
    ViewStackProcessor::GetInstance()->ClaimElementId(rowComponent);
    rowComponent->SetMainAxisSize(MainAxisSize::MIN);
    rowComponent->SetCrossAxisSize(CrossAxisSize::MIN);
    if (space.has_value() && space->Value() >= 0.0) {
        rowComponent->SetSpace(space.value());
    }
    if (declaration != nullptr) {
        rowComponent->SetAlignDeclarationPtr(declaration);
    }
    ViewStackProcessor::GetInstance()->Push(rowComponent);
}

我们已经在导读篇说过了,Component与Element与RenderNode的关系。这里就比较好理解了,RowComponent主要是设置了当前Row的一些属性,比如主轴与交叉轴的对齐方式,大小等,接着会通过ViewStackProcessor的Push方法把新建立的RowComponent放进去。这里我们也可以知道,RowComponent默认的主轴对齐方式FlexAlign::FLEX_START,同时Row的子Component会被放入一个List中维护。RowComponent为之后的Element生成提供了依据,通过对RowComponent设置属性,很好隔绝了真正渲染逻辑。

创建完成的Component,会被一个叫ViewStackProcessor的单例中。这里有一个非常有趣的点,ViewStackProcessor本身也有一个Pop函数,当满足当前componentsStack_>1并且ShouldPopImmediately方法为true的时候就调用

c 复制代码
void ViewStackProcessor::Push(const RefPtr<Component>& component, bool isCustomView)
{
    CHECK_NULL_VOID(component);
    std::unordered_map<std::string, RefPtr<Component>> wrappingComponentsMap;
    if (componentsStack_.size() > 1 && ShouldPopImmediately()) {
        Pop();
    }
    之后这里存入了一个键值对,main 对应着我们当前创建的component,本例子就是RowComponent
    wrappingComponentsMap.emplace("main", component);
    componentsStack_.push(wrappingComponentsMap

其中ShouldPopImmediately如下

ruby 复制代码
bool ViewStackProcessor::ShouldPopImmediately()
{
    auto type = AceType::TypeName(GetMainComponent());
    auto componentGroup = AceType::DynamicCast<ComponentGroup>(GetMainComponent());
    auto multiComposedComponent = AceType::DynamicCast<MultiComposedComponent>(GetMainComponent());
    auto soleChildComponent = AceType::DynamicCast<SoleChildComponent>(GetMainComponent());
    auto menuComponent = AceType::DynamicCast<MenuComponent>(GetMainComponent());
    return (strcmp(type, AceType::TypeName<TextSpanComponent>()) == 0 ||
            !(componentGroup || multiComposedComponent || soleChildComponent || menuComponent));
}

也就是说,当上一个存入的key为main的Component满足是一个TextSpanComponent 或者都不是(ComponentGroup,MultiComposedComponent,SoleChildComponent,MenuComponent)情况下,就需要Pop。

这里的Pop作用是什么呢?我们继续看Pop的实现

scss 复制代码
void ViewStackProcessor::Pop()
{
    if (componentsStack_.empty() || componentsStack_.size() == 1) {
        return;
    }

    auto component = WrapComponents().first;
    if (AceType::DynamicCast<ComposedComponent>(component)) {
        auto childComponent = AceType::DynamicCast<ComposedComponent>(component)->GetChild();
        SetZIndex(childComponent);
        SetIsPercentSize(childComponent);
    } else {
        SetZIndex(component);
        SetIsPercentSize(component);
    }
    更新每一个RenderComponent的位置
    UpdateTopComponentProps(component);

    componentsStack_.pop();
    判断key为main的Componet是否需要添加把子组件加入自身。本例子是当Text创建时便会被加入Row的子组件
    auto componentGroup = AceType::DynamicCast<ComponentGroup>(GetMainComponent());
    auto multiComposedComponent = AceType::DynamicCast<MultiComposedComponent>(GetMainComponent());
    if (componentGroup) {
        componentGroup->AppendChild(component);
    } else if (multiComposedComponent) {
        multiComposedComponent->AddChild(component);
    } else {
        auto singleChild = AceType::DynamicCast<SingleChild>(GetMainComponent());
        if (singleChild) {
            singleChild->SetChild(component);
        }
    }
}

可以看到具体的目的,有以下两个

  1. 确定z轴关系:子组件在父组件之上,同时更新顺序
  2. 确定父子关系:根据Component的不同调用不同的方法加入子组件

Component按照自身的一些特定,分为RenderComponent与BaseComposedComponent。RenderComponent具备渲染能力,比如Text。而BaseComposedComponent只是具备组合能力,比如ForEach。

RenderComponent 除了具备渲染能力之外,还有具备添加子控件的子类,叫做ComponentGroup,比如RowComponent就是ComponentGroup的子类

BaseComposedComponent的子类中,具备组合多个Component能力的是MultiComposedComponent,比如比如ForEach,本身是不具备渲染能力,目的是进行逻辑切换。

在本例子中,RowComponent继承于FlexComponent,FlexComponent继承于ComponentGroup,因此它具有添加子Component的能力。也就是说,每次添加Component且满足一定条件时,或者主动调用Pop时,都会进行一次父子关系的建立与z轴方向的确立。

这里我特点还标注了一下主动调用这一场景。还记得我们一开始说过的observeComponentCreation方法之后便会调用pop函数

scss 复制代码
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Text.create("文本2");
            if (!isInitialRender) {
                Text.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        Text.pop();
        Row.pop();

这里面Row的pop方法最终在C++的实现是

arduino 复制代码
class JSContainerBase : public JSViewAbstract, public JSInteractableView {
public:
    static void Pop();
    static void JSBind(BindingTarget globalObj);
};

JSRow其实就继承于JSContainerBase,因此调用的pop方法实际就是JSContainerBase的Pop方法

css 复制代码
void JSContainerBase::Pop()
{
    ViewStackModel::GetInstance()->PopContainer();
}

最终!JSContainerBase的Pop调到了ViewStackProcessor的PopContainer方法然后又到了Pop方法

ruby 复制代码
void ViewStackProcessor::PopContainer()
{
    auto type = AceType::TypeName(GetMainComponent());
    auto componentGroup = AceType::DynamicCast<ComponentGroup>(GetMainComponent());
    auto multiComposedComponent = AceType::DynamicCast<MultiComposedComponent>(GetMainComponent());
    auto soleChildComponent = AceType::DynamicCast<SoleChildComponent>(GetMainComponent());
    if ((componentGroup && strcmp(type, AceType::TypeName<TextSpanComponent>()) != 0) || multiComposedComponent ||
        soleChildComponent) {
        Pop();
        return;
    }

    while ((!componentGroup && !multiComposedComponent && !soleChildComponent) ||
           strcmp(type, AceType::TypeName<TextSpanComponent>()) == 0) {
        if (componentsStack_.size() <= 1) {
            break;
        }
        Pop();
        type = AceType::TypeName(GetMainComponent());
        componentGroup = AceType::DynamicCast<ComponentGroup>(GetMainComponent());
        multiComposedComponent = AceType::DynamicCast<MultiComposedComponent>(GetMainComponent());
        soleChildComponent = AceType::DynamicCast<SoleChildComponent>(GetMainComponent());
    }
    Pop();
}

整个create到pop的过程,我们就分析完了,可以看到,整个过程还是非常长的,从js到c++,最终完成了整个闭环。

Element创建

我们之前也说过,Component会通过CreateElement方法生成对应的Element

arduino 复制代码
class ACE_EXPORT Component : public virtual AceType {
    DECLARE_ACE_TYPE(Component, AceType);

public:
    Component();
    ~Component() override;

    virtual RefPtr<Element> CreateElement() = 0;

Element的创建是在InflateComponent函数,当调用InflateComponent函数时就会根据Component创建对应的Element

scss 复制代码
RefPtr<Element> Element::InflateComponent(const RefPtr<Component>& newComponent, int32_t slot, int32_t renderSlot)
{
    // confirm whether there is a reuseable element.
    auto retakeElement = RetakeDeactivateElement(newComponent);
    if (retakeElement) {
        retakeElement->SetNewComponent(newComponent);
        retakeElement->Mount(AceType::Claim(this), slot, renderSlot);
        if (auto node = retakeElement->GetRenderNode()) {
            node->SyncRSNode(node->GetRSNode());
        }
        return retakeElement;
    }

    RefPtr<Element> newChild = newComponent->CreateElement();
    if (newChild) {
        newChild->SetNewComponent(newComponent);
        newChild->Mount(AceType::Claim(this), slot, renderSlot);
    }
    return newChild;
}

总结

本篇我们通过一个简单的例子HelloArkUI完成了如何从一个普通ViewPU到一个Component的建立过程,所有对于ViewPU的属性都会反应到Component得属性设置,Component为了之后构建Element与RenderNode建立了基础。通过学习Component与ViewPU的关系,希望读者能够运用这些知识分析其他的控件,我们将会在下一篇分析Element与RenderNode的渲染过程。

相关推荐
QGC二次开发2 分钟前
Vue3 : Pinia的性质与作用
前端·javascript·vue.js·typescript·前端框架·vue
云草桑13 分钟前
逆向工程 反编译 C# net core
前端·c#·反编译·逆向工程
布丁椰奶冻19 分钟前
解决使用nvm管理node版本时提示npm下载失败的问题
前端·npm·node.js
AntDreamer32 分钟前
在实际开发中,如何根据项目需求调整 RecyclerView 的缓存策略?
android·java·缓存·面试·性能优化·kotlin
Leyla44 分钟前
【代码重构】好的重构与坏的重构
前端
影子落人间1 小时前
已解决npm ERR! request to https://registry.npm.taobao.org/@vant%2farea-data failed
前端·npm·node.js
世俗ˊ1 小时前
CSS入门笔记
前端·css·笔记
子非鱼9211 小时前
【前端】ES6:Set与Map
前端·javascript·es6
6230_1 小时前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人1 小时前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js