***导语 ***客户端埋点是数据收集的最基本手段,对于一款APP来说,代码埋点(就是在业务代码中,在需要埋点的view的点击事件回调处做点击上报的处理,当此view被点击时,进行相应事件的上报)是最为常见的埋点方式,但由于业务迭代速度很快,手动埋点方案虽然灵活多变,但是极大的增加了客户端开发人员的工作量。开发完成业务功能需要花费很大的精力处理埋点事宜,而且随着迭代版本,新的需求增加,埋点的数量会越来越多,这些老旧埋点的维护工作也需要付出不小的努力。并且,代码埋点需要跟版本迭代,如果在开发过程中忘记埋点,就只能等待下个版本再埋点。所以,如果能够实现不需要或者很少需要开发人员介入就能实现根据不同业务场景埋点的功能对于提高版本迭代速度和开发人员的幸福感绝对是一件非常有价值的事情。 所以,为了减少代码埋点的不便,不需要开发人员介入,使运营或者用研的同学就可以随时动态调整需要上报的点,我们需要改变下当前的埋点方式。
预先在目标应用采集数据,对特定用户行为或事件进行捕获、处理以一定方式上报至服务器的相关技术及其实施过程。
在大数据话的今天,一个好的产品就应该符合用户的需求,我们做统计就是收集用户的行为,分析用户的喜好,不断的根据用户行为改进我们的产品,当然专业的需要数据分析来完成,我们需要的就是为分析提供用户数据。
原来我们的方式是代码埋点,在每一个要上报的view的点击事件都都要由开发人员加入上报的代码。
public void onClick(View v) {
..
..
//在点击事件回调中走统一的上报
ClickReport.collect(ClickReport.MineAction.MINE_MORE_PRIVILEGE);
..
..
}
复制代码
但由于业务迭代速度很快,手动埋点方案虽然灵活多变,但是也存在很多问题:
1、开发完成业务功能需要花费很大的精力处理埋点事宜,而且多余的代码显得很冗余,浪费开发时间
2、随着迭代版本,新的需求增加,埋点的数量会越来越多,这些老旧埋点的维护工作也需要付出不小的努力。
3、代码埋点需要跟版本迭代,无法实现动态化的配置,如果在开发过程中忘记埋点,就只能等待下个版本再埋点。
所以,为了减少代码埋点的不便,减少开发人员介入,使运营或者用研的同学就可以随时动态化的调整需要上报的点,我们需要改变下当前的埋点方式,开发一个合适客户端埋点的上报模式。
因为不同的app也会有自己的异化处理,埋点的方式也是根据特殊情况有这众多方案的,但是大体上现在主要流行的就是三种方案:
1、代码埋点:将收集数据的代码直接写在需要的地方,当用户点击某个控件或者打开某个页面时调用到该部分代码完成数据的收集。
2、可视化埋点:根据可视化界面进行配置然后上报后台,后台向终端下发配置文件,终端点击时获取当前点击的控件根据配置文件进行选择上报。
3、无埋点:与可视化埋点基本一致。不同点在于可视化埋点是根据配置文件收集数据,无埋点是预先收集所有的用户行为,然后根据配置文件来提取数据。
对于无埋点,和可视化埋点的方案很相似,区别在于无埋点需要将所以的点击都收集,后续在根据配置文件在分析想要的数据,这种方式对于客户端来说很省事,客户端也不用分析是哪个点,只要用户点击了就上报,但是对于后端的筛选和分析一定的影响,而且可能我们需要统计的点并不是很多时可能大多数上报的点都不是我们想要的,这时用无埋点的方式感觉有些得不偿失。
MTA也有一个可视化埋点的功能,但是咱们我们要上报的平台有很多,比如要上报到罗盘或者我们自己服务器的统计平台,仅仅使用MTA的sdk无法满足各种平台的上报,所以我们选择可视化埋点的方式统计用户的行为,并且在统一的模块进行各个平台的上报。
主要的流程可以分为配置埋点模式,和用户模式。
上报的信息:
{
String ViewId; //当前View的唯一ID 由路径获得
String EventId; //View的事件ID 由产品相关人员配置
}
复制代码
在配置模式中,由产品同学或者运营同学配置需要上报的点击view,将点击view的唯一标识viewID和事先定好的用于分析的eventID绑定形成一个配置map表,配置结束后,将已经配置完成的map表上传至后台,完成配置模式。
[图片上传失败...(image-4aaf40-1649852259824)]
<figcaption style="display: block;">图1:可视化埋点流程</figcaption>
在配置模式中,由产品同学或者运营同学配置需要上报的点击view,将点击view的唯一标识viewID和事先定好的用于分析的eventID绑定形成一个配置map表,配置结束后,将已经配置完成的map表上传至后台,完成配置模式。
在用户模式下,就是当用户开启app时,会拉取到在产品或者运营同学在配置模式下配置的map表,然后当用户点击相应点时,可以通过某种方式与配置表中的点做对照,如果在配置表中,就走上报逻辑。
通过上述流程,可以总结出以下三个问题:
下面针对这三个问题进行分析和方案的确定。
因为我门需要在一个统一的地方获取当前的点击事件,来做统一的配置或者筛选上报工作,这些工作不可能在每一个view的点击事件回调中完成,比较普遍的做法就是遍历当前activity下的viewtree,根据UI布局的特性和Android点击事件传递机制来找到当前的view。
主流的方案:通过位置遍历计算
让项目中主框架的BaseFragmentActivity基类重写Activity的dispatchTouchEvent方法,当touch button时,可以获取到按下(DOWN)和抬起(UP)时产生的MotionEvent对象。这个MotionEvent对象有两个方法,getRawX()和getRawY(),通过这两个方法我们可以获取到“点击位置”在界面中的坐标。通过rootview可以层层遍历其下的子view以及所有子View上的控件,这些View和控件在屏幕中的坐标和宽高我们是可以获取到的。然后搜索所有的子View或者控件的布局区域是否包含“点击位置”,从而来判断哪个View或控件被点击。
/**
* 通过遍历的方式获取当前view
*/
private View searchClickView(View view, MotionEvent event,StringBuffer stringBuffer) {
..
..
if (isInView(view, event) && view.getVisibility() == View.VISIBLE) { //这里一定要判断View是可见的
if (view instanceof ViewGroup) { //遇到一些Layout之类的ViewGroup,继续遍历它下面的子View
ViewGroup group = (ViewGroup) view;
for (int i = group.getChildCount() - 1; i >= 0; i--) {
View chilView = group.getChildAt(i);
clickView = searchClickView(chilView, event,stringBuffer);
if (clickView != null) return clickView;
}
}
}
..
..
}
复制代码
但是这种获取view的方式毕竟是靠遍历来获得的,难免对性能有一些影响,其实通过viewGroup的TouchTarget的类型可以有一种更好的方式去得到当前的view。
我们的方案:通过TouchTarget类型获取
ViewGroup中有一个TouchTarget 类型的变量 mFirstTouchTarget,表示消费当前触摸事件的控件列表。例如,点击屏幕上一个按钮,那么按钮所在ViewGroup的mFirstTouchTarget 变量就指向这个按钮。当ViewGroup派发触摸事件时,他会首先判断变量mFirstTouchTarget是否存在,如果变量存在,会循环遍历TouchTarget链表元素,找到能处理该事件的View并将MotionEvent 派发给该View。如果不存在TouchTarget,ViewGroup 会循环遍历所有child view,直到找到一个能处理该事件的View,并将该View作为first touch target 赋值给mFirstTouchTarget。 当用户触发Down事件时,会执行如下逻辑,寻找消费当前事件的TouchTarget。
if (actionMasked == MotionEvent.ACTION_DOWN){
//如果是down事件,遍历child,找到TouchTarget
..
..
final View[] children = mChildren;
..
..
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// child 消费了触摸事件
..
..
// 根据消费了触摸事件的View创建TouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
..
..
break;
}
}
复制代码
addTouchTarget就是获取子view的touchTarget,并将其加入到TouchTarget链的最顶部,因为是从获取到DOWN事件的view层层向上递归,所以TouchTarget链的尾端就是目标view。
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
复制代码
当触发Down事件并且找到TouchTarget,或者触发非Down事件时,执行如下处理逻辑。
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
} else {
//Down事件发生时找到TouchTarget,或者非Down事件直接执行如下逻辑
// 将事件派发给TouchTarget表示的View
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,target.child,target.pointerIdBits)) {
//指定TouchTarget对应的View正确消费了事件
handled = true;
}
..
..
}
..
..
}
}
复制代码
因为根据view的点击事件可知,view点击时事件是从根节点开始向下进行传递,如果viewgroup存在TouchTarget,会从TouchTarget的成员变量中获取当前的处理事件的view或者viewgroup,如果该viewgroup存在TouchTarget,就继续向下查找,直到当前的viewgroup的TouchTarget为空时,就说明是此viewgroup消费了这个事件;或者直到传递到一个view而不是viewgroup时,此view就是当前用户点击的view。
因为mFirstTouchTarget是一个TouchTarget类型的私有成员变量,我们需要通过反射的方式去获取:
/**
* 获取当前点击的view
*/
public final View getTouchTarget(final ViewGroup vg) {
..
..
while (currentTarget != null) {
Field fieldTouchTarget = ReflectHelper.getDeclaredField(ViewGroup.class, "mFirstTouchTarget");
Object touchTarget = ReflectHelper.getFieldValue(fieldTouchTarget, currentTarget);
if (touchTarget == null) {
break;
}
Field fieldChild = ReflectHelper.getDeclaredField(touchTarget.getClass(), "child");
View child = (View) ReflectHelper.getFieldValue(fieldChild, touchTarget);
if (child instanceof ViewGroup) {
preTarget = currentTarget;
currentTarget = child;
continue;
} else if (child instanceof View) {
currentTarget = child;
break;
}
}
..
..
return currentTarget;
}
复制代码
利用ViewGroup的这种事件处理机制,可以在activity的dispatchTouchEvent中添加处理逻辑,如果接收到down事件,就让其传递下去使mFirstTouchTarget被赋值,当接收到up事件时,做相应的获取当前view逻辑,通过rootview的mFirstTouchTarget获取其子view,如果子view的mFirstTouchTarget不为空,就通过这种TouchTarget的链式关系获得这次点击行为的最终view。
根据上述流程,用户模式下,当用户点击某个view时,需要一个唯一的viewID用来在配置表中进行查找,并且需要与其他的view做区分;在配置模式下也需要通过唯一标示的viewID作为key与作为value的eventID共同上报到配置表中,那么应该如何选取这个唯一的标示呢?
主流的方案:遍历viewTree生成路径
对于view来说,Android系统提供了一个ID,view.getId()即可获得一个int型的id用于区分View,但是这个ID因为以下两个原因却并不能满足我们的需要。
所以为了使viewID不受版本和手机的影响,通常的方式就是利用所属activity+viewtree的路径形式来构建viewID。
[图片上传失败...(image-23560b-1649852259824)]
<figcaption style="display: block;">图2:view层级示意图</figcaption>
通过遍历当前activity的viewTree的方式,得到当前view的父类、祖父类一直到根view,然后根据view的路径层级关系再来确定viewID。但是这又需要遍历,我们不应该每获得一个view都要进行遍历,这样也是比较繁琐的。
我们的方案:通过TouchTarget链获取
在第一个问题如果获取当前view时已经说过,可以通过viewGroup的TouchTarget链来获取当前的点击view,并且链上的层级关系就是从根view到父view再到子view的一个层级,所以在获取view的同时就可以顺便记录层级关系,来作为viewID。
可以在获取当前view的过程中,记录每一个节点TouchTarget的mFirstTouchTarget,这样就可以在得到点击view的同时,也得到相应的view的路径比如:DecorView-LinearLayout-FrameLayout-RelativeLayout-Button,再配置上view的class类名等信息,我们就可以同时确定目标控件的唯一标识,将viewID通过setTag进行设置,在需要的地方取出。所以在方案上我们还是用利用获取viewgroup的TouchTarget来得到当前点击的view,同时也可以根据这种链式关系来记录点击view的从父类到子view的路径,从而将此路径作为当前view的唯一标示viewID。
对于可视化埋点操作来说,在用户模式下,当用户开启app时进行配置表拉取的操作,然后当用户进行点击时,通过遍历当前activity下的viewtree获得点击的view,或者通过上述分析的查找viewtree根视图的TouchTarget链表的方式来获取当前的view,然后从配置表中查找出与点击view的viewID一致的数据,进行上报。
主流的方案:view代理监听的方式
还有一种比较常用可视化埋点方式就是view的代理监听的方式,ActivityLifecycleCallbacks,用来监听Activity生命周期,当activity被开启时,遍历当前activity下的所有view,如果view在配置表中,就设置当前view的setAccessibilityDelegate。
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public class VisualAnalysisManager extends View.AccessibilityDelegate{
..
..
view.setAccessibilityDelegate(this);
..
..
}
复制代码
当view产生了click或者long_click等点击事件时,分析View的源码在处理点击事件的回调时调用了 View.performClick 方法,内部调用了sendAccessibilityEvent,会在响应原有的Listener方法后,发送消息给AccessibilityDelegate,然后在继承AccessibilityDelegate的类中重写sendAccessibilityEvent方法来上报自动埋点事件。
public boolean performClick() {
..
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
..
}
public void sendAccessibilityEvent(int eventType) {
if (mAccessibilityDelegate != null) {
mAccessibilityDelegate.sendAccessibilityEvent(this, eventType);
} else {
sendAccessibilityEventInternal(eventType);
}
}
复制代码
代理监听的方式就是在ActivityLifecycleCallbacks监听到activity开启时对viewtree进行遍历,如果遍历view的viewID在后台下发的配置表中,就设置setAccessibilityDelegate,这种方式比较常用,但也要求sdk版本大于14。
我们的方案:获取viewID与配置表对比
用户模式与配置模式都需要获取当前点击的view,所以除类用上述view的代理监听的方式进行上报,还可以与配置模式相同,用TouchTarget链表的方式来获取当前的view,再判断当前的点击view是否需要上报。本需求在用户模式下也是采用TouchTarget链表的方式获取当前view进行上报,与配置模式统一,方便开发也避免代理监听的方式在在sdk小于14无法使用的困扰。
在获取view中,需要获取当前的activity,以及我们可能需要在当前activity生命周期中去做一些设置以及初始化等操作,最简单的方式是通过ActivityLifecycleCallbacks来监听activity的生命周期。然后在回调中做相应的操作,可以实现代码解藕。
public class ActivityTrackUtils implements ActivityTracer.ActivityLifecycleCallbacks {
..
..
private ActivityTracer activityTracer;
public void start(Application app) {
activityTracer.install(app);
activityTracer.registerActivityLifecycleCallbacks(this);
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (callback != null) {
callback.onActivityCreated(activity, savedInstanceState);
}
..
..
复制代码
上面第四章也说明了,获取当前view是通过反射的方式获取viewGroup中的私有成员,这样就可能因为不同手机api版本不同,源码会有一些差异,可能在某个版本的api中就无法获得,虽然这样的概率很小,但是为了保证代码的稳定,提高鲁棒性,还是可以提供一个降级方案:
public final View getTouchTarget(final ViewGroup vg,final MotionEvent event) {
..
..
//方案A
targetView = mVisualAnalysisHelper.getTouchTarget(vg);
//方案B
//如果mFirstTouchTarget的方式查询不出来,走降级方案
if(targetView == null){
targetView = mVisualAnalysisHelper.getTouchTargetForPlanB(vg,event);
}
..
复制代码
可以将一个问题中的获取view的主流方法作为降级方案,通过遍历当前viewTree的形式,判断当前的坐标是否在view的范围之中,并循环遍历viewGroup中的子view,最终可以获取到当前的点击view。
有些控件可能会提前截取点击事件单独做处理,这样事件无法下发,导致无法获取当前的view,TouchTarget为null,比如微云项目中有个大标题中的控件,在我查找了众多原因后发现其实就是它在onInterceptTouchEvent 方法中自己做了处理,阻止了事件的传递,对于这种特殊的问题,只能做一下单独的处理,在它处理之前,判断它的子view中是否有我们需要的view,如果需要就获取view做相应的处理。
还有一个问题就是dialog中的view是无法处理的,因为在Android中,dialog其实与activity都有自己的wondow,独立与activity的view层级,它与activity有各自的点击事件传递,所以要针对dialog的事件传递做单独的处理,在baseDialog的dispatchTouchEvent中做与activity相同的判断,根据模式的不同做不同的处理。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
..
..
/**
* 可视化埋点 在配置模式下就返回false,非配置模式下判断点是否在上报map中,有就上报
*/
VisualAnalysisManager visualAnalysisManager = VisualAnalysisManager.getInstance();
if(ev.getAction() == MotionEvent.ACTION_UP)
if (visualAnalysisManager.getIsInConfigVisual()) {
//配置模式
visualAnalysisManager.handleTouchEventInConfigMode(ev);
return false;
}else {
//用户模式
//截取up事件,让down事件传递下去,来获取当前点击的view
visualAnalysisManager.handleTouchEventInNormalMode(ev);
}
..
..
复制代码
viewID是根据view的层级来确定的,如果项目进行重构或者变更层级,相同view的viewID就会变化,导致很多view要重新配置,有一种约束ID的方案,就是单独生成一个view与viewID的对应表,但是这样在添加新view时又要做相应的对应,也会带来开发上的不便利,所以目前还是维持现有的生成viewID的方案,当遇到重构或者层级变动的问题时就只能将上报的点迁移并重新生成viewID再上报,这是一个待优化的方向,后续想到合适的方案时会将其优化。
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
我即将开始一个将录制和编辑音频文件的项目,我正在寻找一个好的库(最好是Ruby,但会考虑Java或.NET以外的任何库)以进行实时可视化波形。有人知道我应该从哪里开始搜索吗? 最佳答案 要流入浏览器的数据量很大。Flash或Flex图表可能是唯一能提高内存效率的解决方案。Javascript图表往往会因大型数据集而崩溃。 关于ruby-Ruby中的波形可视化,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.c
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO
遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg
最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路
通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复
在ruby中,你可以这样做:classThingpublicdeff1puts"f1"endprivatedeff2puts"f2"endpublicdeff3puts"f3"endprivatedeff4puts"f4"endend现在f1和f3是公共(public)的,f2和f4是私有(private)的。内部发生了什么,允许您调用一个类方法,然后更改方法定义?我怎样才能实现相同的功能(表面上是创建我自己的java之类的注释)例如...classThingfundeff1puts"hey"endnotfundeff2puts"hey"endendfun和notfun将更改以下函数定
我目前有一个reddit克隆类型的网站。我正在尝试根据我的用户之前喜欢的帖子推荐帖子。看起来K最近邻或k均值是执行此操作的最佳方法。我似乎无法理解如何实际实现它。我看过一些数学公式(例如k表示维基百科页面),但它们对我来说并没有真正意义。有人可以推荐一些伪代码,或者可以查看的地方,以便我更好地了解如何执行此操作吗? 最佳答案 K最近邻(又名KNN)是一种分类算法。基本上,您采用包含N个项目的训练组并对它们进行分类。如何对它们进行分类完全取决于您的数据,以及您认为该数据的重要分类特征是什么。在您的示例中,这可能是帖子类别、谁发布了该项