淘特 Flutter 流式场景的深度优化-阿里云开发者社区

开发者社区> 游客2zsucpt4lrnma> 正文

淘特 Flutter 流式场景的深度优化

简介: 通过进行了一系列的深度优化后,平均帧率已经达到50帧之上超越了原生的表现, 但卡顿率依然达不到最佳的体验效果,遇到了难以突破的瓶颈和技术挑战,需要进行技术尝试和突破。
+关注继续查看

一、前言

    淘特在很多业务场景都使用了 Flutter,加上业务场景本身具有一定的复杂性,使得 Flutter 在低端机流式场景的滑动浏览过程中卡顿、跳帧对比使用原生(Android/iOS)开发明显。通过分析业务层在 Flutter 渲染流程中的每个阶段存在的性能问题进行了一系列的深度优化后,平均帧率已经达到50帧之上超越了原生的表现, 但卡顿率依然达不到最佳的体验效果,遇到了难以突破的瓶颈和技术挑战,需要进行技术尝试和突破。

    本文会从底层原理、优化思路、实际场景的优化策略、核心技术实现、优化成果、总结和参考资料进行讲述,期望可以为大家带来一定的启发和帮助,也欢迎多多交流与指正,共建美好的 Flutter 技术社区。

二、渲染机制

原生 vs Flutter

Flutter 本身是基于原生系统之上的,所以渲染机制和 Native 是非常接近的,引用 Google Flutter 团队 Xiao Yu分享[1],如下图所示:

渲染流程

如图左中,Flutter 从接收到 VSync 信号之后整体经历 8 个阶段,其中 Compositing 阶段后会将数据提交给 GPU

Semantics 阶段会将 RenderObject marked 需要做语义化更新的信息传递给系统,实现辅助功能,通过语义化接口可以帮助有视力障碍的用户来理解UI内容,和整体绘制流程关联不大。

Finalize Tree 阶段会将所有添加到 _inactiveElements 的不活跃 Element 全部 unmount 掉,和整体绘制流程关联不大。

所以,Flutter 整体渲染流程主要关注 图右 中的阶段:

GPU Vsync

  • Flutter Engine 在收到垂直同步信号后,会通知 Flutter Framework 进行 beginFrame,进入 Animation 阶段。

Animation

  • 主要执行了 transientCallbacks 回调。

Flutter Engine 会通知 Flutter Framework 进行 drawFrame,进入 Build 阶段

Build

  • 构建要呈现的UI组件树的数据结构,即创建对应的 Widget 以及对应的 Element

Layout

  • 目的是要计算出每个节点所占空间的真实大小进行布局;
  • 然后更新所有 dirty render objects 的布局信息。

Compositing Bits

  • 对需要更新的 RenderObject 进行 update 操作;

Paint

  • 生成 Layer Tree,生成 Layer Tree 并不能直接使用,还需要 Compositing 合成为一个 Scene 并进行 Rasterize 光栅化处理。层级合并的原因是因为一般 Flutter 的层级很多,直接把每一层传递给 GPU 效率很低,所以会先做 Composite 提高效率。光栅化之后才会交给 Flutter Engine 处理。

Compositing

  • 将 Layout Tree 合成为 Scene,并创建场景当前状态的栅格图像,即进行 Rasterize 光栅化处理,然后提交给 Flutter Engine,最后 Skia 通过 Open GL or Vulkan 接口提交数据给 GPU, GPU经过处理后进行显示。

核心渲染阶段

  • Widget

    我们平时在写的大都是 WidgetWidget 其实可以理解为是一个组件树的数据结构,是 Build 阶段的主要部分。其中 Widget Tree 的深度、 StatefulWidgetsetState 合理性、build 函数中是否有不合理逻辑以及使用了调用 saveLayer 的相关Widget往往会成为性能问题。

  • Element

    关联 WidgetRenderObject ,生成 Widget 对应的 Element 存放上下文信息,Flutter 通过遍历 Element 来生成 RenderObject 视图树支撑UI结构;

  • RenderObject

    RenderObject 在 Layout 阶段确定布局信息,Paint 阶段生成为对应的 Layer,可见其重要程度。所以 Flutter 中大部分的绘图性能优化发生在这里。RenderObject 树构建的数据会被加入到 Engine 所需的 LayerTree 中。

三、性能优化思路

了解底层渲染机制和核心渲染阶段,可以将优化分为三层:


这里不具体展开讲每一层的优化细节,本文主要从实际的场景来讲述。

四、流式场景

流式组件原理

在原生开发下,通常使用 RecyclerView/UICollectionView 进行列表场景的开发;在Flutter开发下,Flutter Framework 也提供了ListView的组件,它的实质其实是 SliverList

核心源码

我们从 SliverList 的核心源码来进行分析:

class SliverList extends SliverMultiBoxAdaptorWidget {

  @override
  RenderSliverList createRenderObject(BuildContext context) {
    final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement;
    return RenderSliverList(childManager: element);
  }
}

abstract class SliverMultiBoxAdaptorWidget extends SliverWithKeepAliveWidget {

  final SliverChildDelegate delegate;

  @override
  SliverMultiBoxAdaptorElement createElement() => SliverMultiBoxAdaptorElement(this);

  @override
  RenderSliverMultiBoxAdaptor createRenderObject(BuildContext context);
}

通过查看 SliverList 的源代码可知,SliverList 是一个 RenderObjectWidget ,结构如下:

我们首先看它的 RenderObject 的核心源码:

class RenderSliverList extends RenderSliverMultiBoxAdaptor {
  
  RenderSliverList({
    @required RenderSliverBoxChildManager childManager,
  }) : super(childManager: childManager);
  
  @override
  void performLayout(){
    ...
    //父节点对子节点的布局限制
    final SliverConstraints constraints = this.constraints;
    final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
    final double remainingExtent = constraints.remainingCacheExtent;
    final double targetEndScrollOffset = scrollOffset + remainingExtent;
    final BoxConstraints childConstraints = constraints.asBoxConstraints();
    ...
    insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
    ...
    insertAndLayoutChild(childConstraints,after: trailingChildWithLayout,parentUsesSize: true);
    ...
    collectGarbage(leadingGarbage, trailingGarbage);
    ...
  }
}

abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...{
  @protected
  RenderBox insertAndLayoutChild(BoxConstraints childConstraints, {@required RenderBox after,...}) {
    _createOrObtainChild(index, after: after);
    ...
  }
  
  RenderBox insertAndLayoutLeadingChild(BoxConstraints childConstraints, {@required RenderBox after,...}) {
    _createOrObtainChild(index, after: after);
    ...
  }
  
  @protected
  void collectGarbage(int leadingGarbage, int trailingGarbage) {
    _destroyOrCacheChild(firstChild);
    ...
  }
  
  void _createOrObtainChild(int index, { RenderBox after }) {
    _childManager.createChild(index, after: after);
    ...
  }
  
  void _destroyOrCacheChild(RenderBox child) {
    if (childParentData.keepAlive) {
      //为了更好的性能表现不会进行keepAlive,走else逻辑.
      ...
    } else {
      _childManager.removeChild(child);
      ...
    }
  }
}

查看 RenderSliverList 的源码发现,对于 child 的创建和移除都是通过其父类 RenderSliverMultiBoxAdaptor 进行。而 RenderSliverMultiBoxAdaptor 是通过 _childManagerSliverMultiBoxAdaptorElement 进行的,整个 SliverList 绘制过程中布局大小由父节点给出了限制。

在流式场景下:

  • 在滑动过程中是通过 SliverMultiBoxAdaptorElement.createChild 进行对进入可视区新的 child 的创建;(即业务场景的每一个item卡片)
  • 在滑动过程中是通过 SliverMultiBoxAdaptorElement.removeChild 进行对不在可视区旧的 child 的移除;

我们来看下 SliverMultiBoxAdaptorElement 的核心源码:

class SliverMultiBoxAdaptorElement extends RenderObjectElement implements RenderSliverBoxChildManager {
  final SplayTreeMap<int, Element> _childElements = SplayTreeMap<int, Element>();

  @override
  void createChild(int index, { @required RenderBox after }) {
    ...
    Element newChild = updateChild(_childElements[index], _build(index), index);
    ...
  }
  
  @override
  void removeChild(RenderBox child) {
    ...
    final Element result = updateChild(_childElements[index], null, index);
    ...
  }
  
  @override
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    ...
    final Element newChild = super.updateChild(child, newWidget, newSlot);
    ...
  }
}

通过查看 SliverMultiBoxAdaptorElement 的源码可以发现,对于 child 的操作其实都是通过父类 ElementupdateChild 进行的。

接下来,我们来看下 Element 的核心代码:

abstract class Element extends DiagnosticableTree implements BuildContext {
  @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    Element newChild;
    if (child != null) {
      ...
      bool hasSameSuperclass = oldElementClass == newWidgetClass;;
      if (hasSameSuperclass && child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        newChild = child;
      } else {
        deactivateChild(child);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else {
      newChild = inflateWidget(newWidget, newSlot);
    }
    ...
    return newChild;
  }
  
  @protected
  Element inflateWidget(Widget newWidget, dynamic newSlot) {
    ...
    final Element newChild = newWidget.createElement();
    newChild.mount(this, newSlot);
    ...
    return newChild;
  }
  
  @protected
  void deactivateChild(Element child) {
    child._parent = null;
    child.detachRenderObject(); 
    owner._inactiveElements.add(child); // this eventually calls child.deactivate() & child.unmount()
    ...
  }
}

可以看到主要调用 ElementmountdetachRenderObject,这里我们来看下 RenderObjectElement 的 这两个方法的源码:

abstract class RenderObjectElement extends Element {
  @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    ...
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    ...
  }
  
  @override
  void attachRenderObject(dynamic newSlot) {
    ...
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    _ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
    ...
  }
  
  @override
  void detachRenderObject() {
    if (_ancestorRenderObjectElement != null) {
      _ancestorRenderObjectElement.removeChildRenderObject(renderObject);
      _ancestorRenderObjectElement = null;
    }
    ...
  }
}

通过查看上面源码的追溯,可知:

在流式场景下:

  • 在滑动过程中进入可视区新的 child 的创建,是通过创建全新的 Element 并 mount 挂载到 Element Tree;然后创建对应的 RenderObject,调用了 _ancestorRenderObjectElement?.insertChildRenderObject
  • 在滑动过程中不在可视区旧的 child 的移除,将对应的 Element 从 Element Tree unmount 移除挂载;然后调用了 _ancestorRenderObjectElement.removeChildRenderObject

其实这个 _ancestorRenderObjectElement 就是 SliverMultiBoxAdaptorElement,我们再来看下 SliverMultiBoxAdaptorElement:

class SliverMultiBoxAdaptorElement extends RenderObjectElement implements RenderSliverBoxChildManager {
  
  @override
  void insertChildRenderObject(covariant RenderObject child, int slot) {
    ...
    renderObject.insert(child as RenderBox, after: _currentBeforeChild);
    ...
  }
  
  @override
  void removeChildRenderObject(covariant RenderObject child) {
    ...
    renderObject.remove(child as RenderBox);
  }
}

其实调用的都是 ContainerRenderObjectMixin 的方法,我们再来看下 ContainerRenderObjectMixin:

mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ... {
  void insert(ChildType child, { ChildType after }) {
        ...
    adoptChild(child);// attach render object
    _insertIntoChildList(child, after: after);
  }
  
  void remove(ChildType child) {
    _removeFromChildList(child);
    dropChild(child);// detach render object
  }
}

ContainerRenderObjectMixin 维护了一个双向链表来持有当前 children RenderObject,所以在滑动过程中创建和移除都会同步在 ContainerRenderObjectMixin 的双向链表中进行添加和移除。

最后总结下来:

  • 在滑动过程中进入可视区新的 child 的创建,是通过创建全新的 Element 并 mount 挂载到 Element Tree;然后创建对应的 RenderObject, 通过调用 SliverMultiBoxAdaptorElement.insertChildRenderObject attach 到 Render Tree,并同步将 RenderObject 添加到 SliverMultiBoxAdaptorElement 所 mixin 的双链表中;
  • 在滑动过程中不在可视区旧的 child 的移除,将对应的 Element 从 Element Tree unmount 移除挂载;然后通过用 SliverMultiBoxAdaptorElement.removeChildRenderObject 将对应的 RenderObject 从所 mixin 的双链表中移除并同步将 RenderObject 从 Render Tree detach 掉。

渲染原理

通过核心源码的分析,我们可以对流式场景的 Element 做如下分类:

下面我们来看用户向上滑动查看更多商品卡片并触发加载下一页数据进行展示时,整体的渲染流程和机制:

  • 向上滑动时,顶部 0 和 1 的卡片移出 Viewport 区域(Visible Area + Cache Area),我们定义它为进入 Detach Area,进入 Detach Area 后将对应的 RenderObject 从 Render Tree detach 掉,并且将对应的 Element 从 Element Tree unmount 移除挂载,并同步从双向链表中移除;
  • 通过监听 ScrollController 的滑动计算位置来判断是否需要开始加载下一页数据,然后底部 Loading Footer 组件会进入可视区 or 缓存区,需要对 SliverChildBuilderDelegate 的 childCount +1,最后一个 child 返回 Loading Footer 组件,同时调用 setState 对整个 SliverList 刷新。update 会调用 performRebuild 进行重构建,中间部分在用户可视区会全部进行 update 操作;然后创建 Loading Footer 组件对应新的 ElementRenderObject,并同步添加到双向链表中;
  • 当 loading 结束数据返回后,会再次调用 setState 对整个 SliverList 刷新,update 会调用 performRebuild 进行重构建,中间部分在用户可视区会全部进行 update 操作;然后将 Loading Footer 组件将对应的 RenderObject 从 Render Tree detach 掉,并且将对应的 Element 从 Element Tree unmount 移除挂载,并同步从双向链表中移除;
  • 底部新的 item 会进入可视区 or 缓存区,需要创建对应新的 ElementRenderObject,并同步添加到双向链表中;

优化策略

上面用户向上滑动查看更多商品卡片并触发加载下一页数据进行展示的场景,可以从五个方向进行优化:

  • Load More

    通过监听 ScrollController 的滑动不断进行计算,最好无需判断,自动识别到需要加载下一页数据然后发起 loadMore() 回调。新建 ReuseSliverChildBuilderDelegate 增加 loadMore 以及和 item Builder 同级的 footerBuilder,并默认包含 Loading Footer 组件,在 SliverMultiBoxAdaptorElement.createChild(int index,...) 判断是否需要动态回调 loadMore() 并自动构建 footer 组件。

  • 局部刷新

    参考了闲鱼之前在长列表的流畅度优化[2],在下一页数据回来之后调用 setState 对整个 SliverList 刷新,导致中间部分在用户可视区会全部进行 update 操作,实际只需刷新新创建的部分,优化 SliverMultiBoxAdaptorElement.update(SliverMultiBoxAdaptorWidget newWidget) 的部分实现局部刷新,如下图:

  • Element & RenderObject 复用

    参考了闲鱼之前在长列表的流畅度优化[2] 和 Google Android RecyclerView ViewHolder 复用设计[3],在有新的 item 创建时,可以做类似 Android RecyclerViewViewHolder 对组件进行持有并复用。基于对渲染机制原理分析,在 Flutter 中 Widget 其实可以理解为是一个组件树的数据结构,即更多是组件结构的数据表达。我们需要对移除的 item 的 ElementRenderObject 分组件类型进行缓存持有,在创建新的 item 的时候优先从缓存持有中取出进行复用。同时不破坏 Flutter 本身对 Key 的设计,当如果 item 有使用 Key 的时候,只复用和它 Key 相同的 ElementRenderObject。但在流式场景列表数据都是不同的数据,所以在流式场景中使用了 Key,也就无法进行任何的复用。如果对 ElementRenderObject 进行复用,item 组件不建议使用 Key

    我们在对原有流式场景下 Element 的分类增加一个缓存态:

如下图:

  • GC 抑制

    Dart 自身有 GC 的机制,类似 Java 的分代回收,可以在滑动的过程中对 GC 进行抑制,定制 GC 回收的算法。针对这项和 Google 的 Flutter 专家讨论,其实 Dart 不像 Java 会存在多线程切换进行垃圾回收的情况,单线程(主isolate)垃圾回收更快更轻量级,同时需要对 Flutter Engine 做深度的改造,考虑收益不大暂不进行。

  • 异步化

    Flutter Engine 限制非 Main Isolate 调用 Platform 相关 Api,将非跟 Platform Thread 交互的逻辑全部放至新的 isolate 中,频繁 Isolate 的创建和回收也会对性能有一定的影响,Flutter compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String debugLabel }) 每次调用会创建新的 Isolate,执行完任务后会进行回收,实现一个类似线程池的 Isolate 来进行处理非视图任务。经过实际测试提升不明显,不展开讲述。

核心技术实现

​我们可以将调用链路的代码做如下分类:


所有渲染核心在继承自 RenderObjectElementSliverMultiBoxAdaptorElement 中,不破坏原有功能设计以及 Flutter Framework 的结构,新增了 ReuseSliverMultiBoxAdaptorElementElement 来进行优化策略的实现,并且可以直接搭配原有 SliverListRenderSliverList 使用或者自定义的流式组件(例如:瀑布流组件)的 RenderObject 使用。

局部刷新

  • 调用链路优化

ReuseSliverMultiBoxAdaptorElementupdate 方法做是否为局部刷新的判断,如果不是局部刷新依然走 performRebuild;如果是局部刷新,只创建新产生的 item。

  • 核心代码
@override
  void update(covariant ReuseSliverMultiBoxAdaptorWidget newWidget) {
    ...
    //是否进行局部刷新
    if(_isPartialRefresh(oldDelegate, newDelegate)) {
        ...
        Widget newWidget = _buildItem(index);
        ...
        _createChild(index, newWidget);
      } else {
         // need to rebuild
         performRebuild();
      }
  }

Element & RenderObject 复用

  • 调用链路优化

    • 创建

ReuseSliverMultiBoxAdaptorElementcreateChild 方法读取 _cacheElements 对应组件类型缓存的 Element 进行复用;如果没有同类型可复用的 Element 则创建对应新的 ElementRenderObject

  • 移除

ReuseSliverMultiBoxAdaptorElementremoveChild 方法将移除的 RenderObject 从双链表中移除,不进行 Element 的 deactive 和 RenderObject 的 detach,并将对应的 Element_slot 更新为null,使下次可以正常复用,然后将对应的 Element 缓存到 _cacheElements 对应组件类型的链表中。
注:不 deactive Element 其实不进行调用即可实现,但不 detach RenderObject 无法直接做到,需要在 Flutter Framework 层的 object.dart 文件中,新增一个方法 removeOnly 就是只将 RenderObject 从双链表中移除不进行 detach。

  • 核心代码

    • 创建
  //新增的方法,createChild会调用到这个方法
  _createChild(int index, Widget newWidget){
      ...
      Type delegateChildRuntimeType = _getWidgetRuntimeType(newWidget);
      child = _takeChild(delegateChildRuntimeType,index);
      ...
      newChild = updateChild(child, newWidget, index);
      ...
  }
  • 移除
  @override
  void removeChild(RenderBox child) {
     ...
     removeChildRenderObject(child); // call removeOnly
     ...
     removeElement = _childElements.remove(index);
     _performCacheElement(removeElement);
 }

Load More

  • 调用链路优化

createChild 时候判断是否是构建 footer 来进行处理。

  • 核心代码
  @override
  void createChild(int index, { @required RenderBox after }) {
      ...
      Widget newWidget;
      if(_isBuildFooter(index)){ // call footerBuilder & call onLoadMore
        newWidget = _buildFooter();
      }else{
        newWidget = _buildItem(index);
      }
      ...
      _createChild(index, newWidget);
      ...
  }

整体结构设计

  • 将核心的优化能力内聚在 Element 层,提供底层能力;
  • ReuseSliverMultiBoxAdaptorWidget 做为基类默认返回优化后的 Element
  • 将 loadMore 和 FooterBuilder 的能力统一由继承自 SliverChildBuilderDelegateReuseSliverChildBuilderDelegate 对上层暴露;

  • 如有自己单独定制的流式组件 Widget ,直接把继承关系从 RenderObjectWidget 换为 ReuseSliverMultiBoxAdaptorWidget 即可,例如自定义的单列表组件(ReuseSliverList)、瀑布流组件(ReuseWaterFall)等。

五、优化成果

基于在之前的一系列深度优化以及切换 Flutter Engine 为UC Hummer 之上,单独控制流式场景的优化变量,使用 PerfDog 获取流畅度数据,进行了流畅度测试对比:


可以看到整体性能数据都有优化提升,结合替换 Engine 之前的测试数据平均来看,对帧率有 2-3 帧的提升,卡顿率下降 1.5 个百分点。

六、总结

使用方式

和原生 SliverList 的使用方式一样,Widget 换成对应可以进行复用的组件 (ReuseSliverList/ReuseWaterFall/ CustomSliverList),delegate 如果需要 footerloadMore 使用 ReuseSliverChildBuilderDelegate;如果不需要直接使用原生的 SliverChildBuilderDelegate 即可。

  • 需要分页场景
return ReuseSliverList( // ReuseWaterFall or CustomSliverList
  delegate: ReuseSliverChildBuilderDelegate(
    (BuildContext context, int index) {
      return getItemWidget(index);
    }, 
    //构建footer
    footerBuilder: (BuildContext context) {
      return DetailMiniFootWidget();
    },
    //添加loadMore监听
    addUnderFlowListener: loadMore,
    childCount: dataOfWidgetList.length
  )
);
  • 无需分页场景
return ReuseSliverList( // ReuseWaterFall or CustomSliverList
  delegate: SliverChildBuilderDelegate(
    (BuildContext context, int index) {
      return getItemWidget(index);
    }, 
    childCount: dataOfWidgetList.length
  )
);

注意点

使用的时候 item/footer 组件不要加 Key,否则认为只对同 Key 进行复用。因为复用了 Element,虽然表达组件树数据结果的 Widget 会每次进行更新,但 StatefulElementState 是在 Element 创建的时候生成的,同时也会被复用下来,和 Flutter 本身设计保持一致,所以需要在 didUpdateWidget(covariant T oldWidget)State 缓存的数据重新从 Widget 获取即可。

Reuse Element Lifecycle

将每个 item 的状态进行回调,上层可以做逻辑处理和资源释放等,例如之前在 didUpdateWidget(covariant T oldWidget)State 缓存的数据重新从 Widget 获取可以放置在 onDisappear里或者自动播放的视频流等;

/// 复用的生命周期
mixin ReuseSliverLifeCycle{

  // 前台可见的
  void onAppear() {}

  // 后台不可见的
  void onDisappear() {}
}

七、参考资料

[[1]:Google Flutter 团队 Xiao Yu:Flutter Performance Profiling and Theory](https://files.flutter-io.cn/events/gdd2018/Profiling_your_Flutter_Apps.pdf)
[[2]:闲鱼 云从:他把闲鱼APP长列表流畅度翻了倍](https://mp.weixin.qq.com/s/dlOQ3Hw_U3CFQM91vcTGWQ)
[[3]:Google Android RecyclerView.ViewHolder:RecyclerView.Adapter#onCreateViewHolder](https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.Adapter#onCreateViewHolder(android.view.ViewGroup,%20int))

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
7833 0
阿里云服务器ECS远程登录用户名密码查询方法
阿里云服务器ECS远程连接登录输入用户名和密码,阿里云没有默认密码,如果购买时没设置需要先重置实例密码,Windows用户名是administrator,Linux账号是root,阿小云来详细说下阿里云服务器远程登录连接用户名和密码查询方法
10205 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
9554 0
使用SSH远程登录阿里云ECS服务器
远程连接服务器以及配置环境
2165 0
使用OpenApi弹性释放和设置云服务器ECS释放
云服务器ECS的一个重要特性就是按需创建资源。您可以在业务高峰期按需弹性的自定义规则进行资源创建,在完成业务计算的时候释放资源。本篇将提供几个Tips帮助您更加容易和自动化的完成云服务器的释放和弹性设置。
11659 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
11213 0
腾讯云服务器 设置ngxin + fastdfs +tomcat 开机自启动
在tomcat中新建一个可以启动的 .sh 脚本文件 /usr/local/tomcat7/bin/ export JAVA_HOME=/usr/local/java/jdk7 export PATH=$JAVA_HOME/bin/:$PATH export CLASSPATH=.
4496 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,云吞铺子总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系统盘、创建快照、配置安全组等操作如何登录ECS云服务器控制台? 1、先登录到阿里云ECS服务器控制台 2、点击顶部的“控制台” 3、通过左侧栏,切换到“云服务器ECS”即可,如下图所示 通过ECS控制台的远程连接来登录到云服务器 阿里云ECS云服务器自带远程连接功能,使用该功能可以登录到云服务器,简单且方便,如下图:点击“远程连接”,第一次连接会自动生成6位数字密码,输入密码即可登录到云服务器上。
21019 0
1
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载