我学安卓——运行时hook之onClickListener

最近在做一个无埋点项目,一开始的方案是要做运行时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树,并较多的使用了反射,所以导致性能上会略微差点。

转载于:https://my.oschina.net/tnjin/blog/1408160

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值