当前位置: 首页 > news >正文

驻马店营销型网站建设优化推广怎样加强文化建设

驻马店营销型网站建设优化推广,怎样加强文化建设,网站做的好有什么用,企业官网设计模板Flutter 架构 简单来讲#xff0c;Flutter 从上到下可以分为三层#xff1a;框架层、引擎层和嵌入层#xff0c;下面我们分别介绍#xff1a; 1. 框架层 Flutter Framework#xff0c;即框架层。这是一个纯 Dart实现的 SDK#xff0c;它实现了一套基础库#xff0c;自…Flutter 架构 简单来讲Flutter 从上到下可以分为三层框架层、引擎层和嵌入层下面我们分别介绍 1. 框架层 Flutter Framework即框架层。这是一个纯 Dart实现的 SDK它实现了一套基础库自底向上我们来简单介绍一下 底下两层Foundation 和 Animation、Painting、Gestures在 Google 的一些视频中被合并为一个dart UI层对应的是Flutter中的dart:ui包它是 Flutter Engine 暴露的底层UI库提供动画、手势及绘制能力。 Rendering 层即渲染层这一层是一个抽象的布局层它依赖于 Dart UI 层渲染层会构建一棵由可渲染对象的组成的渲染树当动态更新这些对象时渲染树会找出变化的部分然后更新渲染。渲染层可以说是Flutter 框架层中最核心的部分它除了确定每个渲染对象的位置、大小之外还要进行坐标变换、绘制调用底层 dart:ui 。 Widgets 层是 Flutter 提供的的一套基础组件库在基础组件库之上Flutter 还提供了 Material 和 Cupertino 两种视觉风格的组件库它们分别实现了 Material 和 iOS 设计规范。 Flutter 框架相对较小因为一些开发者可能会使用到的更高层级的功能已经被拆分到不同的软件包中使用 Dart 和 Flutter 的核心库实现其中包括平台插件例如 camera 和 webview 以及和平台无关的功能例如 animations 。 2. 引擎层 Engine即引擎层。毫无疑问是 Flutter 的核心 该层主要是 C 实现其中包括了 Skia 引擎、Dart 运行时Dart runtime、文字排版引擎等。在代码调用 dart:ui库时调用最终会走到引擎层然后实现真正的绘制和显示。 3. 嵌入层 Embedder即嵌入层。Flutter 最终渲染、交互是要依赖其所在平台的操作系统 API嵌入层主要是将 Flutter 引擎 ”安装“ 到特定平台上。嵌入层采用了当前平台的语言编写例如 Android 使用的是 Java 和 C iOS 和 macOS 使用的是 Objective-C 和 Objective-CWindows 和 Linux 使用的是 C。 Flutter 代码可以通过嵌入层以模块方式集成到现有的应用中也可以作为应用的主体。Flutter 本身包含了各个常见平台的嵌入层假如以后 Flutter 要支持新的平台则需要针对该新的平台编写一个嵌入层。 通常来说开发者不需要感知到Engine和Embedder的存在如果不需要调用平台的系统服务Framework是开发者需要直接交互的因而也在整个分层架构模型的最上层。 Widget 接口 在 Flutter 中 widget 的功能是“描述一个UI元素的配置信息”它就是说 Widget 其实并不是表示最终绘制在设备屏幕上的显示元素所谓的配置信息就是 Widget 接收的参数比如对于 Text 来讲文本的内容、对齐方式、文本样式都是它的配置信息。下面我们先来看一下 Widget 类的声明 immutable // 不可变的 abstract class Widget extends DiagnosticableTree {const Widget({ this.key });final Key? key;protectedfactoryElement createElement();overrideString toStringShort() {final String type objectRuntimeType(this, Widget);return key null ? type : $type-$key;}overridevoid debugFillProperties(DiagnosticPropertiesBuilder properties) {super.debugFillProperties(properties);properties.defaultDiagnosticsTreeStyle DiagnosticsTreeStyle.dense;}overridenonVirtualbool operator (Object other) super other;overridenonVirtualint get hashCode super.hashCode;static bool canUpdate(Widget oldWidget, Widget newWidget) {return oldWidget.runtimeType newWidget.runtimeType oldWidget.key newWidget.key;}... }immutable 代表 Widget 是不可变的这会限制 Widget 中定义的属性即配置信息必须是不可变的final为什么不允许 Widget 中定义的属性变化呢这是因为Flutter 中 如果属性发生变化则会重新构建Widget树即重新创建新的 Widget 实例来替换旧的 Widget 实例所以允许 Widget 的属性变化是没有意义的因为一旦 Widget 自己的属性变了自己就会被替换。这也是为什么 Widget 中定义的属性必须是 final 的原因。Widget类继承自DiagnosticableTreeDiagnosticableTree即 “诊断树”主要作用是提供调试信息。Key: 这个key属性类似于 React/Vue 中的key主要的作用是决定是否在下一次build时复用旧的 widget 决定的条件在canUpdate()方法中。createElement()正如前文所述“一个 widget 可以对应多个Element”Flutter 框架在构建UI树时会先调用此方法生成对应节点的Element对象。此方法是 Flutter 框架隐式调用的在我们开发过程中基本不会调用到。debugFillProperties(...) 复写父类的方法主要是设置诊断树的一些特性。canUpdate(...) 是一个静态方法它主要用于 在 widget 树重新build时复用旧的 widget 其实具体来说应该是是否用新的 widget 对象去更新旧UI树上所对应的Element对象的配置通过其源码我们可以看到只要newWidget与oldWidget的runtimeType和key同时相等时就会用new widget去更新Element对象的配置否则就会创建新的Element。 在Flutter中可以说万事万物皆Widget。哪怕一个居中的能力在传统的命令式UI开发中通常会以一个属性的方式进行设置而在Flutter中则抽象成一个名为Center的组件。此外Flutter提倡激进式的组合开发即尽可能通过一系列基础的Widget构造出你的目标Widget。 基于以上两个特点Flutter的代码中将充斥着各种Widget每一帧UI的更新都意味着部分Widget的重建。你可能会担心这种设计是否过于臃肿和低效其实恰恰相反这正是Flutter能够进行高性能渲染的基石。Flutter之所以要如此设计也是基于以下两点事实有意而为之 Widget Tree上的Widget节点越多通过Diff算法得到的需要重建的部分就越精确、范围越小而UI渲染的主要性能瓶颈就是Widget节点的重建。 Dart语言的对象模型 和 GC模型 对小对象的快读分配和回收做了优化而Widget正是这种小对象。 Flutter中的三棵树 既然 Widget 只是描述一个UI元素的配置信息那么真正的布局、绘制是由谁来完成的呢 Flutter 框架的的处理流程是这样的 根据 Widget 树生成一个 Element 树Element 树中的节点都继承自 Element 类。根据 Element 树生成 Render 树渲染树渲染树中的节点都继承自RenderObject 类。根据渲染树生成 Layer 树然后上屏显示Layer 树中的节点都继承自 Layer 类。 真正的布局和渲染逻辑在 Render 树中Element 是 Widget 和 RenderObject 的粘合剂可以理解为一个中间代理。 其中三棵树的各自的作用是 Widget负责配置为Element描述UI的配置信息拥有公开的Api这部分也是开发者可以直接感知和使用的部分。ElementFlutter Virtual DOM 的管理者它管理widget的生命周期代表了内存中真实存在的UI数据它负责树中特定位置的widget的实例化持有widget的引用管理树中的父子关系实际上Widget Tree和 RenderObject Tree 都是由Element Tree驱动生成的。RenderObject负责处理大小、布局和绘制工作它将绘制自身摆放子节点等。 这里需要注意 三棵树中 Element 和 Widget是 一 一 对应的但 Element 和 RenderObject 不是 一 一 对应的。比如 StatelessWidget 和 StatefulWidget 都没有对应的 RenderObject。渲染树在上屏前会生成一棵 Layer 树因此在Flutter中实际上是有四棵树但我们只需了解以上三棵树就行。 StatelessWidget StatelessWidget相对比较简单它继承自widget类重写了createElement()方法 override StatelessElement createElement() StatelessElement(this);StatelessElement 间接继承自Element类与StatelessWidget相对应作为其配置数据。 StatelessWidget用于不需要维护状态的场景它通常在build方法中通过嵌套其他 widget 来构建UI在构建过程中会递归的构建其嵌套的 widget 。 下面是一个简单的例子 class Echo extends StatelessWidget {const Echo({Key? key, required this.text,this.backgroundColor Colors.grey, //默认为灰色}):super(key:key);final String text;final Color backgroundColor;overrideWidget build(BuildContext context) {return Center(child: Container(color: backgroundColor,child: Text(text),),);} }然后我们可以通过如下方式使用它 Widget build(BuildContext context) {return Echo(text: hello world); }按照惯例widget 的构造函数参数应使用命名参数命名参数中的必需要传的参数要添加required关键字这样有利于静态代码分析器进行检查在继承 widget 时第一个参数通常应该是Key。另外如果 widget 需要接收子 widget 那么child或children参数通常应被放在参数列表的最后。同样是按照惯例 widget 的属性应尽可能的被声明为final防止被意外改变。 Context build方法有一个context参数它是BuildContext类的一个实例表示当前 widget 在 widget 树中的上下文每一个 widget 都会对应一个 context 对象。 实际上context是当前 widget 在 widget 树中位置中执行”相关操作“的一个句柄(handle) 比如它提供了从当前 widget 开始向上遍历 widget 树以及按照 widget 类型查找父级 widget 的方法。 下面是在子树中获取父级 widget 的一个示例 class ContextRoute extends StatelessWidget {overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text(Context测试),),body: Container(child: Builder(builder: (context) {// 在 widget 树中向上查找最近的父级Scaffold widget Scaffold scaffold context.findAncestorWidgetOfExactTypeScaffold();// 直接返回 AppBar的title 此处实际上是Text(Context测试)return (scaffold.appBar as AppBar).title;}),),);} }StatefulWidget 有了StatelessWidget为什么还要有一个StatefulWidget呢在Flutter中Widget都是不可变的 但实际上需要根据对应的状态刷新Widget于是就产生了StatefulWdiget, StatefulWidget是由2个对象Widget和State组成的。 StatefulWidget同样是继承自widget类并重写了createElement()方法不同的是返回的Element 对象并不相同返回的是一个StatefulElement另外StatefulWidget类中添加了一个新的接口createState()。 abstract class StatefulWidget extends Widget {const StatefulWidget({ Key key }) : super(key: key);overrideStatefulElement createElement() StatefulElement(this);protectedState createState(); }StatefulElement 间接继承自Element类与StatefulWidget相对应作为其配置数据。StatefulElement中可能会多次调用createState()来创建状态State对象。开发者无需关心该方法。 createState() 用于创建和 StatefulWidget 相关的状态开发者必须在子类中实现该方法。它在StatefulWidget 的生命周期中可能会被多次调用。例如当一个 StatefulWidget 同时插入到 widget 树的多个位置时Flutter 框架就会调用该方法为每一个位置生成一个独立的State实例其实本质上就是一个StatefulElement对应一个State实例。 而在StatefulWidget 中State 对象和StatefulElement具有一 一对应的关系所以在Flutter的SDK文档中可以经常看到“从树中移除 State 对象”或“插入 State 对象到树中”这样的描述此时的树指通过 widget 树生成的 Element 树。Flutter 的 SDK 文档中经常会提到“树” 我们可以根据语境来判断到底指的是哪棵树。其实对于用户来说我们无需关心到底是哪棵树无论哪种树其最终目标都是为了描述 UI 的结构和绘制信息我们只需理解为 “一棵构成用户界面的节点树”即可不必过于纠结于这些概念。 State 一个 StatefulWidget 类会对应一个 State 类State表示与其对应的 StatefulWidget 要维护的状态State 中的保存的状态信息可以 State在 widget 构建时可以被同步读取。State在 widget 生命周期中可以被改变可以手动调用setState()方法改变State这会通知 Flutter 框架状态发生改变Flutter 框架在收到消息后会重新调用其build方法重新构建 widget 树从而达到更新UI的目的。 State 中有两个常用属性 State.widget通过它我们可以拿到与该 State 实例关联的 widget 实例由 Flutter 框架动态设置。注意这种关联并非永久的因为在应用生命周期中UI树上的某一个节点的 widget 实例在重新构建时可能会变化但State实例只会在第一次插入到树中时被创建当在重新构建时如果 widget 被修改了Flutter 框架会动态设置 State.widget 指向新的 widget 实例。 State.context它是StatefulWidget对应的 BuildContext作用同 StatelessWidget 的 BuildContext。当框架需要创建一个StatefulWidget时它会调用StatefulWidget.createState()方法然后会将State对象和 BuildContext关联在一起这种关联关系是永久的State对象永远不会改变它的BuildContext但是BuildContext本身可以在树的周围移动。 State生命周期 在State对象被createState()方法创建之后紧接着会调用initState()方法并开始自己的生命周期 其中各个回调函数的含义 initState()当 widget 第一次插入到 widget 树时会被调用对于每一个State对象Flutter 框架只会调用一次该回调所以通常在该回调中做一些一次性的操作如状态初始化、订阅子树的事件通知等。 注意不能在initState中调用BuildContext.dependOnInheritedWidgetOfExactType该方法用于在 widget 树上获取离当前 widget 最近的一个父级InheritedWidget原因是在初始化完成后 widget 树中的InheritFrom widget也可能会发生变化所以正确的做法应该在在build()方法或didChangeDependencies()中调用它。 didChangeDependencies()当State对象的依赖发生变化时会被调用例如在之前 build() 中包含了一个InheritedWidget 然后在之后的 build() 中 Inherited widget发生了变化那么此时InheritedWidget的子 widget 的didChangeDependencies()回调都会被调用。典型的场景是当系统语言 Locale 或应用主题改变时Flutter 框架会通知 widget 调用此回调。需要注意组件第一次被创建后挂载的时候包括重新创建对应的didChangeDependencies也会被调用。 build()主要用于构建 widget 子树的会在如下场景被调用 在调用initState()之后。在调用didUpdateWidget()之后。在调用setState()之后。在调用didChangeDependencies()之后。在State对象从树中一个位置移除后会调用deactivate又重新插入到树的其他位置之后。 reassemble(): 专门为了开发调试而提供的回调在 热重载(hot reload) 时会被调用此回调在Release模式下永远不会被调用。 didUpdateWidget()在 widget 重新构建时Flutter 框架会调用widget.canUpdate来检测 widget 树中同一位置的新旧节点然后决定是否需要更新如果widget.canUpdate返回true则会调用此回调。 正如之前所述widget.canUpdate会在新旧 widget 的 key 和 runtimeType 同时相等时会返回true也就是说在这种情况时didUpdateWidget()就会被调用。实际上这里flutter框架会创建一个新的Widget,绑定本State并在这个函数中传递老的Widget上级节点rebuild widget时 即上级组件状态发生变化时也会触发子widget执行didUpdateWidget。需要注意的是如果涉及到controller的变更需要在这个函数中移除老的controller的监听并创建新controller的监听。 deactivate()当 State 对象从树中被移除时会调用此回调。在一些场景下Flutter 框架会将 State 对象重新插到树中如包含此 State 对象的子树在树的一个位置移动到另一个位置时可以通过 GlobalKey 来实现。如果移除后没有重新插入到树中则紧接着会调用dispose()方法。 dispose()当 State 对象从树中被永久移除时调用通常在此回调中释放资源、取消订阅、取消动画等。 通过以下计数器的例子来说明 class CounterWidget extends StatefulWidget {const CounterWidget({Key? key, this.initValue 0});final int initValue;override_CounterWidgetState createState() _CounterWidgetState(); }class _CounterWidgetState extends StateCounterWidget {int _counter 0;overridevoid initState() {super.initState();//初始化状态_counter widget.initValue;print(initState);}overrideWidget build(BuildContext context) {print(build);return Scaffold(body: Center(child: TextButton(child: Text($_counter),//点击后计数器自增onPressed: () setState(() _counter,),),),);}overridevoid didUpdateWidget(CounterWidget oldWidget) {super.didUpdateWidget(oldWidget);print(didUpdateWidget );}overridevoid deactivate() {super.deactivate();print(deactivate);}overridevoid dispose() {super.dispose();print(dispose);}overridevoid reassemble() {super.reassemble();print(reassemble);}overridevoid didChangeDependencies() {super.didChangeDependencies();print(didChangeDependencies);} }注意在继承StatefulWidget重写其方法时对于包含mustCallSuper标注的父类方法都要在子类方法中调用父类方法。 接下来创建一个新路由页面在其中我们只显示一个CounterWidget class StateLifecycleTest extends StatelessWidget {const StateLifecycleTest({Key? key}) : super(key: key);overrideWidget build(BuildContext context) {return CounterWidget();} }我们运行应用并打开该路由页面在新路由页打开后屏幕中央就会出现一个数字0然后控制台日志输出 I/flutter ( 5436): initState I/flutter ( 5436): didChangeDependencies I/flutter ( 5436): build可以看到在StatefulWidget插入到 widget 树时首先initState方法会被调用。 然后我们点击⚡️按钮热重载控制台输出日志如下 I/flutter ( 5436): reassemble I/flutter ( 5436): didUpdateWidget I/flutter ( 5436): build可以看到在build方法之前只有 reassemble 和 didUpdateWidget 被调用。 接下来我们在 widget 树中移除 CounterWidget将 StateLifecycleTest 的 build 方法改为 Widget build(BuildContext context) {//移除计数器 //return CounterWidget ();//随便返回一个Text()return Text(xxx); }然后热重载日志如下 I/flutter ( 5436): reassemble I/flutter ( 5436): deactive I/flutter ( 5436): dispose可以看到在 CounterWidget从 widget 树中移除时deactive和dispose会依次被调用。 setState() setState() 方法是Flutter中修改状态的唯一合法途径该方法会通知框架此对象的内部状态已更改调用方式为setState(() {_myState newValue;}); 它会提供一个回调方法开发者必须在该回调中修改状态值该回调将立即被同步调用。注意决不能在该回调中执行耗时计算更不能在其中返回一个Future回调不能是“async”标记的该回调通常只用于包装对状态的实际更改。 调用 setState() 后会将当前widget对应的element对象标记为dirty随后会调用build()方法刷新UI。下面是加入 setState() 后State的生命周期 这样看来其实可以将State生命周期分为三个阶段 初始化 构造函数、initState()、didChangeDependencies()状态变化由配置变化触发didUpdateWidget()导致的build()执行、由setState()导致的build()执行组件移除deactivate()、dispose() StatefulWidget 为什么要将 State 和 Widget 分开呢 StatefulWidget 为什么要将 build 方法放在 State 类中而不是放在 StatefulWidget 中 这主要是为了开发的灵活性和性能考虑。 如果将build()方法放在StatefulWidget中从灵活性上考虑会有两个问题 1. 状态访问不便。 试想一下如果我们的StatefulWidget有很多状态而每次状态改变都要调用build方法由于状态是保存在 State 中的如果build方法在StatefulWidget中那么build方法和 状态 分别在两个类中那么构建时读取状态将会很不方便试想一下如果真的将build方法放在 StatefulWidget 中的话由于构建用户界面过程需要依赖 State所以build方法将必须加一个State参数大概是下面这样 Widget build(BuildContext context, State state){//state.counter...}这样的话就只能将State的所有状态声明为公开的状态这样才能在State类外部访问状态但是将状态设置为公开后状态将不再具有私密性这就会导致对状态的修改将会变的不可控。但如果将build()方法放在State中的话构建过程不仅可以直接访问状态而且也无需公开私有状态这会非常方便。 2. 继承 StatefulWidget 不便。 例如Flutter 中有一个动画 widget 的基类AnimatedWidget它继承自StatefulWidget类。AnimatedWidget中引入了一个抽象方法build(BuildContext context)继承自AnimatedWidget的动画 widget 都要实现这个build方法。现在设想一下如果StatefulWidget 类中已经有了一个build方法正如上面所述此时build方法需要接收一个 State 对象这就意味着AnimatedWidget必须将自己的 State 对象(记为_animatedWidgetState)提供给其子类因为子类需要在其build方法中调用父类的build方法代码可能如下 class MyAnimationWidget extends AnimatedWidget {overrideWidget build(BuildContext context, State state){// 由于子类要用到AnimatedWidget的状态对象_animatedWidgetState// 所以AnimatedWidget必须通过某种方式将其状态对象_animatedWidgetState 暴露给其子类 super.build(context, _animatedWidgetState)} }这样很显然是不合理的因为AnimatedWidget的状态对象是AnimatedWidget内部实现细节不应该暴露给外部。 如果要将父类状态暴露给子类那么必须得有一种传递机制而做这一套传递机制是无意义的因为父子类之间状态的传递和子类本身逻辑是无关的。 综上所述可以发现对于StatefulWidget将build方法放在 State 中可以给开发带来很大的灵活性。 另一个方面主要是性能考虑State管理状态可以理解为ControllerWidget是UI即View)根据状态变化每次生成Widget(即View可以节省内存而不必每次创建状态对象State。 在 widget 树中获取State对象 由于 StatefulWidget 的的具体逻辑都在其 State 中所以很多时候我们需要获取 StatefulWidget 对应的State 对象来调用一些方法比如Scaffold组件对应的状态类ScaffoldState中就定义了打开 SnackBar路由页底部提示条的方法。我们有两种方法在子 widget 树中获取父级 StatefulWidget 的State 对象。 1. 通过Context获取State context对象有一个findAncestorStateOfType()方法该方法可以从当前节点沿着 widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象。下面是实现打开 SnackBar 的示例 class GetStateObjectRoute extends StatefulWidget {const GetStateObjectRoute({Key? key}) : super(key: key);overrideStateGetStateObjectRoute createState() _GetStateObjectRouteState(); }class _GetStateObjectRouteState extends StateGetStateObjectRoute {overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text(子树中获取State对象),),body: Center(child: Column(children: [Builder(builder: (context) {return ElevatedButton(onPressed: () {// 查找父级最近的Scaffold对应的ScaffoldState对象ScaffoldState _state context.findAncestorStateOfTypeScaffoldState()!;// 打开抽屉菜单_state.openDrawer();},child: Text(打开抽屉菜单1),);}),],),),drawer: Drawer(),);} }一般来说如果 StatefulWidget 的状态是私有的不应该向外部暴露那么我们代码中就不应该去直接获取其 State 对象如果StatefulWidget的状态是希望暴露出的通常还有一些组件的操作方法我们则可以去直接获取其State对象。但是通过 context.findAncestorStateOfType 获取 StatefulWidget 的状态的方法是通用的我们并不能在语法层面指定 StatefulWidget 的状态是否私有所以在 Flutter 开发中便有了一个默认的约定如果 StatefulWidget 的状态是希望暴露出的应当在 StatefulWidget 中提供一个of 静态方法来获取其 State 对象开发者便可直接通过该方法来获取如果 State不希望暴露则不提供of方法。这个约定在 Flutter SDK 里随处可见。所以上面示例中的Scaffold也提供了一个of方法我们其实是可以直接调用它的 Builder(builder: (context) {return ElevatedButton(onPressed: () {// 直接通过of静态方法来获取ScaffoldStateScaffoldState _stateScaffold.of(context);// 打开抽屉菜单_state.openDrawer();},child: Text(打开抽屉菜单2),); }),又比如我们想显示 snackbar 的话可以通过下面代码调用 Builder(builder: (context) {return ElevatedButton(onPressed: () {ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(我是SnackBar)),);},child: Text(显示SnackBar),); }),2. 通过GlobalKey获取State Flutter还有一种通用的获取State对象的方法——通过GlobalKey来获取 步骤分两步 给目标StatefulWidget添加GlobalKey。 //定义一个globalKey, 由于GlobalKey要保持全局唯一性我们使用静态变量存储 static GlobalKeyScaffoldState _globalKey GlobalKey(); ... Scaffold(key: _globalKey , //设置key... )通过GlobalKey来获取State对象 _globalKey.currentState.openDrawer()GlobalKey 是 Flutter 提供的一种在整个 App 中引用 element 的机制。如果一个 widget 设置了GlobalKey那么我们便可以通过globalKey.currentWidget获得该 widget 对象、globalKey.currentElement来获得 widget 对应的element对象如果当前 widget 是StatefulWidget则可以通过globalKey.currentState来获得该 widget 对应的state对象。 注意使用 GlobalKey 开销较大如果有其他可选方案应尽量避免使用它。另外同一个 GlobalKey 在整个 widget 树中必须是唯一的不能重复。 导航路由视角下的State生命周期 从StatefulElement视角分析StatefulWidget生命周期 可以看到BuildOwner通过StatefulElement间接驱动了State各个生命周期回调的触发。下面将从源码的角度分析每个回调的触发路径及其含义。 // 代码清单8-1 flutter/packages/flutter/lib/src/widgets/framework.dart StatefulElement(StatefulWidget widget): state widget.createState(), // 触发State创建super(widget) {state._element this; // State持有Element和Widget的引用state._widget widget;assert(state._debugLifecycleState _StateLifecycle.created); }StatefulElement在自身的构造函数中完成State的创建并完成了两个关键字段的赋值。此后会进行该Element节点的Build流程具体逻辑如代码清单8-2所示。 // 代码清单8-2 flutter/packages/flutter/lib/src/widgets/framework.dart override void _firstBuild() {try { // 触发initState回调final dynamic debugCheckForReturnedFuture state.initState() as dynamic;} finally { ...... }state.didChangeDependencies(); // 触发didChangeDependencies回调super._firstBuild(); }因为以上逻辑触发了生命周期中的initState和didChangeDependencies方法所以 initState一般会作为State内部成员变量开始初始化的时间点。 didChangeDependencies虽然在首次构建时也会无条件触发但是它在后续Build流程中依然会被触发如代码清单8-3所示。 // 代码清单8-3 flutter/packages/flutter/lib/src/widgets/framework.dart override // StatefulElement void performRebuild() {if (_didChangeDependencies) { // 通常在代码清单8-13中设置为true详见8.2节state.didChangeDependencies(); // 当该字段为true时再次触发didChangeDependencies_didChangeDependencies false;}super.performRebuild(); }didChangeDependencies标志该Element的依赖节点发生了改变此时 didChangeDependencies 方法会被再次调用所以该回调比较适合响应一些依赖的更新。performnRebuild最终还会触发StatefulElement的build方法如代码清单8-4所示。 // 代码清单8-4 flutter/packages/flutter/lib/src/widgets/framework.dart override Widget build() state.build(this);以上逻辑符合使用StatefulWidget的直觉即build方法是放在State中的该回调主要用于UI的更新。需要注意的是如果当前Element节点标记为dirty则build方法一定会被调用所以不宜进行耗时操作以免影响UI的流畅度。 对于非首次Build的情况通常会触发Element的update方法对StatefulElement来说其逻辑如代码清单8-5所示。 // 代码清单8-5 flutter/packages/flutter/lib/src/widgets/framework.dart void update(StatefulWidget newWidget) { // StatefulElement见代码清单5-47super.update(newWidget);assert(widget newWidget);final StatefulWidget oldWidget state._widget!;_dirty true;state._widget widget as StatefulWidget;try {_debugSetAllowIgnoredCallsToMarkNeedsBuild(true);final dynamic debugCheckForReturnedFuture state.didUpdateWidget(oldWidget) as dynamic;} finally {_debugSetAllowIgnoredCallsToMarkNeedsBuild(false);}rebuild(); // 触发代码清单8-4中的逻辑 }以上逻辑在通过rebuild触发State的build方法之前会触发其didUpdateWidget方 法。移除对oldWidget的一些引用和依赖以及更新一些依赖Widget属性的资源通过该方法进行操作是一个合适的时机。 Element节点在detach阶段会调用deactivateRecursively方 法其具体逻辑如代码清单8-6所示。 // 代码清单8-6 flutter/packages/flutter/lib/src/widgets/framework.dart static void _deactivateRecursively(Element element) { // 见代码清单5-50element.deactivate();assert(element._lifecycleState _ElementLifecycle.inactive);element.visitChildren(_deactivateRecursively); } override void deactivate() { // StatefulElementstate.deactivate(); // 触发deactivate回调super.deactivate(); // 见代码清单8-7 }以上逻辑会触发State的deactivate方法并导致当前节点进入inactive阶段。该回调触发说明当前Element节点被移出Element Tree但仍有可用被再次加入该时机适合释放一些和当前状态强相关的资源而对于那些和状态无关的资源考虑到该Element节点仍有可能进入Element Tree并不适合在此时释放可以类比Android中 Activity 的onPause回调。 而以上逻辑中super的deactivate方法是必须调用的其逻辑如代码清单8-7所示。 // 代码清单8-7 flutter/packages/flutter/lib/src/widgets/framework.dart mustCallSuper void deactivate() { // Elementif (_dependencies ! null _dependencies!.isNotEmpty) { // 依赖清理for (final InheritedElement dependency in _dependencies!)dependency._dependents.remove(this);}_inheritedWidgets null;_lifecycleState _ElementLifecycle.inactive; // 更新状态 }以上逻辑主要用于移除对应的依赖。 在Build流程结束后Element的unmount方法将被调用其逻辑如代码清单8-8所示。 // 代码清单8-8 flutter/packages/flutter/lib/src/widgets/framework.dart override // StatefulElement见代码清单5-52 void unmount() {super.unmount();state.dispose(); // 触发dispose回调state._element null; } mustCallSuper void unmount() { // Elementfinal Key? key _widget.key;if (key is GlobalKey) {key._unregister(this); // 取消注册}_lifecycleState _ElementLifecycle.defunct; }以上逻辑中StatefulElement中主要会调用State的dispose方法。Element中的通用逻辑负责销毁GlobalKey相关的注册。 Flutter视角下的App生命周期 需要指出的是如果想要知道App的生命周期那么需要通过WidgetsBindingObserver的didChangeAppLifecycleState 来获取。通过该接口可以获取的生命周期状态在AppLifecycleState类中。常用状态包含如下几个 resumed界面可见可对用户输入做出响应同Android的onResumeinactive界面退到后台或弹出对话框情况下处于非活动状态即失去了焦点没有接收用户输入但仍可以执行drawFrame回调同Android的onPausepaused应用挂起当前对用户不可见比如退到后台失去了焦点不能响应用户输入且不会收到drawFrame回调engine不会回调PlatformDispatcher的onBeginFrame和onDrawFrame同Android的onStopdetached 应用仍然托管在flutter引擎上但已与任何主机视图分离不会再执行drawFrame回调当应用处于此状态时engine在没有视图的情况下运行。这可能是处在engine首次初始化时附加视图的过程也可能是在由于Navigator弹出而销毁视图之后。 使用示例 class _MyHomePageState extends StateMyHomePage with WidgetsBindingObserver {overridevoid initState() {super.initState();WidgetsBinding.instance.addObserver(this);}overridevoid dispose() {super.dispose();WidgetsBinding.instance.removeObserver(this);}overridevoid didChangeAppLifecycleState(AppLifecycleState state) {print(state);} } Flutter 与其他跨平台解决方案的对比 时至今日业界已经有很多跨平台框架特指 Android 和 iOS 两个平台根据其原理主要分为三类 H5 原生Cordova、Ionic、微信小程序JavaScript 开发 原生渲染 React Native、Weex自绘UI 原生 (Qt for mobile、Flutter) 上表中开发语言主要指应用层的开发语言而开发效率是指整个开发周期的效率包括编码时间、调试时间、以及排错、处理兼容性问题时间。动态化主要指是否支持动态下发代码和是否支持热更新。值得注意的是 Flutter 的Release 包默认是使用 Dart AOT 模式编译的所以不支持动态化但 Dart 还有 JIT 或 snapshot 运行方式这些模式都是支持动态化的。 方案1H5 原生 这类框架主要原理就是将 App 中需要动态变动的内容通过HTML5简称 H5来实现通过原生的网页加载控件WebView Android或 WKWebViewiOS来加载。这种方案中H5 部分是可以随时改变而不用发版动态化需求能满足同时由于 H5 代码只需要一次开发就能同时在 Android 和 iOS 两个平台运行这也可以减小开发成本也就是说H5 部分功能越多开发成本就越小。我们称这种 H5 原生 的开发模式为混合开发 采用混合模式开发的App我们称之为混合应用或 HTMLybrid App 如果一个应用的大多数功能都是 H5 实现的话我们称其为 Web App 。 目前混合开发框架的典型代表有Cordova、Ionic 。大多数 App 中都会有一些功能是 H5 开发的至少目前为止HTMLybrid App 仍然是最通用且最成熟的跨端解决方案。 在此我们需要提一下小程序目前国内各家公司小程序应用层的开发技术栈是 Web 技术栈而底层渲染方式基本都是 WebView 和原生相结合的方式。 混合开发技术点 1) 示例JavaScript调用原生API获取手机型号 下面我们以 Android 为例实现一个获取手机型号的原生 API 供 JavaScript 调用。在这个示例中将展示 JavaScript 调用原生 API 的流程。我们选用 Github上开源库的 dsBridge 作为 JsBridge 来进行通信。 首先在原生中实现获取手机型号的API class JSAPI {JavascriptInterfacepublic Object getPhoneModel(Object msg) {return Build.MODEL;} } 将原生API通过WebView注册到JsBridge中 import wendu.dsbridge.DWebView ... //DWebView继承自WebView由dsBridge提供 DWebView dwebView (DWebView) findViewById(R.id.dwebview); //注册原生API到JsBridge dwebView.addJavascriptObject(new JsAPI(), null);在JavaScript中调用原生API var dsBridge require(dsbridge) //直接调用原生API getPhoneModel var model dsBridge.call(getPhoneModel); //打印机型 console.log(model);上面示例演示了 JavaScript 调用原生 API 的过程同样的一般来说优秀的 JsBridge 也支持原生调用 JavaScript dsBridge 也是支持的如果感兴趣可以去 Github dsBridge 项目主页查看。 现在我们回头来看一下混合应用无非就是在第一步中预先实现一系列 API 供 JavaScript 调用让 JavaScript 有访问系统功能的能力看到这里相信你也可以自己实现一个混合开发框架了。 小结 混合应用的优点是动态内容可以用 H5开发而H5是Web 技术栈Web技术栈生态开放且社区资源丰富整体开发效率高。缺点是性能体验不佳对于复杂用户界面或动画WebView 有时会不堪重任。 方案2JavaScript开发 原生渲染 React 响应式编程 React 是一个响应式的 Web 框架我们先了解一下两个重要的概念DOM 树与响应式编程。 DOM 树 文档对象模型Document Object Model简称DOM是 W3C 组织推荐的处理可扩展标志语言的标准编程接口一种独立于平台和语言的方式访问和修改一个文档的内容和结构。换句话说这是表示和处理一个 HTML 或XML 文档的标准接口。简单来说DOM 就是文档树与用户界面控件树对应在前端开发中通常指 HTML 对应的渲染树但广义的 DOM 也可以指 Android 中的 XML 布局文件对应的控件树而术语DOM操作就是指直接来操作渲染树或控件树 因此可以看到其实 DOM 树和控件树是等价的概念只不过前者常用于 Web 开发中而后者常用于原生开发中。 响应式编程 React 中提出一个重要思想状态改变则UI随之自动改变。而 React 框架本身就是响应用户状态改变的事件而执行重新构建用户界面的工作这就是典型的 响应式 编程范式下面我们总结一下 React 中响应式原理 开发者只需关注状态转移数据当状态发生变化React 框架会自动根据新的状态重新构建UI。React 框架在接收到用户状态改变通知后会根据当前渲染树结合最新的状态改变通过 Diff 算法计算出树中变化的部分然后只更新变化的部分DOM操作从而避免整棵树重构提高性能。 值得注意的是在第二步中状态变化后 React 框架并不会立即去计算并渲染 DOM 树的变化部分相反React会在 DOM 树的基础上建立一个抽象层即虚拟DOM树对数据和状态所做的任何改动都会被自动且高效的同步到虚拟 DOM 最后再批量同步到真实 DOM 中而不是每次改变都去操作一下DOM。 为什么不能每次改变都直接去操作 DOM 树这是因为在浏览器中每一次 DOM 操作都有可能引起浏览器的重绘或回流重新排版布局确定 DOM 节点的大小和位置 如果 DOM 只是外观风格发生变化如颜色变化会导致浏览器重绘界面。如果 DOM 树的结构发生变化如尺寸、布局、节点隐藏等导致浏览器就需要回流。 而浏览器的重绘和回流都是比较昂贵的操作如果每一次改变都直接对 DOM 进行操作这会带来性能问题而批量操作只会触发一次 DOM 更新会有更高的性能。 React Native React Native 简称 RN 是 Facebook 于 2015 年 4 月开源的跨平台移动应用开发框架是 Facebook 早先开源的 Web 框架 React 在原生移动应用平台的衍生产物目前支持 iOS 和 Android 两个平台。RN 使用JSX 语言扩展后的 JavaScript主要是可以在 JavaScript 中写 HTML标签和 CSS 来开发移动应用。因此熟悉 Web 前端开发的技术人员只需很少的学习就可以进入移动应用开发领域。 由于 RN 和 React 原理相通并且 Flutter在应用层也是受 React 启发很多思想也都是相通的。 上文已经提到 React Native 是 React 在原生移动应用平台的衍生产物那两者主要的区别是什么呢其实主要的区别在于虚拟 DOM 映射的对象是什么。React中虚拟 DOM 最终会映射为浏览器 DOM 树而 RN 中虚拟 DOM会通过 JavaScriptCore 映射为原生控件。 JavaScriptCore 是一个JavaScript解释器它在React Native中主要有两个作用 为 JavaScript 提供运行环境。 是 JavaScript 与原生应用之间通信的桥梁作用和 JsBridge 一样事实上在 iOS 中很多 JsBridge 的实现都是基于 JavaScriptCore 。 而 RN 中将虚拟 DOM 映射为原生控件的过程主要分两步 布局消息传递 将虚拟 DOM 布局信息传递给原生 原生根据布局信息通过对应的原生控件渲染 至此React Native 便实现了跨平台。 相对于混合应用由于React Native是 原生控件渲染所以性能会比混合应用中 H5 好一些同时 React Native 提供了很多原生组件对应的 Web 组件大多数情况下开发者只需要使用 Web 技术栈 就能开发出 App。我们可以发现这样也就做到了维护一份代码便可以跨平台了。 Weex Weex 是阿里巴巴于 2016 年发布的跨平台移动端开发框架思想及原理和 React Native 类似底层都是通过原生渲染的不同是应用层开发语法 即 DSLDomain Specific LanguageWeex 支持 Vue 语法和 Rax 语法Rax 的 DSL(Domain Specific Language) 语法是基于 React JSX 语法而创造而 RN 的 DSL 是基于 React 的不支持 Vue。 小结 JavaScript 开发 原生渲染 的方式主要优点如下 采用 Web 开发技术栈社区庞大、上手快、开发成本相对较低。原生渲染性能相比 H5 提高很多。动态化较好支持热更新。 不足 渲染时需要 JavaScript 和原生之间通信在有些场景如拖动可能会因为通信频繁导致卡顿。JavaScript 为脚本语言执行时需要解释执行 这种执行方式通常称为 JIT即 Just In Time指在执行时实时生成机器码执行效率和编译类语言编译类语言的执行方式为 AOT 即 Ahead Of Time指在代码执行前已经将源码进行了预处理这种预处理通常情况下是将源码编译为机器码或某种中间码仍有差距。由于渲染依赖原生控件不同平台的控件需要单独维护并且当系统更新时社区控件可能会滞后除此之外其控件系统也会受到原生UI系统限制例如在 Android 中手势冲突消歧规则是固定的这在使用不同人写的控件嵌套时手势冲突问题将会变得非常棘手。这就会导致如果需要自定义原生渲染组件时开发和维护成本过高。 方案3自绘UI 原生 这种技术的思路是通过在不同平台实现一个统一接口的渲染引擎来绘制UI而不依赖系统原生控件所以可以做到不同平台UI的一致性。 注意自绘引擎解决的是 UI 的跨平台问题如果涉及其他系统能力调用依然要涉及原生开发。这种平台技术的优点如下 性能高由于自绘引擎是直接调用系统API来绘制UI所以性能和原生控件接近。 灵活、组件库易维护、UI外观保真度和一致性高由于UI渲染不依赖原生控件也就不需要根据不同平台的控件单独维护一套组件库所以代码容易维护。由于组件库是同一套代码、同一个渲染引擎所以在不同平台组件显示外观可以做到高保真和高一致性另外由于不依赖原生控件也就不会受原生布局系统的限制这样布局系统会非常灵活。 不足 动态性不足为了保证UI绘制性能自绘UI系统一般都会采用 AOT 模式编译其发布包所以应用发布后不能像 Hybrid 和 RN 那些使用 JavaScriptJIT作为开发语言的框架那样动态下发代码。应用开发效率低Qt 使用 C 作为其开发语言而编程效率是直接会影响 App 开发效率的C 作为一门静态语言在 UI 开发方面灵活性不及 JavaScript 这样的动态语言另外C需要开发者手动去管理内存分配没有 JavaScript 及Java中垃圾回收GC的机制。 也许你已经猜到 Flutter 就属于这一类跨平台技术没错Flutter 正是实现一套自绘引擎并拥有一套自己的 UI 布局系统且同时在开发效率上有了很大突破。不过自绘制引擎的思路并不是什么新概念Flutter并不是第一个尝试这么做的在它之前有一个典型的代表即大名鼎鼎的Qt。 Qt Mobile Qt 是一个1991年由 Qt Company 开发的跨平台 C 图形用户界面应用程序开发框架。2008年Qt Company 科技被诺基亚公司收购Qt 也因此成为诺基亚旗下的编程语言工具。2012年Qt 被 Digia 收购。2014年4月跨平台集成开发环境 Qt Creator 3.1.0 正式发布实现了对于 iOS 的完全支持新增 WinRT、Beautifier 等插件废弃了无 Python 接口的 GDB 调试支持集成了基于 Clang 的 C/C 代码模块并对 Android 支持做出了调整至此实现了全面支持 iOS、Android、WP它提供给应用程序开发者构建图形用户界面所需的所有功能。 但是Qt 虽然在 PC 端获得了巨大成功备受社区追捧然而其在移动端却表现不佳在近几年很少能听到 Qt 的声音尽管 Qt 是移动端开发跨平台自绘引擎的先驱但却成为了烈士。究其原因可能有以下几点 Qt 移动开发社区太小学习资料不足生态不好。官方推广不利支持不够。移动端发力较晚市场已被其他动态化框架占领 Hybrid 和 RN )。在移动开发中C 开发和Web开发栈相比有着先天的劣势直接结果就是 Qt 开发效率太低。 Flutter Flutter 是 Google 发布的一个用于创建跨平台、高性能移动应用的框架。Flutter 和 Qt mobile 一样都没有使用原生控件相反都实现了一个自绘引擎使用自身的布局、绘制系统。那么我们会担心Qt mobile 面对的问题Flutter是否也一样Flutter会不会步入Qt mobile后尘成为另一个烈士Flutter从2017 年诞生至今经历了多年时间的历练Flutter 的生态系统得以快速增长国内外有非常多基于 Flutter 的成功案例国内的互联网公司基本都有专门的 Flutter 团队。总之Flutter 发展飞快已在业界得到了广泛的关注和认可在开发者中受到了热烈的欢迎成为了移动跨端开发中最受欢迎的框架之一。 现在我们来和 Qt mobile做一个对比 生态Flutter 生态系统发展迅速社区非常活跃无论是开发者数量还是第三方组件都已经非常可观。技术支持现在 Google 正在大力推广FlutterFlutter 的作者中很多人都是来自Chromium团队并且 Github上活跃度很高。另一个角度从 Flutter 诞生到现在频繁的版本发布也可以看出 Google 对 Flutter的投入的资源不小所以在官方技术支持这方面大可不必担心。开发效率一套代码多端运行并且在开发过程中 Flutter 的热重载可帮助开发者快速地进行测试、构建UI、添加功能并更快地修复错误。在 iOS 和 Android 模拟器或真机上可以实现毫秒级热重载并且不会丢失状态。这真的很棒相信我如果你是一名原生开发者体验了Flutter开发流后很可能就不想重新回去做原生了毕竟很少有人不吐槽原生开发的编译速度。 Flutter 中的 GC Flutter使用Dart作为开发语言和运行时机制Dart一直保留着运行时机制无论是在调试模式debug还是发布模式release但是两种构建方式之间存在很大的差异。 在debug模式下Dart将所有的管道需要用到的所有配件全部装载到设备上Dart runtime、JITthe just-in-time编译器/解释器JIT for Android and interpreter for iOS、调试和性能分析服务。在release模式下会采用AOT编译去除JIT依然保留Dart runtime因为Dart runtime是Flutter App的主要贡献者。 Dart的运行时包括一个非常重要的组件垃圾回收器它主要的作用就是在一个对象被实例化instantiated或者变成不可达unreachable时分配和释放内存。 在Flutter运行过程中会有很多的Object。在StatelessWidget在渲染前其实上还有StatefulWidget他们被创建出来。当状态发生变化的时候他们又会被销毁。事实上他们有很短的寿命。当我们构建一个复杂的UI界面时会有成千上万这样的Widget。 所以作为Flutter开发者我们是否需要担心垃圾回收器能否很好的帮助我们管理这些是否会带来很多的性能问题随着Flutter频繁地创建和销毁这些WidgetObjects对象开发人员是否应该采取措施限制这种行为 对于新的Flutter开发人员来说如果他们知道一个Widget不会随着时间的推移而改变时他们会创建一个Widget引用并将它们放在State中这样它们就不会被破坏和重建这并不罕见。 这样做大可不必 担心Dart的GC是完全没有必要的这是因为它的分代架构为了可以让我们频繁的创建和销毁对象专门做了优化。在大多数情况下我们只需要让Flutter的引擎创建并销毁它喜欢的所有Widget即可。 Dart的GC Dart的GC是分代的generational和由两个阶段构成Young Space Scavengerscavenger针对年轻一袋进行回收 和 Parallel Marking and Concurrent Sweepingsweep collectors针对老一代进行回收 Scheduling 为了最大限度地减少GC对应用程序和UI性能的影响垃圾收集器为Flutter引擎提供了hooks当引擎检测到应用程序空闲且没有用户交互时该hooks会提醒它。这使垃圾收集器窗口有机会在不影响性能的情况下运行其收集阶段。 垃圾收集器还可以在这些空闲间隔期间运行滑动压缩sliding compaction从而通过减少内存碎片来最大程度地减少内存开销。 阶段一Young Space Scavenger 这个阶段主要是清理一些寿命很短的对象比如StatelessWidget。当它处于阻塞时它的清理速度远快于第二代的mark/sweep方式。并且结合调度完成可以消除程序运行时的暂停现象。 本质上对象被分配到内存中的一个连续空间当对象被创建时它们被分配到下一个可用空间直到分配的内存被填满。Dart使用碰撞指针(bump pointer)分配来快速分配新的空间使过程非常快速。如果像malloc一样维护free_list再分配效率很低 分配新对象的新空间由两半组成称为半空间。任何时候都只使用一半空间一半处于活动状态另一半处于非活动状态。新对象被分配到活动的一半区域一旦活动的一半被填充完毕活动对象就会从活动区域copy到非活动区域并且清除死亡的Object。然后不活动的一半就变为了活动状态并重复该过程。这跟JVM的分代回收策略中新生代的幸存者空间很像 为了确定哪些Object是存活的或死亡的GC从根对象如堆栈变量开始检查它们引用的内容。然后将有引用的Object存活的移动到非活动状态直接所有的存活Object被移动。死亡的Object没有参照物因此被留下来在将来的垃圾收集事件中活动对象将被复制到它们上面。 有关此的更多信息请查看Cheney算法。 阶段二Parallel Marking and Concurrent Sweeping 当对象达到一定的寿命在第一阶段没有被GC回收它们会被提升到一个由第二代收集器管理的新的内存空间mark-sweep。 这种垃圾收集技术有两个阶段首先遍历对象图然后标记仍在使用的对象。在第二阶段扫描整个内存并回收任何未标记的对象。然后清除所有标志。 这种GC技术阻塞了标记阶段不会发生内存突变并且UI线程也会被阻塞。但是由于短暂的对象在Young Space Scavenger阶段已经被处理所以这种情况阶段非常罕见。但有时Dart运行时需要暂停才能运行这种形式的GC。考虑到Flutter计划收集的能力应将其影响降至最低。 需要注意的是如果一个应用程序不遵循分代的假设即大多数对象英年早逝的假设那么这种形式的GC将更频繁地发生。考虑到Flutter中Widget的工作机制这不太可能发生但需要了解。 Isolate 值得注意的是Dart中的Isolate机制具有私有堆的概念彼此是独立的。每个Isolate有自己单独的线程来运行每个Isolate的GC不影响其他Isolate的性能。使用Isolate是避免阻塞UI和卸载流程密集型活动的好方法。耗时操作可以使用Isolate 到这里你应该明白Dart采用了一个强大的分代垃圾收集器以最大限度的减少Flutter中GC带来的性能影响。 所以你不需要担心Dart的垃圾回收器可以放心的把精力放在业务上。 参考 《Flutter实战·第二版》《Flutter内核源码剖析》Flutter: Don’t Fear the Garbage Collector
http://www.hkea.cn/news/14481568/

相关文章:

  • 黄页网站大全免费网上开店卖货流程
  • 做网站多少钱_西宁君博优选大埔建设工程交易中心网站
  • 网站引流是什么意思wordpress博客增加音乐页面
  • 建站网址做学术用的网站
  • 上海市门户网站怎么创建自己的网址
  • 简约的网站设计做网站推广有用吗
  • 怎么用ps做静态网站上线后wordpress后台无法登陆
  • jsp做网站前端实例在本地做改版如何替换旧网站会影响百度收录吗
  • 怎么免费建商城网站吗网站建设的想法和意见
  • 杭州网站建设浙江网站推广软件工具
  • 网站设计案例欣赏网站建设内容保障制度
  • 杭州精品网站建设公司上海野猪seo
  • 本地手机网站建设网站开发技术部分
  • 福州网站制作维护服务深圳网站设计clh
  • 网站建设如何传视频教程常用软件开发平台
  • 网站备案 新增网络媒体发稿平台
  • 学习网站模板wordpress商城模版
  • 福建做网站公司排名韩国怎么出线
  • 温州网站制作案例百度2345网址导航
  • 百度建设网站的目的江苏园博园建设开发有限公司网站
  • 网站检索功能怎么做廊坊网站专业制作
  • 房产中介网站开发模板科技打破垄断全球的霸权
  • 做网站与全网营销搜索推广排名优化wordpress wp_footer()
  • 电商网站报价wordpress网站建设教程视频
  • 无锡网站排名公司手机设计网站公司
  • 莱芜网站优化是什么招聘销售员网站建设网络推广
  • 栾城网站建设登录设备管理
  • 如何做自己的网站链接id链接wordpress
  • 网站开发什么开发语言好网络设计目标
  • 押注网站建设个人网站备案需要哪些