如何设计一个伪无埋点的框架?

本文同步发布于公众号:移动开发那些事如何设计一个伪无埋点的框架

在前面的文章:Android无埋点技术概览 中提到传统的无埋点有几大缺点:

  • 埋点字段有限,没有办法携带精确的业务字段;
  • 数据量太大,后台存储压力很大;
  • View的唯一ID会随着页面的变化而变化,多个版本的数据需要在后台进行数据映射;

这几个缺点也是很多业务在进行技术方案选型时,不敢去选择无埋点的原因。那么有没有办法做到既可以利用无埋点的特性,并且又能满足业务的数据分析需求呢?
要能满足业务的数据分析需求,又能利用无埋点的特性,这意味着需要满足以下条件:

  • 上报能携带业务字段;
  • 上报可以按需选择;
  • 上报时机由框架控制;

简化成一句话就是:业务控制上报内容与类型,框架决定上报时机,这也是本文要讲述的“伪”无埋点框架的核心;
如无特殊说明,文章里说的无埋点就是指代"伪"无埋点。

1 伪无埋点

在传统的代码埋点的方案,一般是业务同学在需要上报的时机,把要上报的具体的参数调用数据上报SDK的接口进行上报,如,当某个view被点击时,上报点击事件:

// 业务在调用完这个上报接口后,由数据上报SDK把对应的数据上报到后台;
Report.report("event_click", {key:value});

而伪无埋点框架也是基于传统的代码埋点方案演变而来:由业务把某个View要上报的业务参数和对应的业务标识设置给对应的View,框架层负责在合适的时机去调用数据上报SDK的接口去进行上报,使用的伪代码为:

//步骤1 某个要上报的view,设置对应的上报参数
viewA.setReportParsms({key:value}});

// 步骤2,框架回调上报SDK接口进行上报
 Report.report("event_name", {key:value});

那么这里的无埋点框架需要解决两个主要问题:

  • 上报view的检测: 怎样检测到某个View需要做上报;
  • 上报参数的设置: 框架可以怎样获取到View要上报的业务参数;

2 上报view的检测

要想检测到哪些View需要做上报,需要有一个通用的机制来获取到所有的要检测到的View,这里最常规的方案就是通过实现大量的自定义View来实现,如自定义的ViewGroup,自定义Text等。只要需要做这些检测的View都使用这些自定义的View来实现,但这个常规的实现方案的成本很大:

  • 框架的开发成本大,需要自定义大量的View,只要业务有用到的,都需要去自定义;
  • 业务的接入成本过高,需要将现在使用的组件替换为埋点框架的;

那这里有没有其他相对低成本的方案呢?那肯定是有,接下来我们来看一个比常规方案好一点的方案

2.1 进阶方案

大家都知道,Android的页面是有生命周期方法的,不管是Activity还是Fragment在进入前台时,系统都会回调一些生命周期的方法,如onResume,onPause。那么我们是不是可以在系统回调这些方法时,对页面进行一个检测,获取到整个ViewTree呢?只要获取到了ViewTree ,那么我们就可以通过某个标识来判断到哪些是需要做上报的View后续只需要去判断这些View的状态就可以了。这也是某埋点框架采用的方案。但这个方案会有个缺点:没有办法覆盖到所有的view变化的时机 像手动通过addViewremoveView去更新ViewTree的时候,这个方案就没有办法检测到。因此这个方案的时机还需要检测业务同学在某个时机,手动去调用检测的接口,来触发检测。这个进阶方案,也会存在几个缺点:

  • 检测时机不可控,由框架和业务共同控制;
  • 业务的接入成本有点高:
    • 需要在页面的某些生命周期去调用框架的检测方法;
    • 手动操作viewtree时,需要调用框架的检测方法;
    • 存在漏报的可能性(某个场景下,业务漏调用了检测方法);

这个进阶的方案虽然框架的开发成本和接入成本比常规方案有降低,但整体的成本还是比较大的,有没有更完美的方案呢?一个比较完美的方案应该是

  • 能监测到所有ViewTree变化的时机;
  • 比较低成本的开发成本和接入成本;

接下来我们继续去探索一下完美的终极方案是怎样的?

2.2 终极方案

如果我们能想监测到所有ViewTree的变化时机,这里只能是自定义View。有同学看到这,肯定会想“就这?,前面已经说过自定义View的成本很高,现在又说自定义View,这不是有点自相矛盾吗?“。别急,我们一步步来拆解出可以怎样做到低成本的自定义View的终极方案。

这里要做到低成本,关键在于自定义View的个数尽可能要少,并且业务最好能无感知接入。所以这里的问题变成:怎样找到这些少量的View。这里我们就只能从Android的页面Activity入手了。不管我们使用了什么开发框架,应用的页面一定是Activity,而Activity的根view都是一个FrameLayout(这里我们可以去查看Android的源码去确认一下,其实从性能优化的减小布局层次的介绍中(如果布局layout.xml中的根viewFrameLayout的话,推荐使用merge标签)也能看出来了)。这里是不是可以自定义一个FrameLayout就可以解决了。到这里其实答案就已经出来了。

我们通过自定义FrameLayout来监听各种会引起view变化的事件,如

  • void dispatchWindowVisibilityChanged(int visibility)
  • void onLayout(boolean changed, int left, int top, int right, int bottom)

有了这个根view,我们就能获取到当前页面的所有view,然后通过标识就能找到要上报的所有view了。但现在问题又来了,怎样可以把业务的view 替换为这个自定义的view呢?最低成本的方法其实就是运行时替换,在监听到ActivityonResume方法被调用时,动态把根view替换掉就可以了。
综上,这里的终极方案为:

  • 自定义FrameLayout,开发成本低(只需要一个自定义view
  • 运行时替换,业务接入成本低 (业务不用关心检测时机,只关注业务就可以)

通过这个方案,我们就能检测到所有的view,并可以根据view的标识来做一些上报的处理,但我们还需要解决怎样标识一个view 的问题?

3 上报参数的设置

要想把上报参数和对应的view进行绑定,那么view就需要有字段属性来存储我们设置的参数。一听到额外的字段属性,最常见的两种方案:

  • 通过继承类,来增加属性;
  • 通过扩展类,来增加属性;

继承类的方式也就是自定义View的方式,这里的改造成本很大; 而扩展类的方式由于Android开发使用的语言没有这一类的使用方式。因此最常见的这两种方案不适合这里,我们只能从源码出发,寻找有没有现在的字段可以使用;查看了一翻View的源码后,发现有这么一个方法setTag

public class View {
	     /**
     * Sets a tag associated with this view and a key. A tag can be used
     * to mark a view in its hierarchy and does not have to be unique within
     * the hierarchy. Tags can also be used to store data within a view
     * without resorting to another data structure.
     *
     * The specified key should be an id declared in the resources of the
     * application to ensure it is unique (see the <a
     * href="{@docRoot}guide/topics/resources/more-resources.html#Id">ID resource type</a>).
     * Keys identified as belonging to
     * the Android framework or not associated with any package will cause
     * an {@link IllegalArgumentException} to be thrown.
     *
     * @param key The key identifying the tag
     * @param tag An Object to tag the view with
     *
     * @throws IllegalArgumentException If they specified key is not valid
     *
     * @see #setTag(Object)
     * @see #getTag(int)
     */
    public void setTag(int key, final Object tag) {
        // If the package id is 0x00 or 0x01, it's either an undefined package
        // or a framework id
        if ((key >>> 24) < 2) {
            throw new IllegalArgumentException("The key must be an application-specific "
                    + "resource id.");
        }

        setKeyedTag(key, tag);
    }

从这个方法的注释上可以看到这个方法是可以满足我们的需求,只是在使用的过程中需要注意这个key一定要是Android的资源id,再细看源码后,发现还有个需要注意的地方:tagView 内部是通过SparseArray 来存储的,而这个数据结构是非线程安全的,因此在调用setTag方法一定要在主线程里去调用;

3.1 key值的定义

使用setTag方法时的key一定要是资源id,只需要在res目录下的values下新建一个ids.xml的文件,里面的内容示例如下

<resources>

    <item name="VIEW_PARAMS_KEY" type="id" />
  	
</resources>

在使用时,就使用R.id.VIEW_PARAMS_KEY 来获取到具体的值就可以了;

4 总结

本文主要围绕Android中设计一个伪无埋点框架需要解决的两个问题展开:

  • 上报view的检测: 核心是通过自定义FrameLayout,并在运行时替换来解决;
  • 上报参数的设置: 核心是通过设置ViewsetTag方法来进行设置和获取;

通过解决这两个关键问题,大家也就可以快速搭建起一个伪无埋点的框架了;在后面的文章里,会讲述如何基于这个框架,去搭建一个有效曝光的框架;

公众号:

posted @ 2024-09-24 19:14  woodWu  阅读(20)  评论(0编辑  收藏  举报