任性,通过线程检查子线程照样更新UI
前言
果然起名才是码农最大的考验,躲过了撸码的变量起名,却绕不开博客名重复率高。言归正传,关于子线程更新UI的文章,网上资料已经很多了,但还是总结了一下。受篇幅所限,也许大家根本没有耐心看完,这里就先说结论吧
- 主线程和UI线程是两个不同的概念
- UI线程指的是ViewRootImpl实例化时,所在的线程
- 如果ViewRootImpl在主线程实例化,那么主线程就是UI线程,在子线程实例化,子线程就是UI线程
- 即使ViewRootImpl实例化了,通过checkThread线程检查了,还是可以真正意义上在子线程更新UI
1.子线程更新UI报异常的原因
不可免俗的要举个子线程更新UI的例子,通过在在onCreate方法中创建了一个子线程,并进行UI访问操作
public class MainActivity extends AppCompatActivity {
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.main_tv);
new Thread(new Runnable() {
@Override
public void run() {
//SystemClock.sleep(3000);
textView.setText("子线程中访问");
}
}).start();
}
}
此时成功的在子线程更新了UI,并不会报异常。但是加上注释的代码,让子线程休眠一段时间,再去进行UI访问操作,结果会报如下异常:
Only the original thread that created a view hierarchy can touch its views 简单的直译就是只有创建这个view的线程,才能触摸(访问)这个View。(并没有说一定要在主线程才能更新UI哦)
...
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7286)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1155)
...
根据异常日志信息,我们大概可以问题出在ViewRootImpl的checkThread()方法:通过检查Thread类型的mThread变量是否等于当前线程,如果不等于就会报异常。也就是说mThread 和 Thread.currentThread()是同一个线程就不会报错。(还是没有说一定要在主线程才能更新UI哦)
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
再看看前面异常日志中有requestLayout方法,再次走进ViewRootImpl类
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
前面子线程没有sleep的时候,checkThread()并没有抛异常,那就接着点进scheduleTraversals()
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
postCallback方法第二个参数mTraversalRunnable是Runnable类型
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
进入doTraversal()方法
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//关键代码
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
一路追踪到performTraversal()方法,而View的绘制绘制就是从ViewRootImpl的performTraversal()开始的,现在我们知道了,每一次访问了UI,Android都会重新绘制View。
为了更好的进行后面的分析,我们需要知道关于View绘制的一些知识,View的绘制是由ViewRoot实际上是ViewRootImpl来负责的。每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的。那么decorView与ViewRoot的关联关系是在什么时候建立的呢?答案是Activity启动时,ActivityThread.handleResumeActivity()方法中建立了它们两者的关联关系。
首先,我们要简单了解下Activity的创建过程(不太清楚的自行百度):
在ActivityThread#handleLaunchActivity中启动Activity,在这里面会调用到Activity#onCreate方法,里边会有SetContentView()过程,从而完成上面所述的DecorView创建动作,结合前面的例子,子线程没有休眠的时候,在子线程更新UI并不会报错,也就是说没有调用ViewRootImpl的checkThread()方法,说明此时ViewRootImpl还没有实例化。那就继续往后分析
当onCreate()方法执行完毕,在handleLaunchActivity方法会继续调用到ActivityThread#handleResumeActivity方法
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
...
//需要关注的代码
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;
}
进入performanceResume()方法:
final void performResume() {
performRestart();
mFragments.execPendingActions();
mLastNonConfigurationInstances = null;
mCalled = false;
// mResumed is set by the instrumentation
mInstrumentation.callActivityOnResume(this);
...
}
Instrumentation调用了callActivityOnResume方法,callActivityOnResume源码如下:
public void callActivityOnResume(Activity activity) {
activity.mResumed = true;
//需要关注的代码
activity.onResume();
...
}
看到activity.onResume()。这也证实了performResumeActivity方法确实是回调onResume方法的入口。
那么现在我们看回来handleResumeActivity方法,执行完performResumeActivity方法回调了onResume方法后
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
...
//注释1
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
final Activity a = r.activity;
...
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
//注释2
r.activity.makeVisible();
}
}
...
}
执行注释2处r.activity.makeVisible(); 顾名思义还像是显示Activity的意思,进一步跟进:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
往WindowManager中添加DecorView,那现在应该关注的就是WindowManager的addView方法了。而WindowManager是一个接口来的,我们应该找到WindowManager的实现类才行,而WindowManager的实现类是WindowManagerImpl。
找到了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;
...
//注释1
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
...
}
终于找到了ViewRootImpl实例化的地方,在看看ViewRootImpl的构造方法
public ViewRootImpl(Context context, Display display) {
...
mThread = Thread.currentThread();
...
}
一切谜团都解开了,ViewRootImpl是在WindowManagerGlobal的addView方法中创建的,ViewRootImpl的创建在onResume方法回调之后,而我们一开篇是在onCreate方法中创建了子线程并访问UI,在那个时刻,ViewRootImpl是没有创建的,无法检测当前线程是否是UI线程,所以程序没有崩溃一样能跑起来,而之后修改了程序,让线程休眠了一段时间后,程序就崩了。很明显休眠后ViewRootImpl已经创建了,可以执行checkThread方法进行线程检查。
但是但是但是,重要的事情说三遍,从头到尾也没有发现哪里规定一定要在主线程更新UI,如果ViewRootImpl还没有实例化,是可以在子线程更新UI,这种情形分析意义不大,重要的是如果ViewRootImpl已经实例化了,还能不能在子线程更新UI呢?答案是能的,因为线程检查合规的条件只是,进行UI的线程和ViewRootImpl实例化的线程是同一个线程就行了,也就是说,如果ViewRootImpl是在子线程实例化的,那么我们完全可以在子线程进行UI操作。下面就进行代码验证
2.再次在子线程更新UI
还是前面的例子,在子线程休眠一段时间后,看这次会不会抛异常:
public class MainActivity extends AppCompatActivity {
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.main_tv);
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(3000);
Looper.prepare();
TextView tx = new TextView(MainActivity.this);
tx.setText("子线程更新UI");
tx.setTextColor(Color.RED);
WindowManager windowManager = MainActivity.this.getWindowManager();
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
500, 500, 50, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
windowManager.addView(tx, params);
Looper.loop();
}
}).start();
}
}
效果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CPq4yXe9-1588774959760)(F:\oCamFile\子线程更新UI.gif)]
显然,windowManager.addView(xxx)方法在子线程调用,也就是ViewRootImpl在子线程实例化,此时在子线程更新UI是完全没有问题的。如果你耐心看到这里,就应该明白了,主线程和UI线程是两个概念,源码也只是告诉我们,ViewRootImpl创建的线程和操作UI在同一个线程就没有问题,所以UI线程就是ViewRootImpl实例化时所在的线程。
效果如下:
显然,windowManager.addView(xxx)方法在子线程调用,也就是ViewRootImpl在子线程实例化,此时在子线程更新UI是完全没有问题的。如果你耐心看到这里,就应该明白了,主线程和UI线程是两个概念,源码也只是告诉我们,ViewRootImpl创建的线程和操作UI在同一个线程就没有问题,所以UI线程就是ViewRootImpl实例化时所在的线程。
3.小结
- 主线程和UI线程是两个完全不同的概念
- UI线程指的是ViewRootImpl实例化时,所在的线程
- 如果ViewRootImpl在主线程实例化,那么主线程就是UI线程,在子线程实例化,子线程就是UI线程
- 即使ViewRootImpl实例化了,通过checkThread线程检查了,还是可以真正意义上在子线程更新UI
上一篇: 堆的简单操作实现