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

版权声明:本文为博主原创文章,遵循 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的方式可以统一控制,而且没有侵入性。大佬勿喷!!!

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

推荐阅读更多精彩内容

  • 最近发现在小米高系统版本的手机上,Toast的内容会自带应用名称的前缀;百度一下,发现的确不少这些反馈(万恶的小米...
    zl_adams阅读 560评论 1 4
  • 最近在研究插件化开发,插件化开发的基础就是hook技术,现在市面上存在的各种插件化框架,其基础原理都是使用hook...
    在路上的_软件菜鸟阅读 2,411评论 0 1
  • 什么是土司(Toast)? Toast是Android系统提供的一种非常好的提示方式,在程序中可以使用它将一些短小...
    一航jason阅读 1,054评论 0 1
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,531评论 28 53
  • 信任包括信任自己和信任他人 很多时候,很多事情,失败、遗憾、错过,源于不自信,不信任他人 觉得自己做不成,别人做不...
    吴氵晃阅读 6,187评论 4 8