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的对象,通过反射等代码即可实现。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容