Android人知道在安卓中不能在子线程中更新ui,为什么呢?真的不能吗?
先做个试验,我们在子线程中调用toast,看看有什么效果,按照上边一句话的意思,这里应该会报错显示不能再子线程中刷新ui吧
val thread = object : Thread() {
override fun run() {
Toast.makeText(this@MainActivity, "123", Toast.LENGTH_LONG).show()
}
}
thread.start()
不出意外的崩溃了:
java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare()
at android.widget.Toast$TN.(Toast.java:397)
at android.widget.Toast.(Toast.java:117)
at android.widget.Toast.makeText(Toast.java:280)
at android.widget.Toast.makeText(Toast.java:270)
日志可以发现,结果并不像我们想的那样,而是告诉我们需要一个looper,这个错误看着有点眼熟,了解过Handler构造函数的知道,new Handler的时候,如果没有取到当前线程的Looper也会报类似的错误。那是不是我们在toast show之前调用Looper.prepare()就能显示toast了呢?看代码:
val thread = object : Thread() {
override fun run() {
Looper.prepare()
Toast.makeText(this@MainActivity, "123", Toast.LENGTH_LONG).show()
Looper.loop()
}
}
thread.start()
运行后发现确实没问了。当然这里执行的结果就是我们的线程不能退出了。那toast到底是怎么显示出来的呢?
Let’s rtfsc.
首先看看Toast
Toast.makeText(this@MainActivity, "123", Toast.LENGTH_LONG).show()
上边是调用Toast的方式,我们从makeText开始看起
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
//构造Toast实例
Toast result = new Toast(context, looper);
//组装布局
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;
//组装后的toast实例
return result;
}
看看Toast的构造方法
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
mTN = new TN(context.getPackageName(), looper);
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);
}
这里有个关键的对象TN,TN是个什么呢?
private static class TN extends ITransientNotification.Stub {
...
}
他其实是一个binder对象,具体的咱先不说,先看看他的构造函数
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.
//第一部分,设置window参数
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;
//第二部分,拿当前线程的looper,当然这里取到的looper取决于当前代码执行所在的线程
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()");
}
}
//第三部分,创建handler关联looper
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
//第四部分,显示toast
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;
}
}
}
};
}
上边代码好像能看出点东西了,可以看到,文章开头的错误是在这里报出来的,另外toast的显示隐藏都是在一个handler中操作的,这个handler关联的正是我们toast所在的线程的looper,这就可以看出来子线程是可以更新toast的,前提是需要提供looper。
了解过的都知道,Toast、Dialog、PopupWindow都是基于Android的Window的,那么我们遇到的说子线程不能刷新view,如在在线程中setText等操作,这是为什么呢?下一篇我们讲讲所有View的parent->ViewRootImpl