一:子线程更新UI
安卓是单线程模型,那么子线程中是否能更新UI呢,答案是可以。
我们可以自己给它一个ViewRoot(WindowManager的addView中会走到new ViewRootImpl),这样ViewRoot的线程和view更新的线程在同一线程中,checkThread方法便可以执行通过,或者Activity中的ViewRootImpl初始化之前(onResume)更新UI。
第一种情况:WindowManager可以提供ViewRoot,这样就可以用WindowManager更新UI,WindowManager内部是通过Handler机制(addView在队列里加入view更新的消息队列,通过handler取出更新UI)所以需要Looper.prepare和looper开启子线程消息循环。
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周期之间的简短时间内可以执行子线程更新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真正实现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()方法为什么不会导致ANR
安卓整体的运行是由事件驱动的GUI单线程模型,接收事件消息并处理,导致ANR的原因,第一是事件未能得到处理,第二是已经处理但是未能及时处理完成。代码里的体现就是当Loop.looper轮训不到事件的时候,整个应用就退出了,所以没有轮训拿到事件应用是不可能一直有序运行的。