Android Toast在子线程中为什么无法正常使用
来源:互联网 发布:iphone6splus精仿淘宝 编辑:程序博客网 时间:2024/06/05 10:33
1.概述
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.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对象。
- Android Toast在子线程中为什么无法正常使用
- Android开发之在子线程中使用Toast
- android 在子线程中使用Toast等功能
- Android在子线程使用Toast
- Android 子线程中使用Toast
- 在子线程中Toast
- 在子线程中Toast
- 子线程中使用Toast
- Android中Toast如何在子线程中调用
- Android中Toast如何在子线程中调用
- Android中Toast如何在子线程中调用
- 安卓在子线程中使用使用Toast
- 关于在子线程中在run方法执行完之后通知主线程进行操作的方法。(Toast在子线程中无法使用)
- Android-在子线程中显示Toast和Dialog
- Android代码里Toast如何在子线程中调用
- 可在子线程中使用的Toast
- Toast 在子线程中使用的问题
- 在子线程里面使用Toast
- HDU 4798 Skycity【几何】
- UVALive
- docker安装centos后没有ifconfig命令解决办法
- LaTex入门学习!(Updating)
- 关于百度推送10101错误码的解决
- Android Toast在子线程中为什么无法正常使用
- 相关滤波、KCF、循环对角化
- Android旧项目集成React Native简易流程
- SpringMVC日期类型转换问题的几种处理方法
- Android_Webview的使用/内存优化/远程执行漏洞处理
- Python学习笔记(九)—— Dict
- 如何把一个String类型的sparql语句,解析出一系列triple
- jQuery如何对div进行排序
- JAVA 实现 AES 加密