版权声明:本文为博主原创文章,未经博主允许不得转载。
很长一段时间以来,面试的时候我总喜欢问一个问题:为什么只能在UI线程对View
进行操作?Android程序员在涉足android开发的早期应该就有这样一个认识,但是没有多少人知道究竟是为什么。以至于后来,我也就不愿意问这个问题了,不知道这个问题的答案其实也不妨碍候选人做出好的功能。虽然感觉是很自然的事情,但是不弄清楚总觉得有些不舒服,于是,我花了点时间研究了一下。
其实这个问题的答案并不复杂,只要看一下android源码就能够定位出来。我看的是android2.3的源码,2.3系统确实显得有些过时,但是android很多基本的东西从2.3到4.0都没有发生根本性的变化(5.0没有研究过,不敢妄下断论)。
View的一些基本操作
对View
进行操作无非是使其可见或者不可见,给ViewGroup
加一个View
或移除一个View
,改变View
的大小等,这些操作直接或者间接地会调用到View
的invalidate
和requestLayout
接口,这两个接口的调用是递归式的,最终又会调用到ViewRoot
的invalidateChild
和requestLayout
接口,ViewRoot
又是什么?我们都知道View
其实是一个个树状的结构,你可以认为ViewRoot
就是这些树状结构的根节点。重新回来看一下这两个接口的实现如下:
- 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
这个接口,那么这个接口又是检查什么呢:
- 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
直接添加窗口,最终都会调用到WindowManager
的addView
来告诉系统真正要显示的窗口。窗口是个抽象的概念,我们真正能看到并且能够与其进行交互的是View
,所以addView
的参数是View
,而不是其他的诸如Window
的类型。
WindowManager
其实是个interface
,真正实现addView
的是一个叫做WindowManageImpl
的类,其源码实现如下:
- 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
的构造函数是长这样的:
- 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
的那个线程
Activity
中addView
的调用是被封装起来的,我们需要做的只是调用setContentView
来设置Activity
对应的视图,并且从源码中可以看到,Activity
中addView
是在主线程即UI线程上调用的,这部分源码读者可以自行查看。
分析到这里,基本上已经得出了结论,因为Activity
中addView
是在主线程调用,所以只要是对Activity
中的View
进行操作,就要求在主线程上执行,否则就会crash。
那么问题又来了,其他的View
呢?比如Dialog
对应的View
以及通过直接调用WindowManager
的addView
添加的View
。
在非UI线程操作View
于是我试图照着上面的线索做一个试验,尝试在非UI线程改变View
的属性。
首先构造了一个简单的Acitivity
,代码如下:
- 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
发送一个消息,来告诉线程要显示对话框。这样,对话框显示的逻辑就被搬到了NonUIThreadHandler
的handleMessage
接口中。
然后,运行这个程序,点击显示对话框的按键,不出意外,对话框可以正确地显示,如下图所示:
程序中还向对话框中的按键“Change”注册了点击事件,点击之后的行为是修改对话框中TextView
的内容,具体点击之后的UI如下:
从上图中可以看到,对话框中按键的onClick
接口是在id为25592的线程中调用的。这也就算意味着TextView
的setText
接口也是在该线程中调用的,也就完美地验证了对View
的操作其实是可以在非UI线程中完成的。
为什么onClick
接口是在非UI线程中调用的,这不是逆天了么?这个问题也可以从源码中得到解释(不再附上源码),这里做一些简单的说明
Dialog
的show
接口最终调用到的也是WindowManager
的addView
接口,也就是说Dialog
对应的ViewRoot
对象在本次实验中是在线程25592上构造出来的,其中的成员变量mThread
就是对应于这个线程,而不是UI线程。Dialog
的触摸消息是通过ViewRoot
(2.3系统中,ViewRoot
本身就是Handler
)分发到mThread
来进行处理的
综合以上两点,本例对话框中任何控件的onClick
接口就一定是在非UI线程中调用的。
结论
事实上,标题中只能在UI线程操作View这个结论本身是错误的,所以也就没有为什么之说了。对这部分原理比较熟悉的读者可能在看到这篇文章标题的时候就已经呵呵了。不知道这样有没有把这个问题阐述清楚,欢迎讨论。