鴻蒙(HarmonyOS)應(yīng)用性能優(yōu)化實(shí)戰(zhàn)-組件復(fù)用四板斧

概述

在滑動(dòng)場(chǎng)景下,常常會(huì)對(duì)同一類(lèi)自定義組件的實(shí)例進(jìn)行頻繁的創(chuàng)建與銷(xiāo)毀。此時(shí)可以考慮通過(guò)組件復(fù)用減少頻繁創(chuàng)建與銷(xiāo)毀的能耗。組件復(fù)用時(shí),可能存在許多影響組件復(fù)用效率的操作,本篇文章將重點(diǎn)介紹如何通過(guò)組件復(fù)用四板斧提升復(fù)用性能。

組件復(fù)用四板斧:

  • 第一板斧,減少組件復(fù)用的嵌套層級(jí),如果在復(fù)用的自定義組件中再嵌套自定義組件,會(huì)存在節(jié)點(diǎn)構(gòu)造的開(kāi)銷(xiāo),且需要在每個(gè)嵌套的子組件中的aboutToReuse方法中實(shí)現(xiàn)數(shù)據(jù)的刷新,造成耗時(shí)。
  • 第二板斧,優(yōu)化狀態(tài)管理,精準(zhǔn)控制組件刷新范圍,在復(fù)用的場(chǎng)景下,需要控制狀態(tài)變量的刷新范圍,避免擴(kuò)大刷新范圍,降低組件復(fù)用的效率。
  • 第三板斧,復(fù)用組件嵌套結(jié)構(gòu)會(huì)變更的場(chǎng)景,使用reuseId標(biāo)記不同結(jié)構(gòu)的組件構(gòu)成,如:使用if else結(jié)構(gòu)來(lái)控制組件的創(chuàng)建,會(huì)造成組件樹(shù)結(jié)構(gòu)的大幅變動(dòng),降低組件復(fù)用的效率。需使用reuseId標(biāo)記不同的組件結(jié)構(gòu),提升復(fù)用性能。
  • 第四板斧,不要使用函數(shù)/方法作為復(fù)用組件的入?yún)?/strong>,復(fù)用時(shí)會(huì)觸發(fā)組件的構(gòu)造,如果函數(shù)入?yún)⒅写嬖诤臅r(shí)操作,會(huì)影響復(fù)用性能。

組件復(fù)用原理機(jī)制

  1. 如上圖①中,ListItem N-1滑出可視區(qū)域即將銷(xiāo)毀時(shí),如果標(biāo)記了@Reusable,就會(huì)進(jìn)入這個(gè)自定義組件所在父組件的復(fù)用緩存區(qū)。需注意在自定義組件首次顯示時(shí),不會(huì)觸發(fā)組件復(fù)用。后續(xù)創(chuàng)建新組件節(jié)點(diǎn)時(shí),會(huì)復(fù)用緩存區(qū)中的節(jié)點(diǎn),節(jié)約組件重新創(chuàng)建的時(shí)間。尤其是該復(fù)用組件具有相同的布局結(jié)構(gòu),僅有某些數(shù)據(jù)差異時(shí),通過(guò)組件復(fù)用可以提高列表頁(yè)面的加載速度和響應(yīng)速度。

  2. 如上圖②中,復(fù)用緩存池是一個(gè)Map套Array的數(shù)據(jù)結(jié)構(gòu),以reuseId為key,具有相同reuseId的組件在同一個(gè)Array中。如未設(shè)置reuseId,則reuseId默認(rèn)是自定義組件的名字。

  3. 如上圖③中,發(fā)生復(fù)用行為時(shí),會(huì)自動(dòng)遞歸調(diào)用復(fù)用池中取出的自定義組件的aboutToReuse回調(diào),應(yīng)用可以在這個(gè)時(shí)候刷新數(shù)據(jù)。

第一板斧,減少組件復(fù)用的嵌套層級(jí)

在組件復(fù)用場(chǎng)景下,過(guò)深的自定義組件的嵌套會(huì)增加組件復(fù)用的使用難度,比如需要逐個(gè)實(shí)現(xiàn)所有嵌套組件中aboutToReuse回調(diào)實(shí)現(xiàn)數(shù)據(jù)更新;因此推薦優(yōu)先使用@Builder替代自定義組件,減少嵌套層級(jí),利于維護(hù)切能提升頁(yè)面加載速度。正反例如下:

反例:

@Entry
@Component
struct ReduceLevel {
  private data: BasicDateSource = new BasicDateSource();

  aboutToAppear(): void {
    for (let index = 0; index < 30; index++) {
      this.data.pushData(index.toString())
    }
  }

  build() {
    Column() {
      List() {
        LazyForEach(this.data, (item: string) => {
          ListItem() {
            //反例 使用自定義組件
            ComponentA({ desc: item })
          }
        }, (item: string) => item)
      }
    }
  }
}

@Reusable
@Component
struct ComponentA {
  @State desc: string = '';

  aboutToReuse(params: ESObject): void {
    this.desc = params.desc as string;
  }

  build() {
    // 在復(fù)用組件中嵌套使用自定義組件
    ComponentB({ desc: this.desc })
  }
}


@Component
struct ComponentB {
  @State desc: string = '';
  // 嵌套的組件中也需要實(shí)現(xiàn)aboutToReuse來(lái)進(jìn)行UI的刷新
  aboutToReuse(params: ESObject): void {
    this.desc = params.desc as string;
  }

  build() {
    Column() {
      Text('子組件' + this.desc)
        .fontSize(30)
        .fontWeight(30)
    }
  }
}

上述反例的操作中,在復(fù)用的自定義組件中嵌套了新的自定義組件。ArkUI中使用自定義組件時(shí),在build階段將在在后端FrameNode樹(shù)創(chuàng)建一個(gè)相應(yīng)的CustomNode節(jié)點(diǎn),在渲染階段時(shí)也會(huì)創(chuàng)建對(duì)應(yīng)的RenderNode節(jié)點(diǎn)。會(huì)造成組件復(fù)用下,CustomNode創(chuàng)建和和RenderNod渲染e的耗時(shí)。且嵌套的自定義組件ComponentB,也需要實(shí)現(xiàn)aboutToReuse來(lái)進(jìn)行數(shù)據(jù)的刷新。

正例:

@Entry
@Component
struct ReduceLevel {
  private data: BasicDateSource = new BasicDateSource();

  aboutToAppear(): void {
    for (let index = 0; index < 30; index++) {
      this.data.pushData(index.toString())
    }
  }

  build() {
    Column() {
      List() {
        LazyForEach(this.data, (item: string) => {
          ListItem() {
            //  正例
            ChildComponent({ desc: item })
          }
        }, (item: string) => item)
      }
    }
  }
}

// 正例 使用組件復(fù)用
@Reusable
@Component
struct ChildComponent {
  @State desc: string = '';

  aboutToReuse(params: Record<string, Object>): void {
    this.desc = params.desc as string;
  }

  build() {
    Column() {
      // 使用@Builder,可以減少自定義組件創(chuàng)建和渲染的耗時(shí)
      ChildComponentBuilder({ paramA: this.desc })
    }
  }
}

class Temp {
  paramA: string = '';
}

@Builder
function ChildComponentBuilder($$: Temp) {
  Column() {
    // 此處使用`${}`來(lái)進(jìn)行按引用傳遞,讓@Builder感知到數(shù)據(jù)變化,進(jìn)行UI刷新
    Text(子組件 + ${$$.paramA})
      .fontSize(30)
      .fontWeight(30)
  }
}

上述正例的操作中,在復(fù)用的自定義組件中用@Builder來(lái)代替了自定義組件。避免了CustomNode節(jié)點(diǎn)創(chuàng)建和RenderNode渲染的耗時(shí)。

第二板斧,優(yōu)化狀態(tài)管理,精準(zhǔn)控制組件刷新范圍使用

1.使用attributeModifier精準(zhǔn)控制組件屬性的刷新,避免組件不必要的屬性刷新

復(fù)用場(chǎng)景常用在高頻的刷新場(chǎng)景,精準(zhǔn)控制組件的刷新范圍可以有效減少主線程渲染負(fù)載,提升滑動(dòng)性能。正反例如下:

反例:

@Entry
@Component
struct PreciseRefreshing {
  @State mainContentData: VideoDataSource = new VideoDataSource(); // 視頻展示列表

  build() {
    Column() {
      List() {
        LazyForEach(this.mainContentData, (item: VideoDataType) => {
          ListItem() {
            MyComponent({ authorName: item.authorName, fontSize: item.fontWeight })
          }
        }, (item: VideoDataType) => item.desc + item.fontWeight)
      }
    }
  }
}

@Reusable
@Component
export struct MyComponent {
  ...
  @State fontSize: number = 0;

  aboutToReuse(params: ESObject): void {
    this.authorName = params.authorName;
    this.fontSize = params.fontSize;
  }

  build() {
    RelativeContainer() {
      Text(this.videoDesc)
        .textAlign(TextAlign.Center)
        .fontStyle(FontStyle.Normal)
        .fontColor(Color.Pink)
        .id('videoName')
        .margin({ left: 10 })
        .fontWeight(30)
        .alignRules({
          'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
          'left': { 'anchor': 'image', 'align': HorizontalAlign.End }
        })
        // 此處使用屬性直接進(jìn)行刷新,會(huì)造成Text所有屬性都刷新
        .fontSize(this.fontSize)
    }
    .width('100%')
    .height(100)
  }
}

上述反例的操作中,通過(guò)aboutToReuse對(duì)fontSize狀態(tài)變量更新,進(jìn)而導(dǎo)致組件的全部屬性進(jìn)行刷新,造成不必要的耗時(shí)??梢钥紤]對(duì)需要更新的組件的屬性,進(jìn)行精準(zhǔn)刷新,避免不必要的重繪和渲染。

正例:

export class MyTextModifier implements AttributeModifier<TextAttribute> {
  private fontSize: number = 30;

  constructor() {
  }

  setFontSize(instance: TextAttribute,fontSize: number) {
    instance.fontSize = fontSize;
    return this;
  }

  applyNormalAttribute(instance: TextAttribute): void {
    instance.textAlign(TextAlign.Center)
    instance.fontStyle(FontStyle.Normal)
    instance.fontColor(Color.Pink)
    instance.id('videoName')
    instance.margin({ left: 10 })
    instance.fontWeight(30)
    instance.fontSize(10)
    instance.alignRules({
      'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
      'left': { 'anchor': 'image', 'align': HorizontalAlign.End }
    })
  }
}

@Entry
@Component
struct PreciseRefreshing {
  @State mainContentData: VideoDataSource = new VideoDataSource(); // 視頻展示列表


  build() {
    Column() {
      List() {
        LazyForEach(this.mainContentData, (item: VideoDataType) => {
          ListItem() {
            MyComponent({... fontSize: item.fontWeight })
          }
        }, (item: VideoDataType) => item.desc + item.fontWeight)
      }
    }
  }
}


@Reusable
@Component
export struct MyComponent {
  ...
  @State fontSize: number = 0;
  textModifier:MyTextModifier=new MyTextModifier();

  aboutToReuse(params: ESObject): void {
    ...
    this.fontSize = params.fontSize;
    this.textModifier.setFontSize(this.textModifier,this.fontSize)
  }

  build() {
    RelativeContainer() {
        ...
      Text(this.videoDesc)
        // 采用attributeModifier來(lái)對(duì)需要更新的fontSize屬性進(jìn)行精準(zhǔn)刷新,避免不必要的屬性刷新。
        .attributeModifier(this.textModifier)
        ...
    }
  }
}

上述正例的操作中,通過(guò)attributeModifier屬性來(lái)對(duì)text組件需要刷新的fontSize屬性進(jìn)行精準(zhǔn)刷新,避免text其它不需要更改的屬性的刷新。

2.使用@Link/@ObjectLink替代@Prop減少深拷貝,提升組件創(chuàng)建速度

在父子組件數(shù)據(jù)同步時(shí),如果僅僅是需要父組件向子組件同步數(shù)據(jù),不存在修改子組件的數(shù)據(jù)變化不同步給父組件的需求。建議使用@Link/@ObjectLink替代@Prop,@Prop在裝飾變量時(shí)會(huì)進(jìn)行深拷貝,在拷貝的過(guò)程中除了基本類(lèi)型、Map、Set、Date、Array外,都會(huì)丟失類(lèi)型。正反例如下:

反例:

@Component
struct ChildComponent {
  @Prop message: string;

  build() {
    Column() {
      Text(this.message)
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
    }
  }
}

@Entry
@Component
struct FatherComponent {
  @State message: string = 'Hello World';

  build() {
    Column() {
      ChildComponent({ message: this.message })
    }
  }
}

上述反例的操作中,父子組件之間的數(shù)據(jù)同步用了@Prop來(lái)進(jìn)行,每個(gè)@Prop裝飾的變量在初始化時(shí)都在本地拷貝了一份數(shù)據(jù)。會(huì)增加創(chuàng)建時(shí)間及內(nèi)存的消耗,造成性能問(wèn)題。

正例:

@Component
struct ChildComponent {
  @Link message: string;

  build() {
    Column() {
      Text(this.message)
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
    }
  }
}


@Entry
@Component
struct FatherComponent {
  @State message: string = 'Hello World';

  build() {
    Column() {
      ChildComponent({ message: this.message })
    }
    .width('100%')
    .height('100%')
  }
}

上述正例的操作中,父子組件之間的數(shù)據(jù)同步用了@Link來(lái)進(jìn)行,子組件@Link包裝類(lèi)把當(dāng)前this指針注冊(cè)給父組件,會(huì)直接將父組件的數(shù)據(jù)同步給子組件,實(shí)現(xiàn)父子組件數(shù)據(jù)的雙向同步,降低子組件創(chuàng)建時(shí)間和內(nèi)存消耗。

第三板斧,復(fù)用組件嵌套結(jié)構(gòu)會(huì)變更的場(chǎng)景,使用reuseId標(biāo)記不同結(jié)構(gòu)的組件構(gòu)成

在自定義組件復(fù)用的場(chǎng)景中,如果使用if/else條件語(yǔ)句來(lái)控制布局的結(jié)構(gòu),會(huì)導(dǎo)致在不同邏輯創(chuàng)建不同布局結(jié)構(gòu)嵌套的組件,從而造成組件樹(shù)結(jié)構(gòu)的不同。此時(shí)我們應(yīng)該使用reuseId來(lái)區(qū)分不同結(jié)構(gòu)的組件,確保系統(tǒng)能夠根據(jù)reuseId緩存各種結(jié)構(gòu)的組件,提升復(fù)用性能。正反例如下:

反例:

@Entry
@Component
struct ReuseID {
  ...
  build() {
    Column() {
      List({ scroller: this.scroller }) {
        LazyForEach(this.lazyChatList, (chatInfo: ChatSessionEntity | IChat.PublicChat, index: number) => {
          ListItem() {
            Button({ type: ButtonType.Normal }) {
              Row() {
                if (chatInfo['isPublicChat']) {
                  PublicChatItem({ chatInfo: chatInfo as IChat.PublicChat })
                } else {
                  ChatItem({ chatInfo: chatInfo as ChatSessionEntity })
                    .onClick(() => {
                      const sessionType = (chatInfo as ChatSessionEntity).sessionType
                      autoOpenChat({ sessionId: chatInfo.sessionId, sessionType })
                      imLogic.chat.chatSort()
                    })
                }
              }.padding({ left: 16, right: 16 })
            }
            .type(ButtonType.Normal)
            .width('100%')
            .height('100%')
            .backgroundColor('#fff')
            .borderRadius(0)
          }
          .height(72)
          .swipeAction({
            end: this.ChatSwiper(chatInfo, imHelper.chat.checkChatInvalid(chatInfo))
          })
        }, (item: IRenderChatType) => item.sessionId + !!item.unreadcount + item.isTop + item.priority)
        )
      }
      .cachedCount(3)
      .backgroundColor('#fff')
      .onScrollIndex(startIndex => {
        this.listStartIndex = startIndex;
      })
      .width('100%')
      .height('100%')
    }
  }
}
@Reusable
@Component
struct PublicChatItem {
  ...
  aboutToReuse(params: ESObject): void {
    this.chatInfo = params.chatInfo
  }
  build() {
    ...
  }
}
    
@Reusable
@Component
struct ChatItem {
  aboutToReuse(params: ESObject): void {
    this.chatInfo = params.chatInfo
  }
  build() {
    ...
  }
}

上述反例的操作中,通過(guò)if else來(lái)控制組件樹(shù)走不同的分支,分別復(fù)用PublicChatItem組件和ChatItem組件。導(dǎo)致更新if分支時(shí)仍然走刪除重創(chuàng)的邏輯。考慮采用根據(jù)不同的分支設(shè)置不同的reuseId來(lái)提高復(fù)用的性能。

正例:

@Entry
@Component
struct ReuseID {
  ...
  build() {
    Column() {
      List({ scroller: this.scroller }) {
        LazyForEach(this.lazyChatList, (chatInfo: ChatSessionEntity | IChat.PublicChat, index: number) => {
          ListItem() {
            // 使用reuseId進(jìn)行組件復(fù)用的控制
            InnerRecentChat({ chatInfo: chatInfo }).reuseId(this.lazyChatList.getReuseIdByIndex(index))
          }
          .height(72)
          .swipeAction({
            end: this.ChatSwiper(chatInfo, imHelper.chat.checkChatInvalid(chatInfo))
          })
        }, (item: IRenderChatType) => item.sessionId + !!item.unreadcount + item.isTop + item.priority)
        )
      }
      .cachedCount(3)
      .backgroundColor('#fff')
      .onScrollIndex(startIndex => {
        this.listStartIndex = startIndex;
      })
      .width('100%')
      .height('100%')
    }
  }
}

@Reusable
@Component
struct InnerRecentChat {
  ...
  aboutToReuse(params: ESObject): void {
    this.chatInfo = params.chatInfo
  }

  build() {
    Button({ type: ButtonType.Normal }) {
      Row() {
        if (this.chatInfo['isPublicChat']) {
          PublicChatItem({ chatInfo: chatInfo as IChat.PublicChat })
        } else {
          ChatItem({ chatInfo: chatInfo as ChatSessionEntity })
            .onClick(() => {
              const sessionType = (chatInfo as ChatSessionEntity).sessionType
              autoOpenChat({ sessionId: chatInfo.sessionId, sessionType })
              imLogic.chat.chatSort()
            })
        }
      }.padding({ left: 16, right: 16 })
    }
    .type(ButtonType.Normal)
    .width('100%')
    .height('100%')
    .backgroundColor('#fff')
    .borderRadius(0)
  }
}

class MtDataSource extends BasicDataSource{
  private chatList:Array<ChatSessionEntity|IChat.PublicChat>=[];
  private reuseIds:Array<string>=[];

  public totalCount():number{
    return this.chatList.length;
  }

  public set (list:Array<ChatSessionEntity|IChat.PublicChat>){
    this.chatList=list;
    this.reuseIds=list.map((value:ChatSessionEntity|IChat.PublicChat)=>{
      if (value['isPublicChat']) {
        return "public";
      }
      else {
        if ((value as ChatSessionEntity).target?.isEmployeeEntity()) {
          return "employee"
        }else {
          return "group"
        }
      }
    })
    this.notifyDataReload();
  }
    pubilc getReuseIdByIndex(index:number):string{
        return this.reuseIds
    }
}

上述正例的操作中,通過(guò)reuseId來(lái)標(biāo)識(shí)需要復(fù)用的組件,省去走if else刪除重創(chuàng)的邏輯,提高組件復(fù)用的效率和性能。

第四板斧,避免使用函數(shù)/方法作為復(fù)用組件創(chuàng)建時(shí)的入?yún)?/h2>

由于在組件復(fù)用的場(chǎng)景下,每次復(fù)用都需要重新創(chuàng)建組件關(guān)聯(lián)的數(shù)據(jù)對(duì)象,導(dǎo)致重復(fù)執(zhí)行入?yún)⒅械暮瘮?shù)來(lái)獲取入?yún)⒔Y(jié)果。如果函數(shù)中存在耗時(shí)操作,會(huì)嚴(yán)重影響性能。正反例如下:

【反例】

// 下文中BasicDateSource是實(shí)現(xiàn)IDataSource接口的類(lèi),具體可參考LazyForEach用法指導(dǎo)
// 此處為復(fù)用的自定義組件
@Reusable
@Component
struct ChildComponent {
  @State desc: string = '';
  @State sum: number = 0;

  aboutToReuse(params: Record<string, Object>): void {
    this.desc = params.desc as string;
    this.sum = params.sum as number;
  }

  build() {
    Column() {
      Text('子組件' + this.desc)
        .fontSize(30)
        .fontWeight(30)
      Text('結(jié)果' + this.sum)
        .fontSize(30)
        .fontWeight(30)
    }
  }
}

@Entry
@Component
struct Reuse {
  private data: BasicDateSource = new BasicDateSource();

  aboutToAppear(): void {
    for (let index = 0; index < 20; index++) {
      this.data.pushData(index.toString())
    }
  }
    
  // 真實(shí)場(chǎng)景的函數(shù)中可能存在未知的耗時(shí)操作邏輯,此處用循環(huán)函數(shù)模擬耗時(shí)操作
  count(): number {
    let temp: number = 0;
    for (let index = 0; index < 10000; index++) {
      temp += index;
    }
    return temp;
  }

  build() {
    Column() {
      List() {
        LazyForEach(this.data, (item: string) => {
          ListItem() {
            // 此處sum參數(shù)是函數(shù)獲取的,實(shí)際開(kāi)發(fā)場(chǎng)景無(wú)法預(yù)料該函數(shù)可能出現(xiàn)的耗時(shí)操作,每次進(jìn)行組件復(fù)用都會(huì)重復(fù)觸發(fā)此函數(shù)的調(diào)用
            ChildComponent({ desc: item, sum: this.count() })
          }
          .width('100%')
          .height(100)
        }, (item: string) => item)
      }
    }
  }
}

上述反例的操作中,復(fù)用的子組件參數(shù)sum是通過(guò)耗時(shí)函數(shù)生成。該函數(shù)在每次組件復(fù)用時(shí)都需要執(zhí)行,會(huì)造成性能問(wèn)題,甚至是列表滑動(dòng)過(guò)程中的卡頓丟幀現(xiàn)象。

【正例】

// 下文中BasicDateSource是實(shí)現(xiàn)IDataSource接口的類(lèi),具體可參考LazyForEach用法指導(dǎo)
// 此處為復(fù)用的自定義組件
@Reusable
@Component
struct ChildComponent {
  @State desc: string = '';
  @State sum: number = 0;

  aboutToReuse(params: Record<string, Object>): void {
    this.desc = params.desc as string;
    this.sum = params.sum as number;
  }

  build() {
    Column() {
      Text('子組件' + this.desc)
        .fontSize(30)
        .fontWeight(30)
      Text('結(jié)果' + this.sum)
        .fontSize(30)
        .fontWeight(30)
    }
  }
}

@Entry
@Component
struct Reuse {
  private data: BasicDateSource = new BasicDateSource();
  @State sum: number = 0;

  aboutToAppear(): void {
    for (let index = 0; index < 20; index++) {
      this.data.pushData(index.toString())
    }
    // 執(zhí)行該異步函數(shù)
    this.count();
  }

  // 模擬耗時(shí)操作邏輯
  async count() {
    let temp: number = 0;
    for (let index = 0; index < 10000; index++) {
      temp += index;
    }
    // 將結(jié)果放入狀態(tài)變量中
    this.sum = temp;
  }

  build() {
    Column() {
      List() {
        LazyForEach(this.data, (item: string) => {
          ListItem() {
            // 子組件的傳參通過(guò)狀態(tài)變量進(jìn)行
            ChildComponent({ desc: item, sum: this.sum })
          }
          .width('100%')
          .height(100)
        }, (item: string) => item)
      }
    }
  }
}

上述正例的操作中,通過(guò)耗時(shí)函數(shù)count生成的結(jié)果不變,可以將其放到頁(yè)面初始渲染時(shí)執(zhí)行一次,將結(jié)果賦值給this.sum。在復(fù)用組件的參數(shù)傳遞時(shí),通過(guò)this.sum來(lái)進(jìn)行。

寫(xiě)在最后

如果你覺(jué)得這篇內(nèi)容對(duì)你還蠻有幫助,我想邀請(qǐng)你幫我三個(gè)小忙

  • 點(diǎn)贊,轉(zhuǎn)發(fā),有你們的 『點(diǎn)贊和評(píng)論』,才是我創(chuàng)造的動(dòng)力。
  • 關(guān)注小編,同時(shí)可以期待后續(xù)文章ing??,不定期分享原創(chuàng)知識(shí)。
  • 想要獲取更多完整鴻蒙最新學(xué)習(xí)知識(shí)點(diǎn),請(qǐng)移步前往小編:https://gitee.com/MNxiaona/733GH/blob/master/jianshu
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容