最近在做一个无埋点项目,一开始的方案是要做运行时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 SparseArray<String> idNames = new SparseArray<String>();
/**
*反射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<viewGroup.getChildCount();i++){
View childView = viewGroup.getChildAt(i);
try {
injectClickListener(childView);
}catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
/**
*注入自己的clickListener
**/
private static void injectClickListener(View view) throws IllegalAccessException {
if (view == null){
return;
}
Object info = listenerInfoField.get(view);
Object listenerObj = null;
try {
listenerObj = clickListenerField.get(info);
}catch (NullPointerException e){
e.printStackTrace();
}
if (listenerObj == null){
Log.e("SSSS","listenerObj null:"+view.getClass().getName());
return;
}
View.OnClickListener proxy = new ClickListenerProxy((View.OnClickListener) listenerObj);
clickListenerField.set(info,proxy);
}
至此编码完成,这种方式不仅可以对我们平时调用setOnClickListener方法的事件拦截,可以对xml中定义的OnClick事件拦截,只是因为遍历view树,并较多的使用了反射,所以导致性能上会略微差点。