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()的主要代码:
//初始化Looper
Looper.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对象。