Android Toast在子线程中为什么无法正常使用

来源:互联网 发布:iphone6splus精仿淘宝 编辑:程序博客网 时间:2024/06/05 10:33

1.概述

在android中Toast经常会被用到,在主线程中直接使用Toast没任何问题,消息显示正常。一旦将Toast放入子线程中的时候,消息不会有任何响应。网上查了解决办法,直接将写在子线程中的Toast前后加上Looper.prepare(),其后加上Looper.loop(),就可以正常显示了。例:
                new Thread() {                    @Override                    public void run() {                        Looper.prepare();                        Toast.makeText(getApplicationContext(), "发生未知错误!", Toast.LENGTH_SHORT).show();                        Looper.loop();                    }                }.start();

   原来一直是只知其然不知其所以然,现在就看下内在原理,彻底了解为什么。(不建议这么使用,会造成新的问题。当前子线程因为looper的存在,导致一直处于未销毁状态,而占有内存。建议直接将消息发送到UI线程中进行显示。)

2.Looper

首先,来了解下Looper。

看看关于looper这个类的描述说明。

  * Class used to run a message loop for a thread.  Threads by default do  * not have a message loop associated with them; to create one, call  * {@link #prepare} in the thread that is to run the loop, and then  * {@link #loop} to have it process messages until the loop is stopped.  *  * <p>Most interaction with a message loop is through the  * {@link Handler} class.

(大意就是,looper这个类是用来给线程thread运行消息循环的。线程们在默认情况下并没有一个消息循环loop与它们相关联;用looper类中的方法prepare可以在线程中创建一个looper对象用来执行loop用的,然后调用loop方法时就会让之前创建的looper对象来处理循环中的消息,直到循环停止。

Looper与消息循环的大部分交互是通过Handler类。)

看到这,就可以知道looper主要的方法就prepare()和loop()。

1.Looper.prepare()

   static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
   private Looper(boolean quitAllowed) {        mQueue = new MessageQueue(quitAllowed);        mThread = Thread.currentThread();    }
    public static void prepare() {        prepare(true);    }    private static void prepare(boolean quitAllowed) {        if (sThreadLocal.get() != null) {//第5行            throw new RuntimeException("Only one Looper may be created per thread");        }        sThreadLocal.set(new Looper(quitAllowed));    }

从第5行可以看出Looper.prepare()方法中,首先会判断sThreadLocal中是否已经有Looper存在,如果有就抛异常,没有就实例化一个looper set进去。从这就可以看出每个线程中只可能存在一个Looper。

实例化Looper时,会创建一个MessageQueue,同时将当前线程和Looper绑定。

2.Looper.loop()

    public static void loop() {        final Looper me = myLooper();        if (me == null) {            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");        }        final MessageQueue queue = me.mQueue;        // Make sure the identity of this thread is that of the local process,        // and keep track of what that identity token actually is.        Binder.clearCallingIdentity();        final long ident = Binder.clearCallingIdentity();        for (;;) {            Message msg = queue.next(); // might block            if (msg == null) {                // No message indicates that the message queue is quitting.                return;            }            // This must be in a local variable, in case a UI event sets the logger            final Printer logging = me.mLogging;            if (logging != null) {                logging.println(">>>>> Dispatching to " + msg.target + " " +                        msg.callback + ": " + msg.what);            }            final long traceTag = me.mTraceTag;            if (traceTag != 0) {                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));            }            try {                msg.target.dispatchMessage(msg);            } finally {                if (traceTag != 0) {                    Trace.traceEnd(traceTag);                }            }            if (logging != null) {                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);            }            // Make sure that during the course of dispatching the            // identity of the thread wasn't corrupted.            final long newIdent = Binder.clearCallingIdentity();            if (ident != newIdent) {                Log.wtf(TAG, "Thread identity changed from 0x"                        + Long.toHexString(ident) + " to 0x"                        + Long.toHexString(newIdent) + " while dispatching to "                        + msg.target.getClass().getName() + " "                        + msg.callback + " what=" + msg.what);            }            msg.recycleUnchecked();        }    }
    public static @Nullable Looper myLooper() {        return sThreadLocal.get();    }

在调用looper.loop()方法时,首先会从sThreadLocal中取出之前在prepare()方法中set的Looper对象。如果sThreadLocatl中没有Looper对象,就会抛异常,这就说明loop()方法必须在prepare()方法之后。

然后获取到当前Looper的MessageQueue.

之后追踪当前线程的标识。(只是用来提示一个WTF级别的日志)

开始进入死循环,每次循环开始都会调用MessageQueue.next()方法从中取出一个Message,然后将它传递到msg.target的dispatchMessage(msg)中。msg.target就是消息的接收者,其实就是一个Handler。如果消息队列MessageQueue为空,next()就会进入阻塞状态,直到有新的消息到达才会继续执行。

看看msg.target.dispathchMessage()方法

public void dispatchMessage(Message msg) {    if (msg.callback != null) {        handleCallback(msg);    } else {        if (mCallback != null) {            if (mCallback.handleMessage(msg)) {                return;            }        }        handleMessage(msg);    }}
这里如果msg.callbak不为空,就调用handleCallback方法。否则判断mCallback是否为空,不为空,就调用mCallback的handleMessage方法,否则直接调用handler的handleMessage(msg)方法直接将消息传递出去。

通过looper类的说明,了解了looper的原理与使用机制。其中很重要的是,looper是用来给线程处理消息用的,线程默认情况下没有looper对象,也就是线程中的消息不会被处理,自然将消息直接丢在线程中的时候是不会被处理的。必须在线程中创建一个Looper实例,同时创建了一个消息队列(MessageQueue),然后通过无限循环取出消息交由handlerMessage处理,也就是发送消息的对象handler。

说到这里,说明了looper在handler,messageQueue之间的关系,那和Toast有什么关系呢?

3.Toast

我们再使用Toast的时候,一般形式都是:

Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
首先看看makeText:

    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;    }
从这可以看出Toast就是一个简单的布局,里面就一个Textview.

那我们主要就看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        }    }
这里面关键点有2个。INotificationManage和TN。

我们看TN

   public Toast(Context context) {        mContext = context;        mTN = new TN();        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);    }
首先在Toast的构造方法中,实现了mTN。

接下来看TN类。

private static class TN extends ITransientNotification.Stub {        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;            }        };        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();        final Handler mHandler = new Handler() {            @Override            public void handleMessage(Message msg) {                IBinder token = (IBinder) msg.obj;                handleShow(token);            }        };        int mGravity;        int mX, mY;        float mHorizontalMargin;        float mVerticalMargin;        View mView;        View mNextView;        int mDuration;        WindowManager mWM;        static final long SHORT_DURATION_TIMEOUT = 5000;        static final long LONG_DURATION_TIMEOUT = 1000;        TN() {            // 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;        }        /**         * schedule handleShow into the right thread         */        @Override        public void show(IBinder windowToken) {            if (localLOGV) Log.v(TAG, "SHOW: " + this);            mHandler.obtainMessage(0, windowToken).sendToTarget();        }        /**         * schedule handleHide into the right thread         */        @Override        public void hide() {            if (localLOGV) Log.v(TAG, "HIDE: " + this);            mHandler.post(mHide);        }        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();            }        }        private void trySendAccessibilityEvent() {            AccessibilityManager accessibilityManager =                    AccessibilityManager.getInstance(mView.getContext());            if (!accessibilityManager.isEnabled()) {                return;            }            // treat toasts as notifications since they are used to            // announce a transient piece of information to the user            AccessibilityEvent event = AccessibilityEvent.obtain(                    AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);            event.setClassName(getClass().getName());            event.setPackageName(mView.getContext().getPackageName());            mView.dispatchPopulateAccessibilityEvent(event);            accessibilityManager.sendAccessibilityEvent(event);        }                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;            }        }    }
从TN类的源码中可以看到。TN类继承自ITransientNotification.Stub,用于进程间的通讯。

package android.app;/** @hide */oneway interface ITransientNotification {    void show();    void hide();}
ITransientNotification定义了2个方法,show和hide,在TN类中的具体实现为:

        /**         * schedule handleShow into the right thread         */        @Override        public void show(IBinder windowToken) {            if (localLOGV) Log.v(TAG, "SHOW: " + this);            mHandler.obtainMessage(0, windowToken).sendToTarget();        }        /**         * schedule handleHide into the right thread         */        @Override        public void hide() {            if (localLOGV) Log.v(TAG, "HIDE: " + this);            mHandler.post(mHide);        }
到这里我们就能知道,Toast的show和hide方法实现是基于Handler机制。我们可以把Toast理解为创建了一个handler,这样一来发消息的对象在这就是Toast了。

而且我们再TN类中并没有发现任何Looper.perpare()和Looper.loop()方法。所以这里的mhandler调用的就是当前线程的loop对象。

在对looper类说明的时候,知道线程本身默认是没有looper对象的,所以Toast在线程中使用的时候,必须创建一个looper对象。

到了这里,又产生一个疑问?主线程也是线程啊,为什么可以直接使用Toast?那接下来我们再看看主线程是怎么回事。

(如果想对Toast继续深入了解,可以看关于它的源码。这里只要清楚Toast就是创建了一个handler)

4.ActivityThread

ActivityThread是主线程入口的类,实际上并非线程,就只是一个final的类,不像HandlerThread类,ActivityThread并没有真正继承Thread类,只是往往运行在主线程,给人以线程的感觉,其实承载ActivityThread的主线程就是由Zygote fork而创建的进程。

ActivityThread.main()的主要代码:

//初始化LooperLooper.prepareMainLooper();    //创建ActivityThread对象,并绑定到AMS   ActivityThread thread = new ActivityThread();   //一般的应用程序都不是系统应用,因此设置为false,在这里面会绑定到AMS    thread.attach(false);    if (sMainThreadHandler == null) {        sMainThreadHandler = thread.getHandler();    }    if (false) {        Looper.myLooper().setMessageLogging(new                LogPrinter(Log.DEBUG, "ActivityThread"));    }    // End of event ActivityThreadMain.    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);    //开启循环    Looper.loop();    throw new RuntimeException("Main thread loop unexpectedly exited");

从上面的源码就可以看到。为什么我们的Toast(Hander)可以直接在主线程中直接使用了。主线程在创建的时候就直接绑定初始化了一个looper对象。

同时android的四大组件默认都是运行在主线程中的,所以handler可以直接在四大组件中直接使用。

到了这里,我们就清楚了为什么Toast在主线程中可以直接使用,在子线程中就必须初始化looper对象。