(点击
上方公众号
,可快速关注)
来源:伯乐在线专栏作者 - xuyinhuan
链接:http://android.jobbole.com/85148/
点击 → 了解如何加入专栏作者
Android的UI访问是没有加锁的,这样在多个线程访问UI是不安全的。所以Android中规定只能在UI线程中访问UI。
但是有没有极端的情况?使得我们在子线程中访问UI也可以使程序跑起来呢?接下来我们用一个例子去证实一下。
新建一个工程,activity_main.xml布局如下所示:
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
RelativeLayout
xmlns
:
android
=
"http://schemas.android.com/apk/res/android"
android
:
layout_width
=
"match_parent"
android
:
layout_height
=
"match_parent"
>
TextView
android
:
id
=
"@+id/main_tv"
android
:
layout_width
=
"wrap_content"
android
:
layout_height
=
"wrap_content"
android
:
textSize
=
"18sp"
android
:
layout_centerInParent
=
"true"
/>
RelativeLayout
>
很简单,只是添加了一个居中的TextView
MainActivity代码如下所示:
public
class
MainActivity
extends
AppCompatActivity
{
private
TextView
main_tv
;
@Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
activity_main
);
main_tv
=
(
TextView
)
findViewById
(
R
.
id
.
main_tv
);
new
Thread
(
new
Runnable
()
{
@Override
public
void
run
()
{
main_tv
.
setText
(
"子线程中访问"
);
}
}).
start
();
}
}
也是很简单的几行,在onCreate方法中创建了一个子线程,并进行UI访问操作。
点击运行。你会发现即使在子线程中访问UI,程序一样能跑起来。结果如下所示:
咦,那为嘛以前在子线程中更新UI会报错呢?难道真的可以在子线程中访问UI?
先不急,这是一个极端的情况,修改MainActivity如下:
public
class
MainActivity
extends
AppCompatActivity
{
private
TextView
main_tv
;
@Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
setContentView
(
R
.
layout
.
activity_main
);
main_tv
=
(
TextView
)
findViewById
(
R
.
id
.
main_tv
);
new
Thread
(
new
Runnable
()
{
@Override
public
void
run
()
{
try
{
Thread
.
sleep
(
200
);
}
catch
(
InterruptedException
e
)
{
e
.
printStackTrace
();
}
main_tv
.
setText
(
"子线程中访问"
);
}
}).
start
();
}
}
让子线程睡眠200毫秒,醒来后再进行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:6581)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)
……
作为一名开发者,我们应该认真阅读一下这些异常信息,是可以根据这些异常信息来找到为什么一开始的那种情况可以访问UI的。那我们分析一下异常信息:
首先,从以下异常信息可以知道
at
android
.
view
.
ViewRootImpl
.
checkThread
(
ViewRootImpl
.
java
:
6581
)
这个异常是从android.view.ViewRootImpl的checkThread方法抛出的。
那现在跟进ViewRootImpl的checkThread方法瞧瞧,源码如下:
void
checkThread
()
{
if
(
mThread
!=
Thread
.
currentThread
())
{
throw
new
CalledFromWrongThreadException
(
"Only the original thread that created a view hierarchy can touch its views."
);
}
}
只有那么几行代码而已的,而mThread是主线程,在应用程序启动的时候,就已经被初始化了。
由此我们可以得出结论:
在访问UI的时候,ViewRootImpl会去检查当前是哪个线程访问的UI,如果不是主线程,那就会抛出如下异常:
Only the original thread that
created
a
view hierarchy can touch its
views
这好像并不能解释什么?继续看到异常信息
at
android
.
view
.
ViewRootImpl
.
requestLayout
(
ViewRootImpl
.
java
:
924
)
那现在就看看requestLayout方法,
@Override
public
void
requestLayout
()
{
if
(
!
mHandlingLayoutInLayoutRequest
)
{
checkThread
();
mLayoutRequested
=
true
;
scheduleTraversals
();
}
}
这里也是调用了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方法的的第二个参数传入了很像是一个后台任务。那再点进去
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
;
}
}
}
可以看到里面调用了一个performTraversals()方法,View的绘制过程就是从这个performTraversals方法开始的。PerformTraversals方法的代码有点长就不贴出来了,如果继续跟进去就是学习View的绘制了。而我们现在知道了,每一次访问了UI,Android都会重新绘制View。这个是很好理解的。
分析到了这里,其实异常信息对我们帮助也不大了,它只告诉了我们子线程中访问UI在哪里抛出异常。
而我们会思考:当访问UI时,ViewRootImpl会调用checkThread方法去检查当前访问UI的线程是哪个,如果不是UI线程则会抛出异常,这是没问题的。但是为什么一开始在MainActivity的onCreate方法中创建一个子线程访问UI,程序还是正常能跑起来呢??
唯一的解释就是执行onCreate方法的那个时候ViewRootImpl还没创建,无法去检查当前线程。
那么就可以这样深入进去。寻找ViewRootImpl是在哪里,是什么时候创建的。好,继续前进
在ActivityThread中,我们找到handleResumeActivity方法,如下:
final
void
handleResumeActivity
(
IBinder
token
,
boolean
clearHide
,
boolean
isForward
,
boolean
reallyResume
)
{
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler
();
mSomeActivitiesChanged
=
true
;
// TODO Push resumeArgs into the activity for consideration
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
;
}
可以看到r.activity.performResume()这行代码,跟进 performResume方法,如下:
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
();
if
(
mActivityMonitors
!=
null
)
{
synchronized
(
mSync
)
{
final
int
N
=
mActivityMonitors
.
size
();
for
(
int
i
=
0
;
i
N
;
i
++
)
{
final
ActivityMonitor
am
=
mActivityMonitors
.
get
(
i
);
am
.
match
(
activity
,
activity
,
activity
.
getIntent
());