别让APP名字和图标毁了你的Toast!一招教你Android优化技巧

11 阅读13分钟

别让APP名字和图标毁了你的Toast!一招教你Android优化技巧

为啥要去掉 Toast 里的 APP 名字和图标

在如今这个看脸的时代,APP 的颜值也至关重要。统一、美观的 UI 设计,就像给 APP 穿上了一件漂亮的外衣,不仅能提升用户体验,还能让 APP 在众多竞争对手中脱颖而出。

大家在使用 APP 的时候,应该都遇到过 Toast 消息提示吧。这是一种轻量级的消息提示框,通常出现在屏幕底部,用来告知用户一些操作结果或者系统状态。但是,不知道大家有没有注意到,在某些手机上,比如小米手机,Toast 消息会自带 APP 的名字和图标。这在一些情况下,可能会破坏 APP 整体的 UI 风格。

想象一下,你精心设计了一套简洁、大气的 UI 界面,所有的元素都搭配得恰到好处。结果,突然弹出一个 Toast 消息,上面突兀地显示着 APP 的名字和图标,就像一颗老鼠屎坏了一锅粥,瞬间打破了整个界面的美感和协调性。这对于追求完美的开发者和用户来说,简直是不能忍受的。

再比如,有些 APP 为了突出自身的品牌特色,会采用独特的颜色、字体和图标设计。而 Toast 消息自带的 APP 名字和图标,可能与整体的设计风格不匹配,显得格格不入。这不仅会影响用户对 APP 的视觉感受,还可能降低用户对 APP 的好感度和信任度。

所以,为了让 APP 的 UI 风格更加统一、美观,很多开发者都希望能够去掉 Toast 消息中自带的 APP 名字和图标。那么,这该怎么做呢?接下来,就让我们一起探索一下去掉 Android Toast 消息中自带 APP 名字和图标的方法。

Toast 简单介绍

在 Android 开发的世界里,Toast 就像是一个贴心的小助手,默默地为用户提供各种提示信息 。当你在 APP 里进行一些操作,比如点击某个按钮提交表单、删除文件、切换页面等等,操作完成后,屏幕底部可能会突然弹出一个小框,上面显示着 “操作成功”“文件已删除”“页面切换中” 之类的简短文字,这个小框就是 Toast。它就像一个短暂出现的小气泡,轻轻地告诉你刚刚发生了什么,不会打断你的操作流程,也不会占据太多屏幕空间,等你看完信息,它就会自动消失,是不是很方便呢?

一般情况下,我们看到的 Toast 消息都比较简洁,只有文字内容。但在某些手机系统中,比如小米手机的 MIUI 系统,Toast 消息就会变得 “丰富” 起来。除了原本的提示文字,还会在左侧显示 APP 的图标,右侧显示 APP 的名称。比如你在小米手机上使用微信,发送消息成功后,弹出的 Toast 就会有微信的绿色图标和 “微信” 两个字,再加上 “消息已发送” 的提示文字 。这种设计本意可能是为了强化 APP 的品牌标识,让用户更清楚地知道这个提示来自哪个应用。但有时候,它也会带来一些小麻烦,这也是我们想要去掉它的原因。

网上常见方法及为啥不行

(一)常规修改方法展示

在解决这个问题的探索过程中,我们先来看看网上比较常见的一种方法。这种方法看似简单直接,就是先把 Toast 消息的内容设置为空,然后再设置成我们真正想要显示的内容 。从代码实现角度来看,就像下面这样:


Toast toast = Toast.makeText(context, "", Toast.LENGTH_SHORT);
toast.setText("这是真正的提示内容");
toast.show();

这段代码乍一看没什么问题,逻辑也很清晰。先创建一个 Toast 对象,把它的消息内容设为空字符串,然后再用setText方法把真正要展示给用户的提示信息设置进去,最后调用show方法让这个 Toast 消息显示出来。按照这个思路,是不是就能把 Toast 消息里自带的 APP 名字和图标去掉了呢?但实际情况往往没有这么简单。

(二)分析无效原因

虽然上面的方法看似合理,但在实际运行时,却无法从根本上解决问题。这背后的原因,要从 Toast 的工作原理和系统的底层机制说起 。

Toast 是 Android 系统提供的一种轻量级提示机制,它的显示是由系统服务来管理的。当我们调用Toast.makeText方法创建一个 Toast 对象时,系统会对这个对象进行一系列的处理和配置。而 APP 名字和图标在某些系统中被添加到 Toast 消息里,是系统在这个处理过程中就已经决定好的,并且这些设置在系统的底层有一些固定的逻辑和限制 。

上面那种先设置空内容再设置真实内容的方法,只是在应用层面对 Toast 消息的文本内容进行了操作。它并没有改变系统对 Toast 消息整体的配置和处理方式,无法突破系统底层对 APP 名字和图标显示的设置。也就是说,这种方法只是改变了 Toast 消息中的文字部分,而对于系统已经 “默认” 要显示的 APP 名字和图标,它无能为力 。所以,即便我们按照这种方法去写代码,运行之后还是会发现,Toast 消息里的 APP 名字和图标依然顽固地显示在那里,并没有如我们所愿地消失。这也让我们意识到,要彻底解决这个问题,需要从更深入的层面去思考和探索,找到能够突破系统底层限制的方法 。

Hook Toast 方案详细解析

(一)Toast 创建过程剖析

我们先来深入剖析一下 Toast 的创建过程,这有助于我们理解为什么要采用 Hook 方案以及如何实施 Hook。当我们在 Android 代码中创建一个 Toast 时,最常用的方法就是Toast.makeText()。下面我们结合具体代码来看看这个方法的内部实现 :


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方法主要做了两件事:一是根据不同的条件创建一个Toast对象,二是设置要显示的文本内容text和显示时长duration 。但这个方法对于我们想要进行的 Hook 操作并没有直接的帮助,它只是完成了Toast对象的基本构造,并没有涉及到与 APP 名字和图标相关的设置 。

当我们调用Toastshow方法来展示这个提示框时,才真正涉及到关键的逻辑。下面是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
    }
}

show方法中,首先通过getService方法获取到一个INotificationManager类型的service对象 。这个service对象非常关键,它负责将Toast相关的信息发送给系统服务,从而实现Toast的显示 。这里的INotificationManager是一个接口,它定义了一系列与通知管理相关的方法,比如enqueueToastenqueueTextToast方法 。这两个方法用于将Toast加入到系统的显示队列中,其中enqueueToast用于普通的Toast显示,enqueueTextToast用于只包含文本的Toast显示 。而这个接口的存在,为我们后续使用动态代理进行 Hook 操作提供了可能 。因为我们可以通过动态代理创建一个实现了INotificationManager接口的代理对象,然后在代理对象中拦截相关方法的调用,从而实现对Toast显示的干预 。

(二)Hook 实现步骤

了解了 Toast 的创建过程后,接下来我们就可以着手实现 Hook 操作了。具体步骤如下 :

  1. 获取 sService 的 FieldINotificationManager类型的service对象在Toast类中是以静态成员变量sService的形式存在的。我们需要通过反射机制获取到这个Field,以便后续对其进行操作 。代码如下 :

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

这里使用Class.forName方法获取Toast类的Class对象,然后通过getDeclaredField方法获取名为sServiceField对象 。由于sService是私有的成员变量,所以需要调用setAccessible(true)方法来设置其可访问性,这样我们才能在外部访问和修改它 。反射机制在这里起到了关键作用,它允许我们在运行时获取和操作类的私有成员,突破了 Java 语言的访问限制 。

  1. 动态代理替换: 获取到sServiceField后,我们就可以使用动态代理来创建一个代理对象,替换原有的sService 。动态代理是 Java 的一项强大特性,它可以在运行时动态地创建一个实现了指定接口的代理类,并且可以在代理类中拦截方法的调用,执行我们自定义的逻辑 。下面是创建动态代理并替换sService的代码 :

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;
            }
        });
sServiceField.set(null, proxy);

在这段代码中,Proxy.newProxyInstance方法用于创建动态代理对象 。它接收三个参数:第一个参数是类加载器,这里使用Thread.currentThread().getContextClassLoader()获取当前线程的上下文类加载器;第二个参数是一个接口数组,指定代理对象要实现的接口,这里是INotificationManager.class;第三个参数是一个InvocationHandler对象,它定义了代理对象的方法调用处理逻辑 。在这个例子中,我们暂时返回null,后续会在InvocationHandlerinvoke方法中添加具体的 Hook 逻辑 。最后,通过sServiceField.set(null, proxy)将代理对象赋值给sService,完成替换 。

  1. 获取 sService 原始对象: 虽然我们创建了代理对象并替换了sService,但在实际的 Hook 逻辑中,我们往往需要调用原始sService对象的方法,以保证原有功能的正常执行 。所以我们需要获取到sService的原始对象 。由于在 Hook 操作时,sService可能还没有被初始化(因为它是一个懒汉式单例,在第一次调用getService方法时才会初始化),所以不能直接通过sServiceField.get(null)获取 。我们可以通过反射调用getService方法来获取原始对象 。代码如下 :

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

这里首先通过toastClass.getDeclaredMethod("getService", null)获取getService方法的Method对象,然后设置其可访问性,最后通过getServiceMethod.invoke(null)反射调用该方法,获取到原始的sService对象 。

  1. 添加 Hook 逻辑: 在获取到原始对象后,我们就可以在InvocationHandlerinvoke方法中添加具体的 Hook 逻辑了 。我们的目标是在enqueueToast方法被调用时,对Toast的显示进行干预,去掉其中的 APP 名字和图标 。下面是完整的代码及详细注释 :

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")) {
                    // 获取TN对象,TN对象包含了Toast的相关信息
                    Object tn = args[1];
                    // 这里可以添加去掉APP名字和图标的具体逻辑
                    // 比如修改TN对象中的相关属性,或者替换Toast的显示视图等
                }
                // 调用原始sService对象的方法,保证原有功能正常执行
                return method.invoke(service, args);
            }
        });

在这段代码中,invoke方法会在代理对象的任何方法被调用时执行 。我们首先通过method.getName().equals("enqueueToast")判断当前调用的方法是否是enqueueToast 。如果是,就获取args数组中的第二个参数,它是一个TN对象,这个对象包含了Toast的相关信息 。在这个分支中,我们可以添加具体的逻辑来去掉 APP 名字和图标,比如修改TN对象中的相关属性,或者替换Toast的显示视图等 。最后,通过method.invoke(service, args)调用原始sService对象的方法,将方法调用转发给原始对象,保证Toast的原有显示功能不受影响 。通过这样的方式,我们就实现了对Toast显示的 Hook 操作,成功去掉了其中的 APP 名字和图标 。

代码实际运行效果展示

在应用上述 Hook 代码之前,我们在小米手机上运行 APP,弹出的 Toast 消息是这样的 :

可以看到,Toast 消息左侧显示着 APP 的图标,右侧显示着 APP 的名称,中间才是提示文本 。这种显示方式在某些情况下确实会影响 APP 的整体 UI 风格,显得有些杂乱 。

而在应用了 Hook 代码之后,同样的 APP 在小米手机上弹出的 Toast 消息变成了这样 :

对比之下,APP 的图标和名称已经成功消失,只剩下简洁的提示文本 。这样的 Toast 消息看起来更加简洁、美观,与 APP 整体的 UI 风格也更加协调统一 。通过这样的对比,我们可以直观地感受到 Hook 方案在去掉 Android Toast 消息中自带 APP 名字和图标的实际效果 ,为用户带来了更好的视觉体验 。

总结

在追求 APP 极致用户体验的道路上,统一美观的 UI 设计是至关重要的一环。而去掉 Toast 自带的 APP 名字和图标,正是提升 UI 统一性的关键一步 。它能让 Toast 消息与 APP 整体风格完美融合,避免突兀感,为用户打造更加简洁、舒适的交互界面 。

通过深入剖析 Toast 的创建过程,我们找到了 Hook 这个强大的解决方案 。Hook 技术就像是一把神奇的钥匙,让我们能够深入到系统底层,对 Toast 的显示进行精细控制 。从获取关键的sServiceField,到利用动态代理创建代理对象替换原有的sService,再到获取原始对象并添加 Hook 逻辑,每一个步骤都紧密相连,缺一不可 。

在实际应用中,Hook 方案展现出了巨大的优势 。它不仅成功去掉了 Toast 消息中恼人的 APP 名字和图标,而且对 APP 的性能几乎没有任何负面影响 。相比其他复杂且效果不佳的方法,Hook 方案更加简洁高效,为开发者提供了一种可靠的解决思路 。

希望大家通过这篇文章,对去掉 Android Toast 消息中自带 APP 名字和图标的方法有了更深入的理解和掌握 。也期待大家在实际开发中,能够大胆运用 Hook 方案,优化 APP 的用户体验,让我们的 APP 在众多应用中脱颖而出 。