美文网首页学习之鸿蒙&Android学习知识点
Android中的“沙雕”操作之hook Toast

Android中的“沙雕”操作之hook Toast

作者: Camellia666 | 来源:发表于2021-07-07 17:33 被阅读0次

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
    本文链接:https://www.jianshu.com/p/a47bcf62109c

    一,背景

    这是个沙雕操作,原因是:在小米手机的部分机型上,弹Toast时会在吐司内容前面带上app名称,如下:


    1.gif

    此时产品经理发话了:为了统一风格,在小米手机上去掉Toast前的应用名。

    网上有以下解决方案,比如:先给toastmessage设置为空,然后再设置需要提示的message,如下:

    Toast toast = Toast.makeText(context, “”, Toast.LENGTH_LONG);
    toast.setText(message);
    toast.show();
    

    但这些都不能从根本上解决问题,于是Hook Toast的方案诞生了。

    二,分析

    首先分析一下Toast的创建过程.

    Toast的简单使用如下:

    Toast.makeText(this,"abc",Toast.LENGTH_LONG).show();
    

    1,构造toast

    通过makeText()构造一个Toast,具体代码如下:

    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
            Toast result = new Toast(context, looper);
            result.mText = text;
            result.mDuration = duration;
            return result;
        } else {
            Toast result = new Toast(context, looper);
            View v = ToastPresenter.getTextToastView(context, text);
            result.mNextView = v;
            result.mDuration = duration;
    
            return result;
        }
    }
    

    makeText()中也就是设置了时长以及要显示的文本或自定义布局,对Hook没有什么帮助。

    2,展示toast

    接着看下Toast的show():

    public void show() {
        ...
    
        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;
        final int displayId = mContext.getDisplayId();
    
        try {
            if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
                if (mNextView != null) {
                    // It's a custom toast
                    service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
                } else {
                    // It's a text toast
                    ITransientNotificationCallback callback =
                            new CallbackBinder(mCallbacks, mHandler);
                    service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback);
                }
            } else {
                // 展示toast
                service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
            }
        } catch (RemoteException e) {
            // Empty
        }
    }
    

    代码很简单,主要是通过serviceenqueueToast()enqueueTextToast()两种方式显示toast。

    service是一个INotificationManager类型的对象,INotificationManager是一个接口,这就为动态代理提供了可能。

    service是在每次show()时通过getService()获取,那就来看看getService():

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    private static INotificationManager sService;
    
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(
                ServiceManager.getService(Context.NOTIFICATION_SERVICE));
        return sService;
    }
    

    getService()最终返回的是sService,是一个懒汉式单例,因此可以通过反射获取到其实例。

    3,小结

    sService是一个单例,可以反射获取到其实例。

    sService实现了INotificationManager接口,因此可以动态代理。

    因此可以通过Hook来干预Toast的展示。

    三,撸码

    理清了上面的过程,实现就很简单了,直接撸码:

    1,获取sService的Field

    Class<Toast> toastClass = Toast.class;
    
    Field sServiceField = toastClass.getDeclaredField("sService");
    sServiceField.setAccessible(true);
    

    2,动态代理替换

    Object proxy = Proxy.newProxyInstance(Thread.class.getClassLoader(), new Class[]{INotificationManager.class}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            
            return null;
        }
    });
    // 用代理对象给sService赋值
    sServiceField.set(null, proxy);
    

    3,获取sService原始对象

    因为动态代理不能影响被代理对象的原有流程,因此需要在第二步的InvocationHandler()invoke()中需要执行原有的逻辑,这就需要获取sService的原始对象。

    前面已经获取到了sService的Field,它是静态的,那直接通过sServiceField.get(null)获取不就可以了?然而并不能获取到,这是因为整个Hook操作是在应用初始化时,整个应用还没有执行过Toast.show()的操作,因此sService还没有初始化(因为它是一个懒汉单例)。

    既然不能直接获取,那就通过反射调用一下:

    Method getServiceMethod = toastClass.getDeclaredMethod("getService", null);
    getServiceMethod.setAccessible(true);
    Object service = getServiceMethod.invoke(null);
    

    接着完善一下第二步代码:

    Object proxy = Proxy.newProxyInstance(Thread.class.getClassLoader(), new Class[]{INotificationManager.class}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            
            return method.invoke(service, args);
        }
    });
    

    到此,已经实现了对Toast的代理,Toast可以按照原始逻辑正常执行,但还没有加入额外逻辑。

    4,添加Hook逻辑

    InvocationHandlerinvoke()方法中添加额外逻辑:

    Object proxy = Proxy.newProxyInstance(Thread.class.getClassLoader(), new Class[]{INotificationManager.class}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 判断enqueueToast()方法时执行操作
            if (method.getName().equals("enqueueToast")) {
                Log.e("hook", method.getName());
                getContent(args[1]);
            }
            return method.invoke(service, args);
        }
    });
    

    args数组的第二个是TN类型的对象,其中有一个LinearLayout类型的mNextView对象,mNextView中有一个TextView类型的childView,这个childView就是展示toast文本的那个TextView,可以直接获取其文本内容,也可以对其赋值,因此代码如下:

    private static void getContent(Object arg) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        // 获取TN的class
        Class<?> tnClass = Class.forName(Toast.class.getName() + "$TN");
        // 获取mNextView的Field
        Field mNextViewField = tnClass.getDeclaredField("mNextView");
        mNextViewField.setAccessible(true);
        // 获取mNextView实例
        LinearLayout mNextView = (LinearLayout) mNextViewField.get(arg);
        // 获取textview
        TextView childView = (TextView) mNextView.getChildAt(0);
        // 获取文本内容
        CharSequence text = childView.getText();
        // 替换文本并赋值
        childView.setText(text.toString().replace("HookToast:", ""));
        Log.e("hook", "content: " + childView.getText());
    }
    

    最后看一下效果:


    2.gif

    四,总结

    这个一个沙雕操作,实际应用中这种需求也比较少见。通过Hook的方式可以统一控制,而且没有侵入性。大佬勿喷!!!

    相关文章

      网友评论

        本文标题:Android中的“沙雕”操作之hook Toast

        本文链接:https://www.haomeiwen.com/subject/xafaultx.html