Toast源码分析

本文深入剖析了Android Toast机制,澄清了其显示时间的误解,详细解释了Toast内部类TN的显示、隐藏逻辑,以及如何避免内存泄漏。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先,这是我第一次写源码分析,大家不喜勿喷,我只是在阐述我自己的理解,如果有出入欢迎探讨!

之所以写这篇文章是因为要写一个吐司,自定义显示时间那种,于是我就使用了toast的吐司时间设置,但是我最后才发现我工作了这么久竟然记错了,而且一错就是好几年,我也真是服了自己啊!

首先,Toast吐司的事件是固定的,默认是短时间short为4000ms长时间Long为7000ms,这个是很容易查到的,在Toast的内部有一个TN内部类中定义的。

private static class TN extends ITransientNotification.Stub {
 private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();private static final int SHOW = 0;  

private static final int HIDE = 1;  
private static final int CANCEL = 2;  
final Handler mHandler;  

int mGravity;  
int mX, mY;  
float mHorizontalMargin;  
float mVerticalMargin;  


View mView;  
View mNextView;  
int mDuration;  

WindowManager mWM;  

String mPackageName;  

static final long SHORT_DURATION_TIMEOUT = 4000;  
static final long LONG_DURATION_TIMEOUT = 7000;

而之前我所说的记错了好几年的就是时间的设置,Toast不能自定义时间设置显示,它只能通过传参设置显示的时间是长时间还是短时间,如下设置提示:

设置显示时间类型
    /**
     * Set how long to show the view for.
     * @see #LENGTH_SHORT
     * @see #LENGTH_LONG
     */
    public void setDuration(@Duration int duration) {
        mDuration = duration;
        mTN.mDuration = duration;
    }
    
普通的吐司弹窗
    /**
     * Make a standard toast that just contains a text view.
     *
     * @param context  The context to use.  Usually your {@link android.app.Application}
     *                 or {@link android.app.Activity} object.
     * @param text     The text to show.  Can be formatted text.
     * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
     *                 {@link #LENGTH_LONG}
     *
     */
    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        return makeText(context, null, text, duration);
    }
    

时间的注解类
    /** @hide */
    @IntDef(prefix = { "LENGTH_" }, value = {
            LENGTH_SHORT,
            LENGTH_LONG
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Duration {}

关于我的错误是说完了,大家以后一定要注意啊,这点很重要,不要犯和我一样的错误了。下面再介绍一些其他的东西,在这个吐司当中,Toast本身仅仅只是一些参数的调用和初始化,最重要的就是内部的TN内部类,该内部类实现了吐司的显示与隐藏等。

第一步,之所以在Toast当中的构造函数以及maketask方法当中传递Looper是为了给handle使用,是消息传递使用,详细了解请看我的另外两篇文章:https://blog.youkuaiyun.com/cmwly/article/details/94357754

https://blog.youkuaiyun.com/cmwly/article/details/92777440

他的主要作用就是对于消息队列的管理,负责将消息队列当中的数据取出然后并传递出去。这个Looper最后是到了下面的位置:

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) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            try {
                                getService().cancelToast(mPackageName, TN.this);
                            } catch (RemoteException e) {
                            }
                            break;
                        }
                    }
                }
            };

通过上面的代码大家应该发现了这是对于吐司的显示、隐藏、取消的管理,其中最重要的就是就是显示的逻辑,隐藏和取消的没啥都还好。

public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            // If a cancel/hide is pending - no need to show - at this point
            // the window token is already invalid and no need to do any work.
            if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
                return;
            }
            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                // We can resolve the Gravity here by using the Locale for getting
                // the layout direction
                final Configuration config = mView.getContext().getResources().getConfiguration();
                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
                mParams.gravity = gravity;
                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
                    mParams.horizontalWeight = 1.0f;
                }
                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
                    mParams.verticalWeight = 1.0f;
                }
                mParams.x = mX;
                mParams.y = mY;
                mParams.verticalMargin = mVerticalMargin;
                mParams.horizontalMargin = mHorizontalMargin;
                mParams.packageName = packageName;
                mParams.hideTimeoutMilliseconds = mDuration ==
                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
                mParams.token = windowToken;
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
                // Since the notification manager service cancels the token right
                // after it notifies us to cancel the toast there is an inherent
                // race and we may attempt to add a window after the token has been
                // invalidated. Let us hedge against that.
                try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }

这里面有几点好处,首先它使用的Context是App的Context而不是页面实例的Context,这样避免了持有实例的引用,从而导致实例无法释放造成内存泄漏。

      Context context = mView.getContext().getApplicationContext();

第二点就是使用的是系统级别的弹窗,使其不会手当前App实例的影响导致异常或者无法弹出:mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

起始Toast总结下来就以下几点:

  1. 消失时间固定就两种,除非你自己手动取消;

  2. 你可以自定义view显示,但是注意,自定义view之后不用使用Toast的setText()方法,否则会导致崩溃;

  3. Toast本身是配置显示参数的入口以及调用显示隐藏的中转站,没什么其他的了;

  4. 真正的显示隐藏的处理逻辑在Toast的内部类TN中,如果想要自定义Toast,TN是重中之重,如果要处理的话建议使用反射区处理,当然我还没有实测,后面会实测处理试试;

剩下的起始就没啥说的了,只要对Android了解的人基本上瞅一眼就明天怎么回事了,本身这个Toast吐司就不是什么复杂的东西,比较复杂的逻辑其实并不在这个Toast当中,是后面的一些代码,但是我还没有研究到那种程度,后面慢慢来,毕竟我也不是什么大神,仅仅只是普通的开发者中的一员!!

写这篇文章的意义一个是提醒自己犯过的错误,另外一个就是分享下我自己的理解,让大家多多交流,也没什么其他的,大家不喜勿喷。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值