自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

揭秘!如何用Flutter設(shè)計(jì)一個(gè)100%準(zhǔn)確的埋點(diǎn)框架?

開(kāi)發(fā) 架構(gòu)
用戶(hù)行為埋點(diǎn)是用來(lái)記錄用戶(hù)在操作時(shí)的一系列行為,也是業(yè)務(wù)做判斷的核心數(shù)據(jù)依據(jù),如果缺失或者不準(zhǔn)確將會(huì)給業(yè)務(wù)帶來(lái)不可恢復(fù)的損失。

[[273407]]

用戶(hù)行為埋點(diǎn)是用來(lái)記錄用戶(hù)在操作時(shí)的一系列行為,也是業(yè)務(wù)做判斷的核心數(shù)據(jù)依據(jù),如果缺失或者不準(zhǔn)確將會(huì)給業(yè)務(wù)帶來(lái)不可恢復(fù)的損失。閑魚(yú)將業(yè)務(wù)代碼從Native遷移到Flutter上過(guò)程中,發(fā)現(xiàn)原先Native體系上的埋點(diǎn)方案無(wú)法應(yīng)用在Flutter體系之上。而如果只把業(yè)務(wù)功能遷移過(guò)來(lái)就上線,是極其不負(fù)責(zé)任的。因此,經(jīng)過(guò)不斷探索,閑魚(yú)技術(shù)團(tuán)隊(duì)沉淀了一套Flutter上的高準(zhǔn)確率的用戶(hù)行為埋點(diǎn)方案,今天由工程師蘭昊來(lái)和大家分享一下。

用戶(hù)行為埋點(diǎn)定位

先來(lái)講講在我們這里是如何定義用戶(hù)行為埋點(diǎn)的。在如下用戶(hù)時(shí)間軸上,用戶(hù)進(jìn)入A頁(yè)面后,看到了按鈕X,然后點(diǎn)擊了這個(gè)按鈕,隨即打開(kāi)了新的頁(yè)面B。

這個(gè)時(shí)間軸上有如下5個(gè)埋點(diǎn)事件發(fā)生:

  • 進(jìn)入A頁(yè)面。A頁(yè)面首幀渲染完畢,并獲得了焦點(diǎn)。
  • 曝光坑位X。按鈕X處于手機(jī)屏幕內(nèi),且停留一段時(shí)間,讓用戶(hù)可見(jiàn)可觸摸。
  • 點(diǎn)擊坑位X。用戶(hù)對(duì)按鈕X的內(nèi)容很感興趣,于是點(diǎn)擊了它。按鈕X響應(yīng)點(diǎn)擊,然后需要打開(kāi)一個(gè)新頁(yè)面。
  • 離開(kāi)A頁(yè)面。A頁(yè)面失去焦點(diǎn)。
  • 進(jìn)入B頁(yè)面。B頁(yè)面首幀渲染完畢,并獲得焦點(diǎn)。

在這里,打埋點(diǎn)最重要的是時(shí)機(jī),即在什么時(shí)機(jī)下的事件中觸發(fā)什么埋點(diǎn),下面來(lái)看看閑魚(yú)在Flutter上的實(shí)現(xiàn)方案。

實(shí)現(xiàn)方案

進(jìn)入/離開(kāi)頁(yè)面

在Native原生開(kāi)發(fā)中,Android端是監(jiān)聽(tīng)Activity的onResume和onPause事件來(lái)做為頁(yè)面的進(jìn)入和離開(kāi)事件,同理iOS端是監(jiān)聽(tīng)UIViewController的viewWillAppear和viewDidDisappear事件來(lái)做為頁(yè)面的進(jìn)入和離開(kāi)事件。同時(shí)整個(gè)頁(yè)面棧是由Android和iOS操作系統(tǒng)來(lái)維護(hù)。

在Flutter中,Android和iOS端分別是用FlutterActivity和FlutterViewController來(lái)做為容器承載Flutter的頁(yè)面,通過(guò)這個(gè)容器可以在一個(gè)Native的頁(yè)面內(nèi)來(lái)進(jìn)行Flutter頁(yè)面的切換,即Flutter自己維護(hù)了一個(gè)Flutter頁(yè)面的頁(yè)面棧。這樣,原來(lái)我們最熟悉的那套在Native原生上的方案在Flutter上無(wú)法直接運(yùn)作起來(lái)。

針對(duì)這個(gè)問(wèn)題,可能很多人會(huì)想到去注冊(cè)監(jiān)聽(tīng)Flutter的NavigatorObserver,這樣就知道Flutter頁(yè)面的進(jìn)棧(push)和出棧(pop)事件。但是這會(huì)有兩個(gè)問(wèn)題:

  • 假設(shè)A、B兩個(gè)頁(yè)面先后進(jìn)棧(A enter -> A leave -> B enter)。然后B頁(yè)面返回退出(B leave),此時(shí)A頁(yè)面重新可見(jiàn),但是此時(shí)是收不到A頁(yè)面push(A enter)的事件。
  • 假設(shè)在A頁(yè)面彈出一個(gè)Dialog或者BottomSheet,而這兩類(lèi)也會(huì)走push操作,但實(shí)際上A頁(yè)面并未離開(kāi)。

好在Flutter的頁(yè)面棧不像Android Native的頁(yè)面棧那么復(fù)雜,所以針對(duì)第一個(gè)問(wèn)題,我們可以維護(hù)一個(gè)和頁(yè)面棧匹配的索引列表。當(dāng)收到A頁(yè)面的push事件時(shí),往隊(duì)列里塞入A的索引。當(dāng)收到B頁(yè)面的push事件時(shí),檢測(cè)列表內(nèi)是否有頁(yè)面,如有,則對(duì)列表最后一個(gè)頁(yè)面執(zhí)行離開(kāi)頁(yè)面事件,再對(duì)B頁(yè)面執(zhí)行進(jìn)入頁(yè)面事件,接著往隊(duì)列里塞B的索引。當(dāng)收到B頁(yè)面的pop事件時(shí),先對(duì)B頁(yè)面執(zhí)行離開(kāi)頁(yè)面事件記錄,再對(duì)隊(duì)列里存在的最后一個(gè)索引對(duì)應(yīng)的頁(yè)面(假設(shè)為A)進(jìn)行判斷是否在棧頂(ModalRoute.of(context).isCurrent),如果是,則對(duì)A頁(yè)面執(zhí)行進(jìn)入頁(yè)面事件。

針對(duì)第二個(gè)問(wèn)題,Route類(lèi)內(nèi)有個(gè)成員變量overlayEntries,可以獲取當(dāng)前Route對(duì)應(yīng)的所有圖層OverlayEntry,在OverlayEntry對(duì)象中有個(gè)成員變量opaque可以判斷當(dāng)前這個(gè)圖層是否全屏覆蓋,從而可以排除Dialog和BottomSheet這種類(lèi)型。再結(jié)合問(wèn)題1,還需要在上述方案中加上對(duì)push進(jìn)來(lái)的新頁(yè)面來(lái)做判斷是否為一個(gè)有效頁(yè)面。如果是有效頁(yè)面,才對(duì)索引列表中前一個(gè)頁(yè)面做離開(kāi)頁(yè)面事件,且將有效頁(yè)面加到索引列表中。如果不是有效頁(yè)面,則不操作索引列表。

以上并不是閑魚(yú)的方案,只是筆者給出的一個(gè)建議。因?yàn)殚e魚(yú)APP在一開(kāi)始落地Flutter框架時(shí),就沒(méi)有使用Flutter原生的頁(yè)面棧管理方案,而是采用了Native+Flutter混合開(kāi)發(fā)的方案,因此接下來(lái)也是基于此來(lái)闡述閑魚(yú)的方案。

閑魚(yú)的方案如下(以Android為例,iOS同理):

注:首次打開(kāi)指的是基于混合棧新打開(kāi)一個(gè)頁(yè)面,非首次打開(kāi)指的是通過(guò)回退頁(yè)面的方式,在后臺(tái)的頁(yè)面再次到前臺(tái)可見(jiàn)。

看到這個(gè)方案可能會(huì)有人問(wèn),為什么這么繞,為什么不全部交給Native側(cè)去直接管理呢?交給Native側(cè)去直接管理這樣做針對(duì)非首次打開(kāi)這個(gè)場(chǎng)景是合適的,但是對(duì)首次打開(kāi)這個(gè)場(chǎng)景卻是不合適的。但是在首次打開(kāi)這個(gè)場(chǎng)景下,onResume時(shí)Flutter頁(yè)面尚未初始化,此時(shí)還不知道頁(yè)面信息,因此也就不知道進(jìn)入了什么頁(yè)面,所以需要在Flutter頁(yè)面初始化(init)時(shí)再回過(guò)來(lái)調(diào)Native側(cè)的進(jìn)入頁(yè)面埋點(diǎn)接口。而為了避免開(kāi)發(fā)人員去關(guān)注是否為首次打開(kāi)Flutter頁(yè)面,因此我們統(tǒng)一在Flutter側(cè)來(lái)直接觸發(fā)進(jìn)入/離開(kāi)頁(yè)面事件。

曝光坑位

先講下曝光坑位在我們這里的定義,我們認(rèn)為圖片和文本是有曝光意義的,其他用戶(hù)看不見(jiàn)的是沒(méi)有曝光意義的,在此之上,當(dāng)一個(gè)坑位同時(shí)滿足以下兩點(diǎn)時(shí)才會(huì)被認(rèn)為是一次有效曝光:

  • 坑位在屏幕可見(jiàn)區(qū)域中的面積大于等于坑位整體面積的一半。
  • 坑位在屏幕可見(jiàn)區(qū)域中停留超過(guò)500ms。

基于此定義,我們可以很快得出如下圖所示的場(chǎng)景,在一個(gè)可以滾動(dòng)的頁(yè)面上有A、B、C、D共4個(gè)坑位。其中:

  • 坑位A已經(jīng)滑出了屏幕可見(jiàn)區(qū)域,即invisible;
  • 坑位B即將向上從屏幕中可見(jiàn)區(qū)域滑出,即visible->invisible;
  • 坑位C還在屏幕中央可視區(qū)域內(nèi),即visible;
  • 坑位D即將滑入屏幕中可見(jiàn)區(qū)域,invisible->visible;

那么我們的問(wèn)題就是如何算出坑位在屏幕內(nèi)曝光面積的比例。要算出這個(gè)值,需要知道以下幾個(gè)數(shù)值:

  • 容器相對(duì)屏幕的偏移量
  • 坑位相對(duì)容器的偏移量
  • 坑位的位置和寬高
  • 容器的位置和寬高

其中坑位和容器的寬和高很容易獲取和計(jì)算,這里就不再累述。

獲得容器相對(duì)屏幕的偏移量

  1. //監(jiān)聽(tīng)容器滾動(dòng),得到容器的偏移量 
  2. double 
  3.  _scrollContainerOffset = scrollNotification.metrics.pixels; 

獲得坑位相對(duì)屏幕的偏移量

  1. //曝光坑位Widget的context 
  2. final 
  3.   
  4. RenderObject 
  5.  childRenderObject = context.findRenderObject(); 
  6. final 
  7.   
  8. RenderAbstractViewport 
  9.  viewport =  
  10. RenderAbstractViewport 
  11. .of(childRenderObject); 
  12. if 
  13.  (viewport ==  
  14. null 
  15. ) { 
  16.    
  17. return 
  18. if 
  19.  (!childRenderObject.attached) { 
  20.    
  21. return 
  22. //曝光坑位在容器內(nèi)的偏移量 
  23. final 
  24.   
  25. RevealedOffset 
  26.  offsetToRevealTop = viewport.getOffsetToReveal(childRenderObject,  
  27. 0.0 

邏輯判斷

  1. if 
  2.  (當(dāng)前坑位是invisible && 曝光比例 >= { 0.5) 
  3.  
  4. 記錄當(dāng)前坑位是visible狀態(tài) 
  5.  
  6. 記錄出現(xiàn)時(shí)間 
  7.  
  8. }  else if (當(dāng)前坑位是visible && 曝光比例 <  0.5 ) { 
  9.  
  10. 記錄當(dāng)前坑位是invisible狀態(tài) 
  11.  
  12. if (當(dāng)前時(shí)間-出現(xiàn)時(shí)間 >  { 500ms )
  13.  
  14.  
  15. 調(diào)用曝光埋點(diǎn)接口 
  16.  
  17.  

點(diǎn)擊坑位

點(diǎn)擊坑位埋點(diǎn)沒(méi)什么難點(diǎn),很容易就可以想到下面的方案:

效果

經(jīng)過(guò)多輪迭代和優(yōu)化,目前線上Flutter頁(yè)面的埋點(diǎn)準(zhǔn)確率已經(jīng)達(dá)到100%,有力地支持了業(yè)務(wù)的分析和判斷。同時(shí)這套方案讓業(yè)務(wù)同學(xué)在做開(kāi)發(fā)時(shí),對(duì)于頁(yè)面進(jìn)入/離開(kāi)、曝光坑位可以做到無(wú)感知,即不用關(guān)心何時(shí)去觸發(fā),做到了簡(jiǎn)單易用和無(wú)侵入性。

未來(lái)

此外,針對(duì)頁(yè)面進(jìn)入/離開(kāi)這個(gè)場(chǎng)景,由于閑魚(yú)是基于Flutter Boost混合棧的方案,因此我們的解決方案還不夠通用。不過(guò)未來(lái)隨著閑魚(yú)上的Flutter頁(yè)面越來(lái)越多,我們后續(xù)也會(huì)去實(shí)現(xiàn)基于Flutter原生的方案。

責(zé)任編輯:武曉燕 來(lái)源: 阿里技術(shù)
相關(guān)推薦

2024-09-14 14:14:26

Dubbo框架微服務(wù)

2023-12-13 18:46:50

FlutterAOP業(yè)務(wù)層

2016-12-12 13:42:54

數(shù)據(jù)分析大數(shù)據(jù)埋點(diǎn)

2015-10-12 16:45:26

NodeWeb應(yīng)用框架

2025-04-30 08:56:34

2023-03-06 08:14:48

MySQLRedis場(chǎng)景

2023-02-26 01:37:57

goORM代碼

2021-06-24 10:27:48

分布式架構(gòu)系統(tǒng)

2021-06-25 10:45:43

Netty 分布式框架 IO 框架

2022-04-12 19:41:42

SDK監(jiān)控react

2013-07-01 11:01:22

API設(shè)計(jì)API

2009-10-01 09:19:45

PHP框架ZendFramewoCake

2016-09-06 19:45:18

javascriptVue前端

2018-03-23 10:00:34

PythonTensorFlow神經(jīng)網(wǎng)絡(luò)

2017-03-20 17:59:19

JavaScript模板引擎

2020-03-26 09:36:06

AB Test平臺(tái)的流量

2018-09-18 09:38:11

RPC遠(yuǎn)程調(diào)用網(wǎng)絡(luò)通信

2020-11-02 08:19:18

RPC框架Java

2019-01-07 10:25:44

Gonimo嬰兒監(jiān)視開(kāi)源

2022-04-14 20:43:24

JavaScript原型鏈
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)