Flutter之Hybird-composition View的事件分发流程

Posted 静水流深zz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter之Hybird-composition View的事件分发流程相关的知识,希望对你有一定的参考价值。

前言

最近研究混合栈渲染时,遇到了一个混合栈下的事件分发问题,为此看了一下flutter接入原生view后,其中的事件分发流程。为了方便后期查阅,做此记录,也希望能帮到有需要的人。

文中所讨论的原生view为: androidViewSurface ,即:hybird composition ,其次还将涉及到Flutter的启动流程 ,如果对这两者不熟悉的话,建议浏览这下面的文章:

Flutter——在Android平台上的启动流程浅析

Flutter在Android平台上启动时,Native层做了什么?

Flutter——Hybrid Composition混合图层的原理分析

平台层: Android

由启动流程可知,最先创建的 FlutterView类似于容器,其持有flutter绘制所需的surface,那么我们就由它的onTouchEvent方法看起。

其实,根据Flutter框架的定位,也可以猜到事件分发是由平台负责的。

Flutter:onTouchEvent

@Override
public boolean onTouchEvent(@NonNull MotionEvent event) 
  ...无关代码...

  return androidTouchProcessor.onTouchEvent(event);

这里调用了 androidTouchProcessor.onTouchEvent(event)

AndroidTouchProcessor: onTouchEvent

public boolean onTouchEvent(@NonNull MotionEvent event) 
  return onTouchEvent(event, IDENTITY_TRANSFORM);

此方法会进一步调用同名函数:

public boolean onTouchEvent(@NonNull MotionEvent event, Matrix transformMatrix) 
  int pointerCount = event.getPointerCount();

  // Prepare a data packet of the appropriate size and order.
  // 对event进行转换和存储,并传递给flutter侧
  ByteBuffer packet =
      ByteBuffer.allocateDirect(pointerCount * POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD);
  packet.order(ByteOrder.LITTLE_ENDIAN);
    
    //...省略不少代码...
    //主要是根据event类型,并通过addPointerForIndex(...)方法对packet进行数据填充

  // 将packet发送到flutter
  renderer.dispatchPointerDataPacket(packet, packet.position());

  return true;

最终调用 FlutterRenderer.dispatchPointerDataPacket 进行事件的传递,我们继续看。

dispatchPointerDataPacket

这里相对比较简单,renderer的 dispatchPointerDataPacket 方法直接调用了FlutterJnidispatchPointerDataPacket方法:

//FlutterRenderer
public void dispatchPointerDataPacket(@NonNull ByteBuffer buffer, int position) 
  flutterJNI.dispatchPointerDataPacket(buffer, position);


//FlutterJNI

/** 这里会将pointer packet 转到对应的engine层的方法:nativeDispatchPointerDataPacket*/
@UiThread
public void dispatchPointerDataPacket(@NonNull ByteBuffer buffer, int position) 
  ensureRunningOnMainThread();
  ensureAttachedToNative();
  //调用engine层的方法
  nativeDispatchPointerDataPacket(nativeShellHolderId, buffer, position);


平台层的事件分发还是比较简单明了的,我们跟着事件继续往下走。

中间层: Engine

Platform Thread

通过开头的Flutter在Android平台上启动时,Native层做了什么?, 我们知道应用开始会注册一些engine的方法,具体实现在platform_view_android_jni_impl.cc :

bool RegisterApi(JNIEnv* env) 
  static const JNINativeMethod flutter_jni_methods[] =  
    //...省略部分注册的方法
    
        .name = "nativeDispatchPointerDataPacket", //android 侧的方法名
        .signature = "(JLjava/nio/ByteBuffer;I)V",//android 方法名的签名(参数类型)
        .fnPtr = reinterpret_cast<void*>(&DispatchPointerDataPacket),//对应engine层的方法指针
    ,
    //...省略部分注册的方法
  

当我们在android侧调用nativeDispatchPointerDataPacket方法时,会调用engine的DispatchPointerDataPacket方法,其实现如下:

static void DispatchPointerDataPacket(JNIEnv* env,
                                      jobject jcaller,
                                      jlong shell_holder,
                                      jobject buffer,
                                      jint position) 
                                      
  //通过GetDirectBufferAddress 获取到指向buffer的指针
  uint8_t* data = static_cast<uint8_t*>(env->GetDirectBufferAddress(buffer));
  
  //在通过position 最终生成一个指向packet的指针
  auto packet = std::make_unique<flutter::PointerDataPacket>(data, position);
  
  //然后将packet传递到 platform view的DispatchPointerDataPacket方法中
  ANDROID_SHELL_HOLDER->GetPlatformView()->DispatchPointerDataPacket(
      std::move(packet));

android端的packet通过上面的方法,传递到了platform viewDispatchPointerDataPacket 方法。

//这里 holder 返回了一个PlatformView的子类PlatformViewAndroid
fml::WeakPtr<PlatformViewAndroid> AndroidShellHolder::GetPlatformView() 
  FML_DCHECK(platform_view_);
  return platform_view_;

虽然返回的是 PlatformViewAndroid,但是父类PlatformViewAndroidDispatchPointerDataPacket方法并没有要求子类重写,且PlatformViewAndroid内也确实没有重写这个方法,那么我们直接看其父类的实现:

void PlatformView::DispatchPointerDataPacket(
    std::unique_ptr<PointerDataPacket> packet) 
  delegate_.OnPlatformViewDispatchPointerDataPacket(
      pointer_data_packet_converter_.Convert(std::move(packet)));

此处的delegate_PlatformView内部的一个抽象类,在AttachJNI方法初始化AndroidShellHolder中,其内部对AndroidPlatformView初始化时而实例化的,这里简单介绍一下:

AttachJNI / AndroidShellHolder等 可以参见上面的文章
AndroidShellHolder::AndroidShellHolder(
    flutter::Settings settings,
    std::shared_ptr<PlatformViewAndroidJNI> jni_facade,
    bool is_background_view)
    : settings_(std::move(settings)), jni_facade_(jni_facade) 
    
    ...省略部分代码
    
    //初始化shell时,传入此回调
  Shell::CreateCallback<PlatformView> on_create_platform_view =
      [is_background_view, &jni_facade, &weak_platform_view](Shell& shell) 
        std::unique_ptr<PlatformViewAndroid> platform_view_android;
        
        //这里通过回调,我们将shell作为delegate传入PlatformViewAndroid
        
        platform_view_android = std::make_unique<PlatformViewAndroid>(
            shell,                   // delegate
            shell.GetTaskRunners(),  // task runners
            jni_facade,              // JNI interop
            shell.GetSettings()
                .enable_software_rendering,  // use software rendering
            !is_background_view              // create onscreen surface
        );
        weak_platform_view = platform_view_android->GetWeakPtr();
        auto display = Display(jni_facade->GetDisplayRefreshRate());
        shell.OnDisplayUpdates(DisplayUpdateType::kStartup, display);
        return platform_view_android;
      ;

       ...省略部分代码

ok,通过上面这一段代码,我们知道delegate实例是怎么来的了,接下来回到正题,继续看PlatformView::DispatchPointerDataPacket:

void PlatformView::DispatchPointerDataPacket(
    std::unique_ptr<PointerDataPacket> packet) 
  delegate_.OnPlatformViewDispatchPointerDataPacket(
      pointer_data_packet_converter_.Convert(std::move(packet)));

其又调用了ShellOnPlatformViewDispatchPointerDataPacket方法:

void Shell::OnPlatformViewDispatchPointerDataPacket(
    std::unique_ptr<PointerDataPacket> packet) 
    
  ...省略部分代码
  
  //这里做了个线程切换,向UI Thread 添加了一个任务,
  //并最终执行engine的DispatchPointerDataPacket方法
  //注: 这里的UI thread就是flutter 代码执行的线程
  task_runners_.GetUITaskRunner()->PostTask(
  
      //shell为engine的子类,并持有了engine的一个弱引用weak_engine_
      
      fml::MakeCopyable([engine = weak_engine_, packet = std::move(packet),
                         flow_id = next_pointer_flow_id_]() mutable 
        if (engine) 
          engine->DispatchPointerDataPacket(std::move(packet), flow_id);
        
      ));
  next_pointer_flow_id_++;

UI Thread

engine->DispatchPointerDataPacket 的这个方法有点绕,其实现如下:

void Engine::DispatchPointerDataPacket(
    std::unique_ptr<PointerDataPacket> packet,
    uint64_t trace_flow_id) 
  ...
  pointer_data_dispatcher_->DispatchPacket(std::move(packet), trace_flow_id);

看起来它又调用了dispatcher,但最终还是调用的engine实现的接口方法DoDispatchPacket :

void Engine::DoDispatchPacket(std::unique_ptr<PointerDataPacket> packet,
                              uint64_t trace_flow_id) 
  animator_->EnqueueTraceFlowId(trace_flow_id);
  if (runtime_controller_) 
    runtime_controller_->DispatchPointerDataPacket(*packet);
  

进入RuntimeController::DispatchPointerDataPacket

bool RuntimeController::DispatchPointerDataPacket(
    const PointerDataPacket& packet) 
  if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) 
    ...other code
    
    //这里获取到了id为0的window并调用了DispatchPointerDataPacket
    platform_configuration->get_window(0)->DispatchPointerDataPacket(packet);
    return true;
  

  return false;

这个window,如果你看过关于flutter源码介绍之类的文章(如:渲染流程),应该是不会陌生的,这个window就是与flutter端的window相对应的。

void Window::DispatchPointerDataPacket(const PointerDataPacket& packet) 
  std::shared_ptr<tonic::DartState> dart_state = library_.dart_state().lock();
  if (!dart_state) 
    return;
  
  tonic::DartState::Scope scope(dart_state);
    
    //这里对咱们从android端挪过来的packet 做了个转换
  const std::vector<uint8_t>& buffer = packet.data();
  Dart_Handle data_handle =
      tonic::DartByteData::Create(buffer.data(), buffer.size());
  if (Dart_IsError(data_handle)) 
    return;
  
  
  //类似jni的调用,拉起了flutter端的 "_dispatchPointerDataPacket"方法
  tonic::LogIfError(tonic::DartInvokeField(
      library_.value(), "_dispatchPointerDataPacket", data_handle));

接下来就要转到flutter侧了。

Flutter层

如果你对WidgetsFlutterBinding及其混入类不熟悉,可以取查阅flutter的相关文章,有很多。

我们单刀直入GestureBinding类,可以看到初始化方法:

@override
void initInstances() 
  super.initInstances();
  _instance = this;
  window.onPointerDataPacket = _handlePointerDataPacket;

window.onPointerDataPacket

我们先看window.onPointerDataPacke

set onPointerDataPacket(PointerDataPacketCallback? callback) 
  platformDispatcher.onPointerDataPacket = callback;

platformDispatcher的方法:

PointerDataPacketCallback? get onPointerDataPacket => _onPointerDataPacket;
PointerDataPacketCallback? _onPointerDataPacket;
Zone _onPointerDataPacketZone = Zone.root;
set onPointerDataPacket(PointerDataPacketCallback? callback) 
  _onPointerDataPacket = callback;
  _onPointerDataPacketZone = Zone.current;

最终会将_handlePointerDataPacket方法赋值给platformDispatcher_onPointerDataPacket,而_onPointerDataPacket 则会在下面这个方法中调用:

// Called from the engine, via hooks.dart
void _dispatchPointerDataPacket(ByteData packet) 
  if (onPointerDataPacket != null) 
    _invoke1<PointerDataPacket>(
      onPointerDataPacket,
      _onPointerDataPacketZone,
      _unpackPointerDataPacket(packet),
    );
  

从上面的注释可以看到,这里的方法最终与咱们在engine层中的调用链关联了起来,即被下面这个方法所调用的:

  tonic::LogIfError(tonic::DartInvokeField(
      library_.value(), "_dispatchPointerDataPacket", data_handle));

当然,关联节点是在hooks.dart文件中做的。

@pragma('vm:entry-point')
// ignore: unused_element
void _dispatchPointerDataPacket(ByteData packet) 
  PlatformDispatcher.instance._dispatchPointerDataPacket(packet);

接下来看_handlePointerDataPacket

_handlePointerDataPacket

此方法会绑定到window并用于响应engine的回调,

void _handlePointerDataPacket(ui.PointerDataPacket packet) 
  // We convert pointer data to logical pixels so that e.g. the touch slop can be
  // defined in a device-independent manner.
  _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
  if (!locked)
    _flushPointerEventQueue();

本文旨在分析原生view的事件分发流程,为了避免跑题和压缩篇幅,flutter侧事件分发将会简要概括。

而其内部经过相对简单的调用链最终会进入下面这个方法:

void _handlePointerEventImmediately(PointerEvent event) 
    
    // 第一步
  HitTestResult? hitTestResult;
  if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) 
    assert(!_hitTests.containsKey(event.pointer));
    hitTestResult = HitTestResult();
    hitTest(hitTestResult, event.position);
    if (event is PointerDownEvent) 
      _hitTests[event.pointer] = hitTestResult;
    
    assert(() 
      if (debugPrintHitTestResults)
        debugPrint('$event: $hitTestResult');
      return true;
    ());
   else if (event is PointerUpEvent || event is PointerCancelEvent) 
    hitTestResult = _hitTests.remove(event.pointer);
   else if (event.down) 
    // Because events that occur with the pointer down (like
    // [PointerMoveEvent]s) should be dispatched to the same place that their
    // initial PointerDownEvent was, we want to re-use the path we found when
    // the pointer went down, rather than do hit detection each time we get
    // such an event.
    hitTestResult = _hitTests[event.pointer];
  
  assert(() 
    if (debugPrintMouseHoverEvents && event is PointerHoverEvent)
      debugPrint('$event');
    return true;
  ());
  if (hitTestResult != null ||
      event is PointerAddedEvent ||
      event is PointerRemovedEvent) 
    assert(event.position != null);
    
    // 第二步
    dispatchEvent(event, hitTestResult);
  

这个方法可以大致分为两步,在此做一个简要概括:

第一步:

创建一个root hitTestResult,其内部拥有一个List类型的_path,随后调用rootViewhitTest方法并进而调用其child的hitTest方法,依次向下一直遍历整个render树。

这个过程中`root hitTestResult`会被一直传递

每当遍历到一个节点render,便会根据自身的_size是否包含pointer event positon来确定是否加入到_path中。

第二步:

调用dispatchEvent方法,该方法会遍历_path中的节点,并调用handleEvent方法:

for (final HitTestEntry entry in hitTestResult.path) 
  try 
    //target 为renderObject,其实现了HitTestTarget接口
    entry.target.handleEvent(event.transformed(entry.transform), entry);
   catch (exception, stack) 
    ...other code
  

经过上面这部分,我们大致了解了flutter的事件分发。现在回归正题,来看一下AndroidViewSurface是如何处理事件的。

AndroidViewSurface

关于混合图层的创建即实现原理请参考这篇文章:Flutter——Hybrid Composition混合图层的原理分析

AndroidViewSurface 继承自PlatformViewSurface,它内部的一个重要方法是:

//创建一个render object,自定义过widget的朋友应该不陌生这个方法

@override
RenderObject createRenderObject(BuildContext context) 
  return PlatformViewRenderBox(controller: controller, 
      gestureRecognizers: gestureRecognizers, hitTestBehavior: hitTestBehavior);

我们继续往下走,PlatformViewRenderBox :

//看到它的父类,再结合之前的内容,应该就是这里了
class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin

    PlatformViewRenderBox(
      required PlatformViewController controller,
      required PlatformViewHitTestBehavior hitTestBehavior,
      required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
    ) :  assert(controller != null && controller.viewId != null && controller.viewId > -1),
          assert(hitTestBehavior != null),
          assert(gestureRecognizers != null),
          
          //这个controller 也很重要
          _controller = controller 
      this.hitTestBehavior = hitTestBehavior;
      
      //这里更新了手势识别器
      updateGestureRecognizers(gestureRecognizers);
    
    
    ...non-relative code


这里可以看到,dispatchPointerEvent这个方法是从_controller(AndroidViewController)中取到的。

void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) 
  _updateGestureRecognizersWithCallBack(gestureRecognizers, _controller.dispatchPointerEvent);

我们来看一下_controller.dispatchPointerEvent的实现:

@override
Future<void> dispatchPointerEvent(PointerEvent event) async 
  if (event is PointerHoverEvent) 
    return;
  

  if (event is PointerDownEvent) 
    _motionEventConverter.handlePointerDownEvent(event);
  

  _motionEventConverter.updatePointerPositions(event);

    //将flutter 事件 转成 android 事件
    
  final AndroidMotionEvent? androidEvent =
      _motionEventConverter.toAndroidMotionEvent(event);

  if (event is PointerUpEvent) 
    _motionEventConverter.handlePointerUpEvent(event);
   else if (event is PointerCancelEvent) 
    _motionEventConverter.handlePointerCancelEvent(event);
  

  if (androidEvent != null) 
    //发送转换后的事件
    await sendMotionEvent(androidEvent);
  

跟着进入sendMotionEvent(androidEvent)方法:

Future<void> sendMotionEvent(AndroidMotionEvent event) async 
  await SystemChannels.platform_views.invokeMethod<dynamic>(
    'touch',
    event._asList(viewId),
  );

(○´・д・)ノ, 这里又将点击事件传回了Android端…后面就不再赘述了,这个事件最终会被对应的原生view进行消费。

结语

刚看到这里时我是比较纳闷的,觉得事件绕了很大一圈,不过转念一想Flutter作为上层主要消费区,如果被跳过很可能造成事件错发的问题。

但是,我还是疑惑,混合图层下的事件消费问题是否可以在engine层做个优化呢? 毕竟混合图层的渲染就是在engine做了优化。

至此,要梳理的流程业已完毕,谢谢大家的阅读,如有错误欢迎指出。

其他Flutter相关文章

Flutter 仿网易云音乐App

Flutter&Android 启动页(闪屏页)的加载流程和优化方案

Flutter版 仿.知乎列表的视差效果

Flutter——实现网易云音乐的渐进式卡片切换

Flutter 仿同花顺自选股列表

以上是关于Flutter之Hybird-composition View的事件分发流程的主要内容,如果未能解决你的问题,请参考以下文章

Flutter之beamer路由入门指南

Flutter之beamer路由入门指南

Flutter 命令本质之 Flutter tools 机制源码深入分析

Flutter 命令本质之 Flutter tools 机制源码深入分析

Flutter 命令本质之 Flutter tools 机制源码深入分析

Flutter 之 动画1