Toast源码解析

来源:互联网 发布:淘宝的大拿韩代怎么样 编辑:程序博客网 时间:2024/05/21 18:00

子线程中的Toast

在写代码的时候发现一个现象,在子线程中使用Toast会crash,错误如下

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

可以很明显的看出问题出在当前线程企图创建Handler,但是由于本线程没有Looper所以crash了,这时候我不禁对Toast的实现原理产生兴趣,接下来就一步一步的分析源码。

Toast的创建

在日常使用中都是使用Toast.makeText()来创建一个Toast,代码如下

    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {        Toast result = new Toast(context);        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;    }

其实很简单,就是设置了mNextView和mDuration,创建一个TextView然后设置传入的String就完成mNextView的设置,mDuration参数被@Duration注解标记,注解如下

    @IntDef({LENGTH_SHORT, LENGTH_LONG})    @Retention(RetentionPolicy.SOURCE)    public @interface Duration {}    public static final int LENGTH_SHORT = 0;    public static final int LENGTH_LONG = 1;

@IntDef限制了只能传入两个给定的int,也就是说我们只能设置显示时间的长短,而无法设置具体的时长。

Toast.show()

show()方法源码如下

    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        }    }

可以看出这里需要得到一个INotificationManager的服务,传入了TN的实例,首先我们先看看TN是什么东西

    private static class TN extends ITransientNotification.Stub {        ...    }

查看ITransientNotification的代码

public interface ITransientNotification extends android.os.IInterface{/** Local-side IPC implementation stub class. */public static abstract class Stub extends android.os.Binder implements android.app.ITransientNotification{// 省略大部分代码...public void show() throws android.os.RemoteException;public void hide() throws android.os.RemoteException;}

可以看出这就是一个AIDL的接口,有show()和hide()两个办法,可以猜测主要用于远程服务来控制Toast显示和隐藏的。我们就看看show和hide的具体实现

        final Runnable mHide = new Runnable() {            @Override            public void run() {                handleHide();                // Don't do this in handleHide() because it is also invoked by handleShow()                mNextView = null;            }        };        final Handler mHandler = new Handler() {            @Override            public void handleMessage(Message msg) {                IBinder token = (IBinder) msg.obj;                handleShow(token);            }        };        @Override        public void show(IBinder windowToken) {            if (localLOGV) Log.v(TAG, "SHOW: " + this);            mHandler.obtainMessage(0, windowToken).sendToTarget();        }        @Override        public void hide() {            if (localLOGV) Log.v(TAG, "HIDE: " + this);            mHandler.post(mHide);        }

这里就用到了Handler,通过Handler实现show和hide,这也就解释了为什么子线程会报错。show()最终调用handlerShow()方法

        public void handleShow(IBinder windowToken) {            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView                    + " mNextView=" + mNextView);            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);                mWM.addView(mView, mParams);                trySendAccessibilityEvent();            }        }

可以看出主要就是通过WindowManager来addView显示Toast。hide()最终调用handleHide方法实现

        public void handleHide() {            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);            if (mView != null) {                // note: checking parent() just to make sure the view has                // been added...  i have seen cases where we get here when                // the view isn't yet added, so let's try not to crash.                if (mView.getParent() != null) {                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);                    mWM.removeViewImmediate(mView);                }                mView = null;            }        }

也是通过WindowManager来removevView。

Toast的时长

我们在使用Toast的时候只能穿入LENGTH_LONG活着LENGTH_SHORT两个变量,而具体的时间在handleShow()的代码中可以发现

    mParams.hideTimeoutMilliseconds = mDuration ==                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
        static final long SHORT_DURATION_TIMEOUT = 5000;        static final long LONG_DURATION_TIMEOUT = 1000;

可以看出长短时间分别对应5秒和1秒

为何使用AIDL而不是自己控制显示和隐藏

不知道大家有没有发现,在显示两个Toast的时候,总是第一个显示完毕才会显示第二个,如果让Toast自己控制,那么是很难实现这样的效果的,它并不知道其他Toast的状态,所以所有Toast交由系统同意管理,通过队列来依次显示Toast,并会按照设置的时间来hide Toast。

如何自己控制时间

我们可以跳过AIDL,获取NT对象,直接调用show和hide方法,但是NT对象构造方法是私有的,我们可以通过反射来解决,当然也可以反射设置time,然后正常show。

原创粉丝点击