埋点原理与实现
一、曝光埋点
1、使用 VisibilityDetector
监听子组件可见性的变化(什么时候触发变化)
2、
二、tableview的曝光埋点
- 使用
UIView
的window
属性:可以通过检查视图的window
属性是否为空来判断视图是否在屏幕上可见。如果window
属性为非空值,表示视图正在显示。
- 使用
CGRect
和CGRectIntersectsRect
函数:可以通过将视图的frame
转换到屏幕坐标系中,然后与屏幕的bounds
进行比较,来确定视图是否在屏幕上可见以及可见的比例。
- 使用
CADisplayLink
:可以通过CADisplayLink
来定期检查视图的可见性。在每次屏幕刷新时,都可以检查视图的window
属性和frame
,从而实现对视图可见性的实时监控。
- 使用
isDisplayedInScreen
类别方法:可以通过为UIView
添加一个类别,并在该类别中实现isDisplayedInScreen
方法,来判断视图是否在屏幕上可见。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
- (void)startMonitoringViewVisibility:(UIView *)view { CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(checkViewVisibility:)]; [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; }
- (void)checkViewVisibility:(CADisplayLink *)displayLink { }
- (BOOL)isViewVisible:(UIView *)view { CGRect viewRect = [view convertRect:view.bounds toView:nil]; CGRect screenRect = [UIScreen mainScreen].bounds; CGRect intersectionRect = CGRectIntersection(viewRect, screenRect); CGFloat visibleArea = CGRectGetArea(intersectionRect); CGFloat viewArea = CGRectGetArea(view.bounds); return (visibleArea / viewArea) > 0.5; }
|
曝光(曝光结束才计算曝光。曝光过程不计算,那初始曝光的怎么办)
当然我们对滑动曝光有一些额外的要求:
- 需要滑出一定比例(只出现一点点不算)、一定时间(太快划走不算)的时候才出发曝光
- 滑出视野的模块,再次滑入视野时需要再次上报
- 模块在视野中上下反复移动只触发一次曝光(还未实现)
1、Flutter visibility_detector 原理解析
使用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @override Widget build(BuildContext context) { return VisibilityDetector( key: Key('exposure_$hashCode'), onVisibilityChanged: (visibilityInfo) { var visiblePercentage = visibilityInfo.visibleFraction * 100; var inScreen = visiblePercentage >= widget.exposeFactor; if (inScreen != show) { if (inScreen) { _onExpose(); } else { _onHide(); } } }, child: widget.child, ); }
|
底层核心逻辑代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| class VisibilityDetector extends SingleChildRenderObjectWidget
// 2、 重写其 createRenderObject(BuildContext context) 方法 返回RenderVisibilityDetector @override RenderVisibilityDetector createRenderObject(BuildContext context) { return RenderVisibilityDetector( key: key, onVisibilityChanged: onVisibilityChanged, ); } }
class RenderVisibilityDetector extends RenderProxyBox { RenderVisibilityDetector({ RenderBox child, @required this.key, @required VisibilityChangedCallback onVisibilityChanged, }) : assert(key != null), _onVisibilityChanged = onVisibilityChanged, super(child);
@override void paint(PaintingContext context, Offset offset) { if (onVisibilityChanged == null) { VisibilityDetectorLayer.forget(key); super.paint(context, offset); return; }
final layer = VisibilityDetectorLayer( key: key, widgetSize: semanticBounds.size, paintOffset: offset, onVisibilityChanged: onVisibilityChanged); context.pushLayer(layer, super.paint, offset); } }
|
在 RenderVisibilityDetector 中,在绘制方法 paint() 里 push 了一个 Layer,这个Layer就是 VisibilityDetector 用于监听可见状态的关键。
二、埋点的数据上报
将监听到的数据添加到 final List<Map<String, dynamic>> _buriedPointModels = []; 中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| > _init() { > > > _uploadTimer = Timer.periodic(const Duration(milliseconds: 5000), (timer) { > _uploadBuriedPoints(); > }); > } > > addEvent(String eventName, Map<String, dynamic> eventAttr) async { > Map<String, dynamic> eventMap = {}; > > eventMap.addAll({ > "event_name": eventName, > "event_attr": eventAttr, > "request_time": DateTime.now().millisecondsSinceEpoch, > }); > > _buriedPointModels.add(eventMap); > } >
|
三、曝光率
曝光率(曝光率= 点击量 / 曝光量)来分析用户(比如用户的喜好推荐数据的统计)
参考文档: