任性,经过线程检查子线程照样更新UI

前言

果真起名才是码农最大的考验,躲过了撸码的变量起名,却绕不开博客名重复率高。言归正传,关于子线程更新UI的文章,网上资料已经不少了,但仍是总结了一下。受篇幅所限,也许你们根本没有耐心看完,这里就先说结论吧java

  • 主线程和UI线程是两个不一样的概念
  • UI线程指的是ViewRootImpl实例化时,所在的线程
  • 若是ViewRootImpl在主线程实例化,那么主线程就是UI线程,在子线程实例化,子线程就是UI线程
  • 即便ViewRootImpl实例化了,经过checkThread线程检查了,仍是能够真正意义上在子线程更新UI
  • ViewRootImpl在子线程实例化了,若是在主线程对ViewRootImpl对应的View进行UI操做,一样会抛异常

1.子线程更新UI报异常的缘由

不可免俗的要举个子线程更新UI的例子,经过在在onCreate方法中建立了一个子线程,并进行UI访问操做android

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访问操做,结果会报以下异常:web

Only the original thread that created a view hierarchy can touch its views 简单的直译就是只有建立这个view的线程,才能触摸(访问)这个View。(并无说必定要在主线程才能更新UI哦)app

...
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哦)ide

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

再看看前面异常日志中有requestLayout方法,再次走进ViewRootImpl类svg

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

前面子线程没有sleep的时候,checkThread()并无抛异常,那就接着点进scheduleTraversals()oop

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类型post

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

进入doTraversal()方法this

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。spa

为了更好的进行后面的分析,咱们须要知道关于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

仍是前面的例子,在子线程休眠一段时间后,确保已经经过checkThread()方法进行线程检查了,看此次在子线程更新UI会不会抛异常,而后再点击button,在主线程更新UI,又会是什么结果,代码以下

public class MainActivity extends AppCompatActivity {
	private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button= (Button) findViewById(R.id.main_btn);
		//真正意义上的在子线程更新UI
        new Thread(new Runnable() {

            @Override
            public void run() {
            	//休眠一段时间,确保经过checkThread()进行线程检查
                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();
    }
	
	/** * button的点击事件,在主线程更新UI */
	public void testUpdateUi(View view) {
       	tx.setText("主线程更新UI");
	}
}

效果以下:
在这里插入图片描述
异常信息以下,相信本身的眼睛,在主线程更新UI的确也会报一样的异常。

...
Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original
           thread that created a view hierarchy can touch its views.
...

显然,windowManager.addView(xxx)方法在子线程调用,也就是说ViewRootImpl在子线程实例化,此时在子线程更新UI是彻底没有问题的。反而点击button,在主线程更新UI抛异常了。若是你耐心看到这里,就应该明白了,主线程和UI线程是两个概念,对于button而言,UI线程是主线程,对于TextView tv 而言,UI线程是子线程。源码告诉咱们,ViewRootImpl建立的线程和操做UI在同一个线程就没有问题,因此UI线程就是ViewRootImpl实例化时所在的线程。

3.小结

  • 主线程和UI线程是两个彻底不一样的概念
  • UI线程指的是ViewRootImpl实例化时,所在的线程
  • 若是ViewRootImpl在主线程实例化,那么主线程就是UI线程,在子线程实例化,子线程就是UI线程
  • 即便ViewRootImpl实例化了,经过checkThread线程检查了,仍是能够真正意义上在子线程更新UI
  • ViewRootImpl在子线程实例化了,若是在主线程对ViewRootImpl对应的View进行UI操做,一样会抛异常