Toast源码分析及消息内容hook

  • 最近发现在小米高系统版本的手机上,Toast的内容会自带应用名称的前缀;百度一下,发现的确不少这些反馈(万恶的小米系统开发...),看了几篇解决这个问题的文章,基本如下:
Toast toast = Toast.makeText(context, “”, Toast.LENGTH_LONG);
toast.setText(message);
toast.show();
  • 但是如果我们的项目中,有几十个地方用到了Toast,那就要在几十个地方都去修改,这样太麻烦了,能不能在一个地方做处理,其他地方都不用修改呢。

先查看Toast类的源码:

1、makeText()

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    return makeText(context, null, text, duration);//调用makeText的重载方法,Looper传入为null
}


public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
    //调用Toast的构造方法,先创建一个Toast的实例
   Toast result = new Toast(context, looper);

   //填充id为transient_notification的layout页面,获取id为message的TextView,设置内容为text
   LayoutInflater inflate = (LayoutInflater)
       context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
   TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
   tv.setText(text);

   //把Toast中的mNextView变量赋值为v,这句很重要,后面hook的时候会用到
   result.mNextView = v;
   result.mDuration = duration;

   return result;
}

makeText()第一步就是创建一个Toast实例。

1.1 Toast的构造方法

    public Toast(Context context) {
        this(context, null);
    }

    //Toast的构造方法就是初始化mTN变量(mTN在show()方法中会用到),配置Toast的layout参数
    public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        mTN = new TN(context.getPackageName(), looper);
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
    }

private static class TN extends ITransientNotification.Stub {

    //TN的构造方法就是配置Toast的layout参数
    TN(String packageName, @Nullable Looper looper) {
            // XXX This should be changed to use a Dialog, with a Theme.Toast
            // defined that sets up the layout params appropriately.
            final WindowManager.LayoutParams params = mParams;
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.format = PixelFormat.TRANSLUCENT;
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            params.setTitle("Toast");
            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

            mPackageName = packageName;

            if (looper == null) {
                // Use Looper.myLooper() if looper is not specified.
                looper = Looper.myLooper();
                if (looper == null) {
                    throw new RuntimeException(
                            "Can't toast on a thread that has not called Looper.prepare()");
                }
            }
            mHandler = new Handler(looper, null) {

                ...
            };
     }
}

分析:

  • makeText()方法首先创建一个Toast的实例。
  • Toast的构造方法中会配置好Toast展示所需要的layout参数
  • 创建好Toast后,会填充id为transient_notification的layout布局,实例成View实例,这个View也就是我们能看到的Toast,layout中包含了一个id为message的TextView,给TextView设置内容为传递进来的text。
  • 最后再把mNextView变量赋值为上一步填充形成的View;这个mNextView最后调用show()方法时会用到。

2、show()

    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        //通过getService()方法获取INotificationManager 的实例
        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        //把makeText方法中实例的view赋值给tn的mNextView变量
        tn.mNextView = mNextView;

        try {
            //调用INotificationManager的enqueueToast的方法
            //参数tn的mNextView为View布局,包含了TextView的子控件
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

    //获取INotificationManager 的实例,非空判断确保了sService为单例
    static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }
  • 首先先通过调用 getService()获取INotificationManager的实例;
  • getService()方法返回sService的单例实例;
  • 通过调用INotificationManager的实例service的enqueueToast方法来展示toast。

小结:展示一个Toast,主要经过4个步骤:
1、创建一个Toast实例,同时创建Toast的内部类TN的实例,配置Toast展示时需要的layout参数。
2、填充Toast展示所需要的layout布局,实例化为View类型,然后把view中的TextView控件设置我们传入的text内容文字。
3、把上一步实例出来的view,设置为给TN类的mNextView 变量。
4、通过getService()方法获取INotificationManager实例,调用INotificationManager的enqueueToast方法。

3、hook消息内容

  • 先找到需要hook的对象(最好是个单例对象,这样可以实现无侵入修改)。
  • 然后找到hook对象的持有者(在这里也就是找到Toast类中指向这个被hook的对象的全局变量)。
  • 创建hook对象的代理类,并新建这个代理类的实例。
  • 用代理类的实例替换原先需要hook的对象。
3.1、先确定要hook的对象
    final TN mTN;

    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        return makeText(context, null, text, duration);
    }

    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        Toast result = new Toast(context, looper);

        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        tv.setText(text);

        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }

    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

    private static INotificationManager sService;

    static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }

我们的目的是修改text的内容,而text是设置给TextView控件,也就是我们最终需要hook TextView对象,或者hook TextView对象的持有者或间接持有者(通过TextView对象的持有者获取到TextView,来实现对TextView内容的修改)。

  1. 首先TextView是局部变量,没办法hook。
  2. 而TextView的持有者View被全局变量mNextView所持有,这是一个可以hook的切入点。
  3. 从show()方法中可以看到,mTN的mNextView变量也指向了View,所以mTN也是TextView的持有者,这也是一个可以hook的切入点。
  4. show()方法中,tn对象作为参数传入了enqueueToast方法中,也就是service对象间接持有了tn对象,service间接持有TextView对象,service也是一个可以hook的切入点。

从上面四点,我们可以找到三个可以hook的切入点,而最佳的hook的对象service,因为service对象持有者是sService变量,sService是个单例,多个的Toast对象中我们都只需要替换一次。

3.2创建hook对象的代理类
public class ToastProxy implements InvocationHandler {

    private static final String TAG = "ToastProxy";
    private Object mProxyObject;
    private Context mContext;

    public ToastProxy( Context mContext, Object mProxyObject) {
        this.mContext = mContext;
        this.mProxyObject = mProxyObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Log.i(TAG, "invoke: method == " + method.getName());

        //对Toast类中的INotificationManager实例sService执行enqueueToast方法时,进行拦截操作
        if (method.getName().equals("enqueueToast")){
            if (args != null && args.length > 0) {
                try{
                    //获取tn对象
                    Object tn = args[1];
                    //获取mNextView变量,也就是View, 对应的是LinearLayout对象
                    Field mNextView = tn.getClass().getDeclaredField("mNextView");
                    mNextView.setAccessible(true);
                    LinearLayout linearLayout = (LinearLayout) mNextView.get(args[1]);

                    //从对应的是LinearLayout对象中获取TextView对象
                    if (linearLayout.getChildAt(0) instanceof TextView){
                        TextView textView = (TextView) linearLayout.getChildAt(0);
                        String msgOfToast = textView.getText().toString();//这个就是Toast的内容
                        String appName = mContext.getString(R.string.app_name);
                        if (msgOfToast.contains(appName)){
                            String content = msgOfToast.substring(appName.length() + 1);
                            textView.setText(content);
                        }
                    }
                }catch (NoSuchFieldException e){
                    e.printStackTrace();
                }
            }
        }
        return method.invoke(mProxyObject, args);
    }

}

判断方法名,拦截enqueueToast方法的逻辑,获取到TextView对象,修改文字内容。

3.3、替换需要hook的对象
public class ToastUtil {

    public static void hookToast(Context ctx){
        Looper.prepare();
        Toast toast = new Toast(ctx);
        try {
            Method getService = toast.getClass().getDeclaredMethod("getService");
            getService.setAccessible(true);
            //实例化INotificationManager
            Object sService = getService.invoke(toast);
            ToastProxy toastProxy = new ToastProxy(ctx, sService);
            //创建hook对象的代理类实例
            Object serviceProxy = Proxy.newProxyInstance(toast.getClass().getClassLoader(), sService.getClass().getInterfaces(), toastProxy);
            Field sServiceField = toast.getClass().getDeclaredField("sService");
            sServiceField.setAccessible(true);
            //替换Toast类中已经初始化的单例对象sService
            sServiceField.set(toast, serviceProxy);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

}

创建Toast实例,同时实例化INotificationManager的单例sService;再创建hook的对象service的代理类实例,替换sService所指向的对象。然后在我们业务代码使用Toast前,执行hookToast方法即可。

总结:hook可以帮我们在某些特定的切入点中无侵入式的完成一些代码逻辑的修改;可以在不改变原有的代码业务,插入一些特定的代码业务。而实现hook,只需要我们根据需求,从源代码中找到最佳可以hook的对象,通过反射等代码即可实现。

你可能感兴趣的:(Toast源码分析及消息内容hook)