(Android)自主項目埋點方案討論

吐槽+廢話:多久沒有寫博客了,因爲沒有時間,每天忙着項目,想着項目怎麼做好;產品和技術經理每天都告訴程序員說:你們要有產品思維。可是,連需求場景都沒有說清楚,我們如何有產品思維,產品給的需要是A,我們做出來的功能效果成A1,誰的問題呢,很難說得清楚。產品和技術經理又一直告訴程序員:我們要把自己的項目當成自己的孩子,我們要關心它,放心思在上面,要提出有建設性的建議,要想着把產品做好,並且自己也想去用。好吧,“好好學習,天天向上”的道理都沒錯,可提出了建設性的建議了,好的方案提議了,可是卻一直無法在第一時間得到採納,總是按照傻逼模式的想法去做,沒辦法了,畢竟說到底我也是執行者,也就跟着傻逼模式的做了;慢慢的做到後面了,別人意識到傻逼模式的弊端了,纔想起來我的提議,然後再來修改,我無力吐槽,也懶得改。孩子養的過程不好好教育,等到成人後再來教育,能改變多少,累人累己,最簡單的就是殺了重造。吐槽和廢話就這麼多,開始正文。

需求:統計埋點

背景:因使用友盟埋點無法滿足產品和領導的數據需求,所以要自己做埋點(歸根結底就是產品、技術總監、技術主管沒有摸清楚友盟模式,沒有玩透友盟統計)。

自己做埋點的優勢:1、不需要去友盟查詢數據,可以在自己後臺可以查詢到數據

                              2、用戶數據和埋點數據能夠更好的融合在一起進行漏斗統計,不至於將用戶相應的數據暴露在第三方埋點

                              3、待定

埋點目的:統計數據、統計用戶操作行爲(用於未來業務拓展)、漏斗分析(分析每一步驟數據流失情況)、其他

首先,要清楚漏斗的規則,漏斗是不可逆,漏斗是按照一定的目的性去統計,在這樣的規則下,並且還要要求在每一個步驟都能看到漏斗,也就意味着需要對整個app進行埋點,在客戶端埋點相當於沒有目的性的埋,而後臺處理需要以付費成功或者某一個規則確定埋點目的(終點)。

埋點方案第一版(傻逼模式):對確定的位置進行埋點,當從不同的入口進入的時候,埋點的key值是會變的;例子:對C頁面進行頁面統計,但進入C頁面的入口有多個並且路徑也有多個;假如此時記錄路徑B->C,那麼就得記錄B頁面的KeyB和C頁面的KeyBC,此時只需要判斷進入C頁面的上一層是B就行,但假如現在記錄的路徑變成了A->B->C,那麼這三個頁面記錄的Key值就會變成KeyA->KeyAB->KeyABC,針對於C頁面來說,我們需要判斷上一個頁面進入的是B,但B的上一個頁面又是A,也就是說對一個C頁面的Key值的確定需要清楚所有進入C頁面的所有路徑情況,而且都需要進行傳參進行判斷,這樣會出現的問題有如下情況:1、代碼很亂,爲了統計而傳遞各種參數進行邏輯判斷,使得代碼冗餘但又不好做到統一,因爲頁面都不一樣,特別是當Activity頁面含有Fragment的時候,並且Fragment還要統計的時候更痛苦,需要再進一步的傳到Fragment裏面;2、不好維護,當進入C頁面又出現一種路徑的時候,又得一層一層的傳遞參數判斷,不利於拓展,拓展指數爲0;3、想當然的埋點,所有埋點邏輯又客戶端進行處理,使得正常功能執行效率差。很明顯,當你讀到這句話的時候已經發現你的思維很混亂了,好吧,這種方案確實很混亂的,更不要說邏輯能夠出現好的了;然而這種方式對後臺來說比較容易,因爲後臺設置好所有的路徑就行,完全就是一種死的模式,通過KeyB->KeyBC就知道路徑是B->C,通過KeyA->KeyAB->KeyABC,就知道路徑情況是ABC;但也是個坑,當出現新的路徑的時候,後臺就得再配置一種情況,也是不利於拓展。

埋點方案第二版(拋棄傻逼模式,殺死重造):通過第一方案的實驗,你會發現埋一個點你需要對整個App所有頁面、事件都進行判斷,埋一個點會非常的痛苦,那麼如何能夠避免那些頁面的參數傳遞,並且複用性強,功能實現放便呢?首先要確定一點,同一個頁面、同一個按鈕的Key值是不變的, 比如C頁面,那麼C頁面的Key值永遠是KeyC,不會出現KeyBC,更不會出現KeyABC的情況;這時候可能會再想到一個問題,那麼我如何判斷進入C頁面的上一個頁面是誰呢?那麼很簡單啊,對於B->C路徑,Key值的記錄就是KeyB->KeyC,;對於A->C,那麼Key值記錄就是KeyA->KeyC;對於A->B->C,記錄的Key就是KeyA->KeyB->KeyC啦,對於後臺而言通過每一個頁面的Key值就可以判斷出路徑情況,而後臺也不需要專門去配置所謂的固定路徑了,這種會變得很活躍,通過Key值的配置就能確定路徑和數據。優勢:對於頁面埋點,可以在BaseActivity裏面進行統計,也可以在AppApplication當中進行統計,只需要用Map對象配置好Activity的名稱,和Activity對應的Key值就可以進行統一埋點,在頁面start和pause的時候就可以埋點;沒有冗餘、統一頁面埋點、不需要進行頁面參數判斷,只要有需要,只需要配置頁面名稱和Key就可以,例子代碼如下:

public class ActivityLifecycleImpl implements Application.ActivityLifecycleCallbacks {

    private static final String TAG = ActivityLifecycleImpl.class.getSimpleName();
    private IStatistics mEntity;

    @Override
    public void onActivityCreated(Activity activity, Bundle bundle) {
//        if (activity instanceof BaseActivity) {
//            ((BaseActivity) activity).getSupportFragmentManager().registerFragmentLifecycleCallbacks(mFLImpl, true);
//        }
    }

    @Override
    public void onActivityStarted(Activity activity) {
        //獲取是否有頁面傳遞的key值,沒有就獲取是否有頁面的key
        String eventKey = ForPageHash.getInstance().getPagehash().get(activity.getClass().getSimpleName());
        String params = ForPageHashParams.getPageParams(activity, activity.getClass().getSimpleName());
        //記錄在數據庫之後就清空參數
        if (eventKey != null) {
            //每個onStart都要創建一個對象
            mEntity = new IStatistics();
            mEntity.setStart(Systems.currentTimeSeconds());

            mEntity.setKey(eventKey);
            mEntity.setParams(params);
            Log.e(TAG, params == null || params.isEmpty() ? "沒有" : params);

            //保存當前的key
            PageShare.setEventKey(activity, eventKey);
            //事件觸發日期
            mEntity.setTime(Systems.currentTimeSeconds());
            DBIStatistics.setIStatistics(activity, mEntity,false);

            Log.e(TAG, "ActivityName:" + activity.getClass().getSimpleName());
            Log.e(TAG, "DB count :" + DBIStatistics.datacount() + "");
        }
    }

    @Override
    public void onActivityResumed(Activity activity) {

    }

    @Override
    public void onActivityPaused(Activity activity) {
        if (mEntity != null) {
            String eventKey = ForPageHash.getInstance().getPagehash().get(activity.getClass().getSimpleName());
            String params = ForPageHashParams.getPageParams(activity, activity.getClass().getSimpleName());
            //記錄在數據庫之後就清空參數
            if (eventKey != null) {
                mEntity.setKey(eventKey);
                mEntity.setParams(params);

                //離開頁面的時候,設置離開時間
                mEntity.setEnd(Systems.currentTimeSeconds());

                //保存當前的key
                PageShare.setEventKey(activity, eventKey);
                //事件觸發日期
//                mEntity.setTime(Systems.currentTimeSeconds());
                DBIStatistics.setIStatistics(activity, mEntity,true);

                Log.e(TAG + " " + activity.getClass().getSimpleName()
                        , mEntity.getParams() == null || mEntity.getParams().isEmpty() ? "沒有" : mEntity.getParams());
                Log.e(TAG, "end " + activity.getClass().getSimpleName());
            }
        }
    }

    @Override
    public void onActivityStopped(Activity activity) {

    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {

    }

    @Override
    public void onActivityDestroyed(Activity activity) {
//        if (activity instanceof BaseActivity) {
//            ((BaseActivity) activity).getSupportFragmentManager().unregisterFragmentLifecycleCallbacks(mFLImpl);
//        }
    }
}

/**
 * 統計Activity生命週期的
 * Created by TenXu on 2018/2/5.
 */

public class StatisticsLife {

    private static final String TAG = StatisticsLife.class.getSimpleName();

    private static ActivityLifecycleImpl sLifecycleImpl;

    public static void registerStatisticsLife(Application application) {
        if (sLifecycleImpl == null) {
            synchronized (StatisticsLife.class) {
                if (sLifecycleImpl == null) {
                    sLifecycleImpl = new ActivityLifecycleImpl();
                }
            }
        }
        application.registerActivityLifecycleCallbacks(sLifecycleImpl);
        //啓動服務
        Intent intent = new Intent(application, UploadStatisticsService.class);
        application.startService(intent);
    }

    public static void unregisterStatisticsLife(Application application) {
        if (sLifecycleImpl != null) {
            application.unregisterActivityLifecycleCallbacks(sLifecycleImpl);
        } else {
            Log.i(TAG, "ActivityLifecycleImpl不能爲空");
        }
    }
}

接着,就是在BaseApplication當中啓動:
StatisticsLife.registerStatisticsLife(getApplication());

劣勢:當Activity頁面中含有ViewPager+Fragment的時候,對Fragment頁面的統計比較麻煩,以京東app爲例子,因爲ViewPager對於Fragment有緩存作用,當你打開app的時候,首頁Fragment和分類Fragment都已經開始了start,只是首頁可見,分類不可見而已,假如首頁和分類模塊都需要統計,那麼此時會出現統計了首頁Key之後還會再跟着一個分類的Key,當我點擊了首頁某個需要統計的按鈕A時候,對A的統計會變成Key分類->KeyA,然而正確的是Key首頁->KeyA。確定上傳數據的格式:{"key":"","time":"時間戳"},key是事件或者是頁面的統計的key值,time是頁面進入或者是事件觸發的時間

方案二拋出的問題:1、對ViewPager+Fragment無法進行精確統計,涉及到多個Fragment生命週期混雜在一起的問題,2、例如對List列表的item進行點擊統計的時候,不可能一個item一個key,3、當統計一個被點擊的商品的購買類型以及商品的ID的購買次數的時候,無法統計。

埋點方案第三版(優化,正在使用版本):根據第二方案出來的問題進行優化,對於問題2和問題3,很簡單,只需要在統計的時候,給統計的地方加上參數params給後臺就行;格式修改成如下:{"key":"","time":"時間戳","params":""},當遇到問題2的時候,可以進行如下的方式傳遞:{"key":"","time":"時間戳","params":"iitem_id"},當遇到問題3的時候,可以如下傳遞:{"key":"","time":"時間戳","params":{"type":"type_id","id":"item_id"}}

對於問題1,處理就要相對麻煩點,首先需要在BaseFragment的onStart和onPause方法進行埋點,接着需要在setUserVisibleHint當中進行判斷Fragment是否是對用戶可見的,可見的時候和不可見的時候也是需要埋點(先解釋一下爲什麼start、pause和可見、不可見進行埋點,因爲我們要統計到進入頁面的時間和切換頁面的時間,以此來判斷頁面停留時長),埋點的方式跟Activity一樣,記錄Fragment的名稱和對應的key;在onStart方法當中,還需要通過userVisibleHint參數來判斷頁面是否可見,如下代碼(kotlin版):

override fun onStart() {
    super.onStart()

    if (userVisibleHint) {
        //做統計
    }
}

在onPause方法也是一樣:

override fun onPause() {
    super.onPause()
    if (userVisibleHint) {
        //做統計
    }
}

註釋那裏的“做統計”,最好是通過接口回調出去,並且在Application當中實現並且啓動,這樣能夠更靈活的使用,可以更好的寫邏輯,下同

在setUserVisibleHint方法裏:

override fun setUserVisibleHint(isVisibleToUser: Boolean) {
    super.setUserVisibleHint(isVisibleToUser)

    if (isVisibleToUser) {
        //對用戶可見
        //做統計
    } else {
        //做統計
    }
}

然後這樣做之後還是會發現有問題,至於什麼問題嘛,千奇百怪,但根本原因還是因爲viewpager+fragment導致生命週期的混雜;我是沒有實力處理這種混雜,但也用了其他方式儘量避免,在統計時候存入本地數據庫的時候,需要做些判斷,如下:

1、如果走的onPause或者是不可見時候的統計,在存入數據庫的時候,需要通過當前要存入的數據current的key值匹配出上次最後一次出現這個key值得數據last,這時候要將last進行修改並在存入數據庫。

2、如果走的是onStart或者是可見的時候的統計,在存入數據庫的時候,需要直接從本地數據庫中取出最後一條數據last,將last和當前要存入的數據current進行比較,如果last和current的key值一樣,那麼則更新last,如果不一樣,就新增一條新的數據。

以上兩個判斷就是爲了取出混雜而導致的重複。然而,我本人覺得還是沒有徹底的解決,但願有大神能在這一塊做更好的優化建議。

埋點方案第四版(參數params優化版,最終版,暫時不被接納版,說到底就是後臺懶,而且其他人都沒想透):在第三版的時候說了那麼多,但坑爹的後臺做死了,他做成了只能將params封裝成json字符串才能進行統計,也就說,我的params單獨傳成一個id字符串是不行的,那麼只能封裝成{"type":"","id":""}格式,但這種格式永遠不滿足埋點需求,而是最好能夠通過Map對象傳遞json,並且有時候我不通過Map,也是可以通過單獨的字符串傳遞,這是一種兼容,多種格式的兼容。注:Map對象生成的json格式是{"key1":"value1","key2":"value2"....}這種形式,有經驗的人立馬就能看出來這種格式的json會導致json裏面的所有的key值不確定,不好解析,然而,這只是不好解析,而不是不能解析,在這種需求模式下,我認爲這種傳參方式是最好的,當然,這也是通過友盟得到的注意,然而確實是很有道理;友盟就是可以通過單獨的傳遞字符串,也可以傳遞成map格式的。

然後,最終我們的統計是需要通過後臺來呈現,就以第四方案,我舉例畫出表格,大致描述需要如何呈現:

1、當傳遞的參數只是單純的字符串,如:{"key":"keyC","time":"時間戳","params":"params1/params2/params3"}


2、當傳遞的參數是map,如:{"key":"keyC","time":"時間戳","params":{"k1":"v11/v12/v13...","k2":"v21/v22/v23...","k3":"v31/v32/v33..."...}}


嗯,大概就是以上的形式,只是粗略簡單的按照自己的思維方案畫了,我覺得也沒差多少。

自主評價:整體方案上很正三觀,不過Android埋點實現上對viewpager+fragment的確是個難點,有待改進;然後以上的表格,可能還會有所欠缺,不夠很好的體現數據和埋點,這也就是在表格的設計上了,其實也就是需要在加什麼條件讓表格更完善更通俗易懂而已。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章