技术干货 | Flutter在线编程实践总结
wptr33 2024-12-05 17:01 17 浏览
1.Flutter架构
Flutter的架构主要分成三层:Framework,Engine,Embedder。
1.Framework使用dart实现,包括Material Design风格的Widget,Cupertino(针对iOS)风格的Widgets,文本/图片/按钮等基础Widgets,渲染,动画,手势等。此部分的核心代码是:flutter仓库下的flutter package,以及sky_engine仓库下的io,async,ui(dart:ui库提供了Flutter框架和引擎之间的接口)等package。
2.Engine使用C++实现,主要包括:Skia,Dart和Text。Skia是开源的二维图形库,提供了适用于多种软硬件平台的通用API。
3.Embedder是一个嵌入层,即把Flutter嵌入到各个平台上去,这里做的主要工作包括渲染Surface设置,线程设置,以及插件等。从这里可以看出,Flutter的平台相关层很低,平台(如iOS)只是提供一个画布,剩余的所有渲染相关的逻辑都在Flutter内部,这就使得它具有了很好的跨端一致性。
图1
2.Flutter视图绘制
对于开发者来说,使用最多的还是framework,我就从Flutter的入口函数开始一步步往下走,分析一下Flutter视图绘制的原理。
在Flutter应用中,main()函数最简单的实现如下:
void main() {
runApp(MyApp());
}
runApp方法调用了WidgetsFlutterBinding类ensureInitialized、attachRootWidget(app)、scheduleWarmUpFrame()三个方法,代码如下
// 参数app是一个widget,是Flutter应用启动后要展示的第一个Widget。
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}
2.1 WidgetsFlutterBinding
WidgetsFlutterBinding继承自BindingBase 并混入了很多Binding,查看这些 Binding的源码可以发现这些Binding中基本都是监听并处理Window对象(包含了当前设备和系统的一些信息以及Flutter Engine的一些回调)的一些事件,然后将这些事件按照Framework的模型包装、抽象然后分发。
WidgetsFlutterBinding正是粘连Flutter engine与上层Framework的“胶水”。
1.GestureBinding:
提供了window.onPointerDataPacket 回调,绑定Framework手势子系统,是Framework事件模型与底层事件的绑定入口。
2.ServicesBinding:
提供了window.onPlatformMessage 回调, 用于绑定平台消息通道(message channel),主要处理原生和Flutter通信。
3.SchedulerBinding:
提供了window.onBeginFrame和window.onDrawFrame回调,监听刷新事件,绑定Framework绘制调度子系统。
4. PaintingBinding:
绑定绘制库,主要用于处理图片缓存。
5. SemanticsBinding:
语义化层与Flutter engine的桥梁,主要是辅助功能的底层支持。
6.RendererBinding:
提供了window.onMetricsChanged 、window.onTextScaleFactorChanged 等回调。它是渲染树与Flutter engine的桥梁。
7.WidgetsBinding:
提供了window.onLocaleChanged、onBuildScheduled 等回调。它是Flutter widget层与engine的桥梁。
WidgetsFlutterBinding.ensureInitialized()负责初始化一个WidgetsBinding的全局单例,代码如下:
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance;
}
}
看到这个混入(with)很多的,下面先看父类:BindingBase
abstract class BindingBase {
...
ui.SingletonFlutterWindow get window => ui.window;//获取window实例
@protected
@mustCallSuper
void initInstances() {
assert(!_debugInitialized);
assert(() {
_debugInitialized = true;
return true;
}());
}
}
看到有句代码Window get window => ui.window链接宿主操作系统的接口,也就是Flutter framework 链接宿主操作系统的接口。系统中有一个Window实例,可以从window属性来获取,看看源码:
// window的类型是一个FlutterView,FlutterView里面有一个PlatformDispatcher属性
ui.SingletonFlutterWindow get window => ui.window;
// 初始化时把PlatformDispatcher.instance传入,完成初始化
ui.window = SingletonFlutterWindow._(0, PlatformDispatcher.instance);
// SingletonFlutterWindow的类结构
class SingletonFlutterWindow extends FlutterWindow {
...
// 实际上是给platformDispatcher.onBeginFrame赋值
FrameCallback? get onBeginFrame => platformDispatcher.onBeginFrame;
set onBeginFrame(FrameCallback? callback) {
platformDispatcher.onBeginFrame = callback;
}
VoidCallback? get onDrawFrame => platformDispatcher.onDrawFrame;
set onDrawFrame(VoidCallback? callback) {
platformDispatcher.onDrawFrame = callback;
}
// window.scheduleFrame实际上是调用platformDispatcher.scheduleFrame()
void scheduleFrame() => platformDispatcher.scheduleFrame();
...
}
class FlutterWindow extends FlutterView {
FlutterWindow._(this._windowId, this.platformDispatcher);
final Object _windowId;
// PD
@override
final PlatformDispatcher platformDispatcher;
@override
ViewConfiguration get viewConfiguration {
return platformDispatcher._viewConfigurations[_windowId]!;
}
}
2.2 scheduleAttachRootWidget
scheduleAttachRootWidget紧接着会调用WidgetsBinding的attachRootWidget方法,该方法负责将根Widget添加到RenderView上,代码如下:
void attachRootWidget(Widget rootWidget) {
final bool isBootstrapFrame = renderViewElement == null;
_readyToProduceFrames = true;
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
if (isBootstrapFrame) {
SchedulerBinding.instance!.ensureVisualUpdate();
}
}
renderView变量是一个RenderObject,它是渲染树的根。renderViewElement变量是renderView对应的Element对象。可见该方法主要完成了根widget到根 RenderObject再到根Element的整个关联过程。
RenderView get renderView => _pipelineOwner.rootNode! as RenderView;
renderView是RendererBinding中拿到PipelineOwner.rootNode,PipelineOwner在 Rendering Pipeline 中起到重要作用:
随着 UI 的变化而不断收集『 Dirty Render Objects 』随之驱动 Rendering Pipeline 刷新 UI。
简单讲,PipelineOwner是『RenderObject Tree』与『RendererBinding』间的桥梁。
最终调用attachRootWidget,执行会调用RenderObjectToWidgetAdapter的attachToRenderTree方法,该方法负责创建根element,即RenderObjectToWidgetElement,并且将element与widget 进行关联,即创建出 widget树对应的element树。如果element 已经创建过了,则将根element 中关联的widget 设为新的,由此可以看出element 只会创建一次,后面会进行复用。BuildOwner是widget framework的管理类,它跟踪哪些widget需要重新构建。代码如下:
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element.assignOwner(owner);
});
owner.buildScope(element, () {
element.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}
2.3 scheduleWarmUpFrame
runApp的实现中,当调用完attachRootWidget后,最后一行会调用 WidgetsFlutterBinding 实例的 scheduleWarmUpFrame() 方法,该方法的实现在SchedulerBinding 中,它被调用后会立即进行一次绘制(而不是等待"vsync" 信号),在此次绘制结束前,该方法会锁定事件分发,也就是说在本次绘制结束完成之前Flutter将不会响应各种事件,这可以保证在绘制过程中不会再触发新的重绘。
下面是scheduleWarmUpFrame() 方法的部分实现(省略了无关代码):
void scheduleWarmUpFrame() {
...
Timer.run(() {
handleBeginFrame(null);
});
Timer.run(() {
handleDrawFrame();
resetEpoch();
});
// 锁定事件
lockEvents(() async {
await endOfFrame;
Timeline.finishSync();
});
...
}
该方法中主要调用了handleBeginFrame() 和 handleDrawFrame() 两个方法。
查看handleBeginFrame() 和 handleDrawFrame() 两个方法的源码,可以发现前者主要是执行了transientCallbacks队列,而后者执行了 persistentCallbacks 和 postFrameCallbacks 队列。
1. transientCallbacks:用于存放一些临时回调,一般存放动画回调。可以通过SchedulerBinding.instance.scheduleFrameCallback 添加回调。
2. persistentCallbacks:用于存放一些持久的回调,不能在此类回调中再请求新的绘制帧,持久回调一经注册则不能移除。 SchedulerBinding.instance.addPersitentFrameCallback(),这个回调中处理了布局与绘制工作。
3. postFrameCallbacks:在Frame结束时只会被调用一次,调用后会被系统移除,可由 SchedulerBinding.instance.addPostFrameCallback() 注册。
注意,不要在此类回调中再触发新的Frame,这可以会导致循环。
真正的渲染和绘制逻辑在RendererBinding中实现,查看其源码,发现在其initInstances()方法中有如下代码:
void initInstances() {
... // 省略无关代码
addPersistentFrameCallback(_handlePersistentFrameCallback);
}
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
}
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout(); // 布局
pipelineOwner.flushCompositingBits(); //重绘之前的预处理操作,检查RenderObject是否需要重绘
pipelineOwner.flushPaint(); // 重绘
renderView.compositeFrame(); // 将需要绘制的比特数据发给GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
需要注意的是:由于RendererBinding只是一个mixin,而with它的是WidgetsBinding,所以需要看看WidgetsBinding中是否重写该方法,查看WidgetsBinding的drawFrame()方法源码:
@override
void drawFrame() {
...//省略无关代码
try {
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
super.drawFrame(); //调用RendererBinding的drawFrame()方法
buildOwner.finalizeTree();
}
}
在调用RendererBinding.drawFrame()方法前会调用 buildOwner.buildScope() (非首次绘制),该方法会将被标记为“dirty” 的 element 进行 rebuild()
我们再来看WidgetsBinding,在initInstances()方法中创建BuildOwner对象,然后执行buildOwner!.onBuildScheduled = _handleBuildScheduled;,这里将_handleBuildScheduled赋值给了buildOwnder的onBuildScheduled属性。
BuildOwner对象,它负责跟踪哪些widgets需要重新构建,并处理应用于widgets树的其他任务,其内部维护了一个_dirtyElements列表,用以保存被标“脏”的elements。
每一个element被新建时,其BuildOwner就被确定了。一个页面只有一个buildOwner对象,负责管理该页面所有的element。
// WidgetsBinding
void initInstances() {
...
buildOwner!.onBuildScheduled = _handleBuildScheduled;
...
}());
}
当调用buildOwner.onBuildScheduled()时,便会走下面的流程。
// WidgetsBinding类
void _handleBuildScheduled() {
ensureVisualUpdate();
}
// SchedulerBinding类
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame();
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks:
return;
}
}
当schedulerPhase处于idle状态,会调用scheduleFrame,然后经过window.scheduleFrame()中的performDispatcher.scheduleFrame()去注册一个VSync监听。
void scheduleFrame() {
...
window.scheduleFrame();
...
}
2.4小结
Flutter从启动到显示图像在屏幕主要经过:首先监听处理window对象的事件,将这些事件处理包装为Framework模型进行分发,通过widget创建element树,接着通过scheduleWarmUpFrame进行渲染,接着通过Rendererbinding进行布局,绘制,最后通过调用ui.window.render(scene)Scene信息发给Flutter engine,Flutter engine最后调用渲染API把图像画在屏幕上。
我大致整理了一下Flutter视图绘制的时序图,如下
图 2
3.Flutter性能监控
在对视图绘制有一定的了解后后,思考一个问题,怎么在视图绘制的过程中去把控性能,优化性能,我们先来看一下Flutter官方提供给我们的两个性能监控工具。
3.1 Dart VM Service
1.observatory
observatory: 在engine/shell/testings/observatory可以找到它的具体实现,它开启了一个ServiceClient,用于获取dartvm运行状态.flutter app启动的时候会生成一个当前的observatory服务器的地址
flutter: socket connected in service Dart VM Service Protocol v3.44 listening on http://127.0.0.1:59378/8x9XRQIBhkU=/
图 3
比方说选择了timeline后,可以进行性能分析,如图
图 4
2.devTools
devTools也提供了一些基本的检测,具体的细节没有 Observatory 提供的完善. 可视性比较强。
可以通过下面命令安装:
f lutter pub global activate devtools
安装完成后通过devtools命令打开,输入DartVM地址
图 5
打开后的页面
图 6
devtools中的timeline就是performance,我们选择之后页面如下,操作体验上好了很多
图 7
observatory与devtools都是通过vm_service实现的,网上使用指南比较多,这边就不多赘述了,我这边主要介绍一下Dart VM Service (后面 简称 )vm_service,是 Dart 虚拟机内部提供的一套 Web 服务,数据传输协议是 JSON-RPC 2.0。
不过我们并不需要要自己去实现数据请求解析,官方已经写好了一个可用的 Dart SDK 给我们用:vm_service。vm_service 在启动的时候会在本地开启一个 WebSocket 服务,服务 URI 可以在对应的平台中获得:
1)Android 在 FlutterJNI.getObservatoryUri() 中;
2)iOS 在 FlutterEngine.observatoryUrl 中。
有了 URI 之后我们就可以使用 的服务了,官方有一个帮我们写好的SDK: vm_service
Future<void> connect() async {
ServiceProtocolInfo info = await Service.getInfo();
if (info.serverUri == null) {
print("service protocol url is null,start vm service fail");
return;
}
service = await getService(info);
print('socket connected in service $info');
vm = await service?.getVM();
List<IsolateRef>? isolates = vm?.isolates;
main = isolates?.firstWhere((ref) => ref.name?.contains('main') == true);
main ??= isolates?.first;
connected = true;
}
Future<VmService> getService(info) async {
Uri uri = convertToWebSocketUrl(serviceProtocolUrl: info.serverUri);
return await vmServiceConnectUri(uri.toString(), log: StdoutLog());
}
获取frameworkVersion,调用一个VmService实例的callExtensionService,传入'flutterVersion',就能拿到当前的flutter framework和engine信息
Future<Response?> callExtensionService(String method) async {
if (_extensionService == null && service != null && main != null) {
_extensionService = ExtensionService(service!, main!);
await _extensionService?.loadExtensionService();
}
return _extensionService!.callMethod(method);
}
图 8
获取内存信息,调用一个VmService实例的getMemoryUsage,就能拿到当前的内存信息
Future<MemoryUsage> getMemoryUsage(String isolateId) =>
_call('getMemoryUsage', {'isolateId': isolateId});
图 9
获取 Flutter APP 的 FPS,官方提供了好几个办法来让我们在开发 Flutter app 的过程中可以使用查看 fps等性能数据,如devtools,具体见文档 Debugging Flutter apps 、Flutter performance profiling 等。
// 需监听fps时注册
void start() {
SchedulerBinding.instance.addTimingsCallback(_onReportTimings);
}
// 不需监听时移除
void stop() {
SchedulerBinding.instance.removeTimingsCallback(_onReportTimings);
}
void _onReportTimings(List<FrameTiming> timings) {
// TODO
}
3.2崩溃日志捕获上报
flutter 的崩溃日志收集主要有两个方面:
1)flutter dart 代码的异常(包含app和framework代码两种情况,一般不会引起闪退,你猜为什么)
2)flutter engine 的崩溃日志(一般会闪退)
Dart 有一个 Zone 的概念,有点类似sandbox的意思。不同的 Zone 代码上下文是不同的互不影响,Zone 还可以创建新的子Zone。Zone 可以重新定义自己的print、timers、microtasks还有最关键的how uncaught errors are handled 未捕获异常的处理
runZoned(() {
Future.error("asynchronous error");
}, onError: (dynamic e, StackTrace stack) {
reportError(e, stack);
});
1.Flutter framework 异常捕获
注册 FlutterError.onError 回调,用于收集 Flutter framework 外抛的异常。
runZoned(() {
Future.error("asynchronous error");
}, onError: (dynamic e, StackTrace stack) {
reportError(e, stack);
});
2.Flutter engine 异常捕获
flutter engine 部分的异常,以Android 为例,主要为 libfutter.so发生的错误。
这部份可以直接交给native崩溃收集sdk来处理,比如 firebase crashlytics、 bugly、xCrash 等等
我们需要将 dart 异常及堆栈通过 MethodChannel传递给 bugly sdk 即可。
收集到异常之后,需要查符号表(symbols)还原堆栈。
首先需要确认该 flutter engine 所属版本号,在命令行执行:
flutter --version
输出如下:
Flutter 2.2.3 ? channel stable ? https://github.com/flutter/flutter.git
Framework ? revision f4abaa0735 (4 months ago) ? 2021-07-01 12:46:11 -0700
Engine ? revision 241c87ad80
Tools ? Dart 2.13.4
可以看到 Engine 的 revision 为 241c87ad80。
其次,在 flutter infra 上找到对应cpu abi 的 symbols.zip 并下载,解压后,可以得到带有符号信息的 debug so 文件—— libflutter.so,然后按照平台文档上传进行堆栈还原就可以了,如bugly平台就提供了上传工具
java -jar buglySymbolAndroid.jar -i xxx
4.Flutter性能优化
在业务开发中我们要学会用devtools来检测工程性能,这样有助于我们实现健壮性更强的应用,在排查过程中,我发现视频详情页存在渲染耗时的问题,如图
图 10
4.1build耗时优化
VideoControls控件的build耗时是28.6ms,如图
图 11
所以这里我们的优化方案是提高build效率,降低Widget tree遍历的出发点,将setState刷新数据尽量下发到底层节点,所以将VideoControl内触发刷新的子组件抽取成独立的Widget,setState下发到抽取出的Widget内部
优化后为11.0ms,整体的平均帧率也达到了了60fps,如图
图 12
4.2
paint耗时优化
接下来分析下paint过程有没有可以优化的部分,我们打开debugProfilePaintsEnabled变量分析可以看到Timeline显示的paint层级,如图
图 13
我们发现频繁更新的_buildPositionTitle和其他Widget在同一个layer中,这里我们想到的优化点是利用RepaintBoundary提高paint效率,它为经常发生显示变化的内容提供一个新的隔离layer,新的layer paint不会影响到其他layer
看下优化后的效果,如图
图 14
4.3小结
在Flutter开发过程中,我们用devtools工具排查定位页面渲染问题时,主要有两点:
1.提高build效率,setState刷新数据尽量下发到底层节点。
2.提高paint效率,RepaintBoundry创建单独layer减少重绘区域。
当然 Flutter 中性能调优远不止这一种情况,build / layout / paint 每一个过程其实都有很多能够优化的细节。
5.回顾
5.1回顾
这篇文章主要从三个维度来介绍Flutter这门技术,分别为:
1.绘制原理讲解,我们review了一下源码,发现整个渲染过程就是一个闭环,Framework,Engine,Embedder各司其职,简单来说就是Embedder不断拿回Vsync信号,Framework将dart代码交给Engine翻译成跨平台代码,再通过Embedder回调宿主平台;
2.性能监控就是不断得在这个循环中去插入我们的哨兵,观察整个生态,获取异常数据上报;
3.性能优化通过一次项目实践,学习怎么用工具提升我们定位问题的效率。
5.2优缺点
优点:
我们可以看到Flutter在视图绘制过程中形成了闭环,双端基本保持了一致性,所以我们的开发效率得到了极大的提升,性能监控和性能优化也比较方便。
缺点:
1)声明式开发 动态操作视图节点不是很友好,不能像原生那样命令式编程,或者像前端获取dom节点那般容易;
2)实现动态化机制,目前没有比较好的开源技术可以去借鉴。
欢迎点赞+转发+关注!大家的支持是我分享最大的动力!!!
相关推荐
- 搭建Oracle数据库服务器(oracle数据库服务器安装教程)
-
【十一】搭建Oracle数据库服务器...
- Oracle 删除大量表记录操作总结(oracle删除表记录数据)
-
删除表数据操作清空所有表记录TRUNCATETABLEyour_table_name;...
- 专访搜狗DBA负责人王林平:为何从Oracle转向MySQL?
-
王林平CSDN:首先,请做个自我介绍,目前所负责的领域以及所在公司。王林平:大家好,我是王林平,目前在搜狗商业平台研发部工作。主要负责商业广告数据库的维护、优化、架构设计、流程体系建设、自动化运维平台...
- Oracle数据库知识 day01 Oracle介绍和增删改查
-
一、oracle介绍ORACLE数据库系统是美国ORACLE公司(甲骨文)提供的以分布式数据库为核心的一组软件产品,是目前最流行的客户/服务器(CLIENT/SERVER)或B/S体系结构...
- 深入探索Oracle 回表原理、影响与优化技巧
-
什么是回表当对一个列创建索引之后,索引会包含该列的键值以及键值对应行所在的rowid。通过索引中记录的rowid访问表中的数据就叫回表。执行计划中的TABLEACCESSBYINDEXROW...
- 那些年我们踩过的语句创建oracle 12c cdb实例的坑
-
现在大多数客户使用oracle还是11g版本的,很多小伙伴可能还没接触过12c,所以今天小编要为大家科普下12c版本的oracle的安装过程中会出现的错误。前面步骤其实都是一样的,我们就直接从建好1...
- Oracle高级数据库特性揭秘:存储过程、触发器与权限管理
-
当谈论Oracle高级数据库特性时,存储过程和函数、触发器、权限管理和安全性以及数据库连接和远程访问是关键概念。下面我将为每个主题提供详细的解释,并附上高质量示例。...
- ORACLE内核解密之表空间管理(oracle表空间大小是由什么决定)
-
一、ORACLE表空间管理1、本地表空间管理tablespace(LMT)...
- Oracle 创建磁盘组报错ORA-15137的问题分析与解决思路
-
ASM扩容本来是件很简单的事,当ASM磁盘准备好之后,直接一条命令就会添加上。但是也会有异常情况,最近就碰到Oracle19c在扩容时报错的故障,供大家参考。...
- DBA日记之Oracle数据库索引一(oracle数据库索引有哪几种)
-
什么是索引在oracle数据库中,索引是数据库中一种可选的数据结构,通常与表或簇相关。用户可以在表的一列或数列上建立索引,以提高在此表上执行SQL语句的性能。就像本文档的索引可以帮助读者快速定位所...
- 利用Oracle触发器实现不同数据库之间的数据同步
-
首先在两个数据库之间创建链接(DBLink),然后对要同步地表做一个同义(synonym),最后建一个触发器实现同步。实现步骤如下:1)为保证连接到另一台远程服务器的数据库,需要建立一个DBLin...
- oracle已有表的分表分区优化操作步骤(单表过大)
-
第一章、步骤总览0、获取创建表空间DDL、创建表空间(该步骤在将分区放入不同的表空间时采用)...
- Oracle 表分区在线重定义(oracle表分区后查询语句改变吗)
-
表分区有以下优点:a、改善查询性能:对分区对象的查询可以仅搜索自己关心的分区,提高检索速度。b、增强可用性:如果表的某个分区出现故障,表在其他分区的数据仍然可用;c、维护方便:如果表的某个分区出现故障...
- ORACLE 体系 - 14(oracle 11g的体系结构有几种)
-
【十四】数据移动...
- Oracle-架构、原理、进程(oracle进程结构)
-
详解:首先看张图:对于一个数据库系统来说,假设这个系统没有运行,我们所能看到的和这个数据库相关的无非就是几个基于操作系统的物理文件,这是从静态的角度来看,如果从动态的角度来看呢,也就是说这个数据库系统...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
因果推断Matching方式实现代码 因果推断模型
-
git pull命令使用实例 git pull--rebase
-
面试官:git pull是哪两个指令的组合?
-
git 执行pull错误如何撤销 git pull fail
-
git pull 和git fetch 命令分别有什么作用?二者有什么区别?
-
git fetch 和git pull 的异同 git中fetch和pull的区别
-
git pull 之后本地代码被覆盖 解决方案
-
还可以这样玩?Git基本原理及各种骚操作,涨知识了
-
git命令之pull git.pull
-
- 最近发表
-
- 搭建Oracle数据库服务器(oracle数据库服务器安装教程)
- Oracle 删除大量表记录操作总结(oracle删除表记录数据)
- 专访搜狗DBA负责人王林平:为何从Oracle转向MySQL?
- Oracle数据库知识 day01 Oracle介绍和增删改查
- 深入探索Oracle 回表原理、影响与优化技巧
- 那些年我们踩过的语句创建oracle 12c cdb实例的坑
- Oracle高级数据库特性揭秘:存储过程、触发器与权限管理
- ORACLE内核解密之表空间管理(oracle表空间大小是由什么决定)
- Oracle 创建磁盘组报错ORA-15137的问题分析与解决思路
- DBA日记之Oracle数据库索引一(oracle数据库索引有哪几种)
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)