這裏先聲明一下,由於這篇文章早已有人寫過,但是並非盜取他的成果,這裏的雷同確實有些偶然。。。這是做到一半的時候一個同事跟我說網上有,於是乎我看了他的思路以及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成功,親測成功。