这里先声明一下,由于这篇文章早已有人写过,但是并非盗取他的成果,这里的雷同确实有些偶然。。。这是做到一半的时候一个同事跟我说网上有,于是乎我看了他的思路以及demo,基本跟我差不多,只是他的代码写的可能更好一些,但是并没有做优化以及各种场景并没有想到,只是单纯的hook技术而已,以下是作者的文章链接
背景:
Idea:
hook时机分析:
/**
* 点击监听器
*
* @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:事件监听器都是这个实例保存的
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);//获取已设置过的监听器
onClickObj就是监听器的实例了。拿到实实在在的监听器的实例之后我们再通过代理模式将自定义的监听器代理现有的监听器://hook事件,设置自定义的载体事件监听器
onClickListerField.set(listenerInfoInstance, new OnClickListenerProxy(onClickListerObj, proxyListenerConfigBuilder.getOnClickProxyListener()));
这样基本完成hook.Class.forName("android.view.View");
而不是通过
Class.forName("android.widget.TextView");
来获取ListenerInfo的实例的。
/**
* 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...等等它们是干啥用的呢,聪明的你一定猜出来了,对,就是用于优化的。。。。
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的监听器。
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成功,亲测成功。