查看原文
其他

OpenHarmony源码系列: 鸿蒙页面背后的机制,打通 JS View 与C++世界

Pika 鸿洋
2024-08-24

本文作者


作者:Pika

链接:

https://juejin.cn/post/7347221041569218611

本文由作者授权发布。


注意鸿蒙系统一直在更新,源码可能会有变更,但是核心体系结构变化不会太大,依旧可以帮助大家建立对鸿蒙源码体系的认知。



引言

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

1ViewPU创建过程


ArkUI中,Component是一个个页面的表示,接下来我们以最简单的例子,为大家介绍一下ArkUI背后的秘密。我们声明了一个名为HelloArkUI的Componet,内容如下:
@Component
struct HelloArkUI{
  build(){
    Row(){
      Text("文本1")
      Text("文本2")
    }
  }
}
我们在导读篇说过,一个简单的Component,最终会被编译成一个集成于ViewPU或者View的class,通过反编译,我们可以看到,HelloArkUI继承于ViewPU。
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,用于触发组件的生成。
  protected abstract initialRender(): void;
  protected abstract rerender(): void;
HelloArkUI这个Component,是由build函数内几个基础组件组成的,它实现了initialRender函数。我们也留意到,这里面会根据里面build函数内声明的组件数量,依次执行了observeComponentCreation方法,这个方法实现如下:
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举例子。
this.observeComponentCreation((elmtId, isInitialRender) => {
    ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
    Row.create();
    初始化不执行这里
    if (!isInitialRender) {
        Row.pop();
    }
    ViewStackProcessor.StopGetAccessRecording();
});
第一次创建的时候,isInitialRender会被赋值为true,因此当前组件的pop函数是不会在observeComponentCreation过程中被执行的,而是等当前子组件依次创建完成之后,才会调用pop函数。为什么会是这样呢?其实最大的目的就是确立了视图的层级关系与组件父子关系,Row展示的部分一定是在Text之后,我们之后会说到。

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

2js view绑定


在整个engine初始化的时候,会通过JsBindFormViews方法通过绑定关系把js与C++进行绑定,读者们可以通过这个方法跟踪整个引擎初始化过程,我们这边只关注创建过程。
每个控件都有自己的JSBind方法,通过这里建立起js与C++的方法映射关系。
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
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出来一个对象。
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的时候就调用。
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如下:
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的实现。
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按照自身的一些特定,分为RenderComponentBaseComposedComponentRenderComponent具备渲染能力,比如Text。而BaseComposedComponent只是具备组合能力,比如ForEach。
RenderComponent 除了具备渲染能力之外,还有具备添加子控件的子类,叫做ComponentGroup,比如RowComponent就是ComponentGroup的子类
BaseComposedComponent的子类中,具备组合多个Component能力的是MultiComposedComponent,比如比如ForEach,本身是不具备渲染能力,目的是进行逻辑切换。
在本例子中,RowComponent继承于FlexComponentFlexComponent继承于ComponentGroup,因此它具有添加子Component的能力。也就是说,每次添加Component且满足一定条件时,或者主动调用Pop时,都会进行一次父子关系的建立与z轴方向的确立。
这里我特点还标注了一下主动调用这一场景。还记得我们一开始说过的observeComponentCreation方法之后便会调用pop函数。
this.observeComponentCreation((elmtId, isInitialRender) => {
    ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
    Text.create("文本2");
    if (!isInitialRender) {
        Text.pop();
    }
    ViewStackProcessor.StopGetAccessRecording();
});
Text.pop();
Row.pop();
这里面Row的pop方法最终在C++的实现是:
class JSContainerBase : public JSViewAbstract, public JSInteractableView {
public:
    static void Pop();
    static void JSBind(BindingTarget globalObj);
};
JSRow其实就继承于JSContainerBase,因此调用的pop方法实际就是JSContainerBasePop方法。
void JSContainerBase::Pop()
{
    ViewStackModel::GetInstance()->PopContainer();
}
最终!JSContainerBasePop调到了ViewStackProcessorPopContainer方法然后又到了Pop方法。
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。
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。
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;
}


3总结


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


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

优雅实现网络请求:协程+Flow+Retrofit+OkHttp
Android 7 种方式实现自定义ViewGroup的滚动与惯性滚动
Android ServiceManager和它的兄弟们


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

继续滑动看下一个
鸿洋
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存