这里先声明一下,由于这篇文章早已有人写过,但是并非盗取他的成果,这里的雷同确实有些偶然。。。这是做到一半的时候一个同事跟我说网上有,于是乎我看了他的思路以及demo,基本跟我差不多,只是他的代码写的可能更好一些,但是并没有做优化以及各种场景并没有想到,只是单纯的hook技术而已,以下是作者的文章链接
如有雷同,纯属巧合
背景:
项目中又要求说要监听所有点击事件方便上传数据,所以想了很多办法最终选定这种既不修改原有代码又可以完成需求的优雅方式,hook技术;
随着热修复、动态加载等热门技术的出现,hook技术不再神秘,想必多多少少都已经了解,无非就是java的所谓高级反射技术+设计模式的代理模式就可以完成hook。这只代表个人理解,有误的地方请不吝指正;
Idea:
之前有做过换肤功能,当时有个setFactory(),可以能拿到所有的view,当时下意识的想法既然能拿到所有的view,那应该能拿到监听器,但是仔细一想,这只是刚开始加载view,还没设置监听器,怎么拿都是空的吧。这一想法行不通,只能放弃,setFactory()让我想到了代理方式,而且最近在拜读动态加载,所以知道hook技术,灵光就是那么一闪,下意识的想到,我能不能把整个onClick方法或者onClick的实例给hook了,再结合代理方式,毫无痕迹的入侵了。。。这个想法可以一试,然后就研究onClick的源码,发现View的onclick统一由一个ListenerInfo
类管理,所以直接hook掉,通过代理方式悄无声息的实现全面入侵监听器的功能。
hook时机分析:
1、首先在现有的项目中基本都有一个BaseActivity,所以当activity加载完布局,初始化(view的初始化和设置监听器)完之后,开始hook.
2、第1只是在加载完layout之后,按照执行流程将所有设置监听器的view都hook,但是有一种可能就是进入当前页,初始化完成之后,并没有立即设置监听器也没有更新数据,直到网络加载完毕,将数据返回后才更新view和设置相应的监听器;这时需要怎样才能将最新设置的监听器hook掉,应该在哪儿监听这个状态?这里卖个关子。。
3、当listview或者gridview在网络请求拿到数据之后才设置监听器,这里设置监听器分两种,一个是listview或gridview设置的监听器,一个是adapter里边的view的监听器,这个需要怎么hook?
4、当listview或者gridview滚动的时候adapter中itemview设置的监听器怎么hook,这里又是一个难点?
5、自定义的点击事件。。这里我就不做特殊处理了,这里直接手动添加吧,毕竟这种场景少之又少
解决办法:
1、对于第一种情况,我们只需要在activity的生命周期中的onWindowFocusChange或者onAttachToWindow()的方法里直接hook就可以,因为我们初始化view和设置view的监听器,一般都在oncreate中执行。而onWindowFocusChange()和onAttachToWindow是在onResume之后,而且onAttachToWindow只执行一次,而onWindowFocusChange()会执行两次,一次是加载完layout之后window获取焦点,一次是在要销毁activity之后window失去焦点。所以一开始会在onAttachToWindow()中hook的,但是后来为什么会放在onWindowFocusChange()中hook。。。接下来分析2的时候一起说说
2、由于第一种只是一部分情况,第2、3中更是一种常态,对于开发者来说很容易写出2的方式,即请求完数据之后,我才初始化view,并设置相应的监听器,毕竟有数据我才会操作页面,这里符合用什么就生成什么的原则,这时候这个hook的时机在哪儿,由于更新view的时候一般都会重新layout或者onMeasure,所以这里就查看了view中layout的源码,发现view中有个addLayoutChangeListener,并且是在layout的时候回调,所以一般在baseActivity可以直接为rootView设置这个监听器,但是刚初始化的时候也会多次执行这个方法,所以首次进入的时候要做相应的处理,否则会hook很多次,虽然也做了优化,但是这种hook会频繁调用。。。毕竟交互是频繁的,这里暂时没想到有什么比较好的方式,若知道请不吝赐教,这里先谢了
3、对于第4种在滚动时候都会重新设置监听器,而且item是复用的;这里一般来说项目都有一个统一的Listview,这时我们只需要在onScroll()的时候重新hook一遍listview的所有itemview就好了,但是由于item是复用的可能已经hook过的又重新hook一遍导致会执行两次hook的监听器,(比如只滚动一个itemview,那么只会复用一个itemview,其它itemview还是原来的,这时候都hook,原来的已经hook过了,又hook了一次,这样就会有两个hook的监听器所以会执行2次,周而复始呢,太可怕了。。。)这样的话数据上报就更加不准了,所以这里一定要判断是否已经设置过了
叨咕叨咕这里就叨咕完了,接下来我们看看实现:上代码
/**
* 点击监听器
*
* @param view
* @param isScrollAbsListview lsitview或gridView是否滚动:true:滚动则重新hook,false:表示view不是listview或者gridview或者没滚动
*/
private void hookOnClickListener(View view, boolean isScrollAbsListview) {
if (!view.isClickable()) {//默认是不可点击的,只有设置监听器才会设置为true,证明没有设置点击事件或者初始化时没有设置点击事件
Log.d(TAG, "isClickable name = " + view.getClass().getSimpleName());
return;
}
Log.d(TAG, "null != view.getTag(R.id.tag_onclick) = " + (null != view.getTag(R.id.tag_onclick)));
if (!isScrollAbsListview && null != view.getTag(R.id.tag_onclick)) {//已经hook过,并且不是滚动的listview,不用再hook了
return;
}
try {
//hook view的信息载体实例listenerInfo:事件监听器都是这个实例保存的
Class viewClass = Class.forName("android.view.View");
Method method = viewClass.getDeclaredMethod("getListenerInfo");
method.setAccessible(true);
Object listenerInfoInstance = method.invoke(view);
//hook信息载体实例listenerInfo的属性
Class listenerInfoClass = Class.forName("android.view.View$ListenerInfo");
Field onClickListerField = listenerInfoClass.getDeclaredField("mOnClickListener");
onClickListerField.setAccessible(true);
View.OnClickListener onClickListerObj = (View.OnClickListener) onClickListerField.get(listenerInfoInstance);//获取已设置过的监听器
Log.d(TAG, "onClickListerObj = " + onClickListerObj);
if (isScrollAbsListview && onClickListerObj instanceof OnClickListenerProxy) {//针对adapterView的滚动item复用会导致重复hook代理监听器
return;
}
//hook事件,设置自定义的载体事件监听器
onClickListerField.set(listenerInfoInstance, new OnClickListenerProxy(onClickListerObj, proxyListenerConfigBuilder.getOnClickProxyListener()));
setHookedTag(view, R.id.tag_onclick);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
这里就以hook view的onClick实例为例,其它长按的以此类推都是相似的;
首先:hook view的listenerInfo实例:
//hook view的信息载体实例listenerInfo:事件监听器都是这个实例保存的
Class viewClass = Class.forName("android.view.View");
Method method = viewClass.getDeclaredMethod("getListenerInfo");
method.setAccessible(true);
Object listenerInfoInstance = method.invoke(view);
listenerInfoInstance就是我们声明view的监听器的实例了。拿到实例后,要hook实例的属性字段:
//hook信息载体实例listenerInfo的属性
Class listenerInfoClass = Class.forName("android.view.View$ListenerInfo");
Field onClickListerField = listenerInfoClass.getDeclaredField("mOnClickListener");
onClickListerField.setAccessible(true);
View.OnClickListener onClickListerObj = (View.OnClickListener) onClickListerField.get(listenerInfoInstance);//获取已设置过的监听器
onClickObj就是监听器的实例了。拿到实实在在的监听器的实例之后我们再通过代理模式将自定义的监听器代理现有的监听器:
//hook事件,设置自定义的载体事件监听器
onClickListerField.set(listenerInfoInstance, new OnClickListenerProxy(onClickListerObj, proxyListenerConfigBuilder.getOnClickProxyListener()));
这样基本完成hook.
这里需要注意的是:了解反射技术以及代理技术。反射技术一定要记得子类是无法hook到父类的任何东西的包括继承的属性以及方法等等,一定要用父类的包名+类名来反射得到属性及方法,在通过子类的实例去调用获取到所需要的字段属性
如此例子:我们通过
Class.forName("android.view.View");
而不是通过
Class.forName("android.widget.TextView");
来获取ListenerInfo的实例的。
要记得字段方法属于哪个类就得在哪个类hook,通过子类或者父类直接获取是获取不到的。
hook技术讲解完毕,其它长按监听器以此类推了。。。这里特别的是adapterView(如listview/gridview)就没必要hook了,因为它自身就带有getxxx,可以直接获取实例,重新设置监听器就可以了:代码如下:
/**
* hook到Listview的listener
*
* @param viewGroup
*/
private void hookListViewListener(ViewGroup viewGroup) {//已经设置过的不会重新设置
if (viewGroup instanceof ListView) {
ListView listView = (ListView) viewGroup;
AdapterView.OnItemClickListener itemClickListener = listView.getOnItemClickListener();
if (null != itemClickListener && !(itemClickListener instanceof OnItemClickListenerProxy)) {
if (null == listView.getTag(R.id.tag_onItemClick)) {//还没hook过
listView.setOnItemClickListener(new OnItemClickListenerProxy(itemClickListener, proxyListenerConfigBuilder.getOnItemClickProxyListener()));
setHookedTag(listView, R.id.tag_onItemClick);
}
}
AdapterView.OnItemLongClickListener itemLongClickListener = listView.getOnItemLongClickListener();
if (null != itemLongClickListener && !(itemLongClickListener instanceof OnItemLongClickListenerProxy)) {
if (null == listView.getTag(R.id.tag_onItemLong)) {//还没hook过
listView.setOnItemLongClickListener(new OnItemLongClickListenerProxy(itemLongClickListener, proxyListenerConfigBuilder.getOnItemLongClickProxyListener()));
setHookedTag(listView, R.id.tag_onItemLong);
}
}
AdapterView.OnItemSelectedListener itemSelectedListener = listView.getOnItemSelectedListener();
if (null != itemSelectedListener && !(itemSelectedListener instanceof OnItemSelectedListenerProxy)) {
if (null == listView.getTag(R.id.tag_onitemSelected)) {//还没hook过
listView.setOnItemSelectedListener(new OnItemSelectedListenerProxy(itemSelectedListener, proxyListenerConfigBuilder.getOnItemSelectedProxyListener()));
setHookedTag(listView, R.id.tag_onitemSelected);
}
}
}
}
细心的你一定会发现里边多了好些逻辑,比如setHookTag,比如一系列的if else...等等它们是干啥用的呢,聪明的你一定猜出来了,对,就是用于优化的。。。。
我们没必要重新hook都要将已经hook过得重新hook,不必要hook也hook,所以将hook过以及不需要hook的view直接跳过,提升好大的效率。接下来就结合代码一起讲解
private void hookOnClickListener(View view, boolean isScrollAbsListview) {
if (!view.isClickable()) {//默认是不可点击的,只有设置监听器才会设置为true,证明没有设置点击事件或者初始化时没有设置点击事件
Log.d(TAG, "isClickable name = " + view.getClass().getSimpleName());
return;
}
Log.d(TAG, "null != view.getTag(R.id.tag_onclick) = " + (null != view.getTag(R.id.tag_onclick)));
if (!isScrollAbsListview && null != view.getTag(R.id.tag_onclick)) {//已经hook过,并且不是滚动的listview,不用再hook了
return;
}
//...
}
view大部分默认都是不可点击的(除了button),直到设置监听器才是可点击状态所以第一行就可以把那些没设置监听器的都给过滤掉
if (!isScrollAbsListview && null != view.getTag(R.id.tag_onclick)) {//已经hook过,并且不是滚动的listview,不用再hook了
return;
}
isScrollAbsListview主要是listview或gridView滚动的时候都要重新hook itemview的监听器。
null != view.getTag(R.id.tag_onclick)这是hook过的view不再重新hook,这就保证了唯一性,也不会剩下很大的性能,毕竟我们是通过递归查找所有的子view的,接下来就说说怎么查找所有的view的
子view
public void hookStart(Activity activity) {
if (null != activity) {
View view = activity.getWindow().getDecorView();
if (null != view) {
if (view instanceof ViewGroup) {
hookStart((ViewGroup) view);
} else {
hookOnClickListener(view, false);
hookOnLongClickListener(view, false);
}
}
}
}
通过activity我们就很容易就拿到了decorView,所以根据decorView就可以递归查找所有的子view以及需要hook的
子view
/**
* hook掉viewGroup
*
* @param viewGroup
* @param isScrollAbsListview lsitview或gridView是否滚动:true:滚动则重新hook,false:表示view不是listview或者gridview或者没滚动
*/
public void hookStart(ViewGroup viewGroup, boolean isScrollAbsListview) {
if (viewGroup == null) {
return;
}
int count = viewGroup.getChildCount();
for (int i = 0; i < count; i++) {
View view = viewGroup.getChildAt(i);
if (view instanceof ViewGroup) {//递归查询所有子view
// 若是布局控件(LinearLayout或RelativeLayout),继续查询子View
hookStart((ViewGroup) view, isScrollAbsListview);
} else {
hookOnClickListener(view, isScrollAbsListview);
hookOnLongClickListener(view, isScrollAbsListview);
}
}
hookOnClickListener(viewGroup, isScrollAbsListview);
hookOnLongClickListener(viewGroup, isScrollAbsListview);
hookListViewListener(viewGroup);
}
通过这个直接就可以hook成功,亲测成功。
到这里就分析完毕了,有看不懂的地方或者又发现错误的地方,请不吝赐教。