UI繪制流程及原理

1贷屎、View是如何被添加到屏幕窗口上

1簿废、創(chuàng)建頂層布局容器DecorView
2、在頂層布局中加載基礎(chǔ)布局ViewGroup
3义黎、將ContentView添加到基礎(chǔ)布局中的FrameLayout中

從我們的activity的onCreate()中的setContentView開始



跳到了Window類中的setContentView方法润文,從描述The only existing implementation of this abstract class is android.view.PhoneWindow我們知道PhoneWindow是Window的唯一實(shí)現(xiàn)類姐呐。



PhoneWindow中的setContentView方法主要做了兩件事情

第一件事情:


generateLayout這個(gè)方法比較長,主要有三個(gè)步驟:
1典蝌、通過Window Style樣式皮钠,設(shè)置Window風(fēng)格
2、加載系統(tǒng)中對應(yīng)的layoutResource赠法,作為基礎(chǔ)布局(基礎(chǔ)容器)麦轰,根據(jù)不同的features,初始化不同的布局砖织。如:layoutResource = R.layout.screen_simple;(開發(fā)者自己通過setContentView(int layoutId)設(shè)置的布局文件就是添加到其中的)
3款侵、當(dāng)資源加載完畢,會調(diào)用DecorView的onresourcesLoaded()方法侧纯,進(jìn)而將上述基礎(chǔ)布局添加到頂層布局mDecorView中新锈。

第二件事情:



布局層次如下圖:


2、將View樹添加到Window中

ActivityThread是Activity的啟動入口眶熬, 在它有一個(gè)內(nèi)部類H繼承Handler妹笆,專門用來處理主線程的消息。

class H extends Handler {

    public void handleMessage(Message msg) {

        switch (msg.what) {

            case LAUNCH_ACTIVITY: {

                final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

                r.packageInfo = getPackageInfoNoCheck(

                r.activityInfo.applicationInfo, r.compatInfo);

                handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");

        }

        break;

啟動一個(gè)Activity娜氏,會觸發(fā)handleLaunchActivity()方法

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {

......

// 1拳缠、創(chuàng)建Activity

Activity a = performLaunchActivity(r, customIntent);

......

// 2、調(diào)用activity的onResume方法

handleResumeActivity(r.token, false, r.isForward, !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);

我們先看一下performLaunchActivity()方法的源碼:

private ActivityperformLaunchActivity(ActivityClientRecord r, Intent customIntent) {

    Activity activity =null;

    try {

            java.lang.ClassLoader cl = appContext.getClassLoader();

            // 1贸弥、創(chuàng)建Activity

            activity =mInstrumentation.newActivity(cl, component.getClassName(), r.intent);

         } catch (Exception e) {

         }

    // 2窟坐、創(chuàng)建Application

    Application app = r.packageInfo.makeApplication(false, mInstrumentation);

    // 3、創(chuàng)建Window

    Window window =null;

    if (r.mPendingRemoveWindow !=null && r.mPreserveWindow) {

            window = r.mPendingRemoveWindow;

            r.mPendingRemoveWindow =null;

            r.mPendingRemoveWindowManager =null;

    }

    // 4绵疲、關(guān)聯(lián)activity和window

    activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID,                     r.lastNonConfigurationInstances, config,  r.referrer, r.voiceInteractor, window, r.configCallback);

通過源碼哲鸳,可以看到,performLaunchActivity()方法主要是創(chuàng)建Activity盔憨、Application徙菠、Window對象,并關(guān)聯(lián)到一起郁岩。緊接著婿奔,調(diào)用activity的onCreate()方法芙盘,進(jìn)而調(diào)用setContentView()方法,構(gòu)建View樹脸秽。

然后我們再看一下handleResumeActivity()方法的源碼:

final void handleResumeActivity(IBinder token,  boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {            
 // 1、創(chuàng)建 ViewManager 對象

        ViewManager wm = a.getWindowManager();

        WindowManager.LayoutParams l = r.window.getAttributes();

        if (a.mVisibleFromClient) {

            if (!a.mWindowAdded) {

                    a.mWindowAdded =true;

                    // 2蝴乔、將DecorView添加到Window中

                    wm.addView(decor, l);

        }else {

            a.onWindowAttributesChanged(l);

        }

}

在該方法內(nèi)部记餐,創(chuàng)建ViewManager對象,并調(diào)用addView()方法薇正,將DecorView添加到Window中片酝。(通過查看源碼,ViewManager.addView() --> WindowManager.addView() --> WindowManagerGlobal.addView()挖腰,這不是本文的重點(diǎn)雕沿,就不一一列舉了)

至此,View樹就已經(jīng)添加到Window中猴仑,但是此時(shí)用戶還看不到界面审轮,因?yàn)檫€沒有進(jìn)行繪制。所以接下來就需要將該View繪制出來

3辽俗、View的繪制流程
我們看一下WindowManagerGlobal.addView()疾渣,在該方法內(nèi)部會調(diào)用ViewRootImpl的setView()方法,在該方法內(nèi)部崖飘,會調(diào)用requestLayout()方法,進(jìn)而出發(fā)View的繪制流程:

public void requestLayout() {

    if (!mHandlingLayoutInLayoutRequest) {

            // 1、檢查是否在主線程

            checkThread();

            // 2淑掌、執(zhí)行traversal

            scheduleTraversals();

    }

}

void scheduleTraversals() {

        if (!mTraversalScheduled) {

                mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

        }

}


final TraversalRunnablemTraversalRunnable =new TraversalRunnable();

final class TraversalRunnableimplements Runnable {

    @Override

    public void run() {

        doTraversal();

    }

}

void doTraversal() {

    if (mTraversalScheduled) {

            performTraversals();

        }

    }

}

private void performTraversals() {

        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

        performLayout(lp, mWidth, mHeight);

        performDraw();
}

從源碼中顶捷,可以看到,最終在performTraversals()方法中翰蠢,執(zhí)行了View的測量项乒、布局和繪制過程。

至此梁沧,View繪制完畢后板丽,就可以顯示到屏幕上了。

MeasureSpec

MeasureSpec 表示的是一個(gè) 32 位的整數(shù)值趁尼,它的高 2 位表示測量模式 SpecMode埃碱,低 30 位表示某種測量模式下的規(guī)格大小 SpecSize。MeasureSpec 是 View 類的一個(gè)靜態(tài)內(nèi)部類酥泞,用來說明應(yīng)該如何測量這個(gè)View砚殿。
三種測量模式。

UNSPECIFIED:不指定測量模式芝囤,父視圖沒有限制子視圖的大小似炎,子視圖可以是想要的任何尺寸辛萍,通常用于系統(tǒng)內(nèi)部,應(yīng)用開發(fā)中很少使用到羡藐。
EXACTLY:精確測量模式贩毕,當(dāng)該視圖的 layout_width 或者 layout_height 指定為具體數(shù)值或者 match_parent 時(shí)生效,表示父視圖已經(jīng)決定了子視圖的精確大小仆嗦,這種模式下 View 的測量值就是 SpecSize 的值辉阶。
AT_MOST:最大值模式,當(dāng)前視圖的 layout_width 或者 layout_height 指定為 wrap_content 時(shí)生效瘩扼,此時(shí)子視圖的尺寸可以是不超過父視圖運(yùn)行的最大尺寸的任何尺寸谆甜。
對 DecorView 而言,它的 MeasureSpec 由窗口尺寸和其自身的 LayoutParams 共同決定集绰;對于普通的 View规辱,它的 MeasureSpec 由父視圖的 MeasureSpec 和其本身的 LayoutParams 共同決定。

//父容器不對View做任何限制栽燕,系統(tǒng)內(nèi)部使用
 public static final int UNSPECIFIED = 0 << MODE_SHIFT;//00000000000000000000000000000000
//父容器檢測出View的大小罕袋,Vew的大小就是SpecSize LayoutPamras match_parent 固定大小       
public static final int EXACTLY     = 1 << MODE_SHIFT;//01000000000000000000000000000000
//父容器指定一個(gè)可用大小,View的大小不能超過這個(gè)值碍岔,LayoutPamras wrap_content    
public static final int AT_MOST     = 2 << MODE_SHIFT; //10000000000000000000000000000000

Measure

Measure 用來計(jì)算 View 的實(shí)際大小炫贤。頁面的測量流程從 performMeasure 方法開始。

  private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
  }

具體操作是分發(fā)給 ViewGroup 的付秕,由 ViewGroup 在它的 measureChild 方法中傳遞給子 View兰珍。ViewGroup 通過遍歷自身所有的子 View,并逐個(gè)調(diào)用子 View 的 measure 方法實(shí)現(xiàn)測量操作询吴。

  // 遍歷測量 ViewGroup 中所有的 View
  protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
  }

  // 測量某個(gè)指定的 View
  protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  }

View (ViewGroup) 的 Measure 方法掠河,最終的測量是通過回調(diào) onMeasure 方法實(shí)現(xiàn)的,這個(gè)通常由 View 的特定子類自己實(shí)現(xiàn)猛计,可以通過重寫這個(gè)方法實(shí)現(xiàn)自定義 View唠摹。

  public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ....
  }

  // 如果需要自定義測量,子類需重寫這個(gè)方法
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
  }

  // 如果 View 沒有重寫onMeasure 方法奉瘤,默認(rèn)會直接調(diào)用 getDefaultSize
   public static int getDefaultSize(int size, int measureSpec) {
     int result = size;
     int specMode = MeasureSpec.getMode(measureSpec);
     int specSize = MeasureSpec.getSize(measureSpec);

     switch (specMode) {
       case MeasureSpec.UNSPECIFIED:
          result = size;
          break;
       case MeasureSpec.AT_MOST:
       case MeasureSpec.EXACTLY:
          result = specSize;
          break;
      }
      return result;
   }

ViewGroup measure --> onMeasure(測量子控件的寬高)--> setMeasuredDimension -->setMeasuredDimensionRaw(保存自己的寬高)
View measure --> onMeasure --> setMeasuredDimension -->setMeasuredDimensionRaw(保存自己的寬高)

Layout

Layout 過程用來確定 View 在父容器的布局位置勾拉,他是父容器獲取子 View 的位置參數(shù)后,調(diào)用子 View 的 layout 方法并將位置參數(shù)傳入實(shí)現(xiàn)的盗温。ViewRootImpl 的 performLayout 代碼如下藕赞。
ViewGroup layout(來確定自己的位置,4個(gè)點(diǎn)的位置) -->onLayout(進(jìn)行子View的布局)
View layout(來確定自己的位置卖局,4個(gè)點(diǎn)的位置)

  private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
    ...
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ...
  }

View 的 layout 方法代碼斧蜕。

  public void layout(int l, int t, int r, int b) {
    onLayout(changed, l, t, r, b);
  }

  // 空方法,子類如果是 ViewGroup 類型砚偶,則重寫這個(gè)方法批销,實(shí)現(xiàn) ViewGroup 中所有 View 控件布局
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  }

Draw

ViewGroup
繪制背景 drawBackground(canvas)
繪制自己onDraw(canvas)
繪制子View dispatchDraw(canvas)
繪制前景洒闸,滾動條等裝飾onDrawForeground(canvas)

View
繪制背景 drawBackground(canvas)
繪制自己onDraw(canvas)
繪制前景,滾動條等裝飾onDrawForeground(canvas)
onMeasure --> onLayout(容器) --> onDraw(可選)

Draw 操作用來將控件繪制出來均芽,繪制的流程從 performDraw 方法開始丘逸。performDraw 方法在類 ViewRootImpl 內(nèi),其核心代碼如下掀宋。

  private void performDraw() {
    boolean canUseAsync = draw(fullRedrawNeeded);
  }

  private boolean draw(boolean fullRedrawNeeded) {
    ...
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
      return false;
    }
  }

  private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
     ...
     mView.draw(canvas);
     ...
  }

最終調(diào)用到每個(gè) View 的 draw 方法繪制每個(gè)具體的 View深纲,繪制基本上可以分為六個(gè)步驟。

  public void draw(Canvas canvas) {
    ...
    // Step 1, draw the background, if needed
    if (!dirtyOpaque) {
      drawBackground(canvas);
    }
    ...
    // Step 2, save the canvas' layers
    saveCount = canvas.getSaveCount();
    ...
    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);

    // Step 4, draw the children
    dispatchDraw(canvas);

    // Step 5, draw the fade effect and restore layers
    canvas.drawRect(left, top, right, top + length, p);
    ...
    canvas.restoreToCount(saveCount);
    ...
    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);
  }

參考
UI繪制流程及原理
Android View 的繪制流程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末布朦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子昼窗,更是在濱河造成了極大的恐慌是趴,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件澄惊,死亡現(xiàn)場離奇詭異唆途,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)掸驱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門肛搬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人毕贼,你說我怎么就攤上這事温赔。” “怎么了鬼癣?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵陶贼,是天一觀的道長。 經(jīng)常有香客問我待秃,道長拜秧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任章郁,我火速辦了婚禮枉氮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘暖庄。我一直安慰自己聊替,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布培廓。 她就那樣靜靜地躺著佃牛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪医舆。 梳的紋絲不亂的頭發(fā)上俘侠,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天象缀,我揣著相機(jī)與錄音,去河邊找鬼爷速。 笑死央星,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的惫东。 我是一名探鬼主播莉给,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼廉沮!你這毒婦竟也來了颓遏?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤滞时,失蹤者是張志新(化名)和其女友劉穎叁幢,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坪稽,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡曼玩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了窒百。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片黍判。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖篙梢,靈堂內(nèi)的尸體忽然破棺而出顷帖,到底是詐尸還是另有隱情,我是刑警寧澤渤滞,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布窟她,位于F島的核電站,受9級特大地震影響蔼水,放射性物質(zhì)發(fā)生泄漏震糖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一趴腋、第九天 我趴在偏房一處隱蔽的房頂上張望吊说。 院中可真熱鬧,春花似錦优炬、人聲如沸颁井。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雅宾。三九已至,卻和暖如春葵硕,著一層夾襖步出監(jiān)牢的瞬間眉抬,已是汗流浹背贯吓。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蜀变,地道東北人悄谐。 一個(gè)月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像库北,于是被迫代替她去往敵國和親爬舰。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容