Android 子线程更新 UI
01.Android中子线程可以更新UI吗
- 出自《Android艺术探索》
- 这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,那么为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:
- ①首先加上锁机制会让UI访问的逻辑变得复杂
- ②锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。
- 所以最简单且高效的方法就是采用单线程模型来处理UI操作。
- 为什么说子线程不能更新UI?
- 子线程是不能直接更新UI的。Android实现View更新有两组方法,分别是invalidate和postInvalidate。前者在UI线程中使用,后者在非UI线程即子线程中使用。换句话说,在子线程调用 invalidate 方法会导致线程不安全。熟悉View工作原理的人都知道,invalidate 方法会通知 view 立即重绘,刷新界面。作一个假设,现在用 invalidate 在子线程中刷新界面,同时UI线程也在用 invalidate 刷新界面,这样会不会导致界面的刷新不能同步?这就是invalidate不能在子线程中使用的原因。
02.子线程更新UI有哪些方式
- 主要有这些方法
- 主线程中定义Handler,子线程通过mHandler发送消息,主线程Handler的handleMessage更新UI
- 用Activity对象的runOnUiThread方法
- 创建Handler,传入getMainLooper
- View.post(Runnable r)
03.runOnUiThread如何实现子线程更新UI
如何使用代码如下所示
1
2
3
4
5
6
7
8
9
10
11new Thread(new Runnable() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
tv_0.setText("滚犊子++++");
}
});
}
}).start();看看源码,如下所示
- 如果msg.callback为空的话,会直接调用我们的mCallback.handleMessage(msg),即handler的handlerMessage方法。由于Handler对象是在主线程中创建的,所以handler的handlerMessage方法的执行也会在主线程中。
- 在runOnUiThread程序首先会判断当前线程是否是UI线程,如果是就直接运行,如果不是则post,这时其实质还是使用的Handler机制来处理线程与UI通讯。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
@Override
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}
04.创建Handler,传入getMainLooper
Looper在哪个线程创建,就跟哪个线程绑定,并且Handler是在他关联的Looper对应的线程中处理消息的。
- 在子线程中,是否也可以创建一个Handler,并获取MainLooper,从而在子线程中更新UI呢?首先我们看到,在Looper类中有静态对象sMainLooper,并且这个sMainLooper就是在ActivityThread中创建的MainLooper。
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//源码部分
private static Looper sMainLooper;
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
//在子线程中通过这个sMainLooper来进行更新UI操作
new Thread(new Runnable() {
@Override
public void run() {
Log.e("yc", "yc 4"+Thread.currentThread().getName());
//注意这里创建handler时,需要传入looper
Handler handler = new Handler(getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
//Do Ui method
tv_0.setText("滚犊子————————————---");
Log.e("yc", "yc 5"+Thread.currentThread().getName());
}
});
}
}).start();
05.View.post(Runnable r)更新UI
代码如下所示
1
2
3
4
5
6tv_0.post(new Runnable() {
@Override
public void run() {
tv_0.setText("滚犊子");
}
});源码原理如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}View.post(Runnable r)使用注意事项
- 看源码的注释可知:如果view已经attached,则调用ViewRootImpl中的ViewRootHandler,放入主线程Lopper等待执行。如果detach,则将其暂存在RunQueue当中,等待其它线程取出执行。
- View.post(Runnable r)很多时候在子线程调用,用于进行子线程无法完成的操作,或者在该方法中通过getMeasuredWidth()获取view的宽高。需要注意的是,在子线程调用该函数,可能不会被执行,原因是该view不是attached状态。
06.子线程更新UI总结概括
- handler.post(Runnable r)、 view.post(Runnable r)、activity.runOnUIThread(Runnable r)等方法。跟进去看源码,发现其实它们的实现原理都还是一样,最终都是通过Handler发送消息来实现的。
07.子线程中Toast,showDialog
先来看看问题代码
1
2
3
4
5
6new Thread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "run on thread",Toast.LENGTH_SHORT).show();//崩溃无疑
}
}).start();如何解决上面的问题呢?
1
2
3
4
5
6
7
8new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();
Looper.loop();
}
}).start();在子线程中Toast原理分析
- 在show方法中,我们看到Toast的show方法和普通UI控件不太一样,并且也是通过Binder进程间通讯方法执行Toast绘制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public void show() {
//首先判断吐司内容不能为空
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
//从SMgr中获取名为notification的服务
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}- 把目光放在TN 这个类上,通过TN类,可以了解到它是Binder的本地类。在Toast的show方法中,将这个TN对象传给NotificationManagerService就是为了通讯!并且我们也在TN中发现了它的show方法。
- 使用Handler,所以需要创建Looper对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15private static class TN extends ITransientNotification.Stub {
final Runnable mHide = new Runnable() {
@Override
public void run() {
handleHide();
}
};
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
IBinder token = (IBinder) msg.obj;
handleShow(token);
}
};
}- Toast本质是通过window显示和绘制的(操作的是window),而主线程不能更新UI是因为ViewRootImpl的checkThread方法在Activity维护的View树的行为。Toast中TN类使用Handler是为了用队列和时间控制排队显示Toast,所以为了防止在创建TN时抛出异常,需要在子线程中使用Looper.prepare();和Looper.loop();(但是不建议这么做,因为它会使线程无法执行结束,导致内存泄露)