前言
前面幾篇對動畫可以說是做了非常全面的總結(jié)了(上篇文章最后的4種ViewGroup相關(guān)動畫相信在了解基礎(chǔ)后看些文章也不會太難理解)。在View的工作原理 這一部分我們將對View做全面深入的解析。由于本人是菜鳥,其實無法直接看源碼,也都是通過書籍與文章反復(fù)閱讀,然后才去看的源碼。由于怕忘記寫成博客。希望和我一樣不了解的朋友能在自定義View中不那么迷茫。如果那里有錯誤大家一定指出我將不勝感激。
Activity#setContentView
關(guān)于View的工作原理,大家可能會問:為什么不直接看View呢?因為我覺得Activty是呈現(xiàn)應(yīng)用界面的載體,所有的View都在Acitivity中,并且在理解Activity的啟動XML的加載也是一種了解View工作原理的一個很好的入口。好了廢話不多說:翠花~上酸菜(代碼):
注:在View的工作原理中涉及到源碼為:API=23 以后不再說明 重要的一些源碼或是方法較長我會標(biāo)出在源碼中所在行數(shù)
activity_main :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.ggxiaozhi.activityuicode.MainActivity">
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="Hello World!"
/>
</LinearLayout>
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.tv);
}
布局非常簡單,首先注意兩點:
- 繼承Activity而不是繼承AppCompatActivity
其實了解的知道2者本質(zhì)是沒有區(qū)別的,只是AppCompatActivity做了許多的兼容性方面的處理。自然在流程上相對Activity會在分析上更復(fù)雜些。所以這里繼承自Activity,當(dāng)然你繼承AppCompatActivity也是可以的。
- requestWindowFeature(Window.FEATURE_NO_TITLE);
這個大家應(yīng)該都是比較了解,就是去除Title。也就是標(biāo)題欄。
setContentView
我們查看setContentView發(fā)現(xiàn)他的源碼如下:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
我們在追蹤發(fā)現(xiàn)是調(diào)用Window#setContentView(),但是Window是一個抽象類setContentView是一個抽象方法,并且的進去getWindow()方法中發(fā)現(xiàn)他里面返回mWindow,那么我只需要知道m(xù)Window一定是Window的實現(xiàn)類里面實現(xiàn)了setContentView方法。通過查找發(fā)現(xiàn)在Activity中只有一個方法將mWindow賦值,如下:
mWindow = new PhoneWindow(this, window);//6619
原來是調(diào)用了PhoneWindow#setContentView(),那我們就進入PhoneWindow查看一下,發(fā)現(xiàn)點不了。原因是因為PhoneWindow被隱藏了,我們是看不到的。所以我們需要去sdk源碼中找到這個類。這里我以我的電腦為例,目錄在D:\Android\Administrator\AppData\Local\Android\sdk\sources\android-23。里面包含我們下載的所有sdk版本,也可以直接通過Android Stuido查看路徑再進入\sources目錄下。然后直接在右上角搜索類名。OK!我們繼續(xù)。找到setConentView,如下:
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
//這里mContentParent就是我們布局加載的父View,activity_main就是加載到他里面
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
里面有2個setConentView方法。我們主要看我們用到的。如上。當(dāng)進入Acitvity時mContentParent一定為空,那么就會進入installDecor()方法中。
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor(); ...(1)
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);...(2)
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
...
}
我們這里看主要代碼。首先mDecor為空。然后調(diào)用generateDecor()給mDecor賦值。在generateDecor()方法中主要是創(chuàng)建了一個DecorView。DecorView它是什么呢?其實他是PhoneWindow中的一個內(nèi)部類。繼承FrameLayout。那我們知道了,他是一個布局控件。我們回來繼續(xù)看 (2) 處。在這里對mContentParent進行賦值。我們追蹤generateLayout(mDecor)方法:
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
TypedArray a = getWindowStyle();
.........
/**以下這些是Activity 窗口屬性特征的設(shè)置*/
//窗口是否浮動,一般用于Dialog窗口是否浮動:是否顯示在布局的正中間。
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
& (~getForcedWindowFlags());
if (mIsFloating) {
setLayout(WRAP_CONTENT, WRAP_CONTENT);
setFlags(0, flagsToUpdate);
} else {
setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
}
//設(shè)置窗口是否支持標(biāo)題欄,隱藏顯示標(biāo)題欄操作在此處。
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}
//ActionBar導(dǎo)航欄是否不占布局空間疊加顯示在當(dāng)前窗口之上。
if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) {
requestFeature(FEATURE_ACTION_BAR_OVERLAY);
}
if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) {
requestFeature(FEATURE_ACTION_MODE_OVERLAY);
}
if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) {
requestFeature(FEATURE_SWIPE_TO_DISMISS);
}
//當(dāng)前Activity是否支持全屏
if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
}
................
//設(shè)置狀態(tài)欄的顏色
if (!mForcedStatusBarColor) {
mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000);
}
//設(shè)置導(dǎo)航欄的顏色
if (!mForcedNavigationBarColor) {
mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000);
}
if (mAlwaysReadCloseOnTouchAttr || getContext().getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.HONEYCOMB) {
if (a.getBoolean(
R.styleable.Window_windowCloseOnTouchOutside,
false)) {
setCloseOnTouchOutsideIfNotSet(true);
}
}
WindowManager.LayoutParams params = getAttributes();
//設(shè)置輸入法的狀態(tài)
if (!hasSoftInputMode()) {
params.softInputMode = a.getInt(
R.styleable.Window_windowSoftInputMode,
params.softInputMode);
}
if (a.getBoolean(R.styleable.Window_backgroundDimEnabled,
mIsFloating)) {
/* All dialogs should have the window dimmed */
if ((getForcedWindowFlags()&WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) {
params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
}
if (!haveDimAmount()) {
params.dimAmount = a.getFloat(
android.R.styleable.Window_backgroundDimAmount, 0.5f);
}
}
//設(shè)置當(dāng)前Activity的出現(xiàn)動畫效果
if (params.windowAnimations == 0) {
params.windowAnimations = a.getResourceId(
R.styleable.Window_windowAnimationStyle, 0);
}
// The rest are only done if this window is not embedded; otherwise,
// the values are inherited from our container.
if (getContainer() == null) {
if (mBackgroundDrawable == null) {
if (mBackgroundResource == 0) {
mBackgroundResource = a.getResourceId(
R.styleable.Window_windowBackground, 0);
}
if (mFrameResource == 0) {
mFrameResource = a.getResourceId(R.styleable.Window_windowFrame, 0);
}
mBackgroundFallbackResource = a.getResourceId(
R.styleable.Window_windowBackgroundFallback, 0);
if (false) {
System.out.println("Background: "
+ Integer.toHexString(mBackgroundResource) + " Frame: "
+ Integer.toHexString(mFrameResource));
}
}
mElevation = a.getDimension(R.styleable.Window_windowElevation, 0);
mClipToOutline = a.getBoolean(R.styleable.Window_windowClipToOutline, false);
mTextColor = a.getColor(R.styleable.Window_textColor, Color.TRANSPARENT);
}
//以下代碼為當(dāng)前Activity窗口添加 decor根布局。
// Inflate the window decor.
int layoutResource;
int features = getLocalFeatures();
....
else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;(1)
// System.out.println("Simple!");
}
mDecor.startChanging();//3907
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); (2)
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
...
mDecor.finishChanging();
return contentParent;
簡單說下省略代碼。里面先獲取xml屬性,根據(jù)設(shè)置決定加載什么樣的xml屬性。我們在開頭requestWindowFeature(Window.FEATURE_NO_TITLE);這個設(shè)置就是在這里其中用的。我們繼續(xù)看主要代碼。上面的源碼中根據(jù)我們的設(shè)置加載layoutResource,并將進行加載,添加到decor中然后通過ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);得到contentParent并返回。之前我們說過decor是一布局控件,那么它添加的layoutResource是什么樣的布局呢?可以發(fā)現(xiàn)layoutResource是在 (1) 處的代碼塊處被賦值。那么我們來看看R.layout.screen_simple;
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
它的布局很簡單。這就是我們其實decor的布局。我們 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);中的ID_ANDROID_CONTENT就是 android:id="@android:id/content",所以我們布局中的XML是添加到FrameLayout中了。那么到底是不是這樣的呢?我們啟動應(yīng)用來測試一下。
注:這個布局在D:\Android\Administrator\AppData\Local\Android\sdk\platforms\android-23中搜索

注:此圖是通過hierarchyviewer查看的。不了解可以百度下
通過此圖我們可以知道DecorView是頂級View,它包括通知欄(圖3),底部導(dǎo)航欄(圖4)。從圖2中的布局我們可以看到正是我們上面加載的screen_simple布局。而我們activity_main正是加載到R.id.content中。證實了我們上面的想法。
總結(jié)
通過上面的流程,我們現(xiàn)在就了解了Activity的布局加載,現(xiàn)在我們來梳理下流程:

層級結(jié)構(gòu)關(guān)系:

總結(jié)
- 在實際中當(dāng)使用AppCompatActivity是會發(fā)現(xiàn)requestWindowFeature(Window.FEATURE_NO_TITLE);無效,因為此時標(biāo)題欄為ActionBar導(dǎo)航欄。如果要去除可以使用getSupportActionBar().hide();來隱藏導(dǎo)航欄。當(dāng)然也可以在style.xml中設(shè)置xxxx.NoActionBar
- 通過上面源碼得知,我們在generateLayout()方法中是先根據(jù)requestWindowFeature(Window.FEATURE_NO_TITLE);設(shè)置的屬性來決定是否顯示標(biāo)題欄,然后才加載的布局,所以方法requestWindowFeature(Window.FEATURE_NO_TITLE);需要在 setContentView方法之前使用。
DecorView添加到窗口過程
1.ActivityThread#performResumeActivity
上面我們已經(jīng)了解了,Activity的布局加載過程,當(dāng)我們加載布局完成后我們是如何將我們加載的布局添加到我們的界面窗口的呢?這里我們就提到ActivityThread類。我們知道我們主線程也就是UI線程。我們的Activity就在此線程中,而ActivityThread是管理應(yīng)用進程的主線程的執(zhí)行。當(dāng)我們的頂級View->DecorView加載完成后?;卣{(diào)用ActivityThread#handlerResumeActivity方法。在這里將加載完成的DecorView添加到PhoneWindow窗口。那我們就來看看這個方法:
·
public final ActivityClientRecord performResumeActivity(IBinder token,
boolean clearHide) {
ActivityClientRecord r = performResumeActivity(token, clearHide); // 這里會調(diào)用到onResume()方法
//...省略
if (r.window == null && !a.mFinished && willBeVisible) {
//得到窗口PhoneWindow實體
r.window = r.activity.getWindow();
//得到DecorView
View decor = r.window.getDecorView();
//設(shè)置DecorView可見度
decor.setVisibility(View.INVISIBLE);
//得到前Activity的WindowManagerImpl對象
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) {
//標(biāo)記DecorView已經(jīng)添加到窗口界面
a.mWindowAdded = true;
//將DecorView添加到當(dāng)前Activity的窗口上面
wm.addView(decor, l);
}
//...省略
}
2.WindowManagerImpl#addView()
上面的注解比較詳細,這樣我們就將布局添加到了屏幕上了。那我們來跟中下 wm.addView(decor, l);看它是如何添加的。
WindowManagerImpl#addView()
·
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
3.WindowManagerGlobal#addView()
方法里面調(diào)用了WindowManagerGlobal#addView()。繼續(xù)找它里面的方法:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
......
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
......
//創(chuàng)建ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
//設(shè)置LayoutParams (1)
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 {
//調(diào)用ViewRootImpl#setView添加布局view(參數(shù)view就是DecorView)
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;
}
}
這里面沒有太多需要解釋的,主要的方法都有注釋。這里需要提一句,針對(1)處:設(shè)置LayoutParams,對于DecorView,其MeasureSpec有窗口的尺寸和其自身的LayoutParams來共同決定;對于普通View,其MeasureSpec由父容器和其自身的LayoutParams來共同決定。關(guān)于MeasureSpec不了解的等后續(xù)的文章會說明并結(jié)合measure()詳細講解。這里只需要知道它是測量規(guī)格即可。我們繼續(xù)跟著思路往下走。
4.ViewRootImpl#setView
ViewRootImpl#setView:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
//將頂級視圖DecorView賦值給全局的mView
mView = view;
.............
//標(biāo)記已添加DecorView 默認為false
mAdded = true;
.............
//請求布局
requestLayout();
.............
}
}
ViewRootImpl#requestLayout():
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
里面就一個方法,我們繼續(xù)走:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
這里用到一個回調(diào):mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);我們進入這個遍歷完成的回調(diào)mTraversalRunnable:
//定義mTraversalRunnable
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
5.performTraversals()
ViewRootImpl#doTraversal()內(nèi)部調(diào)用了performTraversals();那我們就直接看此方法:
private void performTraversals() {
// cache mView since it is used so much below...
//上面提到mView就是DecorView根布局
final View host = mView;
// 成員變量mAdded賦值為true,因此條件不成立
if (host == null || !mAdded)
return;
//是否正在遍歷
mIsInTraversal = true;
//是否馬上繪制View
mWillDrawSoon = true;
.............
//頂層視圖DecorView所需要窗口的寬度和高度
int desiredWindowWidth;
int desiredWindowHeight;
.....................
//在構(gòu)造方法中mFirst已經(jīng)設(shè)置為true,表示是否是第一次繪制DecorView
if (mFirst) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
//如果窗口的類型是有狀態(tài)欄或是輸入框窗口,那么頂層視圖DecorView所需要窗口的寬度和高度就是除了狀態(tài)欄或輸入框窗口
if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
|| lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {//否則頂層視圖DecorView所需要窗口的寬度和高度就是整個屏幕的寬高
DisplayMetrics packageMetrics =
mView.getContext().getResources().getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
}
}
............
//獲得view寬高的測量規(guī)格,mWidth和mHeight表示窗口的寬高,lp.widthhe和lp.height表示DecorView根布局寬和高 (1)
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
//執(zhí)行測量操作
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//1864
........................
//執(zhí)行布局操作
performLayout(lp, desiredWindowWidth, desiredWindowHeight);//1931
.......................
//執(zhí)行繪制操作
performDraw();//2067
}
上面提到對于頂級View(DecorView),它的測量規(guī)格是窗口的尺寸大小。普通View則是父容器的測量規(guī)格與自身LayoutParames。那么此時的childWidthMeasureSpec,childHeightMeasureSpec就表示DecorView的測量規(guī)格。然后根據(jù)這個規(guī)格進行測量,布局,繪制。最后這三個方法都是調(diào)用View的measure,layot,draw。
- measure(int ,int) :測量View的大小
- layout(int ,int ,int ,int) :設(shè)置子View的位置
- draw(Canvas) :繪制View內(nèi)容到Canvas畫布上
剩下的就是關(guān)于測量,布局與繪制的相關(guān)知識了。這些知識點后面再系統(tǒng)的講解。
總結(jié)
1. 流程總結(jié)
ViewRoot對應(yīng)ViewRootImpl類。他是鏈接WindowManager和DecorView的紐帶,View的三大流程均通過ViewRootImpl來完成,在ActivityThread中,當(dāng)Activity被創(chuàng)建完成后會將DecorView添加到Window中,同時會創(chuàng)建ViewRootImpl對象,并將ViewRoot對象和DecorView建立聯(lián)系。這個過程可以通過上面代碼中:
root = new ViewRootImpl(view.getContext(), display);
//調(diào)用ViewRootImpl#setView添加布局view(參數(shù)view就是DecorView)
root.setView(view, wparams, panelParentView);
下面我還只用流程圖來總結(jié)下上面的流程。

performTraversals()的工作流程圖(此圖來自Android開發(fā)藝術(shù)探索):

由此我們可以看到在執(zhí)行performTraversals()中里面就是View的三大流程了。這部分內(nèi)容比較多,我們在后續(xù)的篇幅中來講解。
2. 獲取測量寬高為0
View的measure過程和Activity的生命周期方法不是同步執(zhí)行的。因此無法保證在onCreate(),onStart(),onResume()中獲取測量寬高。由上面的代碼也可知道在onResume()之后才開始執(zhí)行三大流程。所以我們在獲取時會得到寬/高=0。解決辦法有很多,提供一種常用的如下:
·
@Override
protected void onStart() {
super.onStart();
mView.post(new Runnable() {
@Override
public void run() {
int width=mView.getMeasuredWidth();
int height=mView.getMeasuredHeight();
}
});
}
結(jié)語
這篇文章的核心是理順View加載的思路與流程。以簡短,清晰,易懂(和我一樣工作時間短的小伙伴)來分析。
關(guān)于自定義View設(shè)計的知識點非常多我覺得也很難掌握,所以利用文章來記錄想,希望對大家有些幫助。由于本人能力有限,如果有錯誤大家一定指出,共同進步。最后希望如果對你有幫助請評個論,點個關(guān)注,讓我更有信心和動力。下篇我們將針對View的三大流程來分析下。
感謝
《Android開發(fā)藝術(shù)探索》
Android View 深度分析