背景
随着业务不断迭代更新,网易大神的页面结构变得越发复杂。在使用自助打点框架之前,我们用过百度统计/哈勃统计等第三方统计工具,但是对于“页面嵌套统计”存在缺陷。由于策划非常重视“页面嵌套的子页面”停留市场,故废弃第三方并建立一套支持“页面嵌套”统计的打点框架。
何为 “页面嵌套” ? 并非指 Android 开发中的 fragment 页面,而是作为用户所能看见界面布局中被划分为 N 个子页面结构,子页面结构依然可以继续划分 M 个子页面结构….
好吧,一开始我真的无 Fuck 话说, 你是什么运营需求需要搞这么花里胡哨的。 不过第三方统计的确能统计到页面 Fragment 内潜的生命周期,但是对于 View 级的处理很不友好。而 Android 的视图层级结构恰好满足这个需求的页面原型结构,故下手开整。
设计
保证原业务逻辑兼容
业务的迁移及扩展的第一要素是 “保证原业务逻辑不能收到影响” 。由于app中的业务逻辑信息通过 json 格式上传到服务器,所以先把页面中所涉及的页面信息通过涉及 Param 接口用于返回特定页面业务信息。
1 | public class Param { |
其中 key 用于表示一块 “逻辑页面”,可能是view/fragment/activity。
Hook原业务页面的底层视图
要想在不同的逻辑页面底层统一监听声明周期并实现特定时刻的业务行为打点,最快速的方法是在基础类之前封装一层 Log 逻辑,由于我们基础类都存放在 libbase 模块,相对来说很是方便。
对于业务打点信息可分为两部分:“页面信息” + “业务信息”
- 页面信息指的是逻辑页面的名称,类型,id,归属parentId,显示时间,隐藏时间;
- 业务信息指的是比如在逻辑页面点击一个登陆按钮,此时的登陆打点信息(登陆按钮id,登陆行为id,行为时间等)为业务信息。
事实上,上传到服务端信息不仅仅包含这些,还包含app信息/设备信息,这里不对这部分信息作扩展。
定义打点信息结构
1 | public class PageViewInfo { |
public interface PageItem {
String generateId();
PageViewInfo buildPageInfo();
//为单例返回用于某些业务场景延迟初始化业务打点信息
PageViewInfo getSinglePageInfo();
}
1 |
|
public class LogActivity extends AppCompatActivity implements PageItem {
public PageViewInfo pageViewInfo;
@Override
protected void onResume() {
super.onResume();
PageManager.start(getSinglePageInfo());
}
@Override
protected void onPause() {
super.onPause();
PageManager.stop(getSinglePageInfo());
}
@Override
protected void onDestroy() {
super.onDestroy();
PageManager.destroy(generateId());
}
@Override
public PageViewInfo buildPageInfo() {
return PageUtils.getDefaultPageInfo();
}
@Override
public PageViewInfo getSinglePageInfo() {
if (pageViewInfo == null) {
pageViewInfo = buildPageInfo();
pageViewInfo.name = PageUtils.getName(this);
pageViewInfo.activityId = PageUtils.getActivityId(this);
pageViewInfo.id = generateId();
pageViewInfo.type = "ACTIVITY";
}
return pageViewInfo;
}
/**
* 继承 activity 的param和parent属性
*
* @param logFragment
* @return
*/
public PageViewInfo copyActivityPageInfo(LogFragment logFragment) {
if (logFragment == null) {
return PageUtils.getDefaultPageInfo();
}
PageViewInfo fragmentPageInfo = PageUtils.getDefaultPageInfo();
fragmentPageInfo.name = PageUtils.getName(logFragment);
fragmentPageInfo.activityId = PageUtils.getActivityId(logFragment);
fragmentPageInfo.id = generateId();
fragmentPageInfo.type = "FRAGMENT";
PageViewInfo activityPageInfo = getSinglePageInfo();
fragmentPageInfo.param = activityPageInfo.param;
fragmentPageInfo.parent = activityPageInfo.parent;
fragmentPageInfo.fromCopyActivity = true;
return fragmentPageInfo;
}
@Override
public String generateId() {
return PageUtils.getId(this);
}
}
1 | 其他的逻辑视图实现也基本一样。这里就不贴代码了。上述代码有几个需要点出 |
import android.support.annotation.Nullable;
import android.text.TextUtils;
import java.util.HashMap;
import java.util.Map;
import im.yixin.gl.log.page.PageConstants;
import im.yixin.gl.log.page.view.LogFragment;
public class PageManager {
public static final Map<String, PageViewInfo> sCache = new HashMap();
public static final Map<String, Long> sInfoTime = new HashMap<>();
public static PageInfoReceiver sPageInfoReceiver;
public static PageViewInfo lastLogPageInfo = null; // 最后一个曝光page
public static PageViewInfo currentVisiblePageInfo = null; // 当前可见的page
public static PageViewInfo lastVisiblePageInfo = null; // 倒数第二个可见的page
@Nullable
public static PageViewInfo getInfo(String id) {
if (!TextUtils.isEmpty(id)) {
return sCache.get(id);
}
return null;
}
//初始化页面信息
public static void start(PageViewInfo pageViewInfo) {
if (!TextUtils.isEmpty(pageViewInfo.id)) {
sInfoTime.put(pageViewInfo.id, System.currentTimeMillis());
sCache.put(pageViewInfo.id, pageViewInfo);
cacheLastVisiblePageInfo(pageViewInfo);
}
}
public static void addPageInfo(LogFragment fragment) {
if (fragment == null) {
return;
}
stop(fragment.getSinglePageInfo());
}
//上传一次页面信息
public static void stop(PageViewInfo pageViewInfo) {
if (pageViewInfo != null && !TextUtils.isEmpty(pageViewInfo.id) && pageViewInfo.param != null) {
PageViewInfo cacheInfo = sCache.get(pageViewInfo.id);
if (cacheInfo == null) {
sCache.put(pageViewInfo.id, pageViewInfo);
}
pageViewInfo.endTime = System.currentTimeMillis();
Long startTime = sInfoTime.get(pageViewInfo.id);
if (startTime != null) {
pageViewInfo.startTime = startTime;
}
pageViewInfo.parent = getParent(pageViewInfo);
lastLogPageInfo = pageViewInfo;
if (TextUtils.equals(pageViewInfo.param.key, PageConstants.UNKNOWN_VALUE) || pageViewInfo.fromCopyActivity) {
if (sPageInfoReceiver != null) {
sPageInfoReceiver.onFilter(pageViewInfo);
}
} else {
if (sPageInfoReceiver != null) {
sPageInfoReceiver.onReceiver(pageViewInfo);
}
}
}
}
//页面销毁是需要移除
public static void destroy(String pageId) {
if (!TextUtils.isEmpty(pageId)) {
sCache.remove(pageId);
sInfoTime.remove(pageId);
}
}
public static void init(PageInfoReceiver pageInfoReceiver) {
sPageInfoReceiver = pageInfoReceiver;
}
/**
* 首页的所有pageview返回null
*
* @return
*/
@Nullable
public static PageViewInfo getParent(PageViewInfo pageViewInfo) {
//如果上一个页面是同个页面
if (lastLogPageInfo != null && TextUtils.equals(lastLogPageInfo.activityId, pageViewInfo.activityId)) {
return lastLogPageInfo.parent;
}
//有可能被回收
if (lastVisiblePageInfo != null) {
return lastVisiblePageInfo;
}
return null;
}
public static void cacheLastVisiblePageInfo(PageViewInfo pageViewInfo) {
if (pageViewInfo == null || TextUtils.isEmpty(pageViewInfo.id)) {
return;
}
if (currentVisiblePageInfo == null) {
currentVisiblePageInfo = pageViewInfo;
} else {
if (!TextUtils.equals(currentVisiblePageInfo.activityId, pageViewInfo.activityId)) {
lastVisiblePageInfo = currentVisiblePageInfo;
currentVisiblePageInfo = pageViewInfo;
} else {
//页面第一次显示,fragment显示层级是从大到小,需要取最后一个
//页面因为上层页面返回,fragment显示层级是从小到大,需要取第一个
PageViewInfo tempInfo = currentVisiblePageInfo;
currentVisiblePageInfo = pageViewInfo;
if (lastLogPageInfo != null && lastLogPageInfo.parent != null && TextUtils.equals(lastLogPageInfo.parent.id, tempInfo.id)) {
currentVisiblePageInfo = tempInfo;
}
}
}
}
}
1 | 代码逻辑比较清晰,就不做阐述了。 |
public interface PageInfoReceiver {
void onReceiver(PageViewInfo pageViewInfo);
void onFilter(PageViewInfo pageViewInfo);
}
```
流程图及类图设计规范
思考
虽然第三方产品也没有提供这样的功能,但很多时候框架/架构孵化于业务,最终也服务于业务。 每一次坑爹的需求都可能让你的技术更上一层楼。好好写代码,好好反思总结。