一. 小结

我们已经知道,Spark组件实际上由两个UIComponent构成,一个是所谓的Skin类,一个是所谓的Component类。Component类负责管理数据和逻辑,Skin类负责管理可视化。

在不同的场合和微博上,在Spark组件发布已近两年的今天,我仍然听到很多人在抱怨Spark和Halo的不同。那么,阅读过本系列文章之后,你会发现,Spark只不过为传统的Halo组件增加了一个UIComponent(即Skin类),把一个UIComponent组件的工作拆分给了两个UIComponent组件。并不是你所想象的那样翻天覆地的变化。

既然二者都是UIComponent,那么本质上,两个UIComponent都要遵从UICompnent组件的生命周期,就是那个著名的三阶段:commitProperties(), measure()和updateDisplayList(),当然也包括对应的invalidate方法。如果你还不甚了解三阶段构成的组件生命周期和对应的invalidate方法,那么请就此打住,先参考Using Adobe Flex4.5理解组件生命周期的基本知识,然后再回到本文。

如果我们已经对此达成共识,那么让我们开始吧。

二. 引擎:mx.managers.LayoutManager

每个稍有经验的Flex开发者,都知道组件生命周期的三阶段commitProperties(),measure()和updateDisplayList()。但很少有人深究过其自何处,这导致Flex组件的生命周期对很多开发者几乎成为一个神秘的神话。实际上,mx.managers.LayoutManager是这一切的驱动者。

2.1/ 引自ActionScript3语言参考:关于LayoutManager

看一下LayoutManager类提供的部分主要方法:

invalidateProperties(), invalidateSize() , invalidateDisplayList() , validateProperties() , validateSize() , validateDisplayList()… 你是否发现这些组件生命周期中的神秘方法?

如ActionScript3参考中所说:LayoutManager 是 Flex 的度量和布局策略所基于的引擎。在ActionScript3语言参考之LayoutManager类中讲到:

LayoutManager 是 Flex 的度量和布局策略所基于的引擎。布局分三个阶段执行:提交、度量和布局。

这三个阶段互不相同,并且,只有在处理完当前阶段的所有 UIComponent 之后才会进入下一阶段。在某个阶段中处理 UIComponent 期间,可能出现另一阶段请求重新处理 UIComponent 的情况。这些请求将进行排队,并且只在下次运行此阶段时才得到处理。

提交阶段从调用 validateProperties() 开始,该方法将遍历一个对象列表(该列表按嵌套级别反向排序),并调用每个对象的 validateProperties() 方法。

列表中的对象是按与嵌套级别正相反的顺序接受处理的,所以最先访问的是嵌套深度最浅的对象。这也可以称为自上而下或从外到内的顺序。

在此阶段中,允许内容依据属性设置而定的组件在进入度量和布局阶段之前进行自我配置。为了获得更好的性能,组件的属性 setter 方法有时不执行更新到新属性值所需的全部操作。但是,属性 setter 会调用 invalidateProperties() 方法,并在运行此阶段之前延迟此操作。这样,可以在多次设置属性时避免执行不必要的操作。

度量阶段从调用 validateSize() 开始,该方法将遍历一个对象列表(该列表按嵌套级别排序),并调用每个对象的 validateSize() 方法,以确定对象大小是否已更改。

如果之前调用过对象的 invalidateSize() 方法,则调用 validateSize() 方法。如果对象的大小或位置因调用 validateSize() 而发生了更改,则会调用对象的 invalidateDisplayList() 方法,这就会将该对象添加到处理队列中,等待下次运行布局阶段时进行处理。此外,已分别调用 invalidateSize() 和 invalidateDisplayList() 为度量和布局这两个阶段标记了对象的父项。

列表中的对象是按嵌套级别的顺序进行处理的,所以最先访问的是嵌套深度最深的对象。这也可以称为自下而上或从内到外的顺序。

布局阶段从调用 validateDisplayList() 方法开始,该方法将遍历一个对象列表(该列表按嵌套级别反向排序),并调用每个对象的 validateDisplayList() 方法,以请求对象确定它所包含的所有组件(即其子对象)的大小和位置。

如果之前调用过对象的 invalidateDisplayList() 方法,则调用 validateDisplayList() 方法。

列表中的对象是按与嵌套级别正相反的顺序接受处理的,所以最先访问的是嵌套深度最浅的对象。这也可以称为自上而下或从外到内的顺序。

通常情况下,组件不会覆盖 validateProperties()、validateSize() 或 validateDisplayList() 方法。对于 UIComponent 而言,大部分组件都会覆盖分别由validateProperties()、validateSize() 或 validateDisplayList() 方法调用的 commitProperties()、measure() 或 updateDisplayList() 方法。

当应用程序启动时,将创建一个 LayoutManager 实例并将其存储在 UIComponent.layoutManager 属性中。所有组件都应使用此实例。如果您无权访问 UIComponent 对象,也可以使用静态LayoutManager.getInstance() 方法访问 LayoutManager。

2.2/ LayoutManager来自何处

所有的UIComponent组件都通过 layoutManager 属性访问其LayoutManager,更重要的是,这些LayoutManager都指向一处。

是的,我要说的其实是:LayoutManager 类是一个单体类。

当应用程序启动时,将创建一个 LayoutManager 实例并将其存储在 UIComponent.layoutManager 属性中。所有组件都应使用此实例。

查看spark.components.Application类的构造器,可以看到:

public function Application(){UIComponentGlobals.layoutManager = ILayoutManager(Singleton.getInstance("mx.managers::ILayoutManager"));UIComponentGlobals.layoutManager.usePhasedInstantiation = true;if (!FlexGlobals.topLevelApplication)FlexGlobals.topLevelApplication = this;super();showInAutomationHierarchy = true;initResizeBehavior();}

需要注意的是,这里默认设置了LayoutManager的 usePhasedInstantiation 属性为true。我们在下文中将会谈到该属性。

而查看LayoutManager类,你会看到如下 getInstance() 方法:

public static function getInstance():LayoutManager
{ if (!instance)instance = new LayoutManager(); return instance;
}

可以想象,LayoutManager是一个多么繁忙而重要的类,Flex应用程序中所有可视化组件的度量和布局都由这一个类实例推动。

2.3/ invalidate方法

在本文中,当说起invalidate方法,我的意思是invalidateProperties(),invalidateSize()和invalidateDisplayList()方法。

然而,如果更严谨地说,有两套invalidate方法。LayoutManager的invalidate方法和UIComponent的invalidate方法。

2.3.1/ UIComponent的invalidate方法

Flex开发者通常调用的是UIComponent类的invalidate方法。调用该方法来确保Flex在Flash下一帧调用对应的commitProperties,measure和updateDisplayList方法。

我们以UIComponent类的invalidateProperties()方法为例:

 public function invalidateProperties():void { if (!invalidatePropertiesFlag) { invalidatePropertiesFlag = true; if (nestLevel && UIComponentGlobals.layoutManager) UIComponentGlobals.layoutManager.invalidateProperties(this);} }

实际上,UIComponent的invalidate方法是个"假李逵",UIComponent的invaliate方法最终会调用LayoutManager类的invalidate方法。

因此,更严谨地说,在本文中,当说器invalidate方法时,我的意思是LayoutManager类的invalidateProperties() ,invalidateSize()和invalidateDisplayList()方法。

2.3.2/ LayoutManager的invalidate方法

我们以invalidateProperties()方法为例,看一下invalidate方法的具体工作:

public function invalidateProperties(obj:ILayoutManagerClient ):void
{
if (!invalidatePropertiesFlag && systemManager) {
invalidatePropertiesFlag = true;
if (!listenersAttached) attachListeners(systemManager);} // trace("LayoutManager adding " + Object(obj) + " to invalidatePropertiesQueue");if (targetLevel < = obj.nestLevel)invalidateClientPropertiesFlag = true; invalidatePropertiesQueue.addObject(obj, obj.nestLevel); // trace("LayoutManager added " + Object(obj) + " to invalidatePropertiesQueue");}

invalidateProperties首先设置invalidatePropertiesFlag为true,然后注册监听器attachListeners(systemManager),最后把自己加入了invalidatePropertiesQueue队列 invalidatePropertiesQueue.addObject(obj, obj.nestLevel)。

设置invalidatePropertiesFlash为true以及attacheListeners方法实现了延迟计算。而通过invalidatePropertiesQueue,LayoutManager维护了不同的invalidated对象队列。

我们接下来逐一分析。

i/ invalidate方法实现延迟计算

invalidate方法的主要工作就是实现延迟计算。查看LayoutManager的attachListeners(systemManager)方法,就会理解他是如何做到的:

public function attachListeners(systemManager:ISystemManager):void{if (!waitedAFrame){systemManager.addEventListener(Event.ENTER_FRAME, waitAFrame);}else{systemManager.addEventListener(Event.ENTER_FRAME, doPhasedInstantiationCallback);if (!usePhasedInstantiation){if (systemManager && (systemManager.stage || usingBridge(systemManager))){systemManager.addEventListener(Event.RENDER, doPhasedInstantiationCallback);if (systemManager.stage)systemManager.stage.invalidate();}}}listenersAttached = true;}

attachListeners方法最重要的是通过 systemManager.addEventListener(Event.ENTER_FRAME, waitAFrame)实现了三阶段的延迟计算。这里我们看到了传统Flash开发者很熟悉的Event.ENTER_FRAME事件,这行命令指定FlashPlayer在下一帧执行waitAFrame方法。该方法其余部分帮助确保"在某个阶段中处理 UIComponent 期间,可能出现另一阶段请求重新处理 UIComponent 的情况。这些请求将进行排队,并且只在下次运行此阶段时才得到处理。"。

waitAFrame方法如下:

private function waitAFrame(event:Event):void{// trace(">>LayoutManager:WaitAFrame");systemManager.removeEventListener(Event.ENTER_FRAME, waitAFrame);systemManager.addEventListener(Event.ENTER_FRAME, doPhasedInstantiationCallback);waitedAFrame = true;// trace("< <LayoutManager:WaitAFrame");}

waitAFrame()方法通过systemManager.addEventListener(Event.ENTER_FRAME, doPhasedInstantiationCallback) 又一次"延迟"调用了doPhasedInstantiationCallback方法。doPhasedInstantiationCallback方法则回调用真正执行三阶段任务的doPhasedInstantiation方法。

通过EventENTER_FRAME事件,LayoutManager保证了当调用invalidate方法时,Flex在下下帧执行真正的计算(并不像有些文档所说在下一帧),即执行validate方法。

按照huang.xinghui的评论,我修改了此处:

通过Event.ENTER_FRAME事件,LayoutManager保证了当调用invalidate方法时,Flex在下帧执行真正的计算,即执行validate方法。

huang.xinghui的注释:

在第一次进入时,因为waitedAFrame变量是false,而等待两帧去执行doPhasedInstantiationCallback函数,但是后续就不会了,waitedAFrame变量设置为true,就直接是在下一帧执行doPhasedInstantiationCallback函数。waitedAFrame重头到尾只有在waitAFrame函数里进行了赋值。第一次的两帧执行,不知道是不是和swf加载时的两帧(第一帧加载preloader,第二帧加载application)有关系?

ii/ LayoutManager管理的invalidate对象队列

LayoutManager类实现了延迟计算,但是在下下帧,Flex需要知道针对哪些对象执行validate操作。这些对象就是所谓的"invalidated"对象,即通过invalidate方法打上了 invalidatePropertiesFlag=true, invalidateSizeFlag=true和invalidateDisplayListFlag=true的对象。

LayoutManager维护了三个数组:invalidatePropertiesQueue,invalidateSizeQueue和invalidateDisplayListQueue。

在invalidate方法中,LayoutManager把每一个调用了invalidate方法的UIComponent都置入了对应的队列中,比如调用了invalidateProperties方法的对象被置入了invaliatePropertiesQueue队列,随之加入的还有其在DisplayList的位置。在invalidateProperties方法中,完成该操作的代码如下:

invalidatePropertiesQueue.addObject(obj, obj.nestLevel);

上述代码中,nestLevel实际上是UIComponent在DisplayList上的位置。

当调用invalidate方法时会把UIComponent对象加入对应的队列,而当调用validate方法时,则会把该对象从队列中移除。

2.4/ 三阶段计算:doPhasedInstantiation

真正的三阶段计算发生在doPhasedInstantiation方法。在我们继续之前,先来整理一下目前为止我们已经开始的"探险旅程"。

UIComponent一旦invalidate,就会调用LayoutManager的invalidate方法,invalidate方法先设置invalidate标记为true,然后添加 Event.ENTER_FRAME的侦听器,以在下一帧执行waitAFrame方法。之后,又把调用invalidate的UIComponent对象添加到对应的队列中。

而在下一帧执行时,waitAFrame方法"人如其名",再次添加 Event.ENTER_FRAME侦听器方法doPhasedInstantiationCallback,而该方法会最终调用doPhasedInstantiation。

现在,我们来到了doPhaseInstantiation门前,马上就要揭开组件 "三阶段" 生命周期神秘的面纱。

喘一口气,别激动。

我们先列出doPhaseInstantiation方法代码如下:

/***  @private*/private function doPhasedInstantiation():void{// trace(">>DoPhasedInstantation");// If phasing, do only one phase: validateProperties(),// validateSize(), or validateDisplayList().if (usePhasedInstantiation){if (invalidatePropertiesFlag){validateProperties();// The Preloader listens for this event.systemManager.document.dispatchEvent(new Event("validatePropertiesComplete"));}else if (invalidateSizeFlag){validateSize();// The Preloader listens for this event.systemManager.document.dispatchEvent(new Event("validateSizeComplete"));}else if (invalidateDisplayListFlag){validateDisplayList();// The Preloader listens for this event.systemManager.document.dispatchEvent(new Event("validateDisplayListComplete"));}}// Otherwise, do one pass of all three phases.else{if (invalidatePropertiesFlag)validateProperties();if (invalidateSizeFlag)validateSize();if (invalidateDisplayListFlag)validateDisplayList();}// trace("invalidatePropertiesFlag " + invalidatePropertiesFlag);// trace("invalidateSizeFlag " + invalidateSizeFlag);// trace("invalidateDisplayListFlag " + invalidateDisplayListFlag);if (invalidatePropertiesFlag ||invalidateSizeFlag ||invalidateDisplayListFlag){attachListeners(systemManager);}else{usePhasedInstantiation = false;listenersAttached = false;var obj:ILayoutManagerClient = ILayoutManagerClient(updateCompleteQueue.removeLargest());while (obj){if (!obj.initialized && obj.processedDescriptors)obj.initialized = true;if (obj.hasEventListener(FlexEvent.UPDATE_COMPLETE))obj.dispatchEvent(new FlexEvent(FlexEvent.UPDATE_COMPLETE));obj.updateCompletePendingFlag = false;obj = ILayoutManagerClient(updateCompleteQueue.removeLargest());}// trace("updateComplete");dispatchEvent(new FlexEvent(FlexEvent.UPDATE_COMPLETE));}// trace("< <DoPhasedInstantation");}

在普通的组件生命周期中,我们需要关注的是779行至807行。默认情况下,LayoutManager的usePhasedInstantiation为true,则执行此段代码,完成ActionScript3语言参考中所描述的:

LayoutManager 允许在各个阶段之间更新屏幕。usePhasedInstantiation=true,则在各阶段都会进行度量和布局,每个阶段结束后都会更新一次屏幕。所有组件都将调用其validateProperties() 和 commitProperties() 方法,直到验证完各自的所有属性。屏幕将随之更新。

然后,所有组件都将调用其 validateSize() 和 measure() 方法,直到测量完所有组件,屏幕也将再次更新。

最后,所有组件都将调用其 validateDisplayList() 和 updateDisplayList() 方法,直到验证完所有组件,屏幕也将再次更新。如果正在验证某个阶段,并且前面的阶段失效,则会重新启动 LayoutManager。当创建和初始化大量组件时,此方法更为高效。框架负责设置此属性。

在应用程序启动时,已经默认设置了该属性为true。参见上文中《 LayoutManager()来自何处》。

在 doPhasedInstantiation方法中,对于每一个阶段,Flex执行了同样的操作:首先通过flag变量判断是否需要执行该阶段,如果需要则执行对应的validate方法,最后抛出对应的validate完成事件。

2.5/ validate方法

2.5.1/ LayoutManager的valiate方法

亘古不变的真理是,有"山东"那么一定就有"山西"。有invalidate那么一定有validate。

说到validate方法,实际上意味着三阶段对应的validateProperties(),validateSize()和validateDisplayList()方法。

与invalidate方法一样,也存在着两套validate方法:LayoutManager类的validate方法和UIComponent类的validate方法。但是不同的是,此时,LayoutManager类的validate方法看起来更像"假李逵"。

让我们以LayoutManager类的validateProperties()方法为例:

  private function validateProperties():void{// trace("--- LayoutManager: validateProperties --->");CONFIG::performanceInstrumentation{var perfUtil:PerfUtil = PerfUtil.getInstance();perfUtil.markTime("validateProperties().start");}// Keep traversing the invalidatePropertiesQueue until we've reached the end.// More elements may get added to the queue while we're in this loop, or a// a recursive call to this function may remove elements from the queue while// we're in this loop.var obj:ILayoutManagerClient = ILayoutManagerClient(invalidatePropertiesQueue.removeSmallest());while (obj){// trace("LayoutManager calling validateProperties() on " + Object(obj) + " " + DisplayObject(obj).width + " " + DisplayObject(obj).height);CONFIG::performanceInstrumentation{var token:int = perfUtil.markStart();}if (obj.nestLevel){currentObject = obj;obj.validateProperties();if (!obj.updateCompletePendingFlag){updateCompleteQueue.addObject(obj, obj.nestLevel);obj.updateCompletePendingFlag = true;}}            CONFIG::performanceInstrumentation{perfUtil.markEnd(".validateProperties()", token, 2 /*tolerance*/, obj);}// Once we start, don't stop.obj = ILayoutManagerClient(invalidatePropertiesQueue.removeSmallest());}if (invalidatePropertiesQueue.isEmpty()){// trace("Properties Queue is empty");invalidatePropertiesFlag = false;}// trace("< --- LayoutManager: validateProperties ---");CONFIG::performanceInstrumentation{perfUtil.markTime("validateProperties().end");}}

validateProperties()方法的主要工作就是遍历invalidatePropertiesQueue队列,对队列中的每个UIComponent对象执行其validateProperties方法。

暂停!再回头看一下上面这句话。

这句话中提到的两个valiadteProperties方法中,第一个validateProperties方法指的是LayoutManager类的方法,而第二个则是UIComponent类的方法。

那么LayoutManager的valiateProperties方法是如何执行遍历的呢?通过invalidatePropertiesQueue.removeSmallest()方法,LayoutManager从DisplayList的最顶层开始,从上之下开始遍历。

让我们回顾一下,在invalidate方法中,我们把UICompoennt对象加入了相应的invalidate队列中(比如invalidatePropertiesQueue),同时传入了nestLevel。我们说过,nestLevel表示了UIComponent对象在显示列表中的位置,Application的nestLevel值为3,从3开始,加入到显示列表中的组件,每低一级,则nestLevel指加一。这意味着,在显示列表中,最低一级组件的nestLevel值最大。

回到validateProperties方法,在while循环之前,通过下面代码从队列中移除并获取了nestLevel值最小的UIComponent,即意味着显示列表中调用过invalidateProperties方法的最外层的组件。在while循环的尾部,又一次调用该方法,来保证循环 invalidatePropertiesQueue 队列。

var obj:ILayoutManagerClient = ILayoutManagerClient(invalidatePropertiesQueue.removeSmallest())

如果你查看LayoutManager的valiadteSize方法,你会发现其调用的是removeLargest()方法,意味着validateSize是从最底层级组件开始循环,一直到最顶部。或者称之为从内到外。

查看LayoutManager的validateDisplayList方法,你会发现调用的是removeSmallest()方法,意味着其同validateProperties()方法一样,是从外向内遍历。

在while循环中,validateProperties方法对invalidatePropertiesQueue中的每个UIComponent都执行了validateProperties()方法。

2.5.2/ UIComponent的validate方法

下面,我们深入到UIComponent类,以其validateProperties()方法为例,看一下其是如何执行的。UIComponent类的validateProperties方法代码如下:

public function validateProperties():void{if (invalidatePropertiesFlag){commitProperties();invalidatePropertiesFlag = false;}}

不出所料,UIComponent类的validate方法调用了commitProperties()方法。你也可以想象得到,validateSize()方法调用了measure()方法,而validateDisplayList()方法调用了updateDisplayList()方法。

三. 总结

至此,你应该更加深入的理解了Flex组件生命周期的三阶段commitProperties, measure以及updateDisplayList,以及相对应的invalidate方法。如我们开篇所说的,尽管这些方法存在于UIComponent类,但是其真正的发动机引擎实际上是单体类LayoutManager。

转载于:https://www.cnblogs.com/moondev/archive/2013/02/04/2891629.html

深入浅出Flex组件生命周期Part4 ─ 引擎LayoutManager【转载】相关推荐

  1. Android开发之旅:组件生命周期(二)

    引言 应用程序组件有一个生命周期--一开始Android实例化他们响应意图,直到结束实例被销毁.在这期间,他们有时候处于激活状态,有时候处于非激活状态:对于活动,对用户有时候可见,有时候不可见.组件生 ...

  2. 组件生命周期管理和通信方案

    随着移动互联网的快速发展,项目的迭代速度越来越快,需求改变越来越频繁,传统开发方式的工程所面临的一些,如代码耦合严重.维护效率低.开发不够敏捷等问题就凸现了出来.于是越来越多的公司开始推行" ...

  3. 学习:组件生命周期(1)

    引言 应用程序组件有一个生命 周期--一开始Android实例化他们响应意图,直到结束实例被销毁.在这期间,他们有时候处于激活状态,有时候处于非激活状态:对于活动,对用户有时 候可见,有时候不可见.组 ...

  4. reactjs组件生命周期:componentWillReceiveProps及新旧版本生命周期钩子函数对比

    reactjs组件生命周期:componentWillReceiveProps及新旧版本生命周期钩子函数对比

  5. React 重温之 组件生命周期

    生命周期 任何事物都不会凭空产生,也不会无故消亡.一个事物从产生到消亡经理的各个阶段,我们称之为 生命周期. 具体到我们的前端组件上来,一个组件的生命周期可以大体分为创建.更新.销毁这个三个阶段. 本 ...

  6. vue父子组件生命周期顺序_vue父子组件生命周期执行顺序

    Parent -- Child1 -- Child2 装载 parent beforeCreate parent created parent beforeMount child1 beforeCre ...

  7. react组件生命周期_React组件生命周期-挂钩/方法介绍

    react组件生命周期 React components have several lifecycle methods that you can override to run your code a ...

  8. Taro+react开发(45)taro中组件生命周期

    组件生命周期# 每一个组件都有几个你可以重写以让代码在处理环节的特定时期运行的"生命周期方法".方法中带有前缀 will 的在特定环节之前被调用,而带有前缀 did 的方法则会在特 ...

  9. Ext js 2.0 Overview(3) 组件生命周期

    Component Life Cycle(组件生命周期) In general, the Component architecture in 2.0 will "just work.&quo ...

最新文章

  1. String转XML
  2. GO语言Windows下Liteide
  3. WPF 4 Ribbon 开发 之 快捷工具栏(Quick Access Toolbar)
  4. 无人机图像处理工具更新——多线程优化版
  5. 如何写一个bootloader
  6. 计算机分类及在信息社会中的应用,計算机在信息社会中的应用.doc
  7. UITextField属性
  8. tornado实现基于websocket的好友一对一聊天功能
  9. 三星Galaxy S21系列将搭载One UI3.1系统:首发声音解锁
  10. html网站 放新闻 文件夹名字 是什么,服务器上传网页文件时应注意哪几点?
  11. CentOS7完全卸载mysql5.7重装8.0
  12. 使用PopWindow时距离边界有间隙的解决办法
  13. 康奈尔大学做笔记的方法——文献
  14. linux usb有线网卡驱动_Linux下安装USB网卡驱动
  15. 码code | 拒绝996,不用服务器也能高效开发小游戏
  16. 跑跑卡丁车道具攻与防
  17. Android之微信界面设计
  18. 志强系列的服务器能吃鸡吗,性能芯变化!三款至强E5 V3服务器体验
  19. Python实现的图书分析大屏展示系统(附源码)
  20. Rectangle和RectangleF结构

热门文章

  1. 重读GhostNet:使用轻量操作代替部分传统卷积层生成冗余特征以减少计算量
  2. “3D几何与视觉技术”全球在线研讨会第八期~识别3D中的物体和场景
  3. 一键提升多媒体内容质量:漫谈图像超分辨率技术
  4. GitHub 6600星,面向中国人:微软AI教育与学习共建社区2.0登场!
  5. Github出现连接超时
  6. linux服务器开发板,linuxnfs服务器的建立,虚拟机、开发板间的通信
  7. 连续七天熬夜3D建模师终于出手,让老板增加薪资待遇,分享使用3D建模软件的6个行业
  8. 计算机视觉论文-2021-07-09
  9. mysql 正则regrx_正则表达式
  10. c语言实现目录下文件的多选 反选,oto高清正版分享(53页)-原创力文档