一个Toast引发的血案

前一阵在自己的app里面实现了一个截图功能,实现的方法比较粗暴,直接使用的screenshot指令,效果拔群。

1
2
3
4
5
6
String cmd = "screencap -p /sdcard/Pictures/Screenshots/" + timecurrentTimeMillis + ".png";
try {
Process p = Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
XposedBridge.log(e.getMessage());
}

后来有朋友说这么截图比较生硬,因为看不到截图的结果,后来就想加一个toast,弹出一个提示就好了。

1
2
3
4
5
6
7
8
String cmd = "screencap -p /sdcard/Pictures/Screenshots/" + timecurrentTimeMillis + ".png";
try {
Process p = Runtime.getRuntime().exec(cmd);
Toast.makeText(context,"screenShot success",Toast.LENGTH_SHORT).show();
} catch (IOException e) {
Toast.makeText(context,"screenShot failed",Toast.LENGTH_SHORT).show();
XposedBridge.log(e.getMessage());
}

再后来发现这种情况虽然能成功弹出toast,但是会有一种比较尴尬的情况,截图的同时,toast也出现在截图中了。

当时处理这个问题的想法也很简单,将toast出现的时间稍微往后延一些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
String cmd = "screencap -p /sdcard/Pictures/Screenshots/" + timecurrentTimeMillis + ".png";
try {
Process p = Runtime.getRuntime().exec(cmd);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
Toast.makeText(context,"screenShot success",Toast.LENGTH_SHORT).show();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
} catch (IOException e) {
Toast.makeText(context,"screenShot failed",Toast.LENGTH_SHORT).show();
XposedBridge.log(e.getMessage());
}

然而使用这种方法之后,报错了。

1
2
3
4
5
6
7
8
9
10
07-07 22:51:58.478 2346-3283/com.android.systemui E/AndroidRuntime: FATAL EXCEPTION: Thread-63
Process: com.android.systemui, PID: 2346
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:200)
at android.os.Handler.<init>(Handler.java:114)
at android.widget.Toast$TN.<init>(Toast.java:345)
at android.widget.Toast.<init>(Toast.java:101)
at android.widget.Toast.makeText(Toast.java:259)
at com.egguncle.xposednavigationbar.hook.btnFunc.BtnScreenShot$1.run(BtnScreenShot.java:76)
at java.lang.Thread.run(Thread.java:818)

查看报错信息,错误是因为没有Looper.prepare引起的,经过测试发现,Toast不能在子线程里面直接弹出。

来看一下toast的源码
http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/widget/Toast.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
构造方法
99 public Toast(Context context) {
100 mContext = context;
101 mTN = new TN();
102 mTN.mY = context.getResources().getDimensionPixelSize(
103 com.android.internal.R.dimen.toast_y_offset);
104 mTN.mGravity = context.getResources().getInteger(
105 com.android.internal.R.integer.config_toastDefaultGravity);
106 }
107
以及show方法
108 /**
109 * Show the view for the specified duration.
110 */
111 public void show() {
112 if (mNextView == null) {
113 throw new RuntimeException("setView must have been called");
114 }
115
116 INotificationManager service = getService();
117 String pkg = mContext.getOpPackageName();
118 TN tn = mTN;
119 tn.mNextView = mNextView;
120
121 try {
122 service.enqueueToast(pkg, tn, mDuration);
123 } catch (RemoteException e) {
124 // Empty
125 }
126 }

在Toast的构造方法里面发现一个叫TN的类,而且在show方法中,tn被设置到toast队列中,似乎是个很重要的类,来跟进一下看看TN到底是什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
358 TN() {
359 // XXX This should be changed to use a Dialog, with a Theme.Toast
360 // defined that sets up the layout params appropriately.
361 final WindowManager.LayoutParams params = mParams;
362 params.height = WindowManager.LayoutParams.WRAP_CONTENT;
363 params.width = WindowManager.LayoutParams.WRAP_CONTENT;
364 params.format = PixelFormat.TRANSLUCENT;
365 params.windowAnimations = com.android.internal.R.style.Animation_Toast;
366 params.type = WindowManager.LayoutParams.TYPE_TOAST;
367 params.setTitle("Toast");
368 params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
369 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
370 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
371 }
372
373 /**
374 * schedule handleShow into the right thread
375 */
376 @Override
377 public void show() {
378 if (localLOGV) Log.v(TAG, "SHOW: " + this);
379 mHandler.post(mShow);
380 }
381

再看一下报错的那一行

1
final Handler mHandler = new Handler();

查看这个TN类的构造方法和show方法,发现是用了window,这也说明了toast并不属于view,而是一个window,而且在show中发现了handler,源码中并没有使用looper.prepare方法,报错的原因应该就在这里了。

解决方法也很简单,使用handler通知主线程弹出toast,执意要在子线程弹出的话,使用looper.prepare和looper.loop就好。