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)之间的关系如下:
handler:绑定到一个线程上,一个线程可以有多个handler
looper:线程跟looper是一一对应的,所以looper不能被调用两次否则会抛出异常
messge:handler利用message来携带消息
messagQueue:用来状态message,一个looper对应一个消息队列