【带你轻松学】鸿蒙原生APP性能优化之提升应用响应速度 ...

打印 上一主题 下一主题

主题 1779|帖子 1779|积分 5337

往期知识点整理



  • 鸿蒙(HarmonyOS)北向开辟知识点记载~
  • 鸿蒙(OpenHarmony)南向开辟保姆级知识点汇总~
  • 被裁员后,踏上了鸿蒙开辟求职之路
  • 记载一场鸿蒙开辟岗位口试履历~
  • 持续更新中……
应用对用户的输入需要快速反馈,以提升交互体验,因此本文提供了以下方法来提升应用响应速度。


  • 克制主线程被非UI使命壅闭
  • 淘汰组件刷新的数量
克制主线程被非UI使命壅闭

在应用响应用户输入期间,应用主线程应尽可能只实行UI使命(待显示数据的准备、可见视图组件的更新等),非UI的耗时使命(长时间加载的内容等)建议通过异步使命延迟处置惩罚或者分配到其他线程处置惩罚。
使用组件异步加载特性

当前系统提供的Image组件默认生效异步加载特性,当应用在页面上展示一批当地图片的时候,会先显示空缺占位块,当图片在其他线程加载完毕后,再替换占位块。这样图片加载就可以不壅闭页面的显示,给用户带来良好的交互体验。因此,只在加载图片耗时比较短的情况下建议下述代码。
  1. @Entry
  2. @Component
  3. struct ImageExample1 {
  4.   build() {
  5.     Column() {
  6.       Row() {
  7.         Image('resources/base/media/sss001.jpg')
  8.           .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%')
  9.         Image('resources/base/media/sss002.jpg')
  10.           .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%')
  11.         Image('resources/base/media/sss003.jpg')
  12.           .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%')
  13.         Image('resources/base/media/sss004.jpg')
  14.           .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%')
  15.       }
  16.     // 此处省略若干个Row容器,每个容器内都包含如上的若干Image组件
  17.     }
  18.   }
  19. }
复制代码
建议:在加载图片的耗时比较短的时候,通过异步加载的效果会大打折扣,建议设置Image的syncLoad属性。
  1. @Entry
  2. @Component
  3. struct ImageExample2 {
  4.   build() {
  5.     Column() {
  6.       Row() {
  7.         Image('resources/base/media/sss001.jpg')
  8.           .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%').syncLoad(true)
  9.         Image('resources/base/media/sss002.jpg')
  10.           .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%').syncLoad(true)
  11.         Image('resources/base/media/sss003.jpg')
  12.           .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%').syncLoad(true)
  13.         Image('resources/base/media/sss004.jpg')
  14.           .border({ width: 1 }).borderStyle(BorderStyle.Dashed).aspectRatio(1).width('25%').height('12.5%').syncLoad(true)
  15.       }
  16.     // 此处省略若干个Row容器,每个容器内都包含如上的若干Image组件
  17.     }
  18.   }
  19. }
复制代码
使用TaskPool线程池异步处置惩罚

当前系统提供了 TaskPool线程池 ,相比worker线程,TaskPool提供了使命优先级设置、线程池自动管理机制,示例如下:
  1. import taskpool from '@ohos.taskpool';
  2. @Concurrent
  3. function computeTask(arr: string[]): string[] {
  4.   // 模拟一个计算密集型任务
  5.   let count = 0;
  6.   while (count < 100000000) {
  7.     count++;
  8.   }
  9.   return arr.reverse();
  10. }
  11. @Entry
  12. @Component
  13. struct AspectRatioExample3 {
  14.   @State children: string[] = ['1', '2', '3', '4', '5', '6'];
  15.   aboutToAppear() {
  16.     this.computeTaskInTaskPool();
  17.   }
  18.   async computeTaskInTaskPool() {
  19.     const param = this.children.slice();
  20.     let task = new taskpool.Task(computeTask, param);
  21.     await taskpool.execute(task);
  22.   }
  23.   build() {
  24.     // 组件布局
  25.   }
  26. }
复制代码
创建异步使命

以下代码展示了将一个长时间实行的非UI使命通过Promise声明成异步使命,主线程可以先辈行用户反馈-绘制初始页面。等主线程空闲时,再实行异步使命。等到异步使命运行完毕后,重绘相干组件刷新页面。
  1. @Entry
  2. @Component
  3. struct AspectRatioExample4 {
  4.   @State private children: string[] = ['1', '2', '3', '4', '5', '6'];
  5.   private count: number = 0;
  6.   aboutToAppear() {
  7.     this.computeTaskAsync();  // 调用异步运算函数
  8.   }
  9.   // 模拟一个计算密集型任务
  10.   computeTask() {
  11.     this.count = 0;
  12.     while (this.count < 100000000) {
  13.       this.count++;
  14.     }
  15.     this.children = this.children.reverse();
  16.   }
  17.   computeTaskAsync() {
  18.     setTimeout(() => { // 这里使用setTimeout来实现异步延迟运行
  19.       this.computeTask();
  20.     }, 1000)
  21.   }
  22.   build() {
  23.     // 组件布局
  24.   }
  25. }
复制代码
淘汰刷新的组件数量

应用刷新页面时需要尽可能淘汰刷新的组件数量,如果数量过多会导致主线程实行测量、结构的耗时过长,还会在自界说组件新建和销毁过程中,多次调用aboutToAppear()、aboutToDisappear()方法,增加主线程负载。
使用容器限制刷新范围

反例:如果容器内有组件被if条件包罗,if条件结果变动会触发创建和销毁该组件,如果此时影响到容器的结构,该容器内所有组件都会刷新,导致主线程UI刷新耗时过长。
以下代码的Text(‘New Page’)组件被状态变量isVisible控制,isVisible为true时创建,false时销毁。当isVisible发生变化时,Stack容器内的所有组件都会刷新:
  1. @Entry
  2. @Component
  3. struct StackExample5 {
  4.   @State isVisible : boolean = false;
  5.   build() {
  6.     Column() {
  7.       Stack({alignContent: Alignment.Top}) {
  8.         Text().width('100%').height('70%').backgroundColor(0xd2cab3)
  9.           .align(Alignment.Center).textAlign(TextAlign.Center);
  10.         // 此处省略100个相同的背景Text组件
  11.         if (this.isVisible) {
  12.           Text('New Page').height("100%").height("70%").backgroundColor(0xd2cab3)
  13.             .align(Alignment.Center).textAlign(TextAlign.Center);
  14.         }
  15.       }
  16.       Button("press").onClick(() => {
  17.         this.isVisible = !(this.isVisible);
  18.       })
  19.     }
  20.   }
  21. }
复制代码
建议:对于这种受状态变量控制的组件,在if外衣一层容器,淘汰刷新范围。
  1. @Entry
  2. @Component
  3. struct StackExample6 {
  4.   @State isVisible : boolean = false;
  5.   build() {
  6.     Column() {
  7.       Stack({alignContent: Alignment.Top}) {
  8.         Text().width('100%').height('70%').backgroundColor(0xd2cab3)
  9.           .align(Alignment.Center).textAlign(TextAlign.Center);
  10.         // 此处省略100个相同的背景Text组件
  11.         Stack() {
  12.           if (this.isVisible) {
  13.             Text('New Page').height("100%").height("70%").backgroundColor(0xd2cab3)
  14.               .align(Alignment.Center).textAlign(TextAlign.Center);
  15.           }
  16.         }.width('100%').height('70%')
  17.       }
  18.       Button("press").onClick(() => {
  19.         this.isVisible = !(this.isVisible);
  20.       })
  21.     }
  22.   }
  23. }
复制代码
按需加载列表组件的元素

反例:this.arr中的每一项元素都被初始化和加载,数组中的元素有10000个,主线程实行耗时长。
  1. @Entry
  2. @Component
  3. struct MyComponent7 {
  4.   @State arr: number[] = Array.from(Array<number>(10000), (v,k) =>k);
  5.   build() {
  6.     List() {
  7.       ForEach(this.arr, (item: number) => {
  8.         ListItem() {
  9.           Text(`item value: ${item}`)
  10.         }
  11.       }, (item: number) => item.toString())
  12.     }
  13.   }
  14. }
复制代码
建议:这种情况下用LazyForEach替换ForEach,LazyForEach一样平常只加载可见的元素,克制一次性初始化和加载所有元素。
  1. class BasicDataSource implements IDataSource {
  2.   private listeners: DataChangeListener[] = []
  3.   public totalCount(): number {
  4.     return 0
  5.   }
  6.   public getData(index: number): string {
  7.     return ''
  8.   }
  9.   registerDataChangeListener(listener: DataChangeListener): void {
  10.     if (this.listeners.indexOf(listener) < 0) {
  11.       console.info('add listener')
  12.       this.listeners.push(listener)
  13.     }
  14.   }
  15.   unregisterDataChangeListener(listener: DataChangeListener): void {
  16.     const pos = this.listeners.indexOf(listener);
  17.     if (pos >= 0) {
  18.       console.info('remove listener')
  19.       this.listeners.splice(pos, 1)
  20.     }
  21.   }
  22.   notifyDataReload(): void {
  23.     this.listeners.forEach(listener => {
  24.       listener.onDataReloaded()
  25.     })
  26.   }
  27.   notifyDataAdd(index: number): void {
  28.     this.listeners.forEach(listener => {
  29.       listener.onDataAdd(index)
  30.     })
  31.   }
  32.   notifyDataChange(index: number): void {
  33.     this.listeners.forEach(listener => {
  34.       listener.onDataChange(index)
  35.     })
  36.   }
  37.   notifyDataDelete(index: number): void {
  38.     this.listeners.forEach(listener => {
  39.       listener.onDataDelete(index)
  40.     })
  41.   }
  42.   notifyDataMove(from: number, to: number): void {
  43.     this.listeners.forEach(listener => {
  44.       listener.onDataMove(from, to)
  45.     })
  46.   }
  47. }
  48. class MyDataSource extends BasicDataSource {
  49.   private dataArray: string[] = Array.from(Array<number>(10000), (v, k) => k.toString());
  50.   public totalCount(): number {
  51.     return this.dataArray.length
  52.   }
  53.   public getData(index: number): string  {
  54.     return this.dataArray[index]
  55.   }
  56.   public addData(index: number, data: string): void {
  57.     this.dataArray.splice(index, 0, data)
  58.     this.notifyDataAdd(index)
  59.   }
  60.   public pushData(data: string): void {
  61.     this.dataArray.push(data)
  62.     this.notifyDataAdd(this.dataArray.length - 1)
  63.   }
  64. }
  65. @Entry
  66. @Component
  67. struct MyComponent {
  68.   private data: MyDataSource = new MyDataSource()
  69.   build() {
  70.     List() {
  71.       LazyForEach(this.data, (item: string) => {
  72.         ListItem() {
  73.             Text(item).fontSize(20).margin({ left: 10 })
  74.         }
  75.       }, (item:string) => item)
  76.     }
  77.   }
  78. }
复制代码
公道使用缓存提升响应速度

缓存可以存储经常访问的数据或资源,当下次需要访问相同数据时,可以直接从缓存中获取,克制了重复的盘算或请求,从而加速了响应速度。
使用AVPlayer实例缓存提升视频加载速度

AVPlayer实例的创建与销毁都很消耗性能,针对这个题目可以使用实例缓存举行优化,首次加载页面时创建两个实例,在打开新页面时切换空闲实例,通过reset方法重置实例到初始化状态。优化点在于不需要频仍创建销毁实例,且reset方法性能优于release方法。下面以AVPlayer为例列出正反例对比供参考。
反例:打开新页面时创建实例,离开页面时使用release方法销毁实例。
  1. import media from '@ohos.multimedia.media';
  2. @Entry
  3. @Component
  4. struct Index {
  5.   private avPlayer: media.AVPlayer | undefined = undefined;
  6.   
  7.   aboutToAppear(): void {
  8.     // 页面创建时初始化AVPlayer实例
  9.     media.createAVPlayer().then((ret) => {
  10.       this.avPlayer = ret;
  11.     });
  12.   }
  13.   
  14.   aboutToDisappear(): void {
  15.     // 离开页面时销毁AVPlayer实例
  16.     if (this.avPlayer) {
  17.       this.avPlayer.release();
  18.     }
  19.     this.avPlayer = undefined;
  20.   }
  21.   
  22.   build() {
  23.     // 组件布局
  24.   }
  25. }
复制代码
正例:首次加载页面时维护两个实例,在切换页面时切换实例,并将之前的实例通过reset方法重置。
  1. import media from '@ohos.multimedia.media';
  2. @Entry
  3. @Component
  4. struct Index {
  5.   private avPlayer: media.AVPlayer | undefined = undefined;
  6.   private avPlayerManager: AVPlayerManager = AVPlayerManager.getInstance();
  7.   aboutToAppear(): void {
  8.     this.avPlayerManager.switchPlayer();
  9.     this.avPlayer = this.avPlayerManager.getCurrentPlayer();
  10.   }
  11.   aboutToDisappear(): void {
  12.     this.avPlayerManager.resetCurrentPlayer();
  13.     this.avPlayer = undefined;
  14.   }
  15.   build() {
  16.     // 组件布局
  17.   }
  18. }
  19. class AVPlayerManager {
  20.   private static instance?: AVPlayerManager;
  21.   private player1?: media.AVPlayer;
  22.   private player2?: media.AVPlayer;
  23.   private currentPlayer?: media.AVPlayer;
  24.   public static getInstance(): AVPlayerManager {
  25.     if (!AVPlayerManager.instance) {
  26.       AVPlayerManager.instance = new AVPlayerManager();
  27.     }
  28.     return AVPlayerManager.instance;
  29.   }
  30.   async AVPlayerManager() {
  31.     this.player1 = await media.createAVPlayer();
  32.     this.player2 = await media.createAVPlayer();
  33.   }
  34.   /**
  35.    * 切换页面时切换AVPlayer实例
  36.    */
  37.   switchPlayer(): void {
  38.     if (this.currentPlayer === this.player1) {
  39.       this.currentPlayer = this.player2;
  40.     } else {
  41.       this.currentPlayer = this.player1;
  42.     }
  43.   }
  44.   getCurrentPlayer(): media.AVPlayer | undefined {
  45.     return this.currentPlayer;
  46.   }
  47.   /**
  48.    * 使用reset方法重置AVPlayer实例
  49.    */
  50.   resetCurrentPlayer(): void {
  51.     this.currentPlayer?.pause(() => {
  52.       this.currentPlayer?.reset();
  53.     });
  54.   }
  55. }
复制代码
公道使用预加载提升响应速度

使用NodeContainer提前渲染降低响应时延

应用启动时有广告页的场景下。如果先渲染广告页而后再渲染首页,很可能造成首页响应时延较长,影响用户体验。针对此类题目可以使用NodeContainer在广告页渲染时同步渲染首页,等到跳转到首页时直接送显,提高响应速度。
反例:按次序依次渲染送显
重要代码逻辑如下:
1、模仿广告页,通过点击差别按钮分别进入普通页面和预加载页面
  1. // Index.ets
  2. import router from '@ohos.router';
  3. @Entry
  4. @Component
  5. struct Index {
  6.   build() {
  7.     Column({ space: 5 }) {
  8.       // 进入普通页面
  9.       Button("普通页面")
  10.         .type(ButtonType.Capsule)
  11.         .onClick(() => {
  12.           router.pushUrl({ url: 'pages/CommonPage' })
  13.         })
  14.       // 进入预加载页面
  15.       Button("预加载页面")
  16.         .type(ButtonType.Capsule)
  17.         .onClick(() => {
  18.           router.pushUrl({ url: 'pages/PreloadedPage' })
  19.         })
  20.     }.height('100%')
  21.     .width('100%')
  22.     .justifyContent(FlexAlign.Center)
  23.   }
  24. }
复制代码
2、普通首页,也即按次序普通渲染的页面
  1. // CommonPage.ets
  2. import { MyBuilder, getNumbers } from '../builder/CustomerBuilder';
  3. @Entry
  4. @Component
  5. struct CommonPage {
  6.   build() {
  7.     Row() {
  8.       MyBuilder(getNumbers())
  9.     }
  10.   }
  11. }
复制代码
3、自界说builder,用来定制页面结构
  1. // CustomerBuilder.ets
  2. @Builder
  3. export function MyBuilder(numbers: string[]) {
  4.   Column() {
  5.     List({ space: 20, initialIndex: 0 }) {
  6.       ForEach(numbers, (item: string) => {
  7.         ListItem() {
  8.           Text('' + item)
  9.             .width('100%')
  10.             .height(50)
  11.             .fontSize(16)
  12.             .textAlign(TextAlign.Center)
  13.             .borderRadius(10)
  14.             .backgroundColor(0xFFFFFF)
  15.         }
  16.       }, (day: string) => day)
  17.     }
  18.     .listDirection(Axis.Vertical) // 排列方向
  19.     .scrollBar(BarState.Off)
  20.     .friction(0.6)
  21.     .divider({ strokeWidth: 2, color: 0xFFFFFF, startMargin: 20, endMargin: 20 }) // 每行之间的分界线
  22.     .edgeEffect(EdgeEffect.Spring) // 边缘效果设置为Spring
  23.     .width('90%')
  24.     .height('100%')
  25.   }
  26.   .width('100%')
  27.   .height('100%')
  28.   .backgroundColor(0xDCDCDC)
  29.   .padding({ top: 5 })
  30. }
  31. export const getNumbers = (): string[] => {
  32.   const numbers: string[] = [];
  33.   for (let i = 0; i < 100; i++) {
  34.     numbers.push('' + i)
  35.   }
  36.   return numbers;
  37. }
复制代码
正例:在启动时预加载首页
重要代码逻辑如下:
1、应用启动时提前创建首页
  1. // EntryAbility.ets  
  2. import { ControllerManager } from '../builder/CustomerController';
  3. import { getNumbers } from '../builder/CustomerBuilder';
  4. export default class EntryAbility extends UIAbility {
  5.   onWindowStageCreate(windowStage: window.WindowStage): void {
  6.     // Main window is created, set main page for this ability
  7.     hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
  8.     windowStage.loadContent('pages/Index', (err, data) => {
  9.       if (err.code) {
  10.         hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
  11.         return;
  12.       }
  13.       hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
  14.     });
  15.     window.getLastWindow(this.context, (err: BusinessError, data) => {
  16.       if (err.code) {
  17.         console.error('Failed to obtain top window. Cause:' + JSON.stringify(err));
  18.         return;
  19.       }
  20.       // 提前创建
  21.       ControllerManager.getInstance().createNode(data.getUIContext(), getNumbers());
  22.     })
  23.   }
  24.   onWindowStageDestroy(): void {
  25.     // Main window is destroyed, release UI related resources
  26.     hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  27.     // 清空组件,防止内存泄漏
  28.     ControllerManager.getInstance().clearNode();
  29.   }
  30. }
复制代码
2、预加载的首页,使用NodeContainer举行占位,当跳转到本页时直接将提前创建完成的首页填充
  1. // PreloadedPage.ets
  2. import { ControllerManager } from '../builder/CustomerController';
  3. @Entry
  4. @Component
  5. struct PreloadedPage {
  6.   build() {
  7.     Row() {
  8.       NodeContainer(ControllerManager.getInstance().getNode())
  9.     }
  10.   }
  11. }
复制代码
3、自界说NodeController,并提供提前创建首页的能力
  1. // CustomerController.ets
  2. import { UIContext } from '@ohos.arkui.UIContext';
  3. import { NodeController, BuilderNode, FrameNode } from "@ohos.arkui.node";
  4. import { MyBuilder } from './CustomerBuilder';
  5. export class MyNodeController extends NodeController {
  6.   private rootNode: BuilderNode<[string[]]> | null = null;
  7.   private wrapBuilder: WrappedBuilder<[string[]]> = wrapBuilder(MyBuilder);
  8.   private numbers: string[] | null = null;
  9.   constructor(numbers: string[]) {
  10.     super();
  11.     this.numbers = numbers;
  12.   }
  13.   makeNode(uiContext: UIContext): FrameNode | null {
  14.     if (this.rootNode != null) {
  15.       // 返回FrameNode节点
  16.       return this.rootNode.getFrameNode();
  17.     }
  18.     // 返回null控制动态组件脱离绑定节点
  19.     return null;
  20.   }
  21.   // 通过UIContext初始化BuilderNode,再通过BuilderNode中的build接口初始化@Builder中的内容
  22.   initNode(uiContext: UIContext) {
  23.     if (this.rootNode != null) {
  24.       return;
  25.     }
  26.     // 创建节点,需要uiContext
  27.     this.rootNode = new BuilderNode(uiContext)
  28.     // 创建组件
  29.     this.rootNode.build(this.wrapBuilder, this.numbers)
  30.   }
  31. }
  32. export class ControllerManager {
  33.   private static instance?: ControllerManager;
  34.   private myNodeController?: MyNodeController;
  35.   static getInstance(): ControllerManager {
  36.     if (!ControllerManager.instance) {
  37.       ControllerManager.instance = new ControllerManager();
  38.     }
  39.     return ControllerManager.instance;
  40.   }
  41.   /**
  42.    * 初始化需要UIContext 需在Ability获取
  43.    * @param uiContext
  44.    * @param numbers
  45.    */
  46.   createNode(uiContext: UIContext, numbers: string[]) {
  47.     // 创建NodeController
  48.     this.myNodeController = new MyNodeController(numbers);
  49.     this.myNodeController.initNode(uiContext);
  50.   }
  51.   /**
  52.    * 自定义获取NodeController实例接口
  53.    * @returns MyNodeController
  54.    */
  55.   getNode(): MyNodeController | undefined {
  56.     return this.myNodeController;
  57.   }
  58.   /**
  59.    * 解除占用,防止内存泄漏
  60.    */
  61.   clearNode(): void {
  62.     this.myNodeController = undefined;
  63.   }
  64. }
复制代码
通过SmartPerf-Host工具抓取相干trace举行分析首页响应时延,其中重要关注两个trace tag分别是DispatchTouchEvent代表点击变乱和MarshRSTransactionData代表响应,如下图所示:
反例响应时延:18.1ms

正例响应时延:9.4ms

由上述对比数据即可得出结论,预加载首页能优化首页响应时延。
最后

经常有很多小同伴抱怨说:不知道学习鸿蒙开辟哪些技术?不知道需要重点把握哪些鸿蒙应用开辟知识点?
为了能够帮助到各人能够有规划的学习,这里特别整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开辟技术的学习门路,包罗了鸿蒙开辟必把握的焦点知识要点,内容有(ArkTS、ArkUI开辟组件、Stage模型、多端部署、分布式应用开辟、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开辟、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。

《鸿蒙 (Harmony OS)开辟学习手册》(共计892页):https://gitcode.com/HarmonyOS_MN/733GH/overview

如何快速入门?

1.根本概念
2.构建第一个ArkTS应用
3.……

鸿蒙开辟口试真题(含参考答案):


《OpenHarmony源码解析》:



  • 搭建开辟环境
  • Windows 开辟环境的搭建
  • Ubuntu 开辟环境搭建
  • Linux 与 Windows 之间的文件共享
  • ……
  • 系统架构分析
  • 构建子系统
  • 启动流程
  • 子系统
  • 分布式使命调度子系统
  • 分布式通信子系统
  • 驱动子系统
  • ……

OpenHarmony 装备开辟学习手册:https://gitcode.com/HarmonyOS_MN/733GH/overview



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

魏晓东

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表