关于主线程更新UI这已经是个老生常谈的话题,几乎所有人都知道Android要在主线程更新UI。
Android官方文档这样描述:Android UI操作并不是线程安全的,并且这些操作必须在UI线程执行。
那么,子线程到底能否更新UI,如果不能更新UI,又是为什么?先来看一个简单的例子:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_centerInParent="true"
android:text="Hello World!"/>
</RelativeLayout>
public class MainActivity extends AppCompatActivity {
private TextView mTv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTv = (TextView) findViewById(R.id.tv);
new Thread(new Runnable() {
@Override
public void run() {
mTv.setText("Worker Thread Update UI");
}
}).start();
}
}
在activity的onCreate()中创建并启动子线程,在TextView上面简单的写了一句话,运行效果如下图:
奇怪,子线程更新UI居然不报错,这是为何?让我们再换一种写法:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTv = (TextView) findViewById(R.id.tv);
mTv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
mTv.setText("Worker Thread Update UI");
}
}).start();
}
});
}
运行如图所示:
点击上面的TextView,出现崩溃,找到如下异常信息:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6365)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:890)
at android.view.View.requestLayout(View.java:17516)
at android.view.View.requestLayout(View.java:17516)
at android.view.View.requestLayout(View.java:17516)
at android.view.View.requestLayout(View.java:17516)
at android.view.View.requestLayout(View.java:17516)
at android.view.View.requestLayout(View.java:17516)
at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:360)
at android.view.View.requestLayout(View.java:17516)
at android.widget.TextView.checkForRelayout(TextView.java:6943)
at android.widget.TextView.setText(TextView.java:4100)
at android.widget.TextView.setText(TextView.java:3948)
at android.widget.TextView.setText(TextView.java:3923)
at com.wzw.threadtest.MainActivity$1$1.run(MainActivity.java:22)
at java.lang.Thread.run(Thread.java:818)
发现这个异常是在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是否是同一个线程,如果不是,就抛出这个异常。
那么,现在有一个问题,这个mThread是主线程吗?而且,为什么在onCreate中子线程更新UI就不报错,而把更新UI的子线程放到点击事件里就报错了呢?我们可以猜想,是不是onCreate()方法在checkThread()方法之前执行,这样的话,就可以避免这个异常。光猜想没用,要用源码来证实我们的猜想是不是正确的。
好了,接下来开启源码之旅,一步一步跟进,看看系统在这里面到底做了些什么。
首先,先分析View的更新流程,View的更新会调用invalidate()方法,找到这个方法:
public void invalidate() {
invalidate(true);
}
一路跟进,发现最终调用到invalidateInternal()方法,点进去,忽略其它细节,只寻找我们需要的地方:
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
...
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
...
}
可以找到p.invalidateChild(this, damage)这个方法,这个方法是声明在ViewParent里面的,而ViewParent是个接口,我们找到它的实现类ViewGroup,并且找到这个方法,跟进去瞧瞧,同样,只寻找我们需要的地方:
public final void invalidateChild(View child, final Rect dirty) {
...
parent = parent.invalidateChildInParent(location, dirty);
...
}
在这里,实现了invalidateChildInParent()这个抽象方法的是ViewRootImpl,找到这个类,寻找这个方法:
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
if (DEBUG_DRAW) Log.v(TAG, "Invalidate child: " + dirty);
if (dirty == null) {
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
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);
}
}
invalidateRectOnScreen(dirty);
return null;
}
终于走到checkThread了,View更新一路走来,也就是在这里,会检查当前的Thread,如果和mThread这个成员变量不一样,就会抛出CalledFromWrongThreadException这个异常,那么问题来了,mThread这个变量是在哪里被赋值的呢?一般情况下,构造方法中会初始化很多变量,那我们去构造方法中瞧瞧:
public ViewRootImpl(Context context, Display display) {
mContext = context;
mWindowSession = WindowManagerGlobal.getWindowSession();
mDisplay = display;
mBasePackageName = context.getBasePackageName();
mDisplayAdjustments = display.getDisplayAdjustments();
mThread = Thread.currentThread(); //mThread在这里被赋值
mLocation = new WindowLeaked(null);
mLocation.fillInStackTrace();
mWidth = -1;
mHeight = -1;
mDirty = new Rect();
mTempRect = new Rect();
mVisRect = new Rect();
mWinFrame = new Rect();
mWindow = new W(this);
mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
mViewVisibility = View.GONE;
mTransparentRegion = new Region();
mPreviousTransparentRegion = new Region();
mFirst = true; // true for the first time the view is added
mAdded = false;
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
mAccessibilityManager = AccessibilityManager.getInstance(context);
mAccessibilityInteractionConnectionManager =
new AccessibilityInteractionConnectionManager();
mAccessibilityManager.addAccessibilityStateChangeListener(
mAccessibilityInteractionConnectionManager);
mHighContrastTextManager = new HighContrastTextManager();
mAccessibilityManager.addHighTextContrastStateChangeListener(
mHighContrastTextManager);
mViewConfiguration = ViewConfiguration.get(context);
mDensity = context.getResources().getDisplayMetrics().densityDpi;
mNoncompatDensity = context.getResources().getDisplayMetrics().noncompatDensityDpi;
mFallbackEventHandler = new PhoneFallbackEventHandler(context);
mChoreographer = Choreographer.getInstance();
mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
loadSystemProperties();
}
我们可以看到,在构造方法中,获取了当前的Thread作为mThread,可是我们还是不能说明mThread就一定是主线程。走到这里,我们已经找到了最开始抛异常的地方,回到最开始我们提出的那个疑问,checkThread这个方法执行的时间是不是比Activity的onCreate()方法执行的要晚呢?那我们只有看一看Activity的生命周期是在哪里被回调的。从Activity的startActivity开始,一步一步跟进源码,这里涉及到Activity的启动流程,由于和本篇文章关系不大,所以也就不在此分析了,我就只找出关键的地方来和大家一起分析:在我们的app启动时,系统会调用ActivityThread的main方法,并且把ActivityThread作为我们app的主线程,在ActivityThread中,找到handleLaunchActivity()这个方法:
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
Activity a = performLaunchActivity(r, customIntent);
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
Bundle oldState = r.state;
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed);
...
}
直接找到关键地方,一个是performLaunchActivity(),另一个是handleResumeActivity(),先进performLaunchActivity()这个方法:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
...
}
功夫不负有心人,终于找到了onCreate被回调的地方,顺着callActivityOnCreate()一路走下去,会调用activity的onCreate()
再看另一个关键地方,handleResumeActivity()方法,跟进里面去瞧瞧:
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
...
ActivityClientRecord r = performResumeActivity(token, clearHide);//这里是activity的onResume被调用的入口
...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
wm.addView(decor, l);//这里是将View和window关联的地方
}
} else if (!willBeVisible) {
if (localLOGV) Slog.v(
TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
...
}
这里我们找到了View和Window关联的地方wm.addView(decor, l);由于WindowManager是个接口,我们找到它的实现类WindowManagerImpl,并且找到addView方法:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
顺着mGlobal.addView(view, params, mDisplay, mParentWindow)这一句,继续跟进:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
...
}
无关的代码我都用省略号表示,我们发现ViewRootImpl 对象在这里被创建,到底为止,文章开始提出的疑问也就解决了,onCreate方法比onResume方法回调的早,而ViewRootImpl 对象是在activity的onResume之后创建,ViewRootImpl 中的mThread变量是在构造器中被赋值,所以,在onCreate更新UI,并不会出现异常,而我们添加点击事件以后,由于activity已经可见并且可交互,处于Resume状态,这时ViewRootImpl中的mThread已经是主线程,所以,我们在启动的分线程中更新UI就会报错。
看了这么长的源码,终于完全清晰的了解了子线程更新UI报错的原理。
可是我依然不满足,还是在想,既然ViewRootImpl的变量mThread获得的是当前的Thread,那如果它获得了当前的Thread和操作UI的Thread相同,是不是就不会报错了呢?那我们如何实现这个想法呢?
我们可以自己手动addView,这样就会让ViewRootImpl初始化一次,在分线程初始化它,在分线程操作UI,是不是不会报错呢?哈哈,这个想法很奇葩,我们来实现一下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTv = (TextView) findViewById(R.id.tv);
mTv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
TextView tv = new TextView(MainActivity.this);
tv.setTextColor(Color.RED);
tv.setGravity(Gravity.CENTER);
tv.setText("Worker Thread Update UI");//在子线程中创建一个TextView
WindowManager windowManager = MainActivity.this.getWindowManager();
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
windowManager.addView(tv, params);
Looper.loop();
}
}).start();
}
});
}
运行这个demo,如下图所示:
OK,成功了,通过这个demo可以看到,说到底子线程是可以更新UI的,只不过需要有它自己的ViewRoot,Android系统在ActivityThread中,也就是主线程中创建了ViewRoot(onResume之后),而在子线程中,需要我们自己手动创建ViewRoot,这样才可以操作UI,但是我们通常并不会这样做。
通过这么长的源码探索之路,我们对文章开篇提出的几个问题也有了深刻的理解,当我们看到问题的表象时,也应当挖掘其内部原理,这样,我们对于Android的理解才能成为一个体系,而不是杂乱无章的。