Android之为什么只能在UI线程操作View

 

Android之为什么只能在UI线程操作View

标签: android
  856人阅读  评论(1)  收藏  举报
  分类:

目录(?)[+]

很长一段时间以来,面试的时候我总喜欢问一个问题:为什么只能在UI线程对View进行操作?Android程序员在涉足android开发的早期应该就有这样一个认识,但是没有多少人知道究竟是为什么。以至于后来,我也就不愿意问这个问题了,不知道这个问题的答案其实也不妨碍候选人做出好的功能。虽然感觉是很自然的事情,但是不弄清楚总觉得有些不舒服,于是,我花了点时间研究了一下。

其实这个问题的答案并不复杂,只要看一下android源码就能够定位出来。我看的是android2.3的源码,2.3系统确实显得有些过时,但是android很多基本的东西从2.3到4.0都没有发生根本性的变化(5.0没有研究过,不敢妄下断论)。

View的一些基本操作

View进行操作无非是使其可见或者不可见,给ViewGroup加一个View或移除一个View,改变View的大小等,这些操作直接或者间接地会调用到ViewinvalidaterequestLayout接口,这两个接口的调用是递归式的,最终又会调用到ViewRootinvalidateChildrequestLayout接口,ViewRoot又是什么?我们都知道View其实是一个个树状的结构,你可以认为ViewRoot就是这些树状结构的根节点。重新回来看一下这两个接口的实现如下:

public void invalidateChild(View child, Rect dirty) {
    checkThread();
    if (DEBUG_DRAW) Log.v(TAG, "Invalidate child: " + dirty);
    if (mCurScrollY != 0 || mTranslator != null) {
        mTempRect.set(dirty);
        dirty = mTempRect;
        if (mCurScrollY != 0) {
            dirty.offset(0, -mCurScrollY);
        }
        if (mTranslator != null) {
            mTranslator.translateRectInAppWindowToScreen(dirty);
        }
        if (mAttachInfo.mScalingRequired) {
            dirty.inset(-1, -1);
        }
    }
    mDirty.union(dirty);
    if (!mWillDrawSoon) {
        scheduleTraversals();
    }
}


public void requestLayout() {
    checkThread();
    mLayoutRequested = true;
    scheduleTraversals();
}

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

值得注意的是,这两个接口中都调用到了checkThread这个接口,那么这个接口又是检查什么呢:

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

原来真相就在这里,如果当前调用这个接口的线程和ViewRoot中维护的线程即mThread不同,就会报一个错误,这个错误大家应该已经很熟悉了,只能在UI线程里进行View操作的概念估计也是来自于这个crash。

那么问题来了,

如果说只能在UI线程里对View做操作的话,意味着ViewRoot中维护的这个mThread就一定是UI线程。

事实是这样吗?对此,我有些怀疑,如果是我来写这部分代码,我会明确把这个成员变量命名为mMainThread,而且android源码的其他部分凡是用到主线程的地方也确实是用mMainThread来命名的,带着这个疑问,我们先来看看mThread是在哪里被赋值的。

ViewRoot的构造

不论是Activity还是Dialog,亦或是通过WindowManager直接添加窗口,最终都会调用到WindowManageraddView来告诉系统真正要显示的窗口。窗口是个抽象的概念,我们真正能看到并且能够与其进行交互的是View,所以addView的参数是View,而不是其他的诸如Window的类型。

WindowManager其实是个interface,真正实现addView的是一个叫做WindowManageImpl的类,其源码实现如下:

public class WindowManagerImpl implements WindowManager {

    ......

    private void addView(View view, ViewGroup.LayoutParams params, boolean nest)
    {
        if (Config.LOGV) Log.v("WindowManager", "addView view=" + view);

        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException(
                    "Params must be WindowManager.LayoutParams");
        }

        final WindowManager.LayoutParams wparams
                = (WindowManager.LayoutParams)params;

        ViewRoot root;
        View panelParentView = null;

        synchronized (this) {
            ......

            root = new ViewRoot(view.getContext());
            root.mAddNesting = 1;

            ......
        }
        // do this last because it fires off messages to start doing things
        root.setView(view, wparams, panelParentView);
    }

    ......
}

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

从上面的源码中可以看到,每次向系统添加一个View的时候,都会构造一个ViewRoot对象,而ViewRoot的构造函数是长这样的:

public final class ViewRoot extends Handler implements ViewParent,
        View.AttachInfo.Callbacks {
    public ViewRoot(Context context) {
        super();

        ......

        mThread = Thread.currentThread();

        ......
    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

所以,mThread其实是在ViewRoot的构造函数中赋值的,另外,这段构造函数还给出了两个线索:

  • ViewRoot是继承自Handler的,所以ViewRoot需要在有Looper的线程上构造

  • mThread就是构造ViewRoot的那个线程,往上追溯,就是调用addView的那个线程

ActivityaddView的调用是被封装起来的,我们需要做的只是调用setContentView来设置Activity对应的视图,并且从源码中可以看到,ActivityaddView是在主线程即UI线程上调用的,这部分源码读者可以自行查看。

分析到这里,基本上已经得出了结论,因为ActivityaddView是在主线程调用,所以只要是对Activity中的View进行操作,就要求在主线程上执行,否则就会crash。

那么问题又来了,其他的View呢?比如Dialog对应的View以及通过直接调用WindowManageraddView添加的View

在非UI线程操作View

于是我试图照着上面的线索做一个试验,尝试在非UI线程改变View的属性。 
首先构造了一个简单的Acitivity,代码如下:

public class MainActivity extends Activity {

    private HandlerThread mNonUIThread;

    private NonUIThreadHandler mHandler;

    private static final int MESSAGE_SHOW_DIALOG = 1;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mNonUIThread = new HandlerThread("NonUIThread");
        mNonUIThread.start();

        mHandler = new NonUIThreadHandler(mNonUIThread.getLooper());
        View showDialogBtn = findViewById(R.id.show_dialog_btn);
        showDialogBtn.setOnClickListener(mClickListener);
    }

    private View.OnClickListener mClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.show_dialog_btn:
                    Message msg = Message.obtain();
                    msg.what = MESSAGE_SHOW_DIALOG;
                    mHandler.sendMessage(msg);
                    break;
            }
        }
    };

    private class NonUIThreadHandler extends android.os.Handler {

        private Dialog mDialog;
        private TextView mDialogContent;
        public NonUIThreadHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_SHOW_DIALOG:
                    if (mDialog == null) {
                        Dialog dialog = new Dialog(MainActivity.this, R.style.DialogTheme);
                        dialog.setContentView(R.layout.dialog_layout);
                        final TextView info = (TextView)dialog.getWindow().getDecorView().findViewById(R.id.dialog_info);
                        info.setText("Dialog shows in thread " + Thread.currentThread().getId());
                        Button change = (Button) dialog.getWindow().getDecorView().findViewById(R.id.change_btn);
                        change.setOnClickListener(new View.OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                info.setText("onClick is called in thread " + Thread.currentThread().getId());
                            }
                        });
                        mDialog = dialog;
                    }
                    if (!mDialog.isShowing()) {

                        mDialog.show();
                    }
                    break;
             }
        }
    }

}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

这个Acitivity相当简单,界面布局只有一个按键,代码逻辑是点击这个按键就会显示Dialog。按照以往的经验,我们会在按键的onClick接口中构造出Dialog,并且调用它的show接口。这里做了一些不同的处理,先是构造了一个HandlerThread,这个线程一旦开始运行就会构造出一个Looper,把自己变成一个消息处理线程。在按键的onClick接口中,不是直接显示Dialog,而是向HandlerThread发送一个消息,来告诉线程要显示对话框。这样,对话框显示的逻辑就被搬到了NonUIThreadHandlerhandleMessage接口中。

然后,运行这个程序,点击显示对话框的按键,不出意外,对话框可以正确地显示,如下图所示: 

程序中还向对话框中的按键“Change”注册了点击事件,点击之后的行为是修改对话框中TextView的内容,具体点击之后的UI如下:

从上图中可以看到,对话框中按键的onClick接口是在id为25592的线程中调用的。这也就算意味着TextViewsetText接口也是在该线程中调用的,也就完美地验证了对View的操作其实是可以在非UI线程中完成的

为什么onClick接口是在非UI线程中调用的,这不是逆天了么?这个问题也可以从源码中得到解释(不再附上源码),这里做一些简单的说明

  • Dialogshow接口最终调用到的也是WindowManageraddView接口,也就是说Dialog对应的ViewRoot对象在本次实验中是在线程25592上构造出来的,其中的成员变量mThread就是对应于这个线程,而不是UI线程。
  • Dialog的触摸消息是通过ViewRoot(2.3系统中,ViewRoot本身就是Handler)分发到mThread来进行处理的

综合以上两点,本例对话框中任何控件的onClick接口就一定是在非UI线程中调用的。

结论

事实上,标题中只能在UI线程操作View这个结论本身是错误的,所以也就没有为什么之说了。对这部分原理比较熟悉的读者可能在看到这篇文章标题的时候就已经呵呵了。不知道这样有没有把这个问题阐述清楚,欢迎讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值