一:子線程更新UI
安卓是單線程模型贴汪,那么子線程中是否能更新UI呢务嫡,答案是可以赚爵。
我們可以自己給它一個ViewRoot(WindowManager的addView中會走到new ViewRootImpl),這樣ViewRoot的線程和view更新的線程在同一線程中理张,checkThread方法便可以執(zhí)行通過析藕,或者Activity中的ViewRootImpl初始化之前(onResume)更新UI召廷。
第一種情況:WindowManager可以提供ViewRoot,這樣就可以用WindowManager更新UI账胧,WindowManager內(nèi)部是通過Handler機制(addView在隊列里加入view更新的消息隊列柱恤,通過handler取出更新UI)所以需要Looper.prepare和looper開啟子線程消息循環(huán)。
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
Looper.prepare();
TextView tx = new TextView(MainActivity.this);
WindowManager windowManager = MainActivity.this.getWindowManager();
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
windowManager.removeViewImmediate(tx);
windowManager.addView(tx, params);
Looper.loop();
// ((TextView)findViewById(R.id.txt)).setText("子線程改變ui");
}
}).start();
第二種情況:
Activity的onResume中才會初始化ViewRootImpl找爱,所以在onCreate和onResume周期之間的簡短時間內(nèi)可以執(zhí)行子線程更新UI操作
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
((TextView)findViewById(R.id.txt)).setText("子線程改變ui");
}
}).start();
二:子線程彈Toast
源碼分析
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真正實現(xiàn)toast顯示到Window上
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
TN源碼
private static class TN extends ITransientNotification.Stub {
................
final Handler mHandler;
WindowManager mWM;
TN(String packageName, @Nullable Looper looper) {
.........
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
looper = Looper.myLooper();
if (looper == null) {
throw new RuntimeException(
## 直接子線程中顯示toast會報這個錯誤
"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;
}
}
}
};
}
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
// If a cancel/hide is pending - no need to show - at this point
// the window token is already invalid and no need to do any work.
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
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呢
new Thread(new Runnable() {
@Override
public void run() {
#子線程開啟
Looper.prepare();
Toast.makeText(MainActivity.this,"子線程吐司",Toast.LENGTH_LONG).show();
Looper.loop();
}
}).start();
三:Loop.looper()方法為什么不會導(dǎo)致ANR
安卓整體的運行是由事件驅(qū)動的GUI單線程模型,接收事件消息并處理泡孩,導(dǎo)致ANR的原因车摄,第一是事件未能得到處理,第二是已經(jīng)處理但是未能及時處理完成仑鸥。代碼里的體現(xiàn)就是當Loop.looper輪訓(xùn)不到事件的時候吮播,整個應(yīng)用就退出了,所以沒有輪訓(xùn)拿到事件應(yīng)用是不可能一直有序運行的眼俊。