Android之hook dispatchTouchEvent方法
无埋点操作,是通过gradle的Transform API在编译期扫描整个项目生成的class文件,再利用ASM API对class文件插入我们的埋点方法来实现的。
在各种事件方法里插桩埋点,基本上满足我们的大部分埋点需求,但是产品会有这样的需求,想看看用户在某个界面里哪些区域点击比较频繁,就需要知道用户点击的坐标。获取点击事件可以办到,还可以拿到点击事件里的View,但是无法获取点击的屏幕坐标。那点击屏幕坐标在哪里获取呢?在View的dispatchTouchEvent里,如果我们在此处插桩,可以拿到它的参数MotionEvent,通过它我们就可以拿到控件的点击坐标。
但是dispatchTouchEvent是系统类View的方法,项目编译的时候是没有android.jar供Transform扫描的。办法是可以通过其他方式实现,比如每打开一个界面都创建一个透明层在屏幕上,通过自定义透明层View来重写dispatchTouchEvent获取MotionEvent。或者所有的控件都自定义View的方式重写dispatchTouchEvent获取MotionEvent。这两种方式开销都比较大,难以维护,不可取。
这里介绍通过另一种方式来实现。大家都知道android系统出的support包和现在的androidx包都是为了向下兼容,使得老控件,如textview、edittext等等,可以像Appcompattextview、Appcompatedittext一样有更好的着色和主题样式。但是怎么实现呢?答案在Activity启动过程里。Activity的启动过程大家自行百度,这里先讲oncreate方法
在AppcompatActivity的onCreate方法里,有一个getDelegate()方法,返回AppCompatDelegate,
进去后,可以看到new了一个AppCompatDelegateImpl实现类,这个实现类又继承自AppCompatDelegate。
到这里还没看到关键信息,我们先看下xml布局文件是怎么映射成控件view的,
在AppCompatDelegateImpl找createView方法,在createView里有一个AppCompatViewInflater类,默认类名或设置为null就new一个AppCompatViewInflater,否则就反射获取AppCompatViewInflater对象。
到这里,AppCompatActivity的getDelegate()方法,只是new了一个AppCompatDelegateImpl类,并未看到实质的东西。在里面找一下可以看到createView这个方法,可以猜到肯定跟这个createView方法有关系。那它是什么时候被调用的呢,我们回到AppCompatActivity,getDelegate()方法的下一行delegate.installViewFactory();
,进去后可以看到实例化了系统服务LAYOUT_INFLATER_SERVICE,并把它设置进setFactory2方法里,
可以看到factory赋值给mFactory2接口类
回到setContentView()方法,我们可以看到LayoutInflater.from(mContext).inflate(resId, contentParent);
,步骤是在LayoutInflater里先用xmlpullparser解析xml,根据tag来生成对应的控件
最终还是通过一系列调用createViewFromTag->tryCreateView->mFactory2.onCreateView(接口回调到AppCompatDelegateImpl
)->onCreateView->createView->mAppCompatViewInflater.createView,最后进入AppCompatViewInflater类里,可以看到
final View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs, boolean inheritContext,boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {final Context originalContext = context;// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy// by using the parent's contextif (inheritContext && parent != null) {context = parent.getContext();}if (readAndroidTheme || readAppTheme) {// We then apply the theme on the context, if specifiedcontext = themifyContext(context, attrs, readAndroidTheme, readAppTheme);}if (wrapContext) {context = TintContextWrapper.wrap(context);}View view = null;// We need to 'inject' our tint aware Views in place of the standard framework versionsswitch (name) {case "TextView":view = createTextView(context, attrs);verifyNotNull(view, name);break;case "ImageView":view = createImageView(context, attrs);verifyNotNull(view, name);break;case "Button":view = createButton(context, attrs);verifyNotNull(view, name);break;case "EditText":view = createEditText(context, attrs);verifyNotNull(view, name);break;case "Spinner":view = createSpinner(context, attrs);verifyNotNull(view, name);break;case "ImageButton":view = createImageButton(context, attrs);verifyNotNull(view, name);break;case "CheckBox":view = createCheckBox(context, attrs);verifyNotNull(view, name);break;case "RadioButton":view = createRadioButton(context, attrs);verifyNotNull(view, name);break;case "CheckedTextView":view = createCheckedTextView(context, attrs);verifyNotNull(view, name);break;case "AutoCompleteTextView":view = createAutoCompleteTextView(context, attrs);verifyNotNull(view, name);break;case "MultiAutoCompleteTextView":view = createMultiAutoCompleteTextView(context, attrs);verifyNotNull(view, name);break;case "RatingBar":view = createRatingBar(context, attrs);verifyNotNull(view, name);break;case "SeekBar":view = createSeekBar(context, attrs);verifyNotNull(view, name);break;case "ToggleButton":view = createToggleButton(context, attrs);verifyNotNull(view, name);break;default:// The fallback that allows extending class to take over view inflation// for other tags. Note that we don't check that the result is not-null.// That allows the custom inflater path to fall back on the default one// later in this method.view = createView(context, name, attrs);}if (view == null && originalContext != context) {// If the original context does not equal our themed context, then we need to manually// inflate it using the name so that android:theme takes effect.view = createViewFromTag(context, name, attrs);}if (view != null) {// If we have created a view, check its android:onClickcheckOnClickListener(view, attrs);}return view;}
google在这里做了个偷梁换柱的switch,把老控件替换成新控件,如果有点击事件,也再重新给绑定上。这里简直就是个奇幻喵喵屋,大家可以进去尽情发挥自己的潜能。
我们要做的是hook dispatchTouchEvent方法,到这只说了google是怎么兼容老控件的,google把老控件用switch重新new一个新控件,如果我们可以让它去new一个我们自定义的view,一切的view都由我们创建,那岂不快哉。那样我们就可以在自己的自定义view里去重写dispatchTouchEvent方法,这样就可以往重写的方法里插桩拿MotionEvent。但实现呢,简单。
重写一个CustomAppCompatViewInflater类,在里面加上新控件
switch (name) {case "androidx.appcompat.widget.AppCompatTextView":case "TextView":view = createTextView(context, attrs);verifyNotNull(view, name);break;case "androidx.appcompat.widget.AppCompatImageView":case "ImageView":view = createImageView(context, attrs);verifyNotNull(view, name);break;case "androidx.appcompat.widget.AppCompatButton":case "Button":view = createButton(context, attrs);verifyNotNull(view, name);break;case "androidx.appcompat.widget.AppCompatEditText":case "EditText":view = createEditText(context, attrs);verifyNotNull(view, name);break;case "androidx.appcompat.widget.AppCompatSpinner":case "Spinner":view = createSpinner(context, attrs);verifyNotNull(view, name);break;case "androidx.appcompat.widget.AppCompatImageButton":case "ImageButton":view = createImageButton(context, attrs);verifyNotNull(view, name);break;case "androidx.appcompat.widget.AppCompatCheckBox":case "CheckBox":view = createCheckBox(context, attrs);verifyNotNull(view, name);break;case "androidx.appcompat.widget.AppCompatRadioButton":case "RadioButton":view = createRadioButton(context, attrs);verifyNotNull(view, name);break;case "androidx.appcompat.widget.AppCompatCheckedTextView":case "CheckedTextView":view = createCheckedTextView(context, attrs);verifyNotNull(view, name);break;case "AppCompatAutoCompleteTextView":case "AutoCompleteTextView":view = createAutoCompleteTextView(context, attrs);verifyNotNull(view, name);break;case "androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView":case "MultiAutoCompleteTextView":view = createMultiAutoCompleteTextView(context, attrs);verifyNotNull(view, name);break;case "androidx.appcompat.widget.AppCompatRatingBar":case "RatingBar":view = createRatingBar(context, attrs);verifyNotNull(view, name);break;case "androidx.appcompat.widget.AppCompatSeekBar":case "SeekBar":view = createSeekBar(context, attrs);verifyNotNull(view, name);break;case "androidx.appcompat.widget.AppCompatToggleButton":case "ToggleButton":view = createToggleButton(context, attrs);verifyNotNull(view, name);break;default:// The fallback that allows extending class to take over view inflation// for other tags. Note that we don't check that the result is not-null.// That allows the custom inflater path to fall back on the default one// later in this method.view = createView(context, name, attrs);}
然后把自定义的view给new进去,写一个基类继承自AppCompatActivity,
重写getDelegate方法,再写一个CustomCompatDelegate类继承自AppCompatDelegateImpl
在CustomCompatDelegate里重写createView方法
@Overridepublic View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {if (customAppCompatViewInflater == null) {TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);String viewInflaterClassName =a.getString(R.styleable.AppCompatTheme_viewInflaterClass);if ((viewInflaterClassName == null)|| AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {// Either default class name or set explicitly to null. In both cases// create the base inflater (no reflection)customAppCompatViewInflater = new CustomAppCompatViewInflater();} else {try {Class viewInflaterClass = Class.forName(viewInflaterClassName);customAppCompatViewInflater =(CustomAppCompatViewInflater) viewInflaterClass.getDeclaredConstructor().newInstance();} catch (Throwable t) {Log.i(TAG, "Failed to instantiate custom view inflater "+ viewInflaterClassName + ". Falling back to default.", t);customAppCompatViewInflater = new CustomAppCompatViewInflater();}}}boolean inheritContext = false;if (IS_PRE_LOLLIPOP) {inheritContext = (attrs instanceof XmlPullParser)// If we have a XmlPullParser, we can detect where we are in the layout? ((XmlPullParser) attrs).getDepth() > 1// Otherwise we have to use the old heuristic: shouldInheritContext((ViewParent) parent);}return customAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */true, /* Read read app:theme as a fallback at all times for legacy reasons */VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */);}
把里面的AppCompatViewInflater替换成CustomAppCompatViewInflater。
替换完成后,我们的界面都继承自这个基类Activity,重写dispatchTouchEvent方法,这样所有的view都可以获取触摸坐标了。
这里要感谢海哥的无私提醒,才能让我打通这一关。
Android之hook dispatchTouchEvent方法相关推荐
- [Android]objection hook指定方法
最近调试一款app,遇到个问题是某个类里面有多个重名的重载函数,但我只想hook某个指定的函数.看了一下网上很多文章基本都是简略带过,只有这篇还提示了一下可以带上参数来区分: objection 指南 ...
- Android中免root的hook框架学习——whale(二)实战hook java方法
文末附项目完整代码下载地址 一.在android studio中创建一个新的项目 Hook Java, 把whale项目里的java文件夹的代码复制到自己的项目中. 复制 built/Android ...
- android socket_盘点Android常用Hook技术
Android平台开发测试过程中,Hook技术是每个开发人员都常用的技术.可以用于绕过系统限制.修改别人发布的代码.动态化.调用隐藏API.插件化.组件化.自动化测试.沙箱等等. Hook如果要跨进程 ...
- Android滑动冲突解决方法
Android滑动冲突解决方法 滑动冲突 首先讲解一下什么是滑动冲突.当你需要在一个ScrollView中嵌套使用ListView或者RecyclerView的时候你会发现只有ScrollView能够 ...
- Android Art Hook 技术方案
Android Art Hook 技术方案 by 低端码农 at 2015.4.13 www.im-boy.net 0x1 开始 Anddroid上的ART从5.0之后变成默认的选择,可见ART的重要 ...
- Android Native Hook工具
前言 在目前的安卓APP测试中对于Native Hook的需求越来越大,越来越多的APP开始逐渐使用NDK来开发核心或者敏感代码逻辑.个人认为原因如下: 安全的考虑.各大APP越来越注重安全性,NDK ...
- Android主流HOOK框架介绍与应用--游戏破解游戏外挂的必杀技
概述 使用HOOK方案主要是在分析的时候会经常用到,虽然二次打包重新修改代码也可以做到,但是一方面效率低,另一方面如果APP有校验的逻辑就需要进一步绕过,总体还是比较费时费力.所以,通过动态HOOK的 ...
- 如何写一个Android inline hook框架
Android_Inline_Hook https://github.com/GToad/Android_Inline_Hook_ARM64 有32和64的实现,但是是分离的,要用的话还要自己把两份代 ...
- android 拦截点击事件,Android双击事件拦截方法
下文我们介绍两种双击事件拦截的方式 1.通过Android的事件分发机制进行拦截(dispatchTouchEvent) 话不多说,直接上代码: /** 判断是否是快速点击 */ private st ...
最新文章
- Git本地仓库管理远程库(GitHub)——clone(下载)、push(提交)、pull(拉取)操作
- @keyframes中translate和scale混用问题
- Error running tomcat8 Address localhost:1099 is already in use 错误解决
- centos7搭建git代码仓库
- linux下直接清空日志的方法
- 用Jenkins编译asp.net
- 线段树(区间合并) LA 3989 Ray, Pass me the dishes!
- win7计算机名称格式,win7笔记本电脑如何显示文件扩展名
- Bootstrap自定义图标
- 云智巡在连锁药店的巡检作用
- php调用接口及编写接口
- 全球主流云桌面传输协议
- 2022年中国工业软件市场现状及发展前景预测分析
- 你的童年经历过放牛,放羊,干农活吗?然后你现在对这些都怀念吗?
- JavaEE项目bug修复记——一场由特殊空字符(160号ASCII码)引发的血案
- Android系统初识
- 利用python获取自己的qq群成员信息!
- 电脑硬盘分区软件哪个好用,无损分区软件哪个好
- 体育生考大学能学计算机专业吗,体育生可以报考的大学和专业有哪些
- 酷比魔方可以PHP编程么,酷比魔方iwork12麻烦适配一下,找了很久,没有适配这个本子的rom...