技术干货 | Flutter在线编程实践总结

1.Flutter架构

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内部,这就使得它具有了很好的跨端一致性。

技术干货 | 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()方法源码:

@overridevoid 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。

// WidgetsBindingvoid 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视图绘制的时序图,如下

技术干货 | 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=/

技术干货 | Flutter在线编程实践总结

图 3

比方说选择了timeline后,可以进行性能分析,如图

技术干货 | Flutter在线编程实践总结

图 4

2.devTools

devTools也提供了一些基本的检测,具体的细节没有 Observatory 提供的完善. 可视性比较强。

可以通过下面命令安装:

f lutter pub global activate devtools

安装完成后通过devtools命令打开,输入DartVM地址

技术干货 | Flutter在线编程实践总结

图 5

打开后的页面

技术干货 | Flutter在线编程实践总结

图 6

devtools中的timeline就是performance,我们选择之后页面如下,操作体验上好了很多

技术干货 | Flutter在线编程实践总结

图 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);  }

技术干货 | Flutter在线编程实践总结

图 8

获取内存信息,调用一个VmService实例的getMemoryUsage,就能拿到当前的内存信息

  Future<MemoryUsage> getMemoryUsage(String isolateId) =>      _call('getMemoryUsage', {'isolateId': isolateId});

技术干货 | Flutter在线编程实践总结

图 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.gitFramework • revision f4abaa0735 (4 months ago) • 2021-07-01 12:46:11 -0700Engine • revision 241c87ad80Tools • 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来检测工程性能,这样有助于我们实现健壮性更强的应用,在排查过程中,我发现视频详情页存在渲染耗时的问题,如图

技术干货 | Flutter在线编程实践总结

图 10

4.1build耗时优化

VideoControls控件的build耗时是28.6ms,如图

技术干货 | Flutter在线编程实践总结

图 11

所以这里我们的优化方案是提高build效率,降低Widget tree遍历的出发点,将setState刷新数据尽量下发到底层节点,所以将VideoControl内触发刷新的子组件抽取成独立的Widget,setState下发到抽取出的Widget内部

优化后为11.0ms,整体的平均帧率也达到了了60fps,如图

技术干货 | Flutter在线编程实践总结

图 12

4.2

paint耗时优化

接下来分析下paint过程有没有可以优化的部分,我们打开debugProfilePaintsEnabled变量分析可以看到Timeline显示的paint层级,如图

技术干货 | Flutter在线编程实践总结

图 13

我们发现频繁更新的_buildPositionTitle和其他Widget在同一个layer中,这里我们想到的优化点是利用RepaintBoundary提高paint效率,它为经常发生显示变化的内容提供一个新的隔离layer,新的layer paint不会影响到其他layer

看下优化后的效果,如图

技术干货 | Flutter在线编程实践总结

图 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)实现动态化机制,目前没有比较好的开源技术可以去借鉴。

欢迎点赞+转发+关注!大家的支持是我分享最大的动力!!!

内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/93523.html

(0)

相关推荐

  • 从新手妈妈到老手妈妈的你只差一本书

    “小朋友在半个月至一个月之间要晒晒太阳,可以去黄疸;多让他接触室空气浴,可以锻炼呼吸道黏膜;婴儿在两个月左右可以尝试喂养果汁;等等。”嫂子在我怀孕之后经常给我传授各种育儿经验。突然间觉得我真的就是个小白,什么都不懂,给我听得一愣一愣。

    生活 2021年10月4日
  • 豆瓣被下架;交警现场查酒驾“翻车”,官方回应;证券公司员工因替客户考研被捕|孝南早报

    编辑 | 杨文瑾豆瓣被下架据微信公众号“工信微报”,工业和信息化部信息通信管理局12月9日发布“关于下架侵害用户权益APP名单的通报”,共下架106款APP,包括豆瓣、唱吧、爱回收、看看新闻、妈妈网孕育等。通报称:今年以来,工信部持续推进APP侵害用户权益专项整治行动,加大常态化检查力度,先后三次组织对用户反映强烈的重点问题开展“回头看”。11月3日,工信部针对APP超范围、高频次索取权限,非服务场景所必需收集用户个人信息,欺骗误导用户下载等违规行为进行了检查,并对未按要求完成整改的APP进行了公开通报。截至目前,尚有5款APP未按工信部要求完成整改。各通信管理局按照工信部统筹部署,积极开展APP技术检测,截至目前尚有101款APP仍未完成整改。依据《个人信息保护法》《网络安全法》等相关法律要求,工信部组织对上述共计106款APP进行下架,相关应用商店应在本通报发布后,立即组织对名单中应用软件进行下架处理。针对部分违规情节严重、拒不整改的APP,属地通信管理局应对APP运营主体依法予以行政处罚。(来源:工信微报)不止APP,很多小程序不给读取位置和个人信息就不能用。孙卓户籍地为何在黑龙江?“养母”这么说…12月9日,据山东阳谷县公安局通报,专案组已对被拐孩子孙卓、符建涛的户口办理问题展开调查。警方初步查明,孙海洋之子孙卓现用姓名国某,户籍地在黑龙江省某市。符建涛现用姓名为吴某某,户籍地在阳谷县。孙卓养母表示,因当年计划生育山东生三胎罚款严重,便让父亲帮忙把孙卓户口安在黑龙江。孙海洋在接受采访时表示,被拐儿童户口也是其最关注的问题,他暂未与养父母见面,应由法律去教育他们。(来源:新京报)高考要去黑龙江考了?明确了!职业本科与普通本科学位证书有同样效力国务院学位委员会办公室日前印发《关于做好本科层次职业学校学士学位授权与授予工作意见》。意见明确了职业本科学士学位授权、授予等的政策依据及工作范围,对职业本科学士学位授予权的审批权限和申请基本条件及授予方式、基本程序、授予标准、授予类型、学士学位证书和学位授予信息提出了要求。在执行方面,普通本科和职业本科都按照《中华人民共和国学位条例》《中华人民共和国学位条例暂行实施办法》《学士学位授权和授予管理办法》进行学士学位授权、授予、管理和质量监督;在证书效用方面,两者价值等同,在就业、考研、考公等方面具有同样的效力。(来源:教育部网站)好消息!唐山交警直播查酒驾“翻车”?官方通报:涉事民警和辅警已停职近日,有网友称河北唐山曹妃甸区交警直播查酒驾时“翻车”了。一名司机两次吹气测试,数值从“103”降至“45”,之后交警中断了直播。网传视频显示,此事发生在12月6日晚上。当时,曹妃甸交警正在道路上设卡查酒驾,“曹妃甸交警”抖音号同时进行直播。网友称,交警让一名驾驶员吹气,第一次吹出的数值为“103”,再次吹气后,数值显示“45”,随后直播被关了。12月9日下午,曹妃甸警方发布通报称,涉事民警和辅警已停职接受调查。根据相关规定,驾驶人员每100毫升血液酒精含量大于或等于20毫克小于80毫克为饮酒后驾车;每100毫升血液酒精含量大于或等于80毫克为醉酒驾车。因此酒精测试中,“103”和“45”,两个不同的数字,处罚截然不同。(来源:曹妃甸公安、极目新闻)103降到45,吹一下还能解酒?证券公司员工替客户考研被抓12月9日,“证券公司员工代替客户考研被抓”的消息登上热搜。该员工是中信证券大连分公司员工,为了年底冲业绩,铤而走险代替客户进行考研。12月8日,中国检察网公布一份代替考试罪起诉书。起诉书内容显示,李某是中信证券公司大连分公司员工,研究生学历。2020年12月26日,李某代替兴某(客户)考试时被考务室监考老师发现并带至凌河公安分局派出所执勤民警处。经核实李某代替考试的情况属实,将李某当场抓获。经法院审理查明,兴某报考参加2020年全国研究生招生考试,因害怕考试不通过,找到李某要求其代替自己参加考试。兴某了解到李某是证券行业新人,客户少、销售业绩不佳,就对李某承诺,如果其同意代替被告人兴某参加考试,就给其拉客户购买证券,以提高李某的年底销售业绩。2021年1月15日兴某投案自首。被告人李某和兴某,均因涉嫌代替考试罪被锦州市公安凌河分局取保候审。李某为拉业务代替客户考试的消息迅速在网络流传,网友在看到二人涉嫌违法的同时,也不禁感叹证券公司员工为了考核这么“拼”吗?不少网友都惊讶问道,“金融行业已经卷成这样了吗?”(来源:新京报)自由诚可贵,业绩价更高。

    科技 2021年12月11日
  • 299的实力,小米有品“高压清洗机”开箱:洗宝马车变得简单多了

    我是8年老米粉,小米的东西都很符合我的胃口,就连它旗下电商平台小米有品的东西我都觉得很特别,尤其是众筹的产品更是有趣,上星期我就在小米有品申请了一台正在众筹的清洗机回来玩了一下。

    科技 2021年12月7日
  • 手机的发展史简单介绍,智能手机发展历程

    从座机到手机,从功能机到智能机,手机的发展史你知道吗?这篇文章用2分钟的时间带你重温手机发展史上的经典机型。

    科技 2021年10月22日
  • 人生与电影不一样,人生辛苦多了。所以解放你的胃吧

    海鲜不管在哪个地方都是餐桌上必不可少的美味。经常吃海鲜的好处,可以进行补钙,海鲜的营养价值也是比较丰富的,对身体也有一定的好处,但是有的人吃了海鲜之后会导致食物中毒,或者是过敏的症状,也有可能会诱发胃炎的情况。平时也可以适当的吃海鲜,一定要适量的去吃,如果过敏要禁止食用,以免引起身体不适。

    生活 2021年9月28日
  • 一汽大众新车型,一汽大众最新车型

    文章长度:0太短了,请勿浪费资源

    科技 2021年12月9日