序言
從Android開發的第一課開始,我們就有一個常識,即子線程不能更新UI,只能在轉到主線程去更新?所以我們在編碼時都遵照着這個原則,獲取到數據後通過handler去轉到主線程,通過Message拿到子線程發送過來的數據,具體可以看我的另一篇博客:
Android之Handler消息機制——深入理解 Looper、Handler、Message、MessageQueue
那麼問題來了,爲什麼我們不能在子線程去更新UI呢?我們先動手做一個實驗!
子線程更新UI
在XML文件中只添加一個TextView,在onCreate()方法中新建一個線程名爲Thread#2的線程,並在Thread#2線程中去更新UI
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (TextView) findViewById(R.id.tv);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Log.i("TAG", Thread.currentThread().getName());
tv.setText("隨機數" + (1 + Math.random() * (100 - 1 + 1)));
}
});
thread.setName("Thread#2");
thread.start();
通過點擊生成隨機數,並打印當前執行更新UI是否在子線程
以上是執行和日誌,可以看到,當前更新UI執行的線程是Thread#2,但是我們的APP並沒有死掉,textView的內容也更新了!也就是說,子線程可以更新UI!但是爲什麼谷歌官方禁止在子線程更新UI呢?我們接下來繼續做實例!
我們在子線程進行延時2秒,在看下效果!
竟然崩潰了,那問題來了,到底子線程能不能更新Ui呢?
2019-12-18 14:12:57.299 18904-18995/com.insigma.sdcard E/AndroidRuntime: FATAL EXCEPTION: Thread#2
Process: com.insigma.sdcard, PID: 18904
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8000)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1292)
at android.view.View.requestLayout(View.java:23147)
at android.view.View.requestLayout(View.java:23147)
at android.view.View.requestLayout(View.java:23147)
at android.view.View.requestLayout(View.java:23147)
at android.view.View.requestLayout(View.java:23147)
at android.view.View.requestLayout(View.java:23147)
at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3026)
at android.view.View.requestLayout(View.java:23147)
at android.widget.TextView.checkForRelayout(TextView.java:8914)
at android.widget.TextView.setText(TextView.java:5736)
at android.widget.TextView.setText(TextView.java:5577)
at android.widget.TextView.setText(TextView.java:5534)
at com.insigma.sdcard.MainActivity$82.run(MainActivity.java:1336)
at java.lang.Thread.run(Thread.java:764)
我們看一下報錯日誌,出錯的線程是Thread#2,也就是我們創建的線程,出錯的原因大概意思就是:只有創建視圖層級的原始線程,有權利處理它的視圖,創建視圖的線程就是主線程(UI線程),那爲什麼第一個例子沒有出現崩潰呢?
我們看一下錯誤日誌中的處理,是在checkThread()方法出現了崩潰,大概調用的流程是:
setText() ——> checkForRelayout()——>requestLayout()——>ViewRootImpl.checkThread()
但是還是沒有解釋爲什麼一下能更新一下又不能更新,不要着急,我們跟着調用的流程繼續去解析源碼!
源碼解析
setText()
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
//省略其他代碼
if (mLayout != null) {
checkForRelayout();
}
sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);
//省略其他代碼
}
sendOnTextChanged()和onTextChanged()方法應該都是用來繪製文本的,我們接着看checkForRelayout()方法
checkForRelayout()
private void checkForRelayout() {
// If we have a fixed width, we can just swap in a new text layout
// if the text height stays the same or if the view height is fixed.
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
//省略代碼
// We lose: the height has changed and we have a dynamic height.
// Request a new view layout using our new text layout.
requestLayout();
invalidate();
} else {
// Dynamic width, so we have no choice but to request a new
// view layout with a new text layout.
nullLayouts();
requestLayout();
invalidate();
}
}
invalidate()方法是用來重新繪製視圖的
我們繼續看requestLayout()方法
requestLayout()
public void requestLayout() {
//省略其他代碼
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
//View獲取到父View,mParent,然後調用父View的requestLayout方法,比如示例中的父View就是xml文件的
//根佈局就是RelativeLayout
@Override
public void requestLayout() {
super.requestLayout();
mDirtyHierarchy = true;
}
//通過調用父類的ViewRootImp.requestLayout(),最終到checkThread()
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
最重要的是checkThread()方法了,光看名字應該是用來檢查線程的
void checkThread() {
// 如果當前線程不是主線程就拋出異常
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
最重要的方法來了,mThread線程是主線程,Thread.currentThread()是當前線程,即我們運行的子線程
假如當前更新UI不在主線程,就會導致CalledFromWrongThreadException異常
由此可見,每一次刷新View都會調用ViewRootImp的checkThread()方法去檢測是否在主線程
問題:爲什麼onCreate裏面可以在子線程更新UI?延時之後又不能了呢?
繼續上一個問題,爲什麼onCreate裏面可以更新UI?是因爲沒有調用checkThread()方法嗎?
這個時候,我們可以搬出Activity的生命週期來解釋一下了!
activity的聲明週期經過oncreate()->onstart()->onResume()->onRestart()->onPouse()->onStop()->onDestory()
- onCreate:Activity已經創建了,View還沒有進行繪製
- onStart:Activity處於可見狀態,可以做一些初始化的操作
- onResume:View已經繪製完成,並且已經處於可見狀態
先強行解釋一波:會不會是因爲onCreate()時UI還沒有繪製完,所以不需要通過ViewRootImp去檢查是否是哪個線程,延時兩秒之後,Activity回調onResume()方法,View繪製完成,刷新View需要走checkThread()方法
我們直接看onResume()方法:
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...省略
// 初始化onResume方法
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
...
if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
...省略
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
...省略
}
直接看 r.activity.makeVisible()
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
makeVisible方法將View添加到WindowMannager窗口中,即準備展示View
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
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.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
在addView方法中,我們纔會對ViewRootImpl對象進行初始化,也就是說,執行onCreate()在子線程中更新UI時ViewRootImpl還沒出生,當然也不會去檢查線程!
問題:如果不做這個校驗,是不是我也是可以正常在子線程更新UI呢?
按理來說,這樣是可以的!但是google爲什麼要這樣去設計呢?
(1)如果在不同的線程去控制用一個控件,由於網絡延時或者大量耗時操作,會使UI繪製錯亂,出了問題也很難去排查到底是哪個線程更新時出了問題;
(2)主線程負責更新,子線程負責耗時操作,能夠大大提高響應效率
(3)UI線程非安全線程,多線程進行併發訪問有可能會導致內存溢出,降低硬件使用壽命;且非線程安全不能加Lock線程鎖,否則會阻塞其他線程對View的訪問,效率就會變得低下!