本文为稀土掘金技术社区首发签约文章,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);
}
- 通过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_));
}
}
}
- 通过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")
}
}
}
- 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++整个链路走下去。