最近在做一个无埋点项目,一开始的方案是要做运行时hook,后来改成了编译期hook,但是我认为运行时的hook还是有技术场景的,所以分享一下。
关于hook
其实所谓的hook,并没有想的那么高深。hook本来是钩子的意思,以前在windows平台上做东西,经常需要通过某种手段去改变系统API的一个行为,把系统的某个方法或者某个属性指向他处,从而改变系统的工作流程,这是我最早接触的hook技术。然而java中,一般来说,不需要这么底层,只需要将原本的某个对象A,替换为我们的另一个对象B就可以,原本由A执行的逻辑,交给了B来执行,而对此常用的具体技术就是反射和代理。 我们使用hook技术,主要是关心hook点在哪,而这需要对源码有一定的了解。
获取view的OnClickListener
我们想要更改OnClickListener,那么首先我们要能获取到view的OnClickListener才行,很遗憾,view并没有提供getOnClickListener的方法,那么就只能看看源码了。 我们从setOnClickListen这个方法开始看,
public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; }
可以看到是交给了getListenerInfo方法返回的对象,由此跟踪到View中有个ListenerInfo的属性,即mListenerInfo,它才是OnClickListener即mOnClickListener的真正持有者———这就是我们要hook的点了。
编写自己的OnClickListener
这部分没什么好说的,看代码就行:
/** * clickListener的代理类 */ static class ClickListenerProxy implements View.OnClickListener{ private View.OnClickListener onClickListener; public ClickListenerProxy(View.OnClickListener onClickListener) { this.onClickListener = onClickListener; } @Override public void onClick(View v) { //执行自己的逻辑 Log.e("SSSS","proxy:"+fetchIDName(v.getContext(),v.getId())); //执行原来的逻辑 if(onClickListener != null){ onClickListener.onClick(v); } } }
对于方法fetchIDName的定义如下:
//id与id名称的对应private static final SparseArrayidNames = new SparseArray ();/***反射R类,获取对应id的id名称**/public static String fetchIDName(Context context, int idValue){ initIdNames(context.getPackageName()); String name = idNames.get(idValue); return name; } private static void initIdNames(String packageName){ if(idNames.size()>0){ return; } try { Class idClz = Class.forName(packageName + ".R$id"); Field[] fields = idClz.getDeclaredFields(); if (fields == null || fields.length == 0) { return ; } for (Field field : fields) { if (field == null) { continue; } field.setAccessible(true); try { idNames.put((Integer) field.get(null), field.getName()); }catch (Exception e){ e.printStackTrace(); } } }catch (Exception e){ e.printStackTrace(); } }
查找所有view,并替换原来绑定的OnClickListern
这一部分就是遍历ViewTree来查找view,并更改其中的ListenerInfo对象的OnClickListener属性。
反射获取hook点
/***获取View的Class,ListenerInfo的class及属性**/private static void initOnce() throws ClassNotFoundException, NoSuchFieldException { if(viewClass == null) { viewClass = Class.forName("android.view.View"); } if (listenerInfoField == null) { listenerInfoField = viewClass.getDeclaredField("mListenerInfo"); listenerInfoField.setAccessible(true); } if(listenerInfoClass == null) { listenerInfoClass = Class.forName("android.view.View$ListenerInfo"); } if(clickListenerField == null) { clickListenerField = listenerInfoClass.getDeclaredField("mOnClickListener"); clickListenerField.setAccessible(true); } }
遍历viewTree,逐一hook
public static void injectListerner(Activity activity) throws NoSuchFieldException, ClassNotFoundException { initOnce();//获取hook点 View view = getContentView(activity); if (view instanceof ViewGroup){ ViewGroup viewGroup = (ViewGroup)view; for(int i = 0;i
至此编码完成,这种方式不仅可以对我们平时调用setOnClickListener方法的事件拦截,可以对xml中定义的OnClick事件拦截,只是因为遍历view树,并较多的使用了反射,所以导致性能上会略微差点。