Hook初体验

324 阅读2分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

Hook - View的点击事件

button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "" + ((Button) v).getText(), Toast.LENGTH_SHORT).show();
            }
        });

上面代码很简单,定义button组件,并且设置点击监听器,打印button上的文字.现在的需求是不修改上面的代码情况下,通过Hook((Button) v).getText()的内容修改.

View.java 首先要知道setOnClickListener设置的值传到哪里。

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;
}


static class ListenerInfo {
    //设置的OnClickListener,最后赋值在这里
    public OnClickListener mOnClickListener;
}

通过阅读View的源码可以知道,setOnClickListener方法是把OnClickListener参数的值设置给getListenerInfo()返回对象的mOnClickListener属性中.

要做到动态修改代码的功能,可以使用动态代理,或者ASM技术。这里就使用动态代理

        Class mViewClass = Class.forName("android.view.View");
        Method getListenerInfoMethod = mViewClass.getDeclaredMethod("getListenerInfo");
        getListenerInfoMethod.setAccessible(true); // 设置为public
        // 执行方法
        Object  mListenerInfo = getListenerInfoMethod.invoke(view);

        // 因为ListenerInfo是静态类
        Class mListenerInfoClass = Class.forName("android.view.View$ListenerInfo");

        Field mOnClickListenerField = mListenerInfoClass.getField("mOnClickListener");

        final Object mOnClickListenerObj = mOnClickListenerField.get(mListenerInfo); 

要实现动态代理,最重要的一步是拿到原来的mOnClickListener,首先通过Class.forName("android.view.View"),拿到ViewClass,然后调用它的getListenerInfo方法,因为它是私有的,所以必须setAccessible(true),然后反射获取到ListenerInfo实例,因为ListenerInfo是静态类,所以获取它的class时,需要添加$,如Class.forName("android.view.View$ListenerInfo"),获取到ListenerInfoclass之后,就可以通过 mListenerInfoClass.getField("mOnClickListener")获取到属性mOnClickListener的值了。

通过上面几步,已经获取到当前button设置的listener了,现在要做的时在原来的listener包装一层,狸猫换太子

Object mOnClickListenerProxy = Proxy.newProxyInstance(MainActivity.class.getClassLoader(), // 1加载器

                new Class[]{View.OnClickListener.class}, // 2要监听的接口,监听什么接口,就返回什么接口

                new InvocationHandler() { // 3监听接口方法里面的回调

                 
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Log.d("hook", "拦截到了 OnClickListener的方法了");
                        Button button = new Button(MainActivity.this);
                        button.setText("Rc在努力....");

                        // 让系统程序片段 --- 正常继续的执行下去
                        return method.invoke(mOnClickListenerObj, button);
                    }
                });

通过动态代理可以拦截到实现OnClickListener接口的所有方法,因为OnClickListener接口只有一个方法onClick,所以在这里不需要对是哪个方法进行判断,然后修改onClick回调的参数,最后把method.invoke(mOnClickListenerObj, button);的结果返回.

最后一步,需要把狸猫换太子的OnClickListener进行替换.

  mOnClickListenerField.set(mListenerInfo, mOnClickListenerProxy); // 替换的 我们自己的动态代理

最后贴上完整代码

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = (Button) findViewById(R.id.button);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "" + ((Button) v).getText(), Toast.LENGTH_SHORT).show();
            }
        });




        // 在不修改以上代码的情况下,通过Hook把 ((Button) v).getText() 内容给修改
        try {
            hook(button); // button就是View对象
        } catch (Exception e) {
            e.printStackTrace();

            Toast.makeText(this, "Hook失败" + e.toString(), Toast.LENGTH_SHORT).show();
        }
    }

    private void hook(View view) throws Exception {

        Class mViewClass = Class.forName("android.view.View");
        Method getListenerInfoMethod = mViewClass.getDeclaredMethod("getListenerInfo");
        getListenerInfoMethod.setAccessible(true); 
        // 执行方法
        Object  mListenerInfo = getListenerInfoMethod.invoke(view);

        // 替 换  public OnClickListener mOnClickListener; 替换我们自己的
        Class mListenerInfoClass = Class.forName("android.view.View$ListenerInfo");

        Field mOnClickListenerField = mListenerInfoClass.getField("mOnClickListener");

        final Object mOnClickListenerObj = mOnClickListenerField.get(mListenerInfo); 

        Object mOnClickListenerProxy = Proxy.newProxyInstance(MainActivity.class.getClassLoader(), // 1加载器

                new Class[]{View.OnClickListener.class}, // 2要监听的接口,监听什么接口,就返回什么接口

                new InvocationHandler() { // 3监听接口方法里面的回调

            
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 加入了自己逻辑
                        Log.d("hook", "拦截到了 OnClickListener的方法了");
                        Button button = new Button(MainActivity.this);
                        button.setText("Rc在努力....");

                        // 让系统程序片段 --- 正常继续的执行下去
                        return method.invoke(mOnClickListenerObj, button);
                    }
                });



        // 狸猫换太子 把系统的 mOnClickListener  换成 我们自己写的 动态代理
        mOnClickListenerField.set(mListenerInfo, mOnClickListenerProxy); // 替换的 我们自己的动态代理
    }
}

获取Application

在组件化中,在组件中获取application还是相对困难的,通过反射就能获取全局Application对象的方式相比于在Application#OnCreate保存一份的方式更加通用了。

通过查看ActivityThread方法,会发现currentApplication是它的静态方法,这为反射调用获取Application提供可能

ActivityThread.java

public static Application currentApplication() {
    ActivityThread am = currentActivityThread();
    return am != null ? am.mInitialApplication : null;
}
object AppGlobals {
    private var application: Application? = null
    fun get(): Application? {
        if (application == null) {
            try {
                application = Class.forName("android.app.ActivityThread")
                    .getMethod("currentApplication")
                    .invoke(null) as Application
            } catch (ex: Exception) {
                ex.printStackTrace()
            }
        }
    return application
}
}