ArkUI Engine - UI重新绘制

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

前文概要

通过前两篇文章,我们知道了一个@Component 修饰的TS类是如何生成UI的具体过程。在ArkUI中,每一个用ts编写的控件,都可以在C++中找到最终的Component映射。接下来我们将继续学习,ArkUI中,如果UI发生改变时,engine是如何完成Component与Element的刷新。学习完本篇,你将会了解到@State 装饰器修饰的变量如何触发UI的刷新,同时也能够了解到Element是如何驱动元素发生更新。

UI重新绘制

我们把前两章的Index例子做一个小改动,在这里我们添加一个isShow的State变量,当isShow为True时,会显示父布局为Column,Text内容为"Column"的一个文本。当isShow为false时,父布局为Row,Text内容为"Row"。同时我们也添加一个点击事情,当每次点击时,都会把当前isShow变量赋值为相反值,代码如下:

scss 复制代码
@Component
struct Index {
  @State isShow:Boolean = false
build() {
    Row() {
      if (this.isShow){
        Column() {
          Text("Column")
        }
      }else {
        Row(){
          Text("Row")
        }
      }
    }
    .height('100%')
    .onClick((event)=>{
      this.isShow = !this.isShow
    })
  }
}

这里例子非常简单,当触发UI刷新时,Text的内容与父容器都会发生改变,产生一次UI的刷新。对应的abc字节码反编译代码如下:

scss 复制代码
class Index extends ViewPU {
    //构造函数
    constructor(parent, params, __localStorage, elmtId = -1) {
        super(parent, __localStorage, elmtId);
        // 创建一个ObservedPropertyObjectPU对象,它是后面UI刷新的起点,其中创建了一个隐藏变量__isShow
        this.__isShow = new ObservedPropertyObjectPU(false, this, "isShow");
        this.setInitiallyProvidedValue(params);
    }
    // 初始化isShow的值为指定的默认值false
    setInitiallyProvidedValue(params) {
        if (params.isShow !== undefined) {
            this.isShow = params.isShow;
        }
    }
    updateStateVars(params) {
    }
    purgeVariableDependenciesOnElmtId(rmElmtId) {
        this.__isShow.purgeDependencyOnElmtId(rmElmtId);
    }
    aboutToBeDeleted() {
        this.__isShow.aboutToBeDeleted();
        SubscriberManager.Get().delete(this.id__());
        this.aboutToBeDeletedInternal();
    }
    get isShow() {
        return this.__isShow.get();
    }
    set isShow(newValue) {
        this.__isShow.set(newValue);
    }
    initialRender() {
        //observeComponentCreation 构造组件树
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Row.create();
            Row.height('100%');
            Row.onClick((event) => {
                // 发生改变时,调用set isShow(newValue) 方法
                this.isShow = !this.isShow;
            });
            if (!isInitialRender) {
                Row.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            If.create();
            if (this.isShow) {
                this.ifElseBranchUpdateFunction(0, () => {
                    this.observeComponentCreation((elmtId, isInitialRender) => {
                        ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
                        Column.create();
                        if (!isInitialRender) {
                            Column.pop();
                        }
                        ViewStackProcessor.StopGetAccessRecording();
                    });
                    this.observeComponentCreation((elmtId, isInitialRender) => {
                        ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
                        Text.create("Column");
                        if (!isInitialRender) {
                            Text.pop();
                        }
                        ViewStackProcessor.StopGetAccessRecording();
                    });
                    Text.pop();
                    Column.pop();
                });
            }
            else {
                this.ifElseBranchUpdateFunction(1, () => {
                    this.observeComponentCreation((elmtId, isInitialRender) => {
                        ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
                        Row.create();
                        if (!isInitialRender) {
                            Row.pop();
                        }
                        ViewStackProcessor.StopGetAccessRecording();
                    });
                    this.observeComponentCreation((elmtId, isInitialRender) => {
                        ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
                        Text.create("Row");
                        if (!isInitialRender) {
                            Text.pop();
                        }
                        ViewStackProcessor.StopGetAccessRecording();
                    });
                    Text.pop();
                    Row.pop();
                });
            }
            if (!isInitialRender) {
                If.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        If.pop();
        Row.pop();
    }
    rerender() {
        this.updateDirtyElements();
    }
}

学习完上一章后,我们对以上代码一定不陌生。当定义了一个@State 装饰器修饰的变量时,其实会在对应的js类中,同步生成一个状态属性,比如Link还是其他的装饰器也一样,这里State生成的是ObservedPropertyObjectPU类型的变量,同时也会根据@State修饰的变量名添加"__"作为ObservedPropertyObjectPU对象的名称。当isShow变量改变时,最终会驱动ObservedPropertyObjectPU对象触发UI刷新,这里的关键就是ObservedPropertyObjectPU持有了当前Index类的this指针

ObservedPropertyObjectPU 类继承于ObservedPropertyPU,关键的刷新方法都在ObservedPropertyPU中

scala 复制代码
// class definitions for backward compatibility
class ObservedPropertyObjectPU<T> extends ObservedPropertyPU<T> {

}
scala 复制代码
class ObservedPropertyPU<T> extends ObservedPropertyAbstractPU<T>
  implements PeerChangeEventReceiverPU<T>, ObservedObjectEventsPUReceiver<T> {

  private wrappedValue_: T;
  
  constructor(localInitValue: T, owningView: IPropertySubscriber, propertyName: PropertyInfo) {
    super(owningView, propertyName);
   
    this.setValueInternal(localInitValue);
  }

本质上,ObservedPropertyPU持有了owningView的指针,当属性发生改变时,其实会通过监听变化,触发一系列的回调,比如当属性值发生改变时,会触发notifyPropertyHasChangedPU方法

kotlin 复制代码
  protected notifyPropertyHasChangedPU() {
    stateMgmtProfiler.begin("ObservedPropertyAbstractPU.notifyPropertyHasChangedPU");
    stateMgmtConsole.debug(`${this.debugInfo()}: notifyPropertyHasChangedPU.`)
    if (this.owningView_) {
      if (this.delayedNotification_ == ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay) {
        // send viewPropertyHasChanged right away
        //这里的owningView_就是传入的this
        this.owningView_.viewPropertyHasChanged(this.info_, this.dependentElmtIdsByProperty_.getAllPropertyDependencies());
      } else {
        // mark this @StorageLink/Prop or @LocalStorageLink/Prop variable has having changed and notification of viewPropertyHasChanged delivery pending
        this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending;
      }
    }

通过持有owningView_,当属性发生改变的时候,就可以让owningView_触发UI重新绘制,viewPropertyHasChanged调用如下:

kotlin 复制代码
viewPropertyHasChanged(varName: PropertyInfo, dependentElmtIds: Set<number>): void {
      省略前面无关代码

      if (dependentElmtIds.size && !this.isFirstRender()) {
        if (!this.dirtDescendantElementIds_.size && !this.runReuse_) {
          // mark ComposedElement dirty when first elmtIds are added
          // do not need to do this every time
          this.markNeedUpdate();
        }

最终,调用的是markNeedUpdate 方法。markNeedUpdate方法我们在导读篇有介绍,它是整个ArkUI更新模型NativeViewPartialUpdate中的一个基类方法,内容实现是在C++层中

scss 复制代码
declare class NativeViewPartialUpdate {
  constructor(    );
  markNeedUpdate(): void;
  findChildById(compilerAssignedUniqueChildId: string): View;
  syncInstanceId(): void;
  isFirstRender(): boolean;
  restoreInstanceId(): void;
  static create(newView: NativeViewPartialUpdate): void;
  finishUpdateFunc(elmtId: number): void;
  isLazyItemRender(elmtId : number) : boolean;
  setCardId(cardId: number): void;
  getCardId(): number;
  resetRecycleCustomNode(): void;
}

对应的C++实现为

ruby 复制代码
void JSViewPartialUpdate::JSBind(BindingTarget object)
{
    JSClass<JSViewPartialUpdate>::Declare("NativeViewPartialUpdate");
    MethodOptions opt = MethodOptions::NONE;

    JSClass<JSViewPartialUpdate>::StaticMethod("create", &JSViewPartialUpdate::Create, opt);
    JSClass<JSViewPartialUpdate>::StaticMethod("createRecycle", &JSViewPartialUpdate::CreateRecycle, opt);
    JSClass<JSViewPartialUpdate>::Method("markNeedUpdate", &JSViewPartialUpdate::MarkNeedUpdate);
    JSClass<JSViewPartialUpdate>::Method("syncInstanceId", &JSViewPartialUpdate::SyncInstanceId);
    JSClass<JSViewPartialUpdate>::Method("restoreInstanceId", &JSViewPartialUpdate::RestoreInstanceId);
    JSClass<JSViewPartialUpdate>::CustomMethod("getInstanceId", &JSViewPartialUpdate::GetInstanceId);
    JSClass<JSViewPartialUpdate>::Method("markStatic", &JSViewPartialUpdate::MarkStatic);
    JSClass<JSViewPartialUpdate>::Method("finishUpdateFunc", &JSViewPartialUpdate::JsFinishUpdateFunc);
    JSClass<JSViewPartialUpdate>::Method("setCardId", &JSViewPartialUpdate::JsSetCardId);
    JSClass<JSViewPartialUpdate>::CustomMethod("getCardId", &JSViewPartialUpdate::JsGetCardId);
    JSClass<JSViewPartialUpdate>::Method("elmtIdExists", &JSViewPartialUpdate::JsElementIdExists);
    JSClass<JSViewPartialUpdate>::CustomMethod("isLazyItemRender", &JSViewPartialUpdate::JSGetProxiedItemRenderState);
    JSClass<JSViewPartialUpdate>::CustomMethod("isFirstRender", &JSViewPartialUpdate::IsFirstRender);
    JSClass<JSViewPartialUpdate>::CustomMethod(
        "findChildByIdForPreview", &JSViewPartialUpdate::FindChildByIdForPreview);
    JSClass<JSViewPartialUpdate>::CustomMethod(
        "resetRecycleCustomNode", &JSViewPartialUpdate::JSResetRecycleCustomNode);
    JSClass<JSViewPartialUpdate>::CustomMethod(
        "queryNavDestinationInfo", &JSViewPartialUpdate::JSGetNavDestinationInfo);
    JSClass<JSViewPartialUpdate>::CustomMethod("getUIContext", &JSViewPartialUpdate::JSGetUIContext);
    JSClass<JSViewPartialUpdate>::InheritAndBind<JSViewAbstract>(object, ConstructorCallback, DestructorCallback);
}

markNeedUpdate

当UI需要重新绘制的时候,就会调用markNeedUpdate进行标记,

css 复制代码
void JSViewPartialUpdate::MarkNeedUpdate()
{
    needsUpdate_ = ViewPartialUpdateModel::GetInstance()->MarkNeedUpdate(viewNode_);
}

ViewPartialUpdateModel的实现有几种,默认模型为ViewPartialUpdateModelImpl

scss 复制代码
bool ViewPartialUpdateModelImpl::MarkNeedUpdate(const WeakPtr<AceType>& node)
{
    ACE_SCOPED_TRACE("JSView::MarkNeedUpdate");
    auto weakElement = AceType::DynamicCast<ComposedElement>(node);
    if (weakElement.Invalid()) {
        LOGE("Invalid Element weak ref, internal error");
        return false;
    }
    //走到这里返回值就永远是true了
    auto element = weakElement.Upgrade();
    if (element) {
        element->MarkDirty();
    }
    return true;
}

首先是拿到当前元素的element,如果它是ComposedElement的话,就可以调用当前element的MarkDirty方法。这里需要注意的是,element是ComposedElement才需要标记为dirty。

element 的类型取决于对应的Component,我们在JSView 与C++这篇说过,Component会通过CreateElement 创建对应的Element,这两者的关系将贯穿整个ArkUI中。

这里有一个细节需要我们注意,这里的element它并不一定是ComposedElement,但是无论是与否返回结果都是true,都需要更新。

如果需要更新,会在每个jsview进行刷新时调用ExecuteRerender方法,触发rerender过程。

scss 复制代码
    auto updateFunction = [weak = AceType::WeakClaim(this)]() -> void {
        auto jsView = weak.Upgrade();
        CHECK_NULL_VOID(jsView);
        ContainerScope scope(jsView->GetInstanceId());
        if (!jsView->needsUpdate_) {
            return;
        }
        jsView->needsUpdate_ = false;
        {
            ACE_SCOPED_TRACE("JSView: ExecuteRerender");
            jsView->jsViewFunction_->ExecuteRerender();
        }
        for (const UpdateTask& updateTask : jsView->pendingUpdateTasks_) {
            ViewPartialUpdateModel::GetInstance()->FlushUpdateTask(updateTask);
        }
        jsView->pendingUpdateTasks_.clear();
    };

这里的rerender 对应着上文反编译的rerender函数,由C++发起到js的调用。rerender方法通常是由arkcomplier自动生成,不对开发者暴露,方便后续的绘制调整

javascript 复制代码
    生成类中
    rerender() {
        this.updateDirtyElements();
    }

rerender过程还是比较简单的,同时与MarkNeedUpdate之后的流程有相交关系,读者可以自行阅读,这里先回到我们的MarkNeedUpdate主线。

ComposedElement对应的是ComposedComponent,它并不具备渲染能力,但是具备子控件的逻辑处理能力。

kotlin 复制代码
// ComposedElement just maintain a child element may have render node.
class ACE_EXPORT ComposedElement : public Element {
    DECLARE_ACE_TYPE(ComposedElement, Element);

相反,具备渲染能力的是RenderElement

kotlin 复制代码
// RenderElement will have a RenderNode and displays in the screen.
class ACE_EXPORT RenderElement : public Element {
    DECLARE_ACE_TYPE(RenderElement, Element);

这里的MarkDirty会把当前加入element加入dirty列表,并标记需要Rebuild

scss 复制代码
void Element::MarkDirty()
{
    RefPtr<PipelineContext> context = context_.Upgrade();
    if (context) {
        context->AddDirtyElement(AceType::Claim(this));
        MarkNeedRebuild();
    }
}

我们最外层的这个Index,其实就是组合了所有可渲染的Row,Column等RenderElement,因此接下来就是等engine进行rebuild

Rebuild

Rebuild过程中,有着最关键的3步

scss 复制代码
void Element::Rebuild()
{ 
    //判断是否需要被rebuild
    if (!needRebuild_) {
        return;
    }

    needRebuild_ = false;

    // When rebuild comes, newComponent_ should not be null, and will go to these 3 steps:
    // 1. Update self using new component
    // 2. PerformBuild will build and update child recursively
    // 3. Finish update and release the new component
    Update();
    PerformBuild();
    SetNewComponent(nullptr);
}
  1. 通过Update方法,使用newComponent_这个变量去进行调整,我们还是以ComposedElement举例子,通过当前最新的component_,去更新整个element树,同时清除MarkNeedUpdate函数中的needupdate标记。
scss 复制代码
void ComposedElement::Update()
{
    const RefPtr<ComposedComponent> compose = AceType::DynamicCast<ComposedComponent>(component_);
    if (compose != nullptr) {
        name_ = compose->GetName();
        SetElementId(compose->GetElementId());
        if (id_ != compose->GetId()) {
            auto context = context_.Upgrade();
            if (addedToMap_ && context != nullptr) {
                context->RemoveComposedElement(id_, AceType::Claim(this));
                context->AddComposedElement(compose->GetId(), AceType::Claim(this));
            }
            id_ = compose->GetId();
        }
        compose->ClearNeedUpdate();
    }
    if (HasPageTransitionFunction()) {
        auto pageElement = GetPageElement();
        if (pageElement) {
            pageElement->SetPageTransitionFunction(std::move(pageTransitionFunction_));
        }
    }
}
  1. 通过PerformBuild方法,进行当前范围内的子控件重建,ComposedComponent为例子就是需要通知子控件进行更新
scss 复制代码
void ComposedElement::PerformBuild()
{
    auto context = context_.Upgrade();
    .... 
    auto child = children_.empty() ? nullptr : children_.front();
    auto composedComponent = AceType::DynamicCast<ComposedComponent>(component_);
    if (composedComponent) {
        auto composedChild = composedComponent->GetChild();
        if (HasRenderFunction() && composedComponent->GetNeedReserveChild()) {
            auto flexItem = AceType::DynamicCast<SoleChildComponent>(composedChild);
            if (flexItem) {
                flexItem->SetChild(component);
                UpdateChild(child, flexItem);
                return;
            }
        }
    }

    UpdateChild(child, component);
}

UpdateChild最终调用UpdateChildWithSlot 方法进行重建,这里面的逻辑也比较有意思,判断UI是更新还是删除还是新建,其实都是根据child 与当前newComponent决定的,下面是4种case:分别对应着:空状态,新增,删除,更新流程,从而改变整个element树

scss 复制代码
RefPtr<Element> Element::UpdateChildWithSlot(
    const RefPtr<Element>& child, const RefPtr<Component>& newComponent, int32_t slot, int32_t renderSlot)
{
    // Considering 4 cases:
    // 1. child == null && newComponent == null  -->  do nothing
    如果Element 为 null 并且 Component为null,则什么也不做
    if (!child && !newComponent) {
        return nullptr;
    }

    // 2. child == null && newComponent != null  -->  create new child configured with newComponent
    新增:child == null && newComponent != null:通过Component建立对应的element
    if (!child) {
        auto newChild = InflateComponent(newComponent, slot, renderSlot);
        ElementRegister::GetInstance()->AddElement(newChild);
        return newChild;
    }

    // 3. child != null && newComponent == null  -->  remove old child
   删除:child != null && newComponent == null:移除elemnt
    if (!newComponent) {
        ElementRegister::GetInstance()->RemoveItemSilently(child->GetElementId());
        DeactivateChild(child);
        return nullptr;
    }

    // 4. child != null && newComponent != null  -->  update old child with new configuration if possible(determined by
    //    [Element::CanUpdate]), or remove the old child and create new one configured with newComponent.
    更新:child != null && newComponent != null
    auto context = context_.Upgrade();
    不支持更新,那么删除旧的element,添加新的element
    if (!child->CanUpdate(newComponent)) {
        // Can not update
        auto needRebuildFocusElement = AceType::DynamicCast<Element>(GetFocusScope());
        if (context && needRebuildFocusElement) {
            context->AddNeedRebuildFocusElement(needRebuildFocusElement);
        }
        ElementRegister::GetInstance()->RemoveItemSilently(child->GetElementId());
        DeactivateChild(child);
        auto newChild = InflateComponent(newComponent, slot, renderSlot);
        ElementRegister::GetInstance()->AddElement(newChild);
        return newChild;
    }
    
    .....
    能够更新
     auto newChild = DoUpdateChildWithNewComponent(child, newComponent, slot, renderSlot);
    if (newChild != nullptr) {
        newChild->SetElementId(newComponent->GetElementId());
        ElementRegister::GetInstance()->AddElement(newChild);
    }
    return newChild;

    ..... 
    return newChild;
}

值得注意的是更新流程,更新流程取决于element的CanUpdate方法,分为不可更新与能够更新两种情况。

不可更新 可更新
删除当前element树的旧element,然后通过Component生成一个新的element再加入 通过新的Component配置去更新element

这里的CanUpdate取决于具体Element,我们拿ifelse来说

如果isShow都是true,那么branchId就是同一个,就可以直接走更新流程,提高效率,而不是删除旧的element再添加

scss 复制代码
Row() {
      if (this.isShow){
        Column() {
          Text("Column")
        }
      }else {
        Row(){
          Text("Row")
        }
      }
    }
  1. SetNewComponent:把需要更新的Compoent设置为null,component_就为null,不会再次发起rebuild
ini 复制代码
    virtual void SetNewComponent(const RefPtr<Component>& newComponent)
    {
        component_ = newComponent;
        if (newComponent) {
            retakeId_ = newComponent->GetRetakeId();
            componentTypeId_ = AceType::TypeId(component_);
            ignoreInspector_ = newComponent->IsIgnoreInspector();
            SetElementId(newComponent->GetElementId());
            MarkNeedRebuild();
        }
    }

总结

通过以一个@State装饰器例子出发,我们能够学习到ArkUI绘制链路的一个全过程,通过学习这个链路,读者应该能更加明白Componet与 Element的联系。对于想要接下来学习任何ArkUI的控件源码,我们都可以通过这个思路,从js到c++整个链路走下去。

相关推荐
sunly_2 小时前
Flutter:AnimatedPadding动态修改padding
android·flutter
AlgorithmAce2 小时前
Live2D嵌入前端页面
前端
nameofworld2 小时前
前端面试笔试(六)
前端·javascript·面试·学习方法·递归回溯
诸神黄昏EX2 小时前
Android 常用命令和工具解析之GPU相关
android
顾北川_野2 小时前
Android 布局菜单或按钮图标或Menu/Item设置可见和不可见
android
练习本2 小时前
android 动画原理分析
android
别拿曾经看以后~2 小时前
原生Android调用uniapp项目中的方法
android·vue.js·uni-app
Winston Wood2 小时前
Android Binder技术概览
android·binder·进程通信
踏雪羽翼2 小时前
android 使用实现音效--Equalizer
android·音效·eqequalizer·bassboost·presetreverb
老码沉思录2 小时前
Android开发实战班 - Android开发基础之 Kotlin语言基础与特性
android·微信·kotlin