为什么子线程都不能刷新UI?

1. 理论上的原因

1.1 Android主线程是线程不安全的?

网上文章常常有说:Android主线程是线程不安全的。我就纳闷了,线程还有安全一说?

不能说主线程是线程不安全。线程没有安全不安全这一说。而是更新UI的方法不是线程安全的,即只能在单线程中完成UI的更新,不能使用多线程。(为什么呢?因为子线程可能会有多个,存在多个线程同时操作一个控件的情况)因此,只能在主线程中进行UI更新。

1.2 Android的单线程模型

Android的单线程模型有两条原则:

  • 不要阻塞UI线程。
  • 不要在UI线程之外访问Android UI toolkit(主要是这两个包中的组件:android.widget and android.view

1.3 APP Process

在一个Android 程序开始运行的时候,会单独启动一个进程Process。默认的情况下,所有这个程序中的Activity或者Service(Service和Activity只是Android提供的Components中的两种,除此之外还有Content Provider和Broadcast Receiver)都会跑在这个进程空间里。

1.4 UI线程(主线程)

一个Android 程序默认情况下也只有一个进程Process,但可以有许多个线程Thread。在这么多Thread当中,有一个Thread,我们称之为UI Thread。UI Thread在Android程序运行的时候就被创建,是一个Process当中的主线程Main Thread,主要是负责控制UI界面的显示、更新和控件交互。

1.5 为什么在主线程更新UI,在子线程执行耗时操作?

在Android程序创建之初,一个Process呈现的是单线程模型,所有的任务都在一个线程中运行。因此,我们认为,UI Thread所执行的每一个函数,所花费的时间都应该是越短越好。

如果所有的工作都在UI线程,一些比较耗时的工作比如(访问网络,下载数据,查询数据库等),很容易造成主线程的阻塞,导致事件停止分发(包括绘制事件)。轻则降低用户体验,更坏的情况是,如果主线程被阻塞超过5秒,就会导致ANR,弹出应用程序没有响应,是等待还是关闭的警告。

另外,Andoid UI toolkit并不是线程安全的,所以不能从非UI线程来操纵UI组件。必须把所有的UI操作放在UI线程里。

1.6 为什么只能有一个线程操作 UI?

  • 两个线程不能同时draw,否则屏幕会花;
  • 不能同时insert map,否则内存会花;
  • 不能同时write buffer,否则文件会花。

需要互斥,比如锁。多线程操作一个UI,很容易导致,或者极其容易导致反向加锁和死锁问题。

结果就是同一时刻只有一个线程可以做ui。那么当两个线程互斥几率较大时,或者保证互斥的代码复杂时,选择其中一个长期持有其他发消息就是典型的解决方案。所以普遍的要求ui只能单线程。

  • https://www.zhihu.com/question/37334646

2. 源码分析

如果在子线程更新 UI:

new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                main_tv.setText("子线程中访问");
            }
        }).start();

Crash msg:

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)

ViewRootImplcheckThread方法:

void checkThread() {
    // mThread是主线程,在应用程序启动的时候,就已经被初始化了
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

requestLayout 方法:

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

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方法的的第二个参数传入了很像是一个后台任务。那再点进去

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

可以看到里面调用了一个 performTraversals() 方法,View 的绘制过程就是从这个 performTraversals 方法开始的。分析到了这里,其实异常信息对我们帮助也不大了,它只告诉了我们子线程中访问UI在哪里抛出异常。

当访问UI时,ViewRootImpl 会调用 checkThread 方法去检查当前访问UI的线程是哪个,如果不是UI线程则会抛出异常,这是没问题的。但是为什么一开始在 MainActivityonCreate方法中创建一个子线程访问UI,程序还是正常能跑起来呢?

唯一的解释就是执行 onCreate 方法的那个时候 ViewRootImpl 还没创建,无法去检查当前线程。

那么就可以这样深入进去。寻找 ViewRootImpl 是在哪里,是什么时候创建的。好,继续前进

ActivityThread 中,我们找到 handleResumeActivity 方法,如下:

  • https://blog.csdn.net/xyh269/article/details/52728861

大致流程是酱紫滴:

  • 第一步,查看:ActivityThread --> handleResumeActivity
  • handleResumeActivity 调用了 performResumeActivity
  • performResumeActivity 调用了 r.activity.performResume()
  • Instrumentation调用了callActivityOnResume方法
  • activity调用了makeVisible
  • 往WindowManager中添加DecorView
  • WindowManagerImpl的addView
  • WindowManagerGlobal的addView方法
  • ViewRootImpl --> root.setView(view, wparams, panelParentView);

简而言之

ViewRootImpl 的创建在 onResume 方法回调之后

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值