如題,Android能否在子線程中更新UI呢?這是一道面試題。那么這道題應該怎么去回答呢?在此我給出個人答案:“Android是不允許在子線程中更新UI的,但是在某種特殊情況下子線程是可以更新UI的”。為什么這么說呢?下面我們來看一個例子:
MainActivity
package example.lc.com.uicheckdemo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private TextView text;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
text= (TextView) findViewById(R.id.text);
new Thread(new Runnable() {
@Override
public void run() {
text.setText("在子線程中更新的UI");
}
}).start();
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="example.lc.com.uicheckdemo.MainActivity">
<TextView
android:layout_centerInParent="true"
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
</RelativeLayout>
例子很簡單就是一個Activity和一個布局,在onCreate中寫了一個Thread并且在子線程中更新了UI,但是程序不會報錯,不信大家可以去試一下。但是,將代碼做如下修改:
MainActivity
package example.lc.com.uicheckdemo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private TextView text;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
text= (TextView) findViewById(R.id.text);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e)
{
e.printStackTrace();
}
text.setText("在子線程中更新的UI");
}
}).start();
}
}
再次運行程序就會崩潰,logcat報錯如下:
01-07 18:21:11.207 1476-2957/example.lc.com.uicheckdemo E/AndroidRuntime: FATAL EXCEPTION: Thread-96 Process: example.lc.com.uicheckdemo, PID: 1476 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6118)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:848)
at android.view.View.requestLayout(View.java:16431)
at android.view.View.requestLayout(View.java:16431)
at android.view.View.requestLayout(View.java:16431)
at android.view.View.requestLayout(View.java:16431)
at android.view.View.requestLayout(View.java:16431)
at android.view.View.requestLayout(View.java:16431)
at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:352)
at android.view.View.requestLayout(View.java:16431)
at android.widget.TextView.checkForRelayout(TextView.java:6600)
at android.widget.TextView.setText(TextView.java:3813)
at android.widget.TextView.setText(TextView.java:3671)
at android.widget.TextView.setText(TextView.java:3646)
at example.lc.com.uicheckdemo.MainActivity$1$override.run(MainActivity.java:25)
at example.lc.com.uicheckdemo.MainActivity$1$override.access$dispatch(MainActivity.java)
at example.lc.com.uicheckdemo.MainActivity$1.run(MainActivity.java:0)
at java.lang.Thread.run(Thread.java:841)
那么這到底是為什么呢?這里我們提出問題,本文下面所有的內容都是要去解答這個問題的,接下來就正式開始我們今天的探索。
**
從錯誤信息入手尋找問題的根源
**
從logcat顯示的信息中我們發現了很有用的幾行:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6118)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:848)
這幾行信息直接告訴了我們錯誤是從ViewRootImpl的checkThread方法中報出來的,那么我們就去證實一下,打開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線程而Thread.currentThread()則是當前線程。由此,我們知道了Android中更新UI的時候檢查線程的操作是在ViewRootImpl中進行的。到這里我們還是沒有辦法去解答我們提出的問題,我們接著分析。
ViewRootImpl是何時創建的?
想要知道ViewRootImpl是在哪里創建的,我們要先要找到handleResumeActivity()方法,這個方法在ActivityThread中,代碼如下:
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
//代碼省略
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 decor = r.window.getDecorView();,這個decor 就是我們熟知的DecorView,但是這不是我們今天研究的重點。第二個是 ViewManager wm = a.getWindowManager();,這個才是今天真正的重點。ViewManager是一個接口其定義了addView、updateViewLayout、removeView三個方法,a.getWindowManager是一個WindowManager,WindowManager也是一個接口并且繼承了ViewManager,也就是說wm 是一個WindowManager。WindowManager是一個借口它的實現類是WindowManagerImpl, 也就是說wm.addView(decor, l);這句話調用的是WindowManagerImpl中的addView方法,我們看下WindowManagerImpl代碼:
package android.view;
import android.annotation.NonNull;
import android.os.IBinder;
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
private final Display mDisplay;
private final Window mParentWindow;
private IBinder mDefaultToken;
public WindowManagerImpl(Display display) {
this(display, null);
}
//代碼省略
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
//代碼省略
}
看到在WindowManagerImpl 的addView方法中,又調用了WindowManagerGlobal的addView方法,接著打開WindowManagerGlobal類,代碼如下:
package android.view;
//代碼省略
public final class WindowManagerGlobal {
private static final String TAG = "WindowManager";
//代碼省略
private Runnable mSystemPropertyUpdater;
private WindowManagerGlobal() {
}
public static void initialize() {
getWindowManagerService();
}
public static WindowManagerGlobal getInstance() {
synchronized (WindowManagerGlobal.class) {
if (sDefaultWindowManager == null) {
sDefaultWindowManager = new WindowManagerGlobal();
}
return sDefaultWindowManager;
}
}
//代碼省略
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent, then hardware acceleration for this view is
// set from the application's hardware acceleration setting.
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// Start watching for system property changes.
if (mSystemPropertyUpdater == null) {
mSystemPropertyUpdater = new Runnable() {
@Override public void run() {
synchronized (mLock) {
for (int i = mRoots.size() - 1; i >= 0; --i) {
mRoots.get(i).loadSystemProperties();
}
}
}
};
SystemProperties.addChangeCallback(mSystemPropertyUpdater);
}
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}
// If this is a panel window, then find the window it is being
// attached to for future reference.
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}
//代碼省略
}
以上是WindowManagerGlobal類,只給出了addView方法其他代碼省略了。我們仔細的來看下addView方法,再把無用的代碼省去,結果如下:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
//代碼省略
ViewRootImpl root;
//代碼省略
int index = findViewLocked(view, false);
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
//代碼省略
}
好了代碼省去的比較多,不過我們終于找到了我們想要的代碼了。 root = new ViewRootImpl(view.getContext(), display); 到這ViewRootImpl就創建完了!也就是說ViewRootImpl是在WindowManagerGlobal中的addView方法中創建的!
那么現在我們來嘗試回答一下我們提出的問題。合理的說法是讓線程睡眠200ms以后再次醒來此時onResume方法已經被調用ViewRootImpl已經創建完成,此時可以檢查線程了。