得到Android團隊無埋點方案

客戶端埋點是數(shù)據(jù)收集的最基本手段啄育,但由于業(yè)務迭代速度很快,手動埋點方案雖然靈活多變,但是極大的增加了客戶端開發(fā)人員的工作量羊娃。開發(fā)完成業(yè)務功能需要花費很大的精力處理埋點事宜,而且隨著迭代版本埃跷,埋點的數(shù)量會越來越多蕊玷,這些老舊埋點的維護工作也需要付出不小的努力。并且弥雹,手動埋點的正確性同樣是個極度考驗開發(fā)人員的耐性和認真程度的問題垃帅,在所難免會出現(xiàn)這樣那樣的問題。所以剪勿,如果能夠研發(fā)出一款不需要或者很少需要開發(fā)人員介入就能實現(xiàn)根據(jù)不同業(yè)務場景埋點的功能sdk對于提高版本迭代速度和開發(fā)人員的幸福感絕對是一件非常有價值的事情贸诚。

更大的價值還在于,不需要開發(fā)人員介入窗宦,運營或者用研的同學就可以隨時動態(tài)調(diào)整數(shù)據(jù)收集方案赦颇。

縱觀目前比較成熟的無埋點方案,存在著如下問題:

問題1:通過XPath定位控件赴涵,理論上可行媒怯,但實踐表明這個方案的復雜度非常高,尤其對于處理像GridView髓窜,ListView扇苞,RecyclerView的控件更是捉襟見肘。不僅如此寄纵,生成xpath的過程本身就是一個及其耗費性能的行為鳖敷,它需要遍歷view tree,存儲非常多的路徑信息到view上程拭。

問題2:獲取控件對應的數(shù)據(jù)是通過 data path的方式解決定踱,每次添加新埋點時,如果需要上報數(shù)據(jù)恃鞋,那用研人員需要和開發(fā)人員逐一確認控件數(shù)據(jù)的path崖媚,這極大的限制了客戶端開發(fā)的自由度亦歉,即使簡單的重構(gòu)也會使得之前配置的埋點信息失效。

針對如上問題畅哑,我們經(jīng)過深挖內(nèi)在邏輯關(guān)系及對比優(yōu)劣肴楷,總結(jié)出了一套更靈活,更合理的無埋點方案荠呐,下面分三個部分逐一介紹實現(xiàn)考量及內(nèi)部機制赛蔫。

一、定位與用戶產(chǎn)生交互行為的目標控件

關(guān)于定位交互控件泥张,我們也考慮過xpath的方案呵恢,但是考慮到其實現(xiàn)的復雜度,不靈活和各種潛在的問題圾结,我們拋棄了這種方案瑰剃。通過反復的閱讀View的touch事件處理相關(guān)的源碼,我們終于發(fā)現(xiàn)了解決問題的更好的方式筝野。

ViewGroup中有一個TouchTarget 類型的變量 mFirstTouchTarget晌姚,表示消費當前觸摸事件的控件列表。例如歇竟,點擊屏幕上一個按鈕挥唠,那么按鈕所在ViewGroup的mFirstTouchTarget 變量就指向這個按鈕。當ViewGroup派發(fā)觸摸事件時焕议,他會首先判斷變量mFirstTouchTarget是否存在宝磨,如果變量存在,會循環(huán)遍歷TouchTarget鏈表元素盅安,找到能處理該事件的View并將MotionEvent 派發(fā)給該View唤锉。如果不存在TouchTarget,ViewGroup 會循環(huán)遍歷所有child view别瞭,直到找到一個能處理該事件的View窿祥,并將該View作為first touch target 賦值給mFirstTouchTarget。

當用戶觸發(fā)Down事件時蝙寨,會執(zhí)行如下邏輯晒衩,尋找消費當前事件的TouchTarget。

if (actionMasked == MotionEvent.ACTION_DOWN){
    //如果是down事件墙歪,遍歷child听系,找到TouchTarget
    ..
    ..
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
       final int childIndex = getAndVerifyPreorderedIndexchildrenCount, i, customOrder);
       final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
       ..
       ..
       if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
          // child 消費了觸摸事件
          ..
          ..
          // 根據(jù)消費了觸摸事件的View創(chuàng)建TouchTarget
           newTouchTarget = addTouchTarget(child, idBitsToAssign);
          ..
          ..
          break;
      }
}

當觸發(fā)Down事件并且找到TouchTarget,或者觸發(fā)非Down事件時虹菲,執(zhí)行如下處理邏輯靠胜。

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
} else {
    //Down事件發(fā)生時找到TouchTarget,或者非Down事件直接執(zhí)行如下邏輯

    // 將事件派發(fā)給TouchTarget表示的View
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;

    while (target != null) {
        final TouchTarget next = target.next;

        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;

            if (dispatchTransformedTouchEvent(ev, cancelChild,target.child,target.pointerIdBits)) {
               //指定TouchTarget對應的View正確消費了事件
                handled = true;
             }
             ..
             ..
         }
     ..
     ..
     }
}

提示:由于消費觸摸事件的控件可能為多個(splitting touch events),所以需要遍歷TouchTarget鏈表浪漠。引用官方原文:
This behavior is enabled by default for applications that target an SDK version of 11 (Honeycomb) or newer. On earlier platform versions this feature was not supported and this method is a no-op.

MotionEvents may be split and dispatched to different child views depending on where each pointer initially went down. This allows for user interactions such as scrolling two panes of content independently, chording of buttons, and performing independent gestures on different pieces of content.

利用ViewGroup的這種事件處理機制菠赚,我們通過在Activity的window上調(diào)用window.setCallback() 接管窗口的事件派發(fā),并在dispatchTouchEvent處理函數(shù)中添加analyzeMotionEvent()方法郑藏。如果接收到up事件,執(zhí)行處理邏輯瘩欺,通過ViewGroup TouchTarget鏈表必盖,找到本次交互行為的目標控件。拿到控件后俱饿,通過 Activity的類名+控件所在的layout文件名+控件id對應的資源名歌粥,我們就可以確定目標控件的唯一標識。

dispatchTouchEvent源碼如下:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!AutoPointer.isAutoPointEnable()) {
            return super.dispatchTouchEvent(ev);
        }

        int actionMasked = ev.getActionMasked();

        if (actionMasked != MotionEvent.ACTION_UP) {
            return super.dispatchTouchEvent(ev);
        }

        long t = System.currentTimeMillis();
        analyzeMotionEvent();

        //非線上版本拍埠,打印執(zhí)行時間
        if (!AutoPointer.isOnlineEnv()) {
            long time = System.currentTimeMillis() - t;
            DDLogger.d(TAG, String.format(Locale.CHINA, "處理時間:%d 毫秒", time));
        }

        return super.dispatchTouchEvent(ev);
    }

analyzeMotionEvent源碼如下:

    /**
     * 分析用戶的點擊行為
     */
    private void analyzeMotionEvent() {
        if (mViewRef == null || mViewRef.get() == null) {
            DDLogger.e(TAG, "window is null");
            return;
        }

        ViewGroup decorView = (ViewGroup) mViewRef.get();
        int content_id = android.R.id.content;
        ViewGroup content = (ViewGroup) decorView.findViewById(content_id);
        if (content == null) {
            content = decorView; //對于非Activity DecorView 的情況處理
        }

        Pair<View, Object> targets = findActionTargets(content);
        if (targets == null) {
            DDLogger.e(TAG, "has no action targets!!!");
            return;
        }

        //發(fā)送任務在單線程池中
        int hashcode = targets.first.hashCode();
        if (mIgnoreViews.contains(hashcode)) return;

        PointerExecutor.getHandler().post(PointPostAction.create(targets.first, targets.second));
    }

二失驶、獲取與目標控件對應的業(yè)務數(shù)據(jù)

對于獲取控件數(shù)據(jù),為了最大化獲取速度枣购,我們在系統(tǒng)中配置了多個數(shù)據(jù)獲取策略嬉探。如果目標控件是AbsListView或者RecyclerView 的child view及child view 的chid,那我們可以通過child view在adapter中的位置獲取到我們想要的數(shù)據(jù)棉圈。這種方式能夠處理大多數(shù)頁面控件數(shù)據(jù)的獲取問題涩堤。系統(tǒng)配置策略的方式如下:

    private static Map<String, DataStrategy> mStrategies = new HashMap<>();

    static {
        //configure RecyclerView and subclass's search strategy
        DataStrategy recyclerViewStrategy = new RecyclerViewStrategy();
        mStrategies.put("RecyclerView", recyclerViewStrategy);
        mStrategies.put("DDCollectionView", recyclerViewStrategy);

        //ExpandableListView
        DataStrategy EListViewStrategy = new ExpandableListViewStrategy();
        mStrategies.put("ExpandableListView", EListViewStrategy);
        mStrategies.put("DDExpandableListView", EListViewStrategy);

        DataStrategy adapterViewStrategy = new AdapterViewStrategy();
        //ListView
        mStrategies.put("ListView", adapterViewStrategy);
        mStrategies.put("DDListView", adapterViewStrategy);
        mStrategies.put("ListViewCompat", adapterViewStrategy);

        //GridView
        mStrategies.put("GridView", adapterViewStrategy);
        mStrategies.put("DDGridView", adapterViewStrategy);

        //ViewPager
        DataStrategy viewPagerStrategy = new ViewPagerStrategy();
        mStrategies.put("ViewPager", viewPagerStrategy);

        //TabLayout
        DataStrategy tabLayoutStrategy = new TabLayoutStrategy();
        mStrategies.put("TabLayout", tabLayoutStrategy);
    }

對于那些完全自定義布局繪制的頁面,例如個人中心等頁面分瘾,業(yè)務開發(fā)人員需要通過框架api建立一個控件樹到數(shù)據(jù)的映射關(guān)系胎围,這樣框架在需要獲取數(shù)據(jù)時,通過這個關(guān)系就可以非常容易的獲取到想要的數(shù)據(jù)德召。

    /**
     * 配制自定義布局的數(shù)據(jù)綁定關(guān)系白魂,自定義布局內(nèi)的任何
     * 控件發(fā)生點擊行為時,發(fā)送的埋點都會攜帶改數(shù)據(jù)
     *
     * @param id
     * @param object
     * @return
     */
    @NonNull
    @Override
    public DataConfigureImp configLayoutData(@IdRes int id, @NonNull Object object) {
        Preconditions.checkNotNull(object);

        mDataLayout.put(id, object);
        return this;
    }

根據(jù)TouchTarget找到數(shù)據(jù)獲取策略或者數(shù)據(jù)映射關(guān)系上岗,我們可以非常簡單的獲取到綁定的數(shù)據(jù)福荸,獲取數(shù)據(jù)的算法如下:

        if (strategyView != null) {
            Object data = strategy.fetchTargetData(strategyView);

            return Pair.create(touchTarget, data);
        }

        if (configDataView != null) {
            return Pair.create(touchTarget, mDataLayout.get(configId));
        }

        //解決自定義布局的數(shù)據(jù)綁定問題
        if (dataAdapter != null) {
            return Pair.create(touchTarget, dataAdapter.getData());
        }

三、實現(xiàn)埋點的動態(tài)可配置

在測試環(huán)境下液茎,用研人員會通過手動模擬點擊的方式獲取sdk上報的控件唯一id和數(shù)據(jù)信息逞姿,在確認id,和數(shù)據(jù)的正確性之后捆等,需要手動配置id和埋點事件的對應關(guān)系滞造,及上報的數(shù)據(jù)字段,并存儲到配置倉庫栋烤。在線上環(huán)境谒养,當用戶啟動app會拉取配置信息并加載到內(nèi)存。這樣,當用戶觸發(fā)點擊行為時买窟,會根據(jù)第一步獲取的id信息查詢配置丰泊,如果在配置中查到對應的條目,會將對應的事件及數(shù)據(jù)上報到服務器始绍。

為了處理配置下拉失敗無法發(fā)送埋點的情況瞳购,我們需要將同樣的配置放在主項目的assets目錄下,每次啟動app請求配置接口判斷配置信息是否發(fā)生變化亏推,如果配置沒有變化学赛,直接使用assets中的配置文件,否則吞杭,下拉最新配置盏浇,使用最新的埋點配置信息。

四芽狗、無痕埋點方案對現(xiàn)有項目的約束

使用無埋點sdk需要遵循一定的開發(fā)規(guī)范绢掰,關(guān)于具體的開發(fā)規(guī)范請查看工程README。為了確保項目編碼的規(guī)范性童擎,我們開發(fā)了一系列l(wèi)int檢查規(guī)則來幫助發(fā)現(xiàn)錯誤滴劲。
lint 工程代碼 https://github.com/jessie345/CustomLintRules.git
集成lint功能 https://github.com/jessie345/CustomLintsUsage.git

五、繼續(xù)優(yōu)化
目前顾复,集成這個無埋點方案有一些使用約束并且需要在主項目中添加一些特定的配置函數(shù)哑芹。下一步需要做的就是解耦。通過javasist技術(shù)捕透,盡量將所有約束遷移到用動態(tài)技術(shù)保證聪姿,而不是通過lint規(guī)范,將其侵入性降到最低乙嘀。

至此,無埋點sdk的核心運作機制已經(jīng)全部梳理清楚末购。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市虎谢,隨后出現(xiàn)的幾起案子盟榴,更是在濱河造成了極大的恐慌,老刑警劉巖婴噩,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件擎场,死亡現(xiàn)場離奇詭異,居然都是意外死亡几莽,警方通過查閱死者的電腦和手機迅办,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門注暗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來死讹,“玉大人,你說我怎么就攤上這事甜癞。” “怎么了矾策?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵磷账,是天一觀的道長。 經(jīng)常有香客問我贾虽,道長逃糟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任蓬豁,我火速辦了婚禮履磨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘庆尘。我一直安慰自己,他們只是感情好巷送,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布驶忌。 她就那樣靜靜地躺著,像睡著了一般笑跛。 火紅的嫁衣襯著肌膚如雪付魔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天飞蹂,我揣著相機與錄音几苍,去河邊找鬼。 笑死陈哑,一個胖子當著我的面吹牛妻坝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播惊窖,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼刽宪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了界酒?” 一聲冷哼從身側(cè)響起圣拄,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎毁欣,沒想到半個月后庇谆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡凭疮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年饭耳,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片执解。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡哥攘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情逝淹,我是刑警寧澤耕姊,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站栅葡,受9級特大地震影響茉兰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜欣簇,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一规脸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧熊咽,春花似錦莫鸭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至衫仑,卻和暖如春梨与,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背文狱。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工粥鞋, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瞄崇。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓呻粹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親苏研。 傳聞我的和親對象是個殘疾皇子尚猿,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容