欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-02-11 21:30:16
...

前言

果然起名才是码农最大的考验,躲过了撸码的变量起名,却绕不开博客名重复率高。言归正传,关于子线程更新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实例化时所在的线程。

效果如下:
任性,通过线程检查子线程照样更新UI

显然,windowManager.addView(xxx)方法在子线程调用,也就是ViewRootImpl在子线程实例化,此时在子线程更新UI是完全没有问题的。如果你耐心看到这里,就应该明白了,主线程和UI线程是两个概念,源码也只是告诉我们,ViewRootImpl创建的线程和操作UI在同一个线程就没有问题,所以UI线程就是ViewRootImpl实例化时所在的线程。

3.小结

  • 主线程和UI线程是两个完全不同的概念
  • UI线程指的是ViewRootImpl实例化时,所在的线程
  • 如果ViewRootImpl在主线程实例化,那么主线程就是UI线程,在子线程实例化,子线程就是UI线程
  • 即使ViewRootImpl实例化了,通过checkThread线程检查了,还是可以真正意义上在子线程更新UI
相关标签: 消息机制