我们知道Android中如果需要更新UI是需要运行在主线程中,如果我们在子线程中执行 View.invalidate
,那会报错。为什么一定要在主线程中才能更新 UI 呢?因为渲染UI的任务是在RenderThread中,如果所有线程都能与RenderThread通信,那肯定会涉及到线程同步和线程安全问题,会影响更新效率。Android中在非主线程更新UI的方式有很多种,下面一一介绍。
runOnUiThread
runOnUiThread是Activity中的一个可以在主线程中运行代码的函数,查看源码可以知道,其会先判断是否在主线程,如果是就立即执行,如果不是就会添加到主线程的消息队列中 :
final Handler mHandler = new Handler();
public final void runOnUiThread(Runnable action){
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}
Handler.post
Handler 其实是作为多线程通信的工具,所以创建Handler 的时候要注意是在什么线程中创建,如果在非主线程中创建Handler 要先初始化Looper。利用Hander更新UI其实也是把任务添加到主线程的消息队列中,因为在创建Handler的时候会获取当前线程的Looper来初始化自己:
public Handler(Callback callback,boolean async){
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
AsyncTask
在AsyncTask中有几个重要的函数 : onPreExecute、onPostExecute、onProgressUpdate、doInBackground。其中onPreExecute、onPostExecute、onProgressUpdate是运行在主线程的。通过查看 AsyncTask的源码可知,AsyncTask中有个InternalHandler 专门处理回调,也就是说AsyncTask是建立在 ThreadPoolExecutor 和 Handler 的基础上的,也就是运行在主线程的回调都是post任务到Hander上执行的。
private static class InternalHandlerextends Handler{
public InternalHandler(){
super(Looper.getMainLooper());
}
@SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
@Override
public void handleMessage(Message msg){
AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
switch (msg.what) {
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;
}
}
}
View.post
View.post 在 SDK23 和 SDK24中的处理方式不一样,比如在SDK 23中的源码 :
public boolean post(Runnable action){
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Assume that post will succeed later
ViewRootImpl.getRunQueue().post(action);
return true;
}
在Android 7.0 (SDK 24)中的源码 :
public 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;
}
相同点是在post前都会检查AttachInfo 是否为null,如果不是null就会通过attachInfo.mHandler来post任务。这个AttachInfo 是在View.dispatchAttachedToWindow中设置的 :
void dispatchAttachedToWindow(AttachInfo info,int visibility){
mAttachInfo = info;
...
View 的dispatchAttachedToWindow 是由 ViewGroup 调用 :
@Override
void dispatchAttachedToWindow(AttachInfo info,int visibility){
mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
super.dispatchAttachedToWindow(info, visibility);
mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
child.dispatchAttachedToWindow(info,
combineVisibility(visibility, child.getVisibility()));
}
final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
for (int i = 0; i < transientCount; ++i) {
View view = mTransientViews.get(i);
view.dispatchAttachedToWindow(info,
combineVisibility(visibility, view.getVisibility()));
}
}
那么又是谁调用了ViewGroup.dispatchAttachedToWindow,在ViewRootImpl.performTraversals中我们可以发现 :
private void performTraversals(){
...
if (mFirst) {
...
if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
host.setLayoutDirection(mLastConfiguration.getLayoutDirection());
}
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
//Log.i(mTag, "Screen on initialized: " + attachInfo.mKeepScreenOn);
}
...
}
这个host 其实就是 Activity 的 DecorView,所以这里有两点结论:
- 只有在ViewRootImpl.performTraversals之后,AttachInfo 才被设置,也就是说ViewRootImpl.performTraversals之后,View.post的任务才会在添加到主线程的消息队列中
- 只有是DecorView 的子 View才会去设置这个View的AttachInfo,这一点很重要,因为我们可能会在代码中直接 new一个View,如果这个View不添加到 DecorView ,那么他的AttachInfo会一直为null。
回到刚才的问题,如果是AttachInfo 为null 的情况,在SDK23 的时候会通过 ViewRootImpl.getRunQueue().post(action);
来执行任务。ViewRootImpl.getRunQueue 源码如下 :
static HandlerActionQueue getRunQueue(){
HandlerActionQueue rq = sRunQueues.get();
if (rq != null) {
return rq;
}
rq = new HandlerActionQueue();
sRunQueues.set(rq);
return rq;
}
其实就是把任务都添加到HandlerActionQueue中,等到ViewRootImpl.performTraversals的时候才执行HandlerActionQueue的任务:
private void performTraversals(){
...
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(mAttachInfo.mHandler);
...
}
在Android 7.0 (SDK 24)的时候呢,任务会被 getRunQueue().post(action);
来执行 ,View.getRunQueue()源码如下:
private HandlerActionQueue getRunQueue(){
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
```
可见View中的HandlerActionQueue 与ViewRootImpl是同一个数据结构,那么View中的在什么时候执行呢,前面说了,是在dispatchAttachedToWindow 的时候 :
```java
void dispatchAttachedToWindow(AttachInfo info,int visibility) {
...
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
...
}
所以在Android 7.0 (SDK 24)的时候,在AttachInfo为null的时候,任务会被添加到 View自己的RunQueue中,等到View.dispatchAttachedToWindow的时候才执行。而且View.RunQueue 比ViewRootImpl.RunQueue 先执行。这里要注意一点,只有DecorView 的子View才会被执行dispatchAttachedToWindow ,也就是说只有添加到DecorView的View才会执行这个View的RunQueue。
View.postInvalidate
View.postInvalidate 也可以在子线程中去更新 UI ,其源码如下 :
public void postInvalidate(int left, int top, int right, int bottom){
postInvalidateDelayed(0, left, top, right, bottom);
}
public void postInvalidateDelayed(long delayMilliseconds){
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
也就是说在attachInfo不为 null的时候才会把任务添加到主线程的消息队列中。
思考问题
在下面这段代码中,你知道打印的顺序是什么吗?
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View v = new View(this);
v.post(new Runnable() {
@Override
public void run(){
Log.d("Test", "A custom view.post");
}
});
findViewById(R.id.activity_main).post(new Runnable() {
@Override
public void run(){
Log.d("Test", "B findViewById view.post");
}
});
new Handler().post(new Runnable() {
@Override
public void run(){
Log.d("Test", "C Handler.post");
}
});
runOnUiThread(new Runnable() {
@Override
public void run(){
Log.d("Test", "D main Thread runOnUiThread");
}
});
new Thread(new Runnable() {
@Override
public void run(){
runOnUiThread(new Runnable() {
@Override
public void run(){
Log.d("Test", "E sub Thread runOnUiThread");
}
});
}
}).start();
}
我们知道 在这些方式中,runOnUiThread会检查是否在主线程,所以D 会首先执行,如果runOnUiThread不在主线程执行,会把任务添加到 Handler的消息队列(主线程消息队列)中,在前面的分析中可以知道,view.post的任务会在ViewRootImpl.performTraversals时才执行,所以执行顺序是DCE。在onCreate中,此时View的AttachInfo肯定为null,所以第一个view.post在SDK 23时会添加到 ViewRootImpl的RunQueue中,在SDK 24时会添加到View自己的RunQueue中,由于没有添加到DecorView所以不会执行,所以在执行顺序是 DCEBA(SDK 23) 或 DCEB(SDK 24)。
在SDK 23的系统中运行结果是 :
Test : D main Thread runOnUiThread Test : C Handler.post Test : E sub Thread runOnUiThread Test : B findViewById view.post Test : A custom view.post
在SDK 24的系统中运行结果是 :
Test : D main Thread runOnUiThread Test : C Handler.post Test : E sub Thread runOnUiThread Test : B findViewById view.post
总结
更新 UI 的各种方式有以下几种,从中可以发现,其实所有方式都是基于Handler来实现的:
-
runOnUiThread
- 直接执行任务或通过Handler执行
-
Handler.post
- 添加到Handler消息队列
-
AsyncTask
- 通过Handler执行
-
View.post
- AttachInfo不为null时,直接用attachInfo.mHandler执行任务
-
AttachInfo为null时 :
- 在SDK23 的时候,任务会被添加到 ViewRootImpl的RunQueue中,等到ViewRootImpl.performTraversals的时候才执行
- 在Android 7.0 (SDK 24)的时候,任务会被添加到 View自己的RunQueue中,等到View.dispatchAttachedToWindow的时候才执行,View.RunQueue 比ViewRootImpl.RunQueue 先执行,只有添加到DecorView 的View 才会执行
-
View.postInvalidate
- 只有AttachInfo不为null的时候才会利用attachInfo.mHandler执行任务
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。