(Android)自主项目埋点方案讨论

  • Post author:
  • Post category:其他


吐槽+废话:多久没有写博客了,因为没有时间,每天忙着项目,想着项目怎么做好;产品和技术经理每天都告诉程序员说:你们要有产品思维。可是,连需求场景都没有说清楚,我们如何有产品思维,产品给的需要是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的确是个难点,有待改进;然后以上的表格,可能还会有所欠缺,不够很好的体现数据和埋点,这也就是在表格的设计上了,其实也就是需要在加什么条件让表格更完善更通俗易懂而已。



版权声明:本文为sinat_28884723原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。