Flutter無痕埋點

Flutter無痕埋點

因爲工做調整的緣由,後面可能將再也不接觸flutter開發,本着技術共享不埋沒的原則,開源一下flutter無痕埋點的技術方案,此方案完成於半年前,應該在閒魚的無痕埋點方案開源前,與閒魚的方案不太同樣,你們有什麼建議能夠普遍留言。另外特別感謝永葵同窗在此技術中的參與和共同努力。app

思考

  • 既然是無痕埋點,Flutter如何實現AOP?像iOS,能夠在運行時經過一些方法交換,消息轉發來實現切面,可是因爲目前Flutter不支持運行時的反射功能,因此第一時間放棄了這個方向。另外因爲當時時間和資源上也不是很充足,也沒有研究編譯期的AOP方案,不過以後閒魚出品的AspectD確實是個不錯的AOP框架。切回正題,因此當時的選擇時閱讀Flutter的源碼,試圖尋找官方的API來解決問題。

Flutter如何遍歷widget

  • 衆所周知,在flutter中有widget樹和element樹,二者成一一對應的關係,那麼想要遍歷widget樹,其實也就是遍歷element樹,在閱讀源碼的過程當中發現framework.dart這個文件下一個visitChildElements(ElementVisitor visitor)的方法,也就是說,經過這個方法,咱們能夠遍歷指定element下全部的elements,而後經過element拿到他所對應的widget。

頁面埋點

導航欄監聽

  • 要想作頁面埋點,第一個想到的就是經過監聽導航欄頁面的變更,就能夠監聽到頁面的變化。經過NavigatorObserver確實就能夠監聽到頁面的push和pop,可是官方的這個類提供的方法並不能拿到具體跳轉頁面的信息(靜態頁面除外)。那麼是否是能夠當監聽到頁面push和pop時,直接遍歷element樹拿到widget信息呢?實踐證實,當監聽到push的同時去遍歷會報錯,由於這個時候頁面還正在渲染,flutter的元素正在生成,因此遍歷會有問題。因此須要監聽頁面的渲染完成的時機。

頁面渲染監聽

  • 如何作頁面渲染的監聽呢?咱們能夠看一下Flutter的啓動函數
void runApp(Widget app) {
 WidgetsFlutterBinding.ensureInitialized()
   ..attachRootWidget(app)
   ..scheduleWarmUpFrame();
}
複製代碼

而後咱們再觀察下WidgetsFlutterBinding這個類,框架

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding 複製代碼

在這個類裏作了很是多的綁定,渲染的綁定,手勢的綁定等等。 SchedulerBinding這是一個調度器,調度任務的安排。 在SchedulerBinding中有一個控制渲染的方法函數

void handleDrawFrame() {
   assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
   Timeline.finishSync(); // end the "Animate" phase
   try {
     // PERSISTENT FRAME CALLBACKS
     _schedulerPhase = SchedulerPhase.persistentCallbacks;
     for (FrameCallback callback in _persistentCallbacks)
       _invokeFrameCallback(callback, _currentFrameTimeStamp);

     // POST-FRAME CALLBACKS
     _schedulerPhase = SchedulerPhase.postFrameCallbacks;
     final List<FrameCallback> localPostFrameCallbacks =
         List<FrameCallback>.from(_postFrameCallbacks);
     _postFrameCallbacks.clear();
     for (FrameCallback callback in localPostFrameCallbacks)
       _invokeFrameCallback(callback, _currentFrameTimeStamp);
   } finally {
     _schedulerPhase = SchedulerPhase.idle;
     Timeline.finishSync(); // end the Frame
     assert(() {
       if (debugPrintEndFrameBanner)
         debugPrint('▀' * _debugBanner.length);
       _debugBanner = null;
       return true;
     }());
     _currentFrameTimeStamp = null;
   }
 }
複製代碼

根據源碼咱們能夠看到當佈局完成後會調用已經註冊的回調postFrameCallbacks,官方很友好的開放了添加回調的方法。佈局

void addPostFrameCallback(FrameCallback callback) {
   _postFrameCallbacks.add(callback);
 }

複製代碼

因此咱們就能夠當監聽到導航欄路由變化時,而且監聽到佈局渲染後去遍歷element樹,拿到咱們想要的widget,去獲取widget上面的信息。post

識別頁面並獲取頁面信息

  • 在樹的遍歷上,咱們須要用從下往上從左往右的方式去遍歷,由於元素是從上往下從右往左添加到樹上的。咱們只須要找到第一個Scaffold,就能夠肯定頁面,從它再往上取1-2層,拿到爲頁面的widget,這中間的一個路徑就能夠做爲咱們頁面的標示了。固然你也能夠加上Scaffold裏頁面的title。

點擊埋點

全局捕獲點擊事件

  • 在flutter中存在一個手勢競技場,競技場中最後獲勝的手勢能夠響應點擊事件。那麼既然競技場最後只能響應一個手勢,可是又不能hook手勢的onTap方法,那咱們要如何實現全局捕獲點擊事件呢?一樣咱們仍然閱讀flutter源碼,能夠看到TapGestureRecognizer中有一個以下方法。
void acceptGesture(int pointer) {
   super.acceptGesture(pointer);
   if (pointer == primaryPointer) {
     _checkDown(pointer);
     _wonArenaForPrimaryPointer = true;
     _checkUp();
   }
 }
 
void rejectGesture(int pointer) {
   super.rejectGesture(pointer);
   if (pointer == primaryPointer) {
     // Another gesture won the arena.
     assert(state != GestureRecognizerState.possible);
     if (_sentTapDown)
       _checkCancel('forced ');
     _reset();
   }
 }
複製代碼

上面的方法是手勢的拒絕和添加。從源碼中咱們能夠看出當另外一個手勢從競技場勝出時,不會執行手勢的成功回調,而是會執行手勢的取消回調。那麼咱們是否是能夠添加一個全局的手勢,而後重寫它的拒絕方法,讓它內部執行手勢的接受方法呢,這樣咱們本身添加的全局手勢也能執行成功回調了?實踐見證確實能夠這樣作。ui

識別點位標示並獲取信息

  • 一樣咱們能夠在全局手勢的回調中拿到當前點位座標。在回調中一樣咱們遍歷element樹,拿到符合點位座標而且widget runTimeType 是Ink的最小單元。

最小單元: 藍色,黃色,綠色都是Ink控件,白色爲點擊的點,那麼綠色即爲最小單元。

把Ink到Scroffld的中間路徑爲做爲一個點擊位的標示,固然中間能夠過濾一些不須要的widget,否則路徑會很長。另外能夠獲取按鈕或者子控件的一些其餘信息,如title和Image name。spa

遇到的坑

無痕埋點的方案基本上是上面的這些,可是中間咱們仍是一路踩坑,細節上有些須要特殊處理。debug

多子元素控件的處理

  • flutter中widget子組件可能不只有child還有些childrens,所對應的element爲SingleChildRenderObjectElement和MultiChildRenderObjectElement,因此在遍歷element是要注意區分爲哪一種element類型,不一樣類型的遍歷上略有不一樣。如在手勢埋點中,在多子元素的組件中,從最後一個開始遍歷,保證獲取到的第一個命中的點擊事件是相應的那個(界面上是最頂層的組件)。一樣像ListView,Tab這種組件想要獲取正確的路徑位置,須要獲取當前element在MultiChildRenderObjectElement中的正確位置。

Tab的特殊處理

  • 當Tab下的頁面切換時,並不會觸發路由的didPush和didPop,因此咱們須要藉助點擊事件的獲取,並分析出是tab切換所觸發的,而後遍歷獲取到頁面信息。

附言

  • 對文章有疑問或者有任何技術想要探討的同窗能夠經過郵箱聯繫我 531889780@qq.com
相關文章
相關標籤/搜索