我們在看一些書或者部落格時總是會看到一句話“android更新UI操作都是在Main主執行緒中,子執行緒中不能進行UI更新操作”那麼,在子執行緒中真的不能進行UI的更新操作嗎?
//原始碼環境申明
compileSdkVersion 24
buildToolsVersion "24.0.2"
defaultConfig {
minSdkVersion 14
targetSdkVersion 24
}複製程式碼
首先我們來看一段程式碼:
在onCreate生命週期中新增子執行緒更新UI操作
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
new Thread(new Runnable() {
@Override
public void run() {
text.setText("子執行緒重新整理UI");
}
}).start();
}複製程式碼
我們執行一下程式,發現APP沒有崩潰,同時頁面上顯示了“子執行緒重新整理UI”咦?不是說子執行緒中不能進行UI的更新操作嘛?為什麼這個可以呢?
我們在onResume生命週期中新增子執行緒更新UI操作
@Override
protected void onResume() {
super.onResume();
new Thread(new Runnable() {
@Override
public void run() {
text.setText("子執行緒重新整理UI");
}
}).start();
}複製程式碼
然後執行一下APP,發現這次APP崩潰了,列印一下Log日誌
其中有一句經典的異常資訊:
Only the original thread that created a view hierarchy can touch its views.複製程式碼
相信這句話大家應該基本都碰到過吧,這又是怎麼回事呢?
為什麼在onCreate中更新沒事,而在onResume中更新就崩潰了?
帶著這個問題,我們來跟蹤一下原始碼,然後給出答案!
首先我們來看一下崩潰日誌,一般情況下我們都可以通過崩潰日誌來追蹤問題。我們通過異常棧來一步一步追蹤根源。
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.複製程式碼
第一句顯示說明異常出現的原因:只有主執行緒才能進行更新UI操作。接下來一句:
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:5288)複製程式碼
我們進入到ViewRootImpl.checkThread方法中
Tip: 對於AndroidStudioIDE我們可以通過雙擊shift鍵來查詢相關的檔案
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}複製程式碼
對於checkThread()方法,只有簡單的幾行程式碼,只是進行常規檢查,如果當前執行緒不是主執行緒的話則會丟擲異常。而mThread 這個變數是在建構函式中進行初始化的(記住,後面需要用到)。
public ViewRootImpl(Context context, Display display) {
//省略無關程式碼
//...
mThread = Thread.currentThread();
//...
//省略無關程式碼
}複製程式碼
上面只是簡單的進行判斷當前執行緒是否是主執行緒,沒什麼價值,我們繼續看第三行異常log資訊
at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:974)複製程式碼
進入到該方法中,檢視
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
if (dirty == null) {
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
//省略無關程式碼
invalidateRectOnScreen(dirty);
return null;
}複製程式碼
好像並沒有什麼有用的提示,第一行也只是進行執行緒判斷,我們進入到 invalidate()中看看,
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
scheduleTraversals();
}
}複製程式碼
也沒什麼,繼續進入scheduleTraversals()方法
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}複製程式碼
好像也沒什麼嘛,但是我們注意觀察,可以發現
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);複製程式碼
裡面有一個引數 mTraversalRunnable,runnable不是執行緒的意思嘛,我們開啟子執行緒經常使用到的,我們跟蹤一下,看看究竟。
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}複製程式碼
咦,好像有點看頭哦!對於執行緒操作,我們一般有兩種方法
- 實現Runnable介面,實現run方法
- 繼承java的原有執行緒Thread類,實現run方法
這兩種方法的區別就是第二種繼承Thread,Thread類也是通過實現Runnable方法來實現的,本質上沒啥多大區別,只不過繼承Thread類可以使用裡面的一些方法而已。
而TraversalRunnable 類是直接實現Runnable介面,裡面只有一個方法,我們進入到裡面瞧瞧:
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的繪製了。通過以上程式碼我們可以知道每一次訪問了UI,Android都會重新繪製View。
其實到了這裡我們可以總結一點了,當訪問UI時,ViewRootImpl會呼叫checkThread方法去檢查當前訪問UI的執行緒是哪個,如果不是UI執行緒則會丟擲異常
但是為什麼在OnCreate中可以操作子執行緒更新UI,而在OnResume中則不可以?而當前執行緒則是在ViewRootImpl的建構函式中進行初始化的,因此我們可以大膽的猜測一下:
ViewRootImpl類在onCreate中還沒建立完成,而在onResume中已經建立完成了。
為了驗證上面的猜想是不是正確的,我們需要解決的問題就是
ViewRootImpl在哪裡進行建立的?
對於以上問題,我們需要用到這個類ActivityThread,這個類是幹啥用的呢?看名字就知道是關於主執行緒的,我們瀏覽一下這個類,發現有個main方法,類似與java的main方法,這就是APP的全域性入口處,
public static void main(String[] args) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
SamplingProfilerIntegration.start();
// CloseGuard defaults to true and can be quite spammy. We
// disable it here, but selectively enable it later (via
// StrictMode) on debug builds, but using DropBox, not logs.
CloseGuard.setEnabled(false);
Environment.initForCurrentUser();
// Set the reporter for event logging in libcore
EventLogger.setReporter(new EventLoggingReporter());
// Make sure TrustedCertificateStore looks in the right place for CA certificates
final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
TrustedCertificateStore.setDefaultUserDirectory(configDir);
Process.setArgV0("<pre-initialized>");
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}複製程式碼
我們在一開始的時候就注意到在onCreate中是可以進行子執行緒UI更新操作的,而在OnResume中是不可以的,我們當時猜測是因為在onResume中ViewRootImpl已經建立初始化完成了,所以能夠進行checkThread檢查,對於此我們需要了解,onResume是在哪裡回撥的,於是我們進入ActivityThread類中,裡面有個方法:
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
//省略無關程式碼...
// TODO Push resumeArgs into the activity for consideration
r = performResumeActivity(token, clearHide, reason);
if (r != null) {
final Activity a = r.activity;
//省略無關程式碼...
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
//省略無關程式碼...複製程式碼
對於這一行程式碼
r = performResumeActivity(token, clearHide, reason);
我們可以大概知道這是進行resume回到的,到底是不是呢?我們進去看看,
public final ActivityClientRecord performResumeActivity(IBinder token,
boolean clearHide, String reason) {
if (r != null && !r.activity.mFinished) {
//省略無關程式碼...
r.activity.performResume();
}複製程式碼
我們大概瀏覽一下方法,然後刪除不必要的程式碼,裡面有個方法,
r.activity.performResume();
繼續進入,然後跟蹤看看
final void performResume() {
performRestart();
//省略無關程式碼...
mCalled = false;
// mResumed is set by the instrumentation
mInstrumentation.callActivityOnResume(this);
}複製程式碼
繼續進入方法
mInstrumentation.callActivityOnResume(this);
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());
}
}
}
}複製程式碼
沒錯了,是在這裡面進行onResume的回撥的,對於此我們可以繼續回到handleResumeActivity方法中去了,接下繼續分析後面的程式碼
if (r != null) {
final Activity a = r.activity;
//省略無關程式碼...
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}複製程式碼
我們進入到makeVisible方法中去看看
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}複製程式碼
往WindowManager中新增DecorView,那現在應該關注的就是WindowManager的addView方法了。而WindowManager是一個介面來的,我們應該找到WindowManager的實現類才行,而WindowManager的實現類是WindowManagerImpl。
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}複製程式碼
進入addview方法檢視
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;
}
}複製程式碼
沒錯,就是這個
root = new ViewRootImpl(view.getContext(), display);
對ViewRootImpl進行初始化,然後進行UI執行緒檢測操作,到了此處,我們程式碼分析基本就結束了,來總結一下原始碼分析的過程。
首先,我們在onCreate中進行子執行緒操作UI,沒有崩潰,而在onResume中進行子執行緒操作UI崩潰了,對於異常log資訊,我們知道,檢測當前執行緒是否是主執行緒的操作是在ViewRootImpl中,此我們猜想,ViewRootImpl的初始化是在onResume中進行的,對於此我們一步步進行跟蹤原始碼進行檢視。從而得出結論是正確的。