編輯:Android資訊
Android單線程模型是這樣描述的:
Android UI操作並不是線程安全的,並且這些操作必須在UI線程執行
如果在其它線程訪問UI線程,Android提供了以下的方式:
Activity.runOnUiThread(Runnable) View.post(Runnable) View.postDelayed(Runnable, long) Handler
為什麼呢?在子線程中就不能操作UI麼?
當一個程序第一次啟動的時候,Android會同時啟動一個對應的主線程,這個主線程就是UI線程,也就是ActivityThread。UI線程主要負責處理與UI相關的事件,如用戶的按鍵點擊、用戶觸摸屏幕以及屏幕繪圖等。系統不會為每個組件單獨創建一個線程,在同一個進程裡的UI組件都會在UI線程裡實例化,系統對每一個組件的調用都從UI線程分發出去。所以,響應系統回調的方法永遠都是在UI線程裡運行,如響應用戶動作的onKeyDown()的回調。
那為什麼選擇一個主線程干這些活呢?換個說法,Android為什麼使用單線程模型,它有什麼好處?
先讓我們看下單線程化的事件隊列模型是怎麼定義的:
采用一個專門的線程從隊列中抽取事件,並把他們轉發給應用程序定義的事件處理器
這看起來就是Android的消息隊列、Looper和Handler嘛。類似知識請參考: 深入理解Message, MessageQueue, Handler和Looper
其實現代GUI框架就是使用了類似這樣的模型:模型創建一個專門的線程,事件派發線程來處理GUI事件。單線程化也不單單存在Android中,Qt、XWindows等都是單線程化。當然,也有人試圖用多線程的GUI,最終由於競爭條件和死鎖導致的穩定性問題等,又回到單線程化的事件隊列模型老路上來。單線程化的GUI框架通過限制來達到現場安全:所有GUI中的對象,包括可視組件和數據模型,都只能被事件線程訪問。
這就解釋了Android為什麼使用單線程模型。
那Android的UI操作並不是線程安全的又是怎麼回事?
Android實現View更新有兩組方法,分別是invalidate和postInvalidate。前者在UI線程中使用,後者在非UI線程中使用。換句話說,Android的UI操作不是線程安全可以表述為invalidate在子線程中調用會導致線程不安全。作一個假設,現在我用invalidate在子線程中刷新界面,同時UI線程也在用invalidate刷新界面,這樣會不會導致界面的刷新不能同步?既然刷新不同步,那麼invalidate就不能在子線程中使用。這就是invalidate不能在子線程中使用的原因。
postInvalidate可以在子線程中使用,它是怎麼做到的?
看看源碼是怎麼實現的:
public void postInvalidate() { postInvalidateDelayed(0); } public void postInvalidateDelayed(long delayMilliseconds) { // We try only with the AttachInfo because there's no point in invalidating // if we are not attached to our window if (mAttachInfo != null) { Message msg = Message.obtain(); msg.what = AttachInfo.INVALIDATE_MSG; msg.obj = this; mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds); } }
說到底還是通過Handler的sendMessageDelayed啊,還是逃不過消息隊列,最終還是交給UI線程處理。所以View的更新只能由UI線程處理。
如果我非要在子線程中更新UI,那會出現什麼情況呢?
android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
拋了一個CalledFromWrongThreadException異常。
相信很多人遇到這個異常後,就會通過前面的四種方式中的其中一種解決:
Activity.runOnUiThread(Runnable) View.post(Runnable) View.postDelayed(Runnable, long) Handler
說到底還沒觸發到根本,為什麼會出現這個異常呢?這個異常在哪裡拋出來的呢?
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } }
該代碼出自 framework/base/core/java/android/view/ViewRootImpl.java
再看下ViewRootImpl的構造函數,mThread就是在這初始化的:
public ViewRootImpl(Context context, Display display) { mContext = context; mWindowSession = WindowManagerGlobal.getWindowSession(); mDisplay = display; mBasePackageName = context.getBasePackageName(); mDisplayAdjustments = display.getDisplayAdjustments(); mThread = Thread.currentThread(); ...... }
再研究一下這個CalledFromWrongThreadException異常的堆棧,會發現最後到了invalidateChild和invalidateChildInParent方法中:
@Override public void invalidateChild(View child, Rect dirty) { invalidateChildInParent(null, dirty); } @Override public ViewParent invalidateChildInParent(int[] location, Rect dirty) { checkThread(); ...... }
最終通過checkThread形成了這個異常。說到底,非UI線程是可以刷新UI的呀,前提是它要擁有自己的ViewRoot。如果想直接創建ViewRoot實例,你會發現找不到這個類。那怎麼做呢?通過WindowManager。
class NonUiThread extends Thread{ @Override public void run() { Looper.prepare(); TextView tx = new TextView(MainActivity.this); tx.setText("non-UiThread update textview"); WindowManager windowManager = MainActivity.this.getWindowManager(); WindowManager.LayoutParams params = new WindowManager.LayoutParams( 200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW, WindowManager.LayoutParams.TYPE_TOAST,PixelFormat.OPAQUE); windowManager.addView(tx, params); Looper.loop(); } }
就是通過windowManager.addView創建了ViewRoot,WindowManagerImpl.java中的addView方法:
@Override public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { applyDefaultToken(params); mGlobal.addView(view, params, mDisplay, mParentWindow); }
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
mGlobal是一個WindowManagerGlobal實例,代碼在 frameworks/base/core/java/android/view/WindowManagerGlobal.java中,具體實現如下:
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; } }
所以,非UI線程能更新UI,只要它有自己的ViewRoot。
延伸一下:Android Activity本身是在什麼時候創建ViewRoot的呢?
既然是單線程模型,就要先找到這個UI線程實現類ActivityThread,看裡面哪裡addView了。沒錯,是在onResume裡面,對應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.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); } // If the window has already been added, but during resume // we started another activity, then don't yet make the // window visible. } else if (!willBeVisible) { if (localLOGV) Slog.v( TAG, "Launch " + r + " mStartedActivity set"); r.hideForNow = true; } ...... }
所以,如果在onCreate中通過子線程直接更新UI,並不會拋CalledFromWrongThreadException異常。但是一般情況下,我們不會在onCreate中做這樣的事情。
這就是Android為我們設計的單線程模型,核心就是一句話:Android UI操作並不是線程安全的,並且這些操作必須在UI線程執行。但這一句話背後,卻隱藏著我們平時看不見的代碼實現,只有搞懂這些,我們才能知其然知其所以然。
前言 最近研究Android推送的實現, 研究了兩天一夜, 有了一點收獲, 寫下來既為了分享, 也為了吐槽. 需要說明的是有些東西偏底層硬件和通信行業, 我對這些
本篇文章小編為大家介紹,Android 軟件自動更新功能實現的方法。需要的朋友參考下。 相信所有的用戶都遇到過軟件提醒更新的情況,下面就將實現此功能 首先看一下程
基於Android 6.0的源碼,剖析存儲架構的設計 Android 存儲系統之源碼篇 Android 存儲系統之架構篇 一、概述 本文講述Android存儲系統
在Android平台,對於和硬件交互相關的模塊來說,比如:和雙卡對應的Telephony模塊、和拍照對應的Camera模塊,以及Bluetooth模塊等等,不同廠