leobert_lan的博客地址:
https://me.csdn.net/a774057695
全链路用户路径分析
在不同类型的APP中,产品经理(或者运营、数据分析师)可能会使用
这三种方式对用户路径进行开拓分析,并试图深入理解用户行为背后的心理、发掘用户对产品价值的期许。
漏斗转化法:漏斗型
特点:目标明确,直观单一。适用于工具型等垂直类APP或者链路单一的APP,对于局部的转化问题可以重点突破
页面跳转分析法:管道分叉型
特点:持续分析。适用于社区类APP,分析出主流路径和小众路径
模块分析法、潜在用户需求路径分析法:章鱼型
特点:路径足够复杂。适合平台类型,做价值归因、个性化推荐
我们在APP中谈论这个问题时,分析用户路径可简可繁,简单到只需要收集页面流向,也可以复杂到:在哪个位置(哪条数据)触发了行为,进入了哪个页面并进行连续的分析。
而APP(小程序也类似)中因为其交互区域小的特性,一个页面中的内容是高内聚的,所以分析由页面所构成的用户路径价值颇大。
如果大家对这块内容比较感兴趣的话,可以在文章下面留言,热度比较高的话,我会向更加专业的小伙伴们请教下,收集资料后专门写一篇博客,从技术人的角度来介绍下这些内容
页面链路信息的价值提炼由哪些部门参与
这里我们先确定一件事情:所有的信息都是由埋点体现出来的,有价值的信息是从一系列的埋点中分析出来的我们将这个分析行为称为价值提炼。埋点的埋入、触发、(可能存在的本地持久化、打包上报)等环节不算入分析行为。
一般对于小型用户规模的应用,这些信息都是交给服务端维护的(或者是利用某蒙、某策等三方平台),客户端只需要上传埋点信息等。
如果你的应用面向的受众是一个很大的群体,并且用户数量已经发展到一定规模后,可能就需要自己维护了,假设公司已经投入了资源去搭建用户数据平台,而且这个平台足够强大,能够仅依靠埋点信息就能支持到用户画像分析建模、目标用户行为分析、价值归因等,并且埋点统计能够session化、时序化(即平台可以按照一个访问者在一次会话生命期内按时序产生的埋点信息),那么恭喜你,客户端可能并不需要干点什么特别的活,传埋点就行了????。但如果没有达到这种能力?
其实在谈论这个问题时,是比较尴尬的,我们的团队因为某些问题,并没有在大数据平台上实现完备的模型分析能力,目前我们将算力集中给了“个性化推荐和归因分析”(而且数据的维度不够广),这导致了我们的数据分析团队运用页面跳转分析法、以及在局部问题上使用漏斗分析法时,只能靠人肉方式进行递归检索,并逐步进行数据分析。这种方式是比较原始、低效的。
但是无论是为了支持这种原始、低效的方式,还是为了未来能够更好的支持“归因分析”,我们在APP中维护页面链路信息(甚至是用户行为日志,并运用客户端算力进行模型预处理)都是有价值的。
为了方便下文展开,先分享一下我们目前页面曝光的埋点,大体上是这样的:
Point {客户端信息实体,//用户id、时间等页面点号,页面主体实体的简要信息,fromPage //来源页的点号
}
我们在下文中会将页面点称为P点(Pager 点),将行为点称为A点(Action 点)。
从埋点实体来看,客户端是参与了价值分析环节的,虽然仅仅是一个简单的信息预处理????,将上一层的P点点号作为本次P点信息。那么是否可以干点看起来更牛逼的活呢?
我们先定一个题目:以社区类APP为例
需要在客户端实现页面路径面包屑,
如果页面内容信息单一,例如“文章的详情”,他有一个单独的P点
如果页面内容是通过Tab聚合在一起且信息价值区分大,每个tab页签对应的页面都有独立的P点,
如果页面内容是通过Tab聚合在一起的,但信息价值区分不大,这一组页面共用一个P点
下面是例子:
接下来就是代码实战了,激不激动。
优雅实现
上面我们已经看到一些页面了,在实现路径埋点之前,我们还是要简单的聊一下以什么方式来写这些页面的。
一般来说,我们会以Activity、Fragment、Dialog、甚至是View的方式去实现客户端页面,Activity和Fragment比较常见,Dialog可以做一些弹窗页面(或者DialogFragment),直接以View的方式去做页面的做法比较少见(虽然我们的文章详情页针对不同的类型,使用了对应封装的View类,但本质上还是Activity)。
为了方便我讨论,我们约定:弹窗先不管,tab页用Fragment,其他的页面都使用Activity。
分析和处理Activity
按照一般的GUI程序特性,页面都符合栈的特性,不绕关子了,Android中也是页面栈管理。我们定义一个栈,每有一个新页面打开,往栈中压入P点信息,并且确保上报埋点在入栈之后。(PS:代码中我们最终使用了LinkedList),我们忽略打开多个Activity的场景(startActivities方式)。
页面栈应该如下:
栈顶第二个位置即来源页面,栈顶元素即为当前页面。再打开新页面则继续往栈顶添加。那么如果在onDestory,或者调用finish()时出栈是否合适呢?简单分析后发现并不合适。如从 A_Activity 打开 B_Activity,并且需要关闭A_Activity。按照上面的分析,在页面打开时P点会入栈,如果A finish的时候出栈,那么页面链信息就不全了????(PS:如果刚好是栈顶那倒是适合出栈)。
OK,简单分析后我们得到一个思路:在一个页面恢复呈现时,维护页面信息的出栈:将对应的栈元素上面的元素全部出栈
onCreate(),如果不是恢复重建的,添加当前页面P点入栈,
onResume() 清理当前页面对应的P点元素上方的元素出栈,
安全起见,onDestory() 时,检验一下栈顶,如果对应当前页面,出栈
我们知道:一个页面打开后,既会onCreate(),也会onResume(),但是这并没有side-effect,毕竟栈顶就是自身!
结合页面启动方式再来思考下
我们前面提到了Android中本身也是栈管理,不同的的启动方式会有所区别:
按照标准模式启动时,都是新增页面实例到栈顶,这对于我们用单一一个栈收集页面链路信息没啥影响。
singleTop在栈顶元素和将要新增的元素不是同一个类的实例时,和standard没有区别,如果是同一个类的实例,则不会新增实例入栈,对生命周期的影响是不走onCreate,而是复用实例,走onNewIntent并走到onResume。如果你使用了这种模式而链路上需要表现为多个页面链路节点时,需要手动维护,(下文会提到一个DummyPager,需要使用它自己维护出链路)
singleTask,在一个页面栈中仅有一个实例,如果使用了这种启动模式,和上面提到的复用实例一样,分析是否每次都要添加页面链路点,如果是的话在onNewIntent中补充处理
singleInstance 同理
所以我们可以得出结论:对于链路栈,在Activity onCreate() 时入栈(非实例被回收后恢复的情况下);在Activity onResume()时:如果栈顶不是自身,则退栈,直到是自身(或者出现非预期情况);在Activity onDestory()时出于严谨检查栈顶,如果是自身则退栈。如果有必要的话,部分场景下在onNewIntent()中利用API维护栈。
按照上面的讨论,结合Android中的页面回收机制,我们不能把页面实例直接存到栈中,这会影响GC导致内存泄漏,所以我们在栈中存的是页面信息(P点信息),并且我们要解决页面实例和P点的对应关系问题。
我们有两个选择:
我们利用第二种方式
private val atomicInteger = AtomicInteger(0)private fun createToken(obj: Any): String {return obj.javaClass.simpleName + "_" + atomicInteger.getAndIncrement()
}
并且定义接口,只有接口的实现类才认为是需要进行页面信息维护
public interface ITrackedPager {void setPagerToken(@NonNull String pagerToken);@NonNullString getPagerToken();interface FragmentInViewPager extends ITrackedPager{}
}
并且直接利用ActivityLifecycleCallbacks,通过向Application实例注册ActivityLifecycleCallbacks接口实现类来实现特定生命周期下的业务。
值得一提的是需要注意ActivityLifecycleCallbacks中的方法被调用的时间点,通过阅读代码,我们知道是在Activity的onCreate、onResume等方法中,调用了对应的dispatchActivityCreated(),dispatchActivityResumed()等方法,其中调用了ActivityLifecycleCallbacks的对应生命周期回调。我们在业务中需要注意子类重写对应生命周期时,修改了super方法的调用时机所带来的影响。
P点信息定义与收集
为了业务代码的阅读体验,以及寻找页面埋点的方便性,我们放弃了在接口中定义方法的方式,而是直接使用注解,当然这对匿名类不太友好,如果我们要对弹窗也加入这套机制的话,需要对原有的业务代码进行一定的改造。
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackedPager {String pagerPoint();ReserveConfig[] reserveConfig() default {};boolean autoReport() default true;FragmentStrategy whenFragment() default FragmentStrategy.ALONE;/*** @return the max limit of pagers backing-retrieve tracking, tracking all if less than 1*/int reserveLimit() default 1;/*** @return true if ignore the page in the chain,even it has implemented {@link ITrackedPager}*/boolean ignore() default false;enum FragmentStrategy {/*** 替代宿主Activity的点*/REPLACE_ACTIVITY {@Overridepublic void manualAddChainNode(ITrackedPager pager, boolean report) {//…先忽略 }},/*** 作为一个独立的新点*/ALONE;public void manualAddChainNode(ITrackedPager pager, boolean report) {//…先忽略}}
}
在我们定义的注解中:
pagerPoint: P点点号信息
reserveConfig: 反向配置(简单变更P点的方式,一个保留设计,理论上应该遵照P点点号,但不排除搞幺蛾子)
autoReport: 是否自动上报(如果是true则在入栈时进行上报)
reserveLimit: 向前查找P点的限制数量(目前仅向前查找一个页面进行上报,如果数据分析对部分页面有更高的要求,可以调整这个值以查到更多的前置页面,另:本地维护了整个页面链路,但具体回溯多少前置页面并上报看需求)
ignore: 忽略上报,(例如推送跳转处理的中间页,在链路中有价值,但从全局(各个端)看可能并不是一个约定的页面,明确该页面不上报P点语义更明确)
作用于Fragment时的策略
分析和处理Fragment
这里必须要说明一点,笔者参与开发维护的APP,并不是一个单Activity并由Fragment导航的APP,所以博客中的方案对于这类APP是没法开箱就用的。
在笔者参与的项目中,Fragment的使用场景还不是非常广泛的,一般集中在以下几个场景:
结合我们开头讨论的内容,这里只有“部分主体内容Tab页”和“有状态机的页面”才有单独P点的必要性,所以,项目中大多数的Fragment都不需要再配置P点,这和Activity是区别较大的.
阅读过代码的同学们会发现,我们对Activity,只要实现了ITrackedPager接口的类,我们都会去处理他,而注解信息只是处理时的“配置”,当配置缺失时,我们还是会将页面(类信息)加入页面链的,这是潜在的需求决定的。
但是对于Fragment,我们项目中并没有这种潜在需求,所以我们对Fragment是既实现了接口也添加了注解时才处理他,否则会丢弃掉。
如果阅读过Google发的那篇博客:Fragment的以前、现在和未来,我们会发现Google的工程师对于在Fragment中被迫添加的代码也是有点不爽的。当时我们在确定这套方案时,也有意的弱化了Fragment生命周期和页面链路的关联。
我们提供了三个关键的Api供Fragment使用:
osp.leobert.android.tracker.pager.PagerChainTracker.Companion#helpFragmentStart
osp.leobert.android.tracker.pager.PagerChainTracker.Companion#helpFragmentOnResumeInViewPager
osp.leobert.android.tracker.pager.PagerChainTracker.Companion#helpFragmentDestroy
同理,第三个Api也仅仅是保持完整性的,只要确保应用退出时将链路清空就可以不管这个API。
第一个API的调用,会处理Fragment的注解信息,并添加到链路中,有两种结果:
第二个API是为了给ViewPager中使用的Fragment使用的(或者其他方式,如hide()、show(),实现的Tab页),这种方式下,无疑,Activity本身没有实质意义的P点点号.
而Fragment具有,我们会使用REPLACE_ACTIVITY方式,当对应的Fragment显示时,使用其P点信息维护页面链路。使用它必须实现接口:osp.leobert.android.tracker.pager.ITrackedPager.FragmentInViewPager。
对于Dialog、PopupWindow等弹窗的处理
一般来说,弹窗不配拥有页面点,但如果他有(或者基本等价的点),也可以按照套路使用,就不再展开了
添加额外的信息
使用API:osp.leobert.android.tracker.pager.PagerChainTracker.Companion#pressData,可以向链路节点中添加信息。
无法修改的类
我们可能会使用一些三方库,如果不是必须收集这部分链路,建议丢弃掉这部分链路;如果可以继承的话,可以考虑继承后再使用。如果不能,那就看能不能添加生命周期监听,并从生命周期推测其实际状态,并继承osp.leobert.android.tracker.pager.PagerChainTracker.Companion.DummyPage,添加注解后使用其实例来维护链路。
上报
以上都是页面链路的维护,按照我们开始拟定的需求,还需要处理上报。如果注解中采用了AutoReport,那么会自动上报,但部分场景下我们需要人为确定上报时机(存在前置条件,例如:需要植入信息,默认显示的页面需要用户产生行为才上报),我们提供了两个API:
但第二个是一处保留设计,还有一定的争议,个人不建议使用
library中还提供了一些深层次的API,可以手动修改链路信息,获取链路信息等,这里就不做过多介绍了,如果你的业务场景和我们类似,那倒是值得看看源码,否则参考下思路也就够了。
项目地址:
https://github.com/leobert-lan/PagerTrackerDemo
关注我获取更多知识或者投稿