前言
最近研究混合棧渲染時,遇到了一個混合棧下的事件分發問題,為此看了一下flutter接入原生view後,其中的事件分發流程。為了方便後期查閱,做此記錄,也希望能幫到有需要的人。
文中所討論的原生view為: AndroidViewSurface
,即:hybird composition
,其次還將涉及到Flutter的啟動流程
,如果對這兩者不熟悉的話,建議瀏覽這下面的文章:
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
方法直接呼叫了FlutterJni
的dispatchPointerDataPacket
方法:
//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 view
的DispatchPointerDataPacket
方法。
//這裡 holder 返回了一個PlatformView的子類PlatformViewAndroid
fml::WeakPtr<PlatformViewAndroid> AndroidShellHolder::GetPlatformView() {
FML_DCHECK(platform_view_);
return platform_view_;
}
複製程式碼
雖然返回的是 PlatformViewAndroid
,但是父類PlatformViewAndroid
的 DispatchPointerDataPacket
方法並沒有要求子類重寫,且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)));
}
複製程式碼
其又呼叫了Shell
的OnPlatformViewDispatchPointerDataPacket
方法:
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
,隨後呼叫rootView
的hitTest方法
並進而呼叫其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做了優化。
至此,要梳理的流程業已完畢,謝謝大家的閱讀,如有錯誤歡迎指出。