UI 繪製整體流程的源碼解讀

子曰:溫故而知新,可以爲師矣。 《論語》-- 孔子


本篇文章會從一個個問題出發,然後結合源碼去講解 UI 的繪製流程



一、Android 程序如何啓動?Activity 生命週期如何調用?

衆所周知,Java 程序的入口是 main() 方法,那麼 Android 程序的入口在哪裏,稍微開發時間久一點的都應該知道 ActivityThread 這個類,這個類中的 main() 方法也就是我們 Android 程序的入口。

在這個 main() 方法中,有這麼兩行代碼需要關注:

//ActivityThread 類
public static void main(String[] args) {
        //...
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);
        //....
    }

見名知意,這邊是開啓了一個 Activity 的線程,調用 attach() 方法進行了一個綁定操作。我們點擊進入 attach() 方法中。可以看到裏面代碼比較多,我們千萬不要一行行代碼都看,要帶着目的去看,那麼下面的代碼就進入到我們的視線中了:

//ActivityThread 類中的 attach() 方法

 @UnsupportedAppUsage
    private void attach(boolean system, long startSeq) {
       //....
        final IActivityManager mgr = ActivityManager.getService();
            try {
                mgr.attachApplication(mAppThread, startSeq);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
       //....
}

這邊看命名猜一下,ActivityManager 管理類調用 getService() 方法(也就是所謂的 AMS)獲取一個接口 IActivityManager ,然後再綁定 Application。接口的實現類是 ActivityManagerService,我們就要到 ActivityManagerService 類中的 attachApplication() 方法:

// ActivityManagerService 類
@Override
    public final void attachApplication(IApplicationThread thread, long startSeq) {
        synchronized (this) {
            int callingPid = Binder.getCallingPid();
            final int callingUid = Binder.getCallingUid();
            final long origId = Binder.clearCallingIdentity();
            attachApplicationLocked(thread, callingPid, callingUid, startSeq);
            Binder.restoreCallingIdentity(origId);
        }
 }

這邊不用想,要點擊 attachApplicationLocked() 方法:

// ActivityManagerService 類

    @GuardedBy("this")
    private final boolean attachApplicationLocked(IApplicationThread thread,
            int pid, int callingUid, long startSeq) {
             //....
             thread.bindApplication(一堆參數)
             //....
}

這個方法裏面代碼非常多,我們只要看圖上面這行代碼,它又執行綁定 Application 操作,這個 thread 是 IApplicationThread 接口,實現類就是 ActivityThread 類中的內部類 ApplicationThread,所以我們又要跳回 ActivityThread 類:

//ActivityThread 類
 public final void bindApplication(一堆參數){
  //...
  sendMessage(H.BIND_APPLICATION, data);
 }

在這個方法中,我們看到了發送消息,這個 H 是內部類,繼承 Handler 類,那麼我們就要找到 handleMessage() 方法:

// ActivitThread類中的 內部類 Handler 類的  handleMessage() 方法
public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                case BIND_APPLICATION:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
                    AppBindData data = (AppBindData)msg.obj;
                    handleBindApplication(data);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                    //....
}

裏面有個 handleBindApplication() 方法,我們點擊進入:

// ActivityThread 類
//...
 try {
                mInstrumentation.callApplicationOnCreate(app);
            } catch (Exception e) {
                if (!mInstrumentation.onException(app, e)) {
                    throw new RuntimeException(
                      "Unable to create application " + app.getClass().getName()
                      + ": " + e.toString(), e);
                }
            }
 //...

裏面的代碼又是很多,我們還是那句老話,對着目標找線索,我們可以看到,這邊終於有我們差不多熟悉的代碼了,就是這個 callApplicationOnCreate() 方法,看着非常像創建 Application ,我們點擊進入:

// Instrumentation 類
public void callApplicationOnCreate(Application app) {
        app.onCreate();
    }  

看到這兒,應該不言而喻了,調用了 Application 對象的 onCreate() 方法。

好了,第一個問題說完了,下面說一說 Activity 是如何啓動的?

ActivityThread 類中有一個 handleLaunchActivity() 的方法,看名字是啓動 Activity,那麼這個方法是如何調用的,中間過程過於複雜了,這邊就不詳細說了,還是 ActivityManageService 這個類中的 attachApplication() 方法經過一系列調用,最終調用了 ActivityThread 類中的 handleLaunchActivity()方法,這個方法裏面又調用了 performLaunchActivity() 方法,在 performLaunchActivity() 方法中調用了 mInstrumentation.callActivityOnCreate() 方法,是不是很熟悉,之前 Application 的 onCreate() 方法也是這個類調用的,最終創建了 Activity,調用了 Activity 類的 onCreate() 方法。



二、在 Activity 的 onCreate() 方法中,setContentView 是如何將 UI 文件加載?

以繼承 Activity 爲例:

我們點擊 setContentView() 方法:

public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

調用 getWindow() 方法返回的對象再調用 setContentView() 方法,傳入我們自己寫的資源 id,那麼我們就需要知道 getWindow() 方法是什麼?

// Activity 類
 public Window getWindow() {
        return mWindow;
    }

此方法返回一個 Window 對象,這是一個抽象類,我們從類最上面的註釋可以知道唯一實現類是 PhoneWindow 類,我們就來到 PhoneWindow 類中的 setContentView() 方法:

 @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
           //...
        }
        // 是否有轉場動畫
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            //....
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
       //.....
    }

在這裏,關注 installDecor()mLayoutInflater.inflate(layoutResID, mContentParent) 這兩行代碼,我們先看 installDecor() 方法:

  private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            //...
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();

            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            if (decorContentParent != null) {
                mDecorContentParent = decorContentParent;
                mDecorContentParent.setWindowCallback(getCallback());
                if (mDecorContentParent.getTitle() == null) {
                    mDecorContentParent.setWindowTitle(mTitle);
                }

                 //...
            } else {
               //...
            }  
            //...     
            }
        }
    }

在這個方法中,可以看到有兩個類 mDecormContentParent ,那麼這兩個類是什麼?

 // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

// This is the view in which the window contents are placed. It is either
    // mDecor itself, or a child of mDecor where the contents go.
    ViewGroup mContentParent;

從文檔中給出的註釋,可以知道 mDecor 是一個頂層容器,而 mContentParent 可能是 mDecor ,也可以是它的 子類

既然知道這兩個類是什麼,那麼我們回到之前的 installDecor() 方法中,首先判斷 mDecor 是否爲空,爲空則調用 generateDecor(-1) 方法,通過new 的方式創建出來 mDecor 對象。

接着往下看,判斷 mContentParent 是否爲空, 爲空則調用 generateLayout(mDecor) 方法傳入之前創建的 mDecor 對象,我們點擊進入此方法:

 protected ViewGroup generateLayout(DecorView decor) {
 //.... 都是一些系統主題的判斷,不用看
// Inflate the window decor.

        int layoutResource;
        int features = getLocalFeatures();
          if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
            setCloseOnSwipeEnabled(true);
           }else if(){
           //....
        }
        //....
       mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
       ViewGroup contentParent =(ViewGroup)findViewById(ID_ANDROID_CONTENT);
        
       //....
       return contentParent;
}

這邊的 layoutResource 不是我們傳入的,可以在下面的代碼中看到是系統的資源佈局,往下看,點擊 onResourcesLoaded() 方法:

// DecorView 類
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
if (mBackdropFrameRenderer != null) {
            //...
        }

       mDecorCaptionView = createDecorCaptionView(inflater);
         // 將資源佈局轉換成 View 對象
        final View root = inflater.inflate(layoutResource, null);
        if (mDecorCaptionView != null) {
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            mDecorCaptionView.addView(root,
                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {

            // Put it below the color views.
            // 添加到 DocorView 中
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
}

需要注意的是,此時已經來到了 DecorView 類中,else 裏面的 addView() 方法,會將之前系統的佈局加載到 DocorView 容器中。然後

好了,我們回到 generateLayout() 方法裏,這個方法返回的就是 contentView,也就是 IDcontentFrameLayout


我們現在來對 installDecor() 方法做一個總結:

  • 創建 DecorView
  • 初始化 mContentParent,根據對應的主題特性加載相應的系統佈局,將系統佈局初始化,通過 addView 方法加載到 DecorView 中。

接着我們就來看一下 mLayoutInflater.inflate(layoutResID, mContentParent) 方法,一路點擊 inflate 方法到底:

// LayoutInflater 類
 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
   //....
  // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
  //....                  
}

這邊的 rootView 就是 那個 mContentParent,也就是幀佈局 FrameLayout,這邊會把我們自己寫的佈局添加到這個 FrameLayout 中。


對上面的源碼閱讀做一下總結,即總結一下 View 是如何被添加到屏幕窗口上的:

  • 創建頂層佈局容器 DecorView
  • 在頂層佈局容器中加載基礎佈局 ViewGroup(系統的)。
  • 將我們自己寫的佈局添加到基礎佈局中的 FrameLayout 中。

這邊稍微說一下如果繼承的是 AppCompatActivity ,View 是如何被添加到屏幕窗口上的:

  • 通過代理類的實現類 AppCompatDelegateImpl 在原來的容器中在套一層佈局。
  • 通過容器 ID 替換,用新的容器 ID 替換原來的 FrameLayout(填之前的坑,做適配)。
  • 將我們自己寫的佈局添加到基礎佈局中的 ContentFrameLayout 中。

這個問題最後用一張圖來總結一下:
setContentView



三、View 的整體繪製流程是什麼?

ActivityThread 類中的 handleResumeActivity() 方法中:

// ActivityThread 類
 public void handleResumeActivity(一堆參數){
 //...
 final Activity a = r.activity;
 //...
 View decor = r.window.getDecorView();
 decor.setVisibility(View.INVISIBLE);
 ViewManager wm = a.getWindowManager();
 WindowManager.LayoutParams l = r.window.getAttributes();
 a.mDecor = decor;
 //....

 wm.addView(decor, l);
 //....
 }

在這邊有個 wm.addView(decor, l) 方法,這個 wma.getWindowManager() 獲取的WindowManager 接口,它的實現類是 WindowManagerImpl ,所以我們就來到 WindowManagerImpl 類中的 addView() 方法:

// WindowManagerImpl 類
@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

在這邊可以看到有一個 mGlobal.addView() 方法 ,那麼點擊進入 WindowManagerGlobaladdView() 方法中:

// WindowManagerGlobal 類
public void addView(
           //...
           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.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }

在這裏,我們可以看到 root 也就是 ViewRootImpl 調用了 setView() 方法,我們點擊進入:

// ViewRootImpl 類
 public void setView(一堆參數){
   //....
    // 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();
   //....
}

在這裏,我們終於看到了熟悉的代碼,一般調用 requestLayout() 方法,會重新佈局,我們點擊此方法:

//  ViewRootImpl 類
 @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

這邊檢查線程後,調用 scheduleTraversals() 方法,點擊進入:

//  ViewRootImpl 類
void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

這邊有個 postCallback 方法,有個 mTraversalRunnable 線程,一般人可能沒注意,我們看這個線程:

//  ViewRootImpl 類
 final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

它會執行 doTraversal() 方法,點擊進入:

//  ViewRootImpl 類
 void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

這邊調用 performTraversals() 方法 ,點擊調用:

//  ViewRootImpl 類
 private void performTraversals() {
 //...
 // Ask host how big it wants to be
 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
 //...
 performLayout(lp, mWidth, mHeight);
 //...
 performDraw();                   
 } 

在這邊,就看到了熟悉的 UI 繪製流程三步,測量,佈局,繪製。



寫在文末

紙上得來終覺淺,絕知此事要躬行。 《冬夜讀書示子聿》-- 陸游

至此,UI 繪製整體流程的源碼算是梳理了一遍,各位看官食用愉快。


碼字不易,如果本篇文章對您哪怕有一點點幫助,請不要吝嗇您的點贊,我將持續帶來更多優質文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章