private void initDeal() {
Thread hanMeiMeiThread = new Thread(){
@Override
public void run() {
Looper.prepare();
mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
Log.e(TAG, "hanMeiMei receiver message: " + ((String) msg.obj));
// mShowTv.setText((String) msg.obj);
Toast.makeText(HomeActivity.this, ((String) msg.obj), Toast.LENGTH_SHORT).show();
}
};
Looper.loop();
}
};
Thread liLeiThread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Message message = mHandler.obtainMessage();
message.obj = "Hi MeiMei";
mHandler.sendMessage(message);
}
};
hanMeiMeiThread.setName("韓梅梅 Thread");
hanMeiMeiThread.start();
liLeiThread.setName("李雷 Thread");
liLeiThread.start();
}
以上代碼是線程間通信,使用Looper方式在子線程中創建handler,但是爲什麼可以在子線程中彈出toast卻不能以此進行ui更改呢?
主要是以下原因:
因爲Handler是依賴於Looper的,所以創建Handler的時候必須先能夠獲取Looper,在工程啓動的時候,會進入ActivityThread的main方法中,其中已經通過Looper.prepareMainLooper()方法進行了創建了全局唯一的UI looper,所以在主線程中,可以直接創建handler,而子線程卻需要Looper.prepare()創建,通過Looper.loop()不斷輪訓執行消息,回到上面代碼當中,雖然通過mHandler.sendMessage(message) 發送了消息,但是進入到了子線程中創建的handler,而UI控件不是線程安全的,如果在多線程中併發訪問可能會導致UI控件處於不可預期的狀態,所以會有線程檢查,因此會拋出異常
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
}
}
上述代碼中Toast.show(),可以看到是創建了TN對象,放入enqueueToast中,讓我們進入TN中
TN(String packageName, @Nullable Looper looper) {
// 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;
mPackageName = packageName;
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;
}
}
}
};
}
看到Switch中的Show中,handleShow()
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 */
}
}
到了這裏我們可以發現
Toast本質是通過window顯示和繪製的(操作的是window),而主線程不能更新UI 是因爲ViewRootImpl的checkThread方法在Activity維護的View樹的行爲。 Toast中TN類使用Handler是爲了用隊列和時間控制排隊顯示Toast,所以爲了防止在創建TN時拋出異常,需要在子線程中使用Looper.prepare();和Looper.loop();
線程(Thread)、循環器(Looper)、處理器(Handler)之間的關係如下:
- 一個線程(Thread)只能綁定一個循環器(Looper);但一個Thread可以有多個處理器(Handler)。
- 一個循環器(Looper)可綁定多個處理器(Handler)。
- 一個處理器(Handler)只能綁定一個循環器(Looper)
handler:綁定到一個線程上,一個線程可以有多個handler
looper:線程跟looper是一一對應的,所以looper不能被調用兩次否則會拋出異常
messge:handler利用message來攜帶消息
messagQueue:用來狀態message,一個looper對應一個消息隊列