文章采集调用(iOS在iOS中重写控件的思路)
优采云 发布时间: 2021-09-04 08:00文章采集调用(iOS在iOS中重写控件的思路)
声明式嵌入的思想是将嵌入代码与具体的交互和业务逻辑解耦。开发者只需关心需要嵌入的控件,并为这些控件声明所需的嵌入数据,从而降低埋点成本。
安卓
在Android中,我们自定义了常用的UI控件,如TextView、LinearLayout、ListView、ViewPager等,并重写了事件响应方法,并在这些方法中自动填充嵌入的代码。重写控件的好处是可以拦截更多的事件,执行效率高,运行稳定。但它的缺点也很明显——移植成本很高!
为了解决这个问题,我们采用了Android v7支持库的思路,通过AppCompatDelegate代理自动替换UI控件。
public class GAAppCompatDelegateV14 extends AppCompatDelegateImplV14 {
@Override
View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
switch (name) {
case "TextView":
return new NovaTextView(context, attrs);
}
return super.callActivityOnCreateView(parent, name, context, attrs);
}
}
这样,开发者只需在自己的Activity基类中重写getDelegate方法,将该方法的返回值替换为修改后的AppCompatDelegate,即可自动替换UI控件。
@Override
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = GAAppCompatUtil.create(this, this);
}
return mDelegate;
}
然而,新的问题也出现了。
如果UI控件在引用的第三方库中被覆盖,上述方法将不会生效,这意味着我们需要一个方法来替换UI控件类的父类。但是,在运行时,我们并没有找到一个可行的方法来替换UI控件类的父类。因此,我们尝试在编译时修改父类并开发了 Gradle 插件。其实这样做并没有运行时效率问题,只是会牺牲一些编译速度。这样,开发者只需要运行这个插件,就可以自动用我们重写的UI控件替换UI控件的父类。
apply plugin: 'com.meituan.judasplugin'
使用声明式埋点后,只需在控件初始化时声明需要的埋点即可。我们不再需要侵入程序的各种响应功能,降低埋点难度。
GAHelper.bindClick(view, bid, lab);
iOS
在iOS中,利用Objective-C关联属性和类别的语法特点,无需重写UI控件即可实现声明式管理。对于UIControl,可以在声明埋点的时候添加一个新的action,在事件发生时自动填写埋点代码。
- (void)nvja_setAnalyticsParams:(NVJAMGEParameter *)params mgeType:(SAKStatisticsEventMGEType)type
{
if (self.wmja_clickParams == nil && type == SAKStatisticsEventClick) {
[self addTarget:self action:@selector(wmja_controlDidTapped:) forControlEvents:UIControlEventTouchUpInside];
}
[super nvja_setAnalyticsParams:params mgeType:type];
}
对于UITableView,可以重写UITableViewDelegate,使用消息传递机制拦截事件,在事件回调方法中自动填埋代码。
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL selector = [anInvocation selector];
if (self.originalDelegate && [self.originalDelegate respondsToSelector:selector]) {
[anInvocation invokeWithTarget:self.originalDelegate];
}
SEL nvjaSelector = [self nvjaSelector:selector];
if ([super respondsToSelector:nvjaSelector]) {
[anInvocation setSelector:nvjaSelector];
[anInvocation invokeWithTarget:self];
}
}
同样,使用声明式嵌入方法后,嵌入代码也得到了简化。
NVJAMGEParameter *parameter = [[NVJAMGEParameter alloc] init];
parameter.bid = @"bid";
parameter.lab = @{@"poi_id":@"1"};
button.nvja_clickParams = parameter;
声明式嵌入可以替代所有的代码嵌入,可以解决前期遇到的移植成本高的问题。但本质上是一种代码埋点,只是代码埋点减少了,不再侵入业务逻辑。如果要满足嵌入式点动态部署和修复的需求,就要彻底杜绝前端写死的嵌入式代码。
无痕埋点
我们注意到,声明式埋点仍然需要写死代码的主要原因有两个:一是需要声明埋点控件的唯一事件标识符,即bid;二是一些业务字段需要在前端埋藏时携带,而这些字段是只有运行时才能知道的值。
对于第一点,我们可以尝试使用一致的规则,在前后端自动生成事件标识,这样后端就可以配置前端埋藏行为,实现自动埋藏。对于第二点,可以尝试通过某种方式自动将业务数据与埋点数据关联起来。这种关联可以发生在前端,也可以发生在后端。
事件 ID
为了自动生成事件标识符,我们需要获取每个控件自身父组件的ID、类名、Index等特征信息,逐层向上遍历,找到根节点。根节点一般是手动标记的,如果没有标记,则默认为视图层次树的顶部节点。最后将遍历生成的路径上所有节点的特征信息组合在一起,即为该事件的识别。考虑到实际布局中可能会有一些动态插入的控件,我们允许父组件的Index存在一定的误差。
配置后端需要维护自动生成的事件标识和投标映射关系,可以向前端下发配置文件。当前端控制事件被触发时,可以通过自动匹配配置文件获得相应的投标。需要注意的是,配置后台维护事件识别并不是一件容易的事。主要的复杂性在于不同版本之间的布局变化引起的事件标识的变化。这就是为什么需要手动标记根节点的原因。因此,一般我们会选择不容易改变的视图节点。
数据关联
为了实现业务数据和埋点数据的自动关联,我们初步尝试了前后端日志关联的方法。即前端请求后端API时,后端将业务数据写入日志,最后在数据清洗时合并相应的前后端日志。这种方式的问题是后端重构成本高,数据清洗成本大,不能广泛应用。但是在一些特殊场景下,比如某些业务数据只能被后端学习,而不能被前端学习,这种关联是必要的。
更常见的数据关联发生在前端数据之间。页面跳转时,业务数据通过规范跳转URI Scheme传递到下一个页面,自动填充到本页面的PV事件中。此页面上生成的所有其他事件将携带与 PV 事件相同的业务数据。
这样,通过自动生成事件标识符并进行数据关联,我们就可以实现“无踪埋点”,埋点节点可以通过配置文件动态下发,从而具备动态部署和修复埋点的能力。但需要注意的是,这种“无痕埋点”并不能解决所有问题。当无法通过数据关联获取业务字段时(这种情况比较常见),开发者代码埋点或声明性埋点指定仍为业务字段。从目前实践阶段的数据来看,业务中大约70%的埋点需求可以通过无缝埋点解决,另外30%的埋点需求,仍然需要使用声明性埋点和代码埋点。
总结
前端数据采集和上报是数据平台搭建过程中最重要的环节。美团点评前端每天上报百亿条数据。为了更好地满足公司各项业务对嵌入点日益复杂的需求,以及嵌入点对准确性、及时性、开发效率的要求,我们演化出了一套基于代码嵌入方案的轻量级和声明式公司前端埋地方案,并在动态埋地和无痕埋地方向做了进一步的探索和实践。目前,声明式埋点已经在部分业务中得到充分应用。从数据质量和开发者反馈来看,都达到了预期的收益。无标记埋点也在部分业务中得到验证和不断优化,未来将在公司内部进一步推广。
在实践中,我们意识到埋点问题无法通过单一的技术方案解决。我们需要在不同的场景下选择不同的埋设方案。例如,对于简单的用户行为事件,可以使用无标记的埋点来解决;而对于业务领域的埋点需求,需要承载大量可以在运行时学习的业务领域,需要声明性埋点来解决。从更高的层面来看,除了前端埋点技术的优化,埋点数据的标准化、前后端协同埋点、数据清洗和关联对于构建更加自动化和动态的埋点系统也很重要未来。