shook

更新时间:2023-01-04 16:07:40 阅读: 评论:0


2023年1月4日发(作者:空姐小花)

全局监控click事件的四种⽅式

本⽂主要给⼤家分享如何在全局上去监听click点击事件,并做些通⽤处理或是拦

截。使⽤场景可能就是具体的全局防快速重复点击,或是通⽤打点分析上报,⽤户⾏

为监控等。以下将以四种不同的思路和实现⽅式去监控全局的点击操作,由简单到复

杂逐⼀讲解。

⽅式⼀,适配监听接⼝,预留全局处理接⼝并作为所有监听器的基类使⽤

抽象出公共基类监听对象,可预留拦截机制和通⽤点击处理,简要代码如下:

kListener{

@Override

publicvoidonClick(Viewview){

if(!interceptViewClick(view)){

onViewClick(view);

}

}

protectedbooleaninterceptViewClick(Viewview){

//TODO:这⾥可做⼀此通⽤的处理如打点,或拦截等。

returnfal;

}

protectedabstractvoidonViewClick(Viewview);

}

使⽤⽅式之⼀匿名对象作为公共监听器

CustClickListenermClickListener=newCustClickListener(){

@Override

protectedvoidonViewClick(Viewview){

xt(,ng(),_SHORT).show();

}

};

@Override

protectedvoidonCreate(@NullableBundlesavedInstanceState){

te(savedInstanceState);

tContentView(ty_login);

findViewById().tOnClickListener(mClickListener);

}

这种⽅式⽐较简单,⽆兼容问题,但是需要⾃始⾄终都要使⽤基于基类的监听器对象,对开

发者约束⽐较⼤。适⽤于新项⽬之初就有此使⽤约定。对于⽼代码重构⼯作量⽐较⼤,⽽且

如果接⼊第三⽅墨盒模块就⽆能为⼒了。

⽅式⼆,反射代理,适时偷梁换柱开发者⽆感知,在适配包装器⾥做通⽤处

理。

以下是代理接⼝和内置监听适配器,全局的监听接⼝需要实现

IProxyClickListener

并设置到内置适配器

WrapClickListener

publicinterfaceIProxyClickListener{

booleanonProxyClick(WrapClickListenerwrap,Viewv);

kListener{

IProxyClickListenermProxyListener;

kListenermBaListener;

publicWrapClickListener(kListenerl,IProxyClickListenerproxyListener){

mBaListener=l;

mProxyListener=proxyListener;

}

@Override

publicvoidonClick(Viewv){

booleanhandled=mProxyListener==null?fal:yClick(,v);

if(!handled&&mBaListener!=null){

k(v);

}

}

}

}

我们需要选择⼀个时机对所有设置有监听器的

View

做监听代理的hook.这个时机可以对Activity的根View添加⼀个视图变化监听(当然也可

选择在Activity的DOWN事件的分发时机):

wTreeObrver().addOnGlobalLayoutListener(alLayoutListener(){

@Override

publicvoidonGlobalLayout(){

hookViews(rootView,0)

}

});

注:以上为了⽅便匿名注册了监听,实际使⽤在Activity退出时要反注册掉。

在进⾏代理前先要反射获取View监听器相关的Method和Field对象如下:

publicvoidinit(){

if(sHookMethod==null){

try{

ClassviewClass=e("");

if(viewClass!=null){

sHookMethod=laredMethod("getListenerInfo");

if(sHookMethod!=null){

essible(true);

}

}

}catch(Exceptione){

reportError(e,"init");

}

}

if(sHookField==null){

try{

ClasslistenerInfoClass=e("$ListenerInfo");

if(listenerInfoClass!=null){

sHookField=laredField("mOnClickListener");

if(sHookField!=null){

essible(true);

}

}

}catch(Exceptione){

reportError(e,"init");

}

}

}

只有保证了

sHookMethod

sHookField

成功获取才能进⼊下⼀步递归去设置监听代理偷梁换柱。以下为具体实现递归设置代理监听的过程。其

mInnerClickProxy

为外部传⼊的的全局处理点击事件的代理接⼝。

privatevoidhookViews(Viewview,intrecycledContainerDeep){

if(ibility()==E){

booleanforceHook=recycledContainerDeep==1;

if(viewinstanceofViewGroup){

booleanexistAncestorRecycle=recycledContainerDeep>0;

ViewGroupp=(ViewGroup)view;

if(!(pinstanceofAbsListView||pinstanceofRecyclerView)||existAncestorRecycle){

hookClickListener(view,recycledContainerDeep,forceHook);

if(existAncestorRecycle){

recycledContainerDeep++;

}

}el{

recycledContainerDeep=1;

}

intchildCount=ldCount();

for(inti=0;i

Viewchild=ldAt(i);

hookViews(child,recycledContainerDeep);

}

}el{

hookClickListener(view,recycledContainerDeep,forceHook);

}

}

}

privatevoidhookClickListener(Viewview,intrecycledContainerDeep,booleanforceHook){

booleanneedHook=forceHook;

if(!needHook){

needHook=kable();

if(needHook&&recycledContainerDeep==0){

needHook=(mPrivateTagKey)==null;

}

}

if(needHook){

try{

ObjectgetListenerInfo=(view);

kListenerbaClickListener=getListenerInfo==null?null:(kListener)(getListenerInfo);//获取已设置过的监听器

if((baClickListener!=null&&!(ickListener))){

(getListenerInfo,ickListener(baClickListener,mInnerClickProxy));

(mPrivateTagKey,recycledContainerDeep);

}

}catch(Exceptione){

reportError(e,"hook");

}

}

}

以上深度优先从Activity的根View进⾏递归设置监听。只会对原来的View本⾝有点击的事件监听器的进⾏设置,成功设置后还会对操作的

View设置⼀个tag标志表明已经设置了代理,避免每次变化重复设置。这个tag具有⼀定的含意,记录该View相对可能存在的可回收容器的

层级数。因为对于像

AbsListView

RecyclerView

的直接⼦View是需要强制重新绑定代理的,因为它们的复⽤机制可能被重新设置了监听。

此⽅式实现实现稍微复杂,但是实现效果⽐较好,对开发者⽆感知进⾏监听器的hook代理。

反射效率上也可以接受速度⽐较快⽆影响。对任何设置了监听器的View都有效。然

⽽AbsListView的Item点击⽆效,因为它的点击事件不是通过onClick实现的,除⾮不是⽤

tItemOnClick⽽是⾃⼰绑定click事件。

⽅式三,通过AccessibilityDelegate捕获点击事件。

分析View的源码在处理点击事件的回调时调⽤了mClick⽅法,内部调⽤了

ndAccessibilityEvent

⽽此⽅法有个托管接

mAccessibilityDelegate

可以由外部处理所有的AccessibilityEvent.正好此托管接⼝的设置也是开放的

tAccessibilityDelegate

,如以下View源

码关键⽚段。

publicbooleanperformClick(){

finalbooleanresult;

finalListenerInfoli=mListenerInfo;

if(li!=null&&ckListener!=null){

playSoundEffect();

k(this);

result=true;

}el{

result=fal;

}

ndAccessibilityEvent(_VIEW_CLICKED);

returnresult;

}

publicvoidndAccessibilityEvent(inteventType){

if(mAccessibilityDelegate!=null){

cessibilityEvent(this,eventType);

}el{

ndAccessibilityEventInternal(eventType);

}

}

publicvoidtAccessibilityDelegate(@NullableAccessibilityDelegatedelegate){

mAccessibilityDelegate=delegate;

}

基于此原理我们可在某个时机给所有的View注册我们⾃⼰的AccessibilityDelegate去监听系统⾏为事件,简要实现代码如下。

ibilityDelegate{

booleanmInstalled=fal;

WeakReferencemRootView=null;

alLayoutListenermOnGlobalLayoutListener=null;

publicViewClickTracker(ViewrootView){

if(rootView!=null&&wTreeObrver()!=null){

mRootView=newWeakReference(rootView);

mOnGlobalLayoutListener=alLayoutListener(){

@Override

publicvoidonGlobalLayout(){

Viewroot=mRootView==null?null:();

booleaninstall=;

if(root!=null&&wTreeObrver()!=null&&wTreeObrver().isAlive()){

try{

installAccessibilityDelegate(root);

if(!mInstalled){

mInstalled=true;

}

}catch(Exceptione){

tackTrace();

}

}el{

destroyInner(fal);

}

}

};

wTreeObrver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);

}

}

privatevoidinstallAccessibilityDelegate(Viewview){

if(view!=null){

essibilityDelegate();

if(viewinstanceofViewGroup){

ViewGroupparent=(ViewGroup)view;

intcount=ldCount();

for(inti=0;i

Viewchild=ldAt(i);

if(ibility()!=){

installAccessibilityDelegate(child);

}

}

}

}

}

@Override

publicvoidndAccessibilityEvent(Viewhost,inteventType){

cessibilityEvent(host,eventType);

if(_VIEW_CLICKED==eventType&&host!=null){

//TODO这⾥处理通⽤的点击事件,host即为相应被点击的View.

}

}

}

以上实现⽐较巧妙,在监测到window上全局视图树发⽣变化后递归的给所有的View安

装AccessibilityDelegate。经测试⼤多数⼚商的机型和版本都是可以的,然⽽部分机型⽆法成功捕获

监控到点击事件,所以不推荐使⽤。

⽅式四,通过分析Activity的dispatchTouchEvent事件并查找事件接受

的⽬标View。

这个⽅式初看有点匪夷所思,但是⼀系列触屏事件发⽣后总归要有⼀个组件消耗了它,查看

ViewGroup

关键源码如下:

//Firsttouchtargetinthelinkedlistoftouchtargets.

privateTouchTargetmFirstTouchTarget;

publicbooleandispatchTouchEvent(MotionEventev){

......

if(newTouchTarget==null&&childrenCount!=0){

for(inti=childrenCount-1;i>=0;i--){

if(dispatchTransformedTouchEvent(ev,fal,child,idBitsToAssign)){

newTouchTarget=addTouchTarget(child,idBitsToAssign);

alreadyDispatchedToNewTouchTarget=true;

break;

}

}

}

......

//Dispatchtotouchtargets.

if(mFirstTouchTarget==null){

//Notouchtargetssotreatthisasanordinaryview.

handled=dispatchTransformedTouchEvent(ev,canceled,null,_POINTER_IDS);

}el{

//Dispatchtotouchtargets,excludingthenewtouchtargetifwealready

//touchtargetsifnecessary.

TouchTargetpredecessor=null;

TouchTargettarget=mFirstTouchTarget;

while(target!=null){

finalTouchTargetnext=;

if(alreadyDispatchedToNewTouchTarget&&target==newTouchTarget){

handled=true;

}el{

finalbooleancancelChild=retCancelNextUpFlag()||intercepted;

......

if(cancelChild){

if(predecessor==null){

mFirstTouchTarget=next;

}el{

=next;

}

e();

target=next;

continue;

}

}

predecessor=target;

target=next;

}

}

}

这⾥发现意愿接受touch事件的直接⼦View都会被添加到

mFirstTouchTarget

这个链式对象⾥,且链经过调整后next⼏乎总是null.这就给我

们⼀个突破⼝。可以从得到当前接受事件的直接⼦View,然后按此⽅法递归去查找直⾄

为null。我们就算是找到了最终touch事件的接受者。这个查找最好的时机应该是在

ACTION_UP或ACTION_CANCEL

通过以上原理我们可以有法获取⼀系列Touch事件最终接受处理的⽬标View,再根据我们记录的按下位置和松开位置及偏移偏量可判断是否为

可能的点击动作。为了加强判断是否为真正的click事件,可进⼀步分析⽬标View是否安装了点击监听器(原理可参考上⾯讲的⽅式⼆。以下获

取和分析事件时机都是在Activity的dispatchTouchEvent⽅法中进⾏的。

记录down和up事件后,以下为实现判断是否为可能的点击判断

//whetheritcouldbeaclickaction

publicbooleanisClickPossible(floatslop){

if(mCancel||mDownId==-1||mUpId==-1||mDownTime==0||mUpTime==0){

returnfal;

}el{

(mDownX-mUpX)

}

}

在up事件发⽣后⽴即查找⽬标View.⾸先要保证反射mFirstTouchTarge相关的准备⼯作。

privatebooleanensureTargetField(){

if(sTouchTargetField==null){

try{

ClassviewClass=e("oup");

if(viewClass!=null){

sTouchTargetField=laredField("mFirstTouchTarget");

essible(true);

}

}catch(Exceptione){

tackTrace();

}

try{

if(sTouchTargetField!=null){

sTouchTargetChildField=e().getDeclaredField("child");

essible(true);

}

}catch(Exceptione){

tackTrace();

}

}

returnsTouchTargetField!=null&&sTouchTargetChildField!=null;

}

然后从Activity的DecorView去递归查找⽬标View.

//notfind

privateViewfindTargetView(){

ViewnextTarget,target=null;

if(ensureTargetField()&&mRootView!=null){

nextTarget=findTargetView(mRootView);

do{

target=nextTarget;

nextTarget=null;

if(targetinstanceofViewGroup){

nextTarget=findTargetView((ViewGroup)target);

}

}while(nextTarget!=null);

}

returntarget;

}

//reflecttofindtheTouchTargetchildview,nullifnotfound.

privateViewfindTargetView(ViewGroupparent){

try{

Objecttarget=(parent);

if(target!=null){

Objectview=(target);

if(viewinstanceofView){

return(View)view;

}

}

}catch(Exceptione){

tackTrace();

}

returnnull;

}

通过以上⽅式所有具有点击功能的View都能正确监听,然⽽可能存在并没有监听点击事件

的View也被认为是⼀次点击事件。要过滤掉这部分可通过分析⽬标View是否安装了点击

监听器,这⾥就不多贴代码了,原理和代码在⽅式⼆中有讲过。

以上四种⽅式各有优劣,效率上都⽐较快,综合对⽐以⽅式⼆⽐较精准。像⽅式三和

试四只作为参考,具有学习意义,特别是⽅式四可应⽤前景⽐较⼴泛,所有的⼿势的

⽬标View都可查找得到

本⽂讲述的是我最近研究的⽤户⾏为监控的⼀个监控点。具体更多的⾏为

监控请参考项⽬InteractionHook⽬前还在持续开发中。

本文发布于:2023-01-04 16:07:40,感谢您对本站的认可!

本文链接:http://www.wtabcd.cn/fanwen/fan/90/91571.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

上一篇:specify
标签:shook
相关文章
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图