Flutter——原生View的Touch事件分發流程

吉哈達發表於2021-07-29

前言

最近研究混合棧渲染時,遇到了一個混合棧下的事件分發問題,為此看了一下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 仿同花順自選股列表

相關文章