为什么不能在子线程中更新UI
2018-03-01 16:17
447 查看
首先声明一点:子线程里面是可以更新UI的——创建一个空白的Activity,在其xml文件中放一个空白法人TextView,Java代码如下:
运行结果表示,更新UI时成功的。
现在我们先让子线程休眠100ms再更新UI:
结果你会发现,程序崩了。抛出了如下很熟悉的异常:
从异常信息可以知道:
异常是从android.view.ViewRootImpl的checkThread方法抛出的。而ViewRootImpl是接口ViewRoot的实现类。ViewRootImpl的checkThread方法的源码如下:
其中mThread是主线程(UI线程或者MainThread线程),在应用程序启动的时候,就已经被初始化了。
由此我们可以得出结论:
再看异常的另一段:
再看看它涉及到的requestLayout方法:
这里先是调用了checkThread()方法来检查当前线程;然后调用scheduleTraversals()方法,scheduleTraversals,字面理解就是线程遍历循环:
注意到postCallback方法的的第二个参数:TraversalRunnable,意思是遍历线程,是一个后台任务:
里面除了调用了doTraversal();方法,啥也没有干,继续看doTraversal():
可以看到里面调用了performTraversals()方法,View的绘制过程就是从performTraversals方法开始的。它的代码量有点大这里就不多说了,如果继续跟就是学习View的绘制了,偏离了我们的目标。
我们现在知道了,每一次访问UI,Android都会重新绘制View,这个很好理解。
到目前为止,我们可以得到结论:
但是为什么一开始在Activity的onCreate方法中创建子线程更新UI不抛异常呢?
唯一的解释就是执行onCreate方法的那个时候ViewRootImpl对象还没创建,无法去检查当前线程。
哪ViewRootImpl对象是在哪里,在什么时候被创建的呢?
在ActivityThread中,通过分析我们找到了handleResumeActivity方法:
内部调用了performResumeActivity方法,这个方法看名字像是回调onResume方法的入口的:
可以看到r.activity.performResume()这行代码:
上面Instrumentation调用了callActivityOnResume方法,callActivityOnResume源码如下:
找到了activity.onResume()。这也证实了performResumeActivity方法是回调onResume方法的入口。那么现在我们再回头看handleResumeActivity方法,执行完performResumeActivity方法回调了onResume方法后, 会再执行这一块代码:
r.activity调用了makeVisible方法,makeVisible方法是干什么的呢?我们跟进去看看:
他是往WindowManager中添加DecorView,那现在应该关注的就是WindowManager的addView方法了。而WindowManager是一个接口,我们找到WindowManager的实现类WindowManagerImpl。这个和ViewRoot是一样的,就名字多了个impl。在WindowManagerImpl中addView方法如下:
里面调用了WindowManagerGlobal的addView方法,那现在就锁定WindowManagerGlobal的addView方法:
终于看到我们想要看的关键信息:ViewRootImpl是在WindowManagerGlobal的addView方法中创建的。
因此得出一个总结:
ViewRootImpl的创建是在onResume方法回调之后,而我们一开篇是在onCreate方法中创建子线程并访问UI,在那个时刻,ViewRootImpl还没有来得及创建,无法检测当前线程是否是UI线程,所以程序没有崩溃。而之后修改了程序,让线程休眠了100毫秒后再更新UI,程序就崩了。很明显在这100毫秒内ViewRootImpl已经完成了创建,并能执行checkThread方法检查当前访问并更新UI的线程是不是UI线程。
同样的,我们还可以猜测,在onStart方法和onResume方法里面创建子线程并访问更新UI,同样是可以运行成功的。这一点留给读者去验证。
这是从源码的角度分析为什么不能在子线程中更新UI。
哪再问:为什么谷歌要提出:“UI更新一定要在UI线程里实现”这一跪着呢?原因如下:
目的在于提高移动端更新UI的效率和和安全性,以此带来流畅的体验。原因是:
Android的UI访问是没有加锁的,多个线程可以同时访问更新操作同一个UI控件。也就是说访问UI的时候,android系统当中的控件都不是线程安全的,这将导致在多线程模式下,当多个线程共同访问更新操作同一个UI控件时容易发生不可控的错误,而这是致命的。所以Android中规定只能在UI线程中访问UI,这相当于从另一个角度给Android的UI访问加上锁,一个伪锁。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_load_gif); tvThread = (TextView) findViewById(R.id.tvThread); new Thread(new Runnable() { @Override tvThread.setText("子线程加载"); }).start(); }
运行结果表示,更新UI时成功的。
现在我们先让子线程休眠100ms再更新UI:
new Thread(new Runnable() { @Override public void run() { try{ Thread.sleep(100); }catch (Exception e){ } tvThread.setText("子线程加载"); }).start();
结果你会发现,程序崩了。抛出了如下很熟悉的异常:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. at android.view.ViewRootImpl.checkThread(ViewRootImpl.Java:6581) at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924) Only the original thread that created a view hierarchy can touch its views:意思是只有创建这个View的线程才能够访问更新这个View。
从异常信息可以知道:
异常是从android.view.ViewRootImpl的checkThread方法抛出的。而ViewRootImpl是接口ViewRoot的实现类。ViewRootImpl的checkThread方法的源码如下:
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views."); } }
其中mThread是主线程(UI线程或者MainThread线程),在应用程序启动的时候,就已经被初始化了。
由此我们可以得出结论:
在访问UI的时候,ViewRoot会去检查当前是哪个线程访问的UI,如果不是主线程,就会抛出异常:Only the original thread that created a view hierarchy can touch its views。
再看异常的另一段:
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)
再看看它涉及到的requestLayout方法:
@Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested=true; } }
这里先是调用了checkThread()方法来检查当前线程;然后调用scheduleTraversals()方法,scheduleTraversals,字面理解就是线程遍历循环:
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; TraversalBarrier= Handler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, TraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }
注意到postCallback方法的的第二个参数:TraversalRunnable,意思是遍历线程,是一个后台任务:
final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } }
里面除了调用了doTraversal();方法,啥也没有干,继续看doTraversal():
void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); } performTraversals(); if (mProfile) { Debug.stopMethodTracing(); } } }
可以看到里面调用了performTraversals()方法,View的绘制过程就是从performTraversals方法开始的。它的代码量有点大这里就不多说了,如果继续跟就是学习View的绘制了,偏离了我们的目标。
我们现在知道了,每一次访问UI,Android都会重新绘制View,这个很好理解。
到目前为止,我们可以得到结论:
当访问UI时,ViewRoot会调用checkThread方法检查当前访问UI的线程是哪个,如果不是UI线程则会抛出异常。
但是为什么一开始在Activity的onCreate方法中创建子线程更新UI不抛异常呢?
唯一的解释就是执行onCreate方法的那个时候ViewRootImpl对象还没创建,无法去检查当前线程。
哪ViewRootImpl对象是在哪里,在什么时候被创建的呢?
在ActivityThread中,通过分析我们找到了handleResumeActivity方法:
final void handleResumeActivity(IBinder token,boolean clearHide, boolean isForward, boolean reallyResume) { unscheduleGcIdler(); mSomeActivitiesChanged = true; ActivityClientRecord r = performResumeActivity(token, clearHide); if (r != null) { final Activity a = r.activity; ...... r.activity.mVisibleFromServer = true; mNumVisibleActivities++; if (r.activity.mVisibleFromClient) { r.activity.makeVisible(); } } ...... } }
内部调用了performResumeActivity方法,这个方法看名字像是回调onResume方法的入口的:
public final ActivityClientRecord performResumeActivity(IBinder token,boolean clearHide) { ActivityClientRecord r = mActivities.get(token); if (localLOGV){ Slog.v(TAG, "Performing resume of " + r + " finished=" + r.activity.mFinished); } if (r != null && !r.activity.mFinished) { ...... r.activity.performResume(); ...... } return r; }
可以看到r.activity.performResume()这行代码:
final void performResume() { performRestart(); mFragments.execPendingActions(); mLastNonConfigurationInstances = null; mCalled = false; mInstrumentation.callActivityOnResume(this); ...... }
上面Instrumentation调用了callActivityOnResume方法,callActivityOnResume源码如下:
public void callActivityOnResume(Activity activity) { activity.mResumed = true; activity.onResume(); if (mActivityMonitors != null) { synchronized (mSync) { final int N = mActivityMonitors.size(); for (int i=0; i<N; i++) { final ActivityMonitor am = mActivityMonitors.get(i); am.match(activity, activity, activity.getIntent()); } } } }
找到了activity.onResume()。这也证实了performResumeActivity方法是回调onResume方法的入口。那么现在我们再回头看handleResumeActivity方法,执行完performResumeActivity方法回调了onResume方法后, 会再执行这一块代码:
...... r.activity.mVisibleFromServer = true; mNumVisibleActivities++; if (r.activity.mVisibleFromClient) { r.activity.makeVisible(); }
r.activity调用了makeVisible方法,makeVisible方法是干什么的呢?我们跟进去看看:
void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); }
他是往WindowManager中添加DecorView,那现在应该关注的就是WindowManager的addView方法了。而WindowManager是一个接口,我们找到WindowManager的实现类WindowManagerImpl。这个和ViewRoot是一样的,就名字多了个impl。在WindowManagerImpl中addView方法如下:
@Override public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { applyDefaultToken(params); mGlobal.addView(view, params, mDisplay, mParentWindow); }
里面调用了WindowManagerGlobal的addView方法,那现在就锁定WindowManagerGlobal的addView方法:
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ...... ViewRootImpl root; View panelParentView = null; ...... root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); } try { root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { synchronized (mLock) { final int index = findViewLocked(view, false); if (index >= 0) { removeViewLocked(index, true); } } throw e; } }
终于看到我们想要看的关键信息:ViewRootImpl是在WindowManagerGlobal的addView方法中创建的。
因此得出一个总结:
ViewRootImpl的创建是在onResume方法回调之后,而我们一开篇是在onCreate方法中创建子线程并访问UI,在那个时刻,ViewRootImpl还没有来得及创建,无法检测当前线程是否是UI线程,所以程序没有崩溃。而之后修改了程序,让线程休眠了100毫秒后再更新UI,程序就崩了。很明显在这100毫秒内ViewRootImpl已经完成了创建,并能执行checkThread方法检查当前访问并更新UI的线程是不是UI线程。
同样的,我们还可以猜测,在onStart方法和onResume方法里面创建子线程并访问更新UI,同样是可以运行成功的。这一点留给读者去验证。
这是从源码的角度分析为什么不能在子线程中更新UI。
哪再问:为什么谷歌要提出:“UI更新一定要在UI线程里实现”这一跪着呢?原因如下:
目的在于提高移动端更新UI的效率和和安全性,以此带来流畅的体验。原因是:
Android的UI访问是没有加锁的,多个线程可以同时访问更新操作同一个UI控件。也就是说访问UI的时候,android系统当中的控件都不是线程安全的,这将导致在多线程模式下,当多个线程共同访问更新操作同一个UI控件时容易发生不可控的错误,而这是致命的。所以Android中规定只能在UI线程中访问UI,这相当于从另一个角度给Android的UI访问加上锁,一个伪锁。
相关文章推荐
- 子线程为什么不能更新UI
- 为什么在子线程中不能更新UI的简单解释
- GCD为什么不能在子线程更新UI
- 为什么不能在子线程中更新UI
- android为什么不允许新开启一个线程来更新UI,而是用handler来更新界面
- Android中子线程真的不能更新UI吗?
- Android中子线程真的不能更新UI吗?
- android 不能在子线程中更新ui的讨论和分析
- android 为什么可以在子线程更新UI
- 为什么我们可以在非UI线程中更新UI
- 为什么我们可以在非UI线程中更新UI
- Andoird主子线程通信,子线程不能更新UI,4大方法子线程更新UI
- EventBus抛异常了,不能在子线程更新UI。
- Android中子线程真的不能更新UI吗?
- 子线程中真的不能更新UI?
- 为什么我们可以在非UI线程中更新UI
- Android提高(16)——第六章 非UI线程真的不能更新UI吗
- android为什么不允许新开启一个线程来更新UI,而是用handler来更新界面
- 在Android中,非主线程不能更新UI
- Android 中非UI线程真的不能更新UI吗?