版權(quán)聲明:本文為LooperJing原創(chuàng)文章,轉(zhuǎn)載請(qǐng)注明出處!
昨天有好幾個(gè)小伙伴簡(jiǎn)信問(wèn)我,View.onClick怎么hook?回想前幾個(gè)月前,公司的項(xiàng)目在百度手機(jī)助手上線,在快速點(diǎn)擊的時(shí)候會(huì)跳轉(zhuǎn)兩次Activity或者兩個(gè)Dialog等等,為了能夠順利通過(guò)百度的測(cè)試,老大叫我將所有onClick全部要優(yōu)化處理,避免用戶快速多次點(diǎn)擊,于是乎,我寫了下面的代碼
public abstract class NoDoubleClickListener implements View.OnClickListener {
private int MIN_CLICK_DELAY_TIME = 1000;
private long lastClickTime = 0;
public abstract void onNoDoubleClick(View v);
@Override
public void onClick(View v) {
long currentTime = Calendar.getInstance().getTimeInMillis();
if (currentTime - lastClickTime > MIN_CLICK_DELAY_TIME) {
lastClickTime = currentTime;
onNoDoubleClick(v);
}
}
}
然后我打算這樣來(lái)弄
btn.setOnClickListener(new NoDoubleClickListener() {
@Override
public void onNoDoubleClick(View v) {
//something
}
});
可是面臨一個(gè)問(wèn)題,有這么多,改到何年馬月???

OK,這個(gè)是我以前碰到的一個(gè)好蛋疼的問(wèn)題,再比如,在不侵入業(yè)務(wù)代碼的情況下監(jiān)聽所有的點(diǎn)擊事件并記錄所有的點(diǎn)擊數(shù),用于統(tǒng)計(jì)熱點(diǎn)頁(yè)面和其他一些分析工作,你怎么辦呢?現(xiàn)在介紹一個(gè)如何Hook掉View的onClick方法,相對(duì)與上一篇,這個(gè)很簡(jiǎn)單了。
1、第一步尋找Hook點(diǎn):
去看setOnClickListener里面做了什么?
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
/**
* Register a callback to be invoked when this view is clicked. If this view is not
* clickable, it becomes clickable.
*
* @param l The callback that will run
*
* @see #setClickable(boolean)
*/
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
看完了上面,就能猜到我們?cè)O(shè)置的Listener最終是被賦值給ListenerInfo的mOnClickListener成員了,ListenerInfo的實(shí)例可以說(shuō)是信息的載體,那么很簡(jiǎn)單,只要把mOnClickListener替換掉,在ListenerInfo中還有mOnLongClickListener,mOnFocusChangeListener兩個(gè)成員,分別對(duì)應(yīng)了長(zhǎng)按事件與焦點(diǎn)變化事件,所以處理長(zhǎng)按事件與焦點(diǎn)變化事件與此類似。
public class HookViewClickUtil {
public static HookViewClickUtil getInstance() {
return UtilHolder.mHookViewClickUtil;
}
private static class UtilHolder {
private static HookViewClickUtil mHookViewClickUtil = new HookViewClickUtil();
}
public static void hookView(View view) {
try {
Class viewClazz = Class.forName("android.view.View");
//事件監(jiān)聽器都是這個(gè)實(shí)例保存的
Method listenerInfoMethod = viewClazz.getDeclaredMethod("getListenerInfo");
if (!listenerInfoMethod.isAccessible()) {
listenerInfoMethod.setAccessible(true);
}
Object listenerInfoObj = listenerInfoMethod.invoke(view);
Class listenerInfoClazz = Class.forName("android.view.View$ListenerInfo");
Field onClickListenerField = listenerInfoClazz.getDeclaredField("mOnClickListener");
if (!onClickListenerField.isAccessible()) {
onClickListenerField.setAccessible(true);
}
View.OnClickListener mOnClickListener = (View.OnClickListener) onClickListenerField.get(listenerInfoObj);
//自定義代理事件監(jiān)聽器
View.OnClickListener onClickListenerProxy = new OnClickListenerProxy(mOnClickListener);
//更換
onClickListenerField.set(listenerInfoObj, onClickListenerProxy);
} catch (Exception e) {
e.printStackTrace();
}
}
//自定義的代理事件監(jiān)聽器
private static class OnClickListenerProxy implements View.OnClickListener {
private View.OnClickListener object;
private int MIN_CLICK_DELAY_TIME = 1000;
private long lastClickTime = 0;
private OnClickListenerProxy(View.OnClickListener object) {
this.object = object;
}
@Override
public void onClick(View v) {
//點(diǎn)擊時(shí)間控制
long currentTime = Calendar.getInstance().getTimeInMillis();
if (currentTime - lastClickTime > MIN_CLICK_DELAY_TIME) {
lastClickTime = currentTime;
Log.e("OnClickListenerProxy", "OnClickListenerProxy");
if (object != null) object.onClick(v);
}
}
}
}
使用起來(lái)也是非常簡(jiǎn)單,首先在MainActivity的View渲染完畢的時(shí)候進(jìn)行注入,即在 getWindow().getDecorView().post()中。
public class MainActivity extends Activity {
private Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final View btn = findViewById(R.id.btn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("MainActivity","Button 被點(diǎn)擊了");
}
});
getWindow().getDecorView().post(new Runnable() {
@Override
public void run() {
HookViewClickUtil.hookView(btn);
}
});
}
}
執(zhí)行結(jié)果:

OK,到此完成了,至于怎么獲取頁(yè)面的所有View,調(diào)用 HookViewClickUtil.hookView(view),就不多說(shuō)了。
Please accept mybest wishes for your happiness and success !