Android繪製View相關的幾個問題
前面關於View繪製的話題好像零散的寫過部落格,雖然好久沒有認真的研究一些東西了,平時忙其他的東西,但是本著每個月必須花幾天時間看看android的想法,今天整理了幾個View繪製相關的問題,這裡不會涉及View測量佈局繪製的那部分細節,因為這些前面已經寫過了。主要有以下幾個問題
1.View繪製流程 invalidate/requestLayout
2.View樹和DecorView
4.真的只能在主執行緒操作UI嗎
5.幾個函式:WindowManagere#addView、ViewGroup#addView以及PhoneWindow#addContentView的區別
View繪製流程 invalidate/requestLayout
我們平時要重新整理介面的時候會主動呼叫Invalidate函式,其實一些函式也會間接呼叫invalidate函式比如,setVisibility等與介面有關的函式都會輾轉呼叫到與invalidate相關的一些函式。當呼叫invalidate函式更新介面重新繪製時,首先要明確,介面的繪製是由ViewRoot開始的。正常情況下,一個Activity就有一個ViewRoot,當某個控制元件狀態改變後呼叫invalidate函式會一層一層的呼叫父控制元件的方法,直到呼叫的View樹的根,也即ViewRoot。最後ViewRoot會呼叫scheduleTraversals函式,裡面會產生一個非同步的繪製事件執行我們都知道的控制元件繪製相關的三個函式measure、layout和draw函式,簡單地說就是某個View要重新繪製時,都是請求它所在的View樹根來繪製的。
函式呼叫流程如圖所示,當某個View呼叫到invalidate函式,它會呼叫父View(準確的說應該是ViewParent物件)的invalidateChild函式,在呼叫到invalidateChildInParent函式,在這個函式裡面會迴圈呼叫上一層父View的invalidateChildInParent函式,這部分所做的處理主要是設定View的mPrivateFlags引數的位標誌,計算要繪製的矩形等,直到呼叫到View樹的根。
當某個View就是View樹的第一個孩子,它呼叫invalidate重新繪製時,就直接呼叫到ViewRoot的invalidateChild函式,也就是圖中虛線部分,在ViewRoot中的invalidateChild函式中也僅僅呼叫了invalidateChildInParent函式。這個View一般來說對應的就是DecorView。
同樣還有一個用來繪製的函式requestLayout,這個函式也會導致View樹的重新測量、佈局,這個函式要比invalidate函式更強一點,前面說的invalidate函式主要作用於draw函式,也就是強制重新繪製當前View,當要繪製的View的大小位置沒有變化時是就可以不重新測量和layout佈局了,只需要繪製即可,而當使用requestLayout函式時,就會強制測量和重新佈局,其實也就是設定了View的mPrivateFlags的某些位的標記。
public void requestLayout() {
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
}
當呼叫到measure時由於PFLAG_FORCE_LAYOUT已經被設定,所以就會重新measure,在layout時也是類似的。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// ...
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
// ...
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
}
View樹和DecorView
回顧Activity的生命週期onResume,啟動一個Activity後最終會呼叫到ActivityThread中的handleResumeActivity,在裡面首先呼叫performResumeActivity的執行最終會執行到activity的生命週期onResume,接下來會執行View的新增和顯示。
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);
}
}
當呼叫WindowManager的addView方法時,會呼叫到WindowManagerImp的addView方法,WindowManagerImp類是對WindowManagerGlobal的封裝。
public void addView(View view, ViewGroup.LayoutParams params) {
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); // 建立一個ViewRoot物件
view.setLayoutParams(wparams);
if (mViews == null) {
index = 1;
mViews = new View[1];
mRoots = new ViewRootImpl[1];
mParams = new WindowManager.LayoutParams[1];
} else {
index = mViews.length + 1;
Object[] old = mViews;
mViews = new View[index];
System.arraycopy(old, 0, mViews, 0, index-1);
old = mRoots;
mRoots = new ViewRootImpl[index];
System.arraycopy(old, 0, mRoots, 0, index-1);
old = mParams;
mParams = new WindowManager.LayoutParams[index];
System.arraycopy(old, 0, mParams, 0, index-1);
}
index--;
mViews[index] = view;
mRoots[index] = root;
mParams[index] = wparams;
}
try {
root.setView(view, wparams, panelParentView);// 呼叫ViewRoot的setView方法
}
// ......
}
在WindowManagerGlobal中mViews儲存了新增的View,mRoots儲存了ViewRoot,mParams儲存了新增該View的佈局引數,這三個陣列中儲存的資料是對應的。然後在setView函式中請求UI重新繪製,並且通過IPC呼叫通知WindowManagerService新增視窗。
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout(); // 請求UI開始繪製重新繪製View樹
// ......
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
// 通知WindowManagerService新增一個視窗 會呼叫到addWindow方法
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mInputChannel);
}
//…
//指定了該View的父親(其實是ViewParent物件)是新建立的ViewRoot
view.assignParent(this);
//…
}
至此就是系統啟動一個Activity時將其DecorView新增到View樹的過程,總結起來就是在Activity執行完onResume後,建立View樹,將DecorView通過IPC呼叫WindowManagerService的addView函式新增到視窗,並且呼叫requestLayout函式請求繪製視窗,注意到在ViewRootImp#setView函式中呼叫view.assignParent(this);也即指定了頂層DecorView的ViewParent為ViewRootImp物件。
上面提到的ViewRoot均為ViewRootImp類,舊版本中確實有這個類,但在新版本的Android原始碼中改名變成了ViewRootImp類,它是實現了ViewParent介面的,並不是一個View。所有的ViewGroup也實現了ViewParent,每個View都在內部儲存了一個ViewParent物件,來表示他的上層”父View”,所以對於最頂層的DecorView來說它的mParent變數指的就是一個ViewRootImp物件,即這棵View樹的樹根。
真的不能在子執行緒更新UI嗎
在上面看到呼叫了ViewRootImp的requestLayout函式,他在執行繪製View樹之前呼叫了一個非常重要的函式,也就是checkThread函式。在呼叫invalidate函式時也是一樣的,當呼叫到ViewRootImp的invalidateChildInParent函式時,也首先呼叫了checkThread函式。
public void requestLayout() {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
我們看一下checkThread函式,發現丟擲的這句異常經常在開發的時候遇到,當在子執行緒中更新UI就會提示這句話,顯然這裡意思是說:只有建立這個View繼承關係的執行緒才能修改這個View。
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
這裡面也只是簡單的比較了mThread的值和當前執行緒是否相同,而mThread的賦值是在ViewRootImp的建構函式裡面。
public ViewRootImpl(Context context, Display display) {
super();
//…
mThread = Thread.currentThread();
// …
}
那麼,我們就知道了,在重新整理View的時候執行checkThread並不一定是說再檢查我們是不是在UI執行緒修改。而是說現在修改的這個View他所在的View樹的根建立的執行緒是否和當前操作View的執行緒一樣。
當然了,DecorView所在的View樹就是在UI執行緒中建立的,因此大多數時候我們操作的View都在這棵樹下面,造成的結果是隻能在建立DecorView的ViewRoot的執行緒也就是UI執行緒中修改View。
那麼有兩個方面可以考慮,一方面這個View樹是在onResume後才建立的,那麼在此之前修改UI由於當前的View樹的樹根還不存在,因此暫時不會繪製介面,只會儲存設定的狀態,在下次請求繪製UI時會再次重新整理UI。另一方面,正如上面新增DecorView的方式既然WindowManager#addView方法可以建立一個ViewRoot,那麼通過這種方式來修改介面就可以有自己的View樹,也就不受限於主執行緒中修改UI了,可以看下面的測試可以正常的執行。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (TextView)findViewById(R.id.tv);
new Thread() {
public void run(){
tv.setText("change text in non-UI Thread");
}
}.start();
}
上面的程式碼可以正常的執行,而下面的程式碼就會報異常。由於睡眠兩秒鐘後才更新UI,這段時間內早已完成了前期的初始化,onResume也已經執行完成,有了自己的View樹,當更新View狀態時會進行執行緒檢查的。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (TextView)findViewById(R.id.tv);
new Thread() {
public void run(){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
tv.setText("change text in non-UI Thread");
}
}.start();
}
建立自己的View樹,這種情況在懸浮框效果中用到過,只不過我們這裡放在子執行緒中。如下面簡單程式碼在子執行緒中呼叫,效果為在螢幕顯示一幅圖片,達到了在子執行緒操作UI的效果。
private void viewInOtherThread(){
view=new View(getApplicationContext());
view.setBackgroundResource(R.drawable.ic_launcher); mWindowManager=(WindowManager)
getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams param=new WindowManager.LayoutParams();
param.type=WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
param.format=1;
param.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
param.flags = param.flags | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
param.flags = param.flags | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
param.alpha = 1.0f;
param.width=200;
param.height=200;
mWindowManager.addView(view, param);
}
需要注意的是,上面的viewInOtherThread在子執行緒中呼叫時需要建立Looper,因為ViewRootImp這個物件中包含一個mHandler也即一個Handler物件,因此需要一個Looper,另外在activity銷燬時記得移除這個view,並且記得新增許可權.
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
新增View的幾個方法的區別
提到View樹,順便可以探討的是以下幾個方法:WindowManagere#addView、ViewGroup#addView以及Activity#addContentView
考慮下面的例子,XML檔案很簡單,就是一個垂直線性佈局裡面有一個TextView。為了清除起見,設定主題為沒有標題欄。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/ll"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"/>
</LinearLayout>
這時View的介面層次如圖
當使用前面WindowManagere#addView方式新增的View,會建立一個新的View樹,使用HierarchyView工具看不出來,但至少沒有在原始的View樹上看到新增的View。
使用ViewGroup#addView方法,在當前線性佈局中新增新的View,View樹如下圖所示,即這個View新增到已有的View樹上,也就是DecorView所在的樹上。
使用Activity#addContentView方式新增一個新的View,View樹如下圖所示,顯然新新增的View是處在和@id/ll這個佈局同一個層級上,也即android:id/content的子View,這從該函式的名字也可以清除的看出來。
setContentView會覆蓋掉android:id/content下面的View,前面呼叫設定過的View被後面的覆蓋掉了
檢視原文:http://qhyuang1992.com/index.php/2016/06/21/android_hui_zhi_view_xiang_guan_de_ji_ge_wen_ti/