一、背景:
做Android开发的工程师都知道一条金科玉律(在主线程操作UI的更新与变化)。我们经常在做工程业务的时候,会使用封装的ToastUtils来统一做toast。如下所示:
public class ToastUtils{
private static Toast sToast;
private static Handler sMainHandler;
private ToastUtils(){
throw new UnsupportedOperationException("it is a utils class, should not be newInstance");
}
public static void show(Context ctx, String content){
//判断当前是否在UI线程
if (Looper.myLooper() != Looper.getMainLooper()) {
if(sMainHandler == null){
sMainHandler = new Handler(Looper.getMainLooper());
}
sMainHandler.post(new Runnable(){
@overrride
public void run(){
showInner(ctx,content);
}
});
}else{
showInner(ctx,content);
}
}
private void showInner(Context ctx, String content){
//避免频繁创建Toast对象,也能防止Toast风暴
if(sToast == null){
sToast = Toast.makeText(ctx,content,Toast.LENGHT_LONG);
}else{
sToast.setText(content);
}
sToast.show();
}
}
二、解惑
但是真的只能在主线程才能弹Toast吗?答案是NO。一般我们在子线程弹toast的时候,会得到提示:"Can't toast on a thread that has not called Looper.prepare()"
我们跟踪一下代码:
- makeText()
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
//这一步骤是关键
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;
return result;
}
- 接着看new Toast()
/**
* Constructs an empty Toast object. If looper is null, Looper.myLooper() is used.
* @hide
*/
//构造Toast对象,传递参数又一个Looper
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);
}
- 再接着,我们看new TN().TN是一个binder实现类,提供给AMS使用
private static class TN extends ITransientNotification.Stub {
...省略
static final long SHORT_DURATION_TIMEOUT = 4000;
static final long LONG_DURATION_TIMEOUT = 7000;
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();
//duang duang duang,我们看到了子线程弹toast的异常提示
//也就是如果当前线程如果没有Looper则会抛出异常。
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;
}
}
}
};
}
...省略的一些方法
}
- 通过上面的代码分析,我们可以知道:之所以子线程弹toast抛出异常是因为没有looper。而主线程天生自带Looper。所以没有问题,也就是说只要我们给子线程创建Looper就能在子线程上弹toast了。
三、正确的子线程弹toast方法:
public void toastInWorkerThread(Context ctx, String content){
new Thread(new Runnable{
@overrride
public void run(){
Lopper.prepare();
Toast.makeText(ctx, content,Toast.LENGHT_LONG).show()
Looper.loop();
}
}).start();
}