DJMTA APP埋点框架的实现

埋点框架升级背景

  随着业务的快速发展,跨端融合的快速推进,传统的埋点框架已不能适应快速迭代的业务需求,需要解决如下问题:
  1、日志存在丢失问题
  2、Java层频繁的I/O读写导致的性能问题
  3、Android&iOS共享底层日志采集和上报策略,降低维护成本
  4、各种资源位和商品需要精准曝光带来的挑战
  5、跨端融合RN、Flutter在App中混合模式下如何支持埋点共享
  6、数据埋点在线问题如何快速降级和修正

日志丢失问题

随着埋点日志类型的增多(点击:click 页面展示:pv 资源曝光:ep),日志如果在内存中聚合上报,会存在当程序异常退出或者置后台被杀的时候,现场的数据就会丢失,造成数据上报的误差。到家App会优先记录日志到本地文件,当本地文件满足阈值4k,自动触发批量上报。同时当页面离开onPause()的时候,会Flush本地未上报完的残留日志。该方式虽然确保了准确性,但Java层频繁的I/O读写容易触发GC,导致卡顿和内存抖动问题,我们进行了优化,升级为Native方式实现底层日志。不仅能满足性能要求,同时也统一了Android和 i
OS
两端。

Native层的设计

  C层日志库的设计初衷是解决 Android 与 iOS 平台的统一日志记录系统,并解决 Android 系统GC的问题。主要是通过简单的 C (Natvie) API,完成对日志的缓冲、分类、内容分片操作。目的是减少频繁上报与每包大小统一的问题,在到家日志系统一般分片大小为 4KB 。设计如下图所示

  需要说明的是 Final 区和 Cache 区做了隔离,这样可以将 Cache 区当作一个沙盒来看,外界不论怎样操作都影响不了缓存日志,当进入 final 区就将日志的操作权交个上传业务了。另外,在切换日志类型时就会触发 flush_log 操作,也就是强制的将 Cache 日志 写入 Final 
区,这样可以缓解数据上传时效不同步的问题。

Native层的实现

  通过对缓存路径和上传路径与最大的埋点日志分片大小进行设置,就可以将日志系统进行初始化,当记录日志时,传入日志类型、与日志内容,系统自动的就可以对内容进行分类与分片,当有待上传日志时就会对上传日志组件进行回调,这样上传模块就可以根据待上传的日志文件列表进行分批次上传了。当然也提供 flush_djmta_log 操作,强制的将缓冲中的文件直接写入待上传目录,这样满足有立即上报的需求。

 /** 初始化log文件系统

@param cache_dir_path 缓冲文件路径

@param output_dir_path 输出文件路径

@param max_file_size 最大文件大小 byte

@return 是否初始化成功 

*/

 int init_djmta_log_file_system(const char *cache_dir_path, const char *output_dir_path, int max_file_size);

 

 /** 写日志

@param type 日志类型 (app / show)

@param log 日志内容

@param log_len 日志长度

@return 是否有待上传日志产生,有返1 ,无返0 

*/ 

 djmta_bool write_djmta_log(DJMTA_LOG_TYPE type, const char *log ,long long log_len);


/** 强制清空缓存区到输出日志目录 @return 如果有输出日志,有返1 ,无返0 */ djmta_bool flush_djmta_log(void);
/** 根据文件名返回log类型 @param log_file_name 文件名 @return 日志类型(app / show) */ DJMTA_LOG_TYPE judge_type_djmta_log(char *log_file_name);
/** 根据文件路径返回log内容 @return 字符串 (注意 用完之后 手动释放 返回char *) */ char * fetch_djmta_log(const char *log_file_path);

精准曝光的挑战

  • 简单列表(recycleview单item)
/** 从屏幕外进入屏幕内 */ 

@Override protected void onAttachedToWindow() 

{ 

   super.onAttachedToWindow();

} 

/** 从屏幕内移除到屏幕外 */ 

@Override protected void onDetachedFromWindow() 

{ 

   super.onDetachedFromWindow(); 

}

  • item过于复杂的列表

简单列表可以通过生命周期的捕获,来处理item露出和移出问题。但是有缺陷,比如大部分 App采用的楼层化设计,楼层的样式相对较复杂,此时就需要对数据model进行切割,既能保证曝光精准,也能达到最小颗粒度的局部渲染

  • 其他滚动列表(Scrollview和自定义ViewGroup)

为了提升性能,在处理Scrollview和普通自定ViewGroup滚动曝光埋点上报时,我们做了如下优化:
1. 把整个视图映射成一个与之对应的数据结构图(视图坐标+数据结构),数据源AllCache[]、当前屏幕currentCache[]、屏幕外首尾topBottom[2],操作数据源相当于操作视图。

2. 模拟RecycleView的滚动缓存视图实现方案,在滑动的过程中,只操作topBottomCache的最顶部据topBottom[0]和最底部数据topBottom[1]即可,对已在缓存中的数据无需重复计算和添加,未在缓存中的数据进行快速选举替换topBottom[0]或者topBottom[1],通过模拟 R
e
cycleV
iew
源码的视图缓存和复用机制,避免了滑动频繁计算和快速滑动带来的性能开销,同时也能满足精准曝光。

混合栈埋点方案

  随着跨端融合的推进,App中跨平台的业务RN、Flutter的广泛使用,我们需要支持混合栈的埋点方案。拿Android举例,RN、Flutter容器在App中都是Activity来承载,那么pv和click的产生都是root容器Activity级别,此时如果产生了RN、Flutter内部自己的跳转,对应的Activity还是不变的,所以统计的pv路径会不精准。如图:  


  混合栈埋点很好的解决了这个问题,同时能够满足RN、Flutter等所有跨平台的业务场景。enterPv和exitPv是埋点栈stack暴露的两个方法。RN通过bridge,Flutter通过plugin ,来实现和原生App埋点的无缝接入,但埋点混合栈在Android上需要解决以下几个问题:

  • Intent跳转执行了clearTop,清理掉了其上的所有Activity怎么办?

  • Intent跳转前执行了finish,销毁了当前Activity怎么办?

  • 置后台被系统异常杀死重启恢复界面怎么处理?

  混合栈的设计如下:


上图是整个混合栈的流转过程,需要注意的是后台重启异常恢复时,需要清理掉本地的埋点栈,恢复数据的时候重新压栈。

数据降级策略

  埋点SDK插件化,各业务插件的埋点数据路由到埋点插件,通过统一的intercept拦截加工处理,完成和C层的对接存储、读取和上报。同时,通过动态更新,来解决线上埋点crash以及其他问题。