Activity從啟動到佈局繪製的簡單分析

sydmobile發表於2018-12-10

這篇文章主要是配合原始碼簡單的介紹一下,程式的載入過程,Activity 中佈局的載入過程,能夠大體的瞭解整個過程。不過過度的追究細節,因為裡面任何一個細節可能都夠你研究一段時間的!先了解掌握大體過程,再慢慢來!

文章最早釋出於我的微信公眾號 Android開發者家園 中,歡迎大家掃描下面二維碼關注微信公眾獲取更多知識內容。
本文為sydMobile原創文章,可以隨意轉載,但請務必註明出處!

開始啟動

我們都知道,Activity 是有生命週期的,onCreate()onStart()onResume 等等那麼這些方法是如何呼叫的呢?它不會平白無故的自己呼叫。其實當我們開啟 APP 的時候會建立一個叫做 ActivityThread 的類,我們可以認為這個類是主類,就和 Java 程式中的啟動類一樣,ActivityThread 類有一個 main 函式,這個函式就是我們的程式入口!

main.jpg

相信看到這個,大家都會恍然大悟了,這就是我們學習 Java 的時候的 main 方法,認為是程式的入口。可以看到方法裡面對一個 Looper 物件進行了初始化,Looper.prepareMainLooper() 通過這個方法就給當前的執行緒初始化了 Looper,這個 Looper 成了 Application 的 main looper,這也是為什麼我們在主執行緒中不用自己再 Looper.prepare 的原因。可以認為任何在主執行緒的操作都會傳送到這個 Looper 對應的 Handler 中去。很容易可以找到 Looper 對應的 Handler,其實就是 ActivityThread 的一個內部類,繼承了 Handler

handler-1.jpg

handler-2.jpg

這個就是 ActivityThreadHandler 的部分程式碼實現,然後看到 Handler 裡面的這段程式碼

public void handlerMessage(Message msg){
    .....;
    switch(msg.what){
        case LAUNCH_ACTIVITY:
            .....;
            // r 是在 msg 裡面拿到的物件 msg.obj
            handlerLaunchActivity(r,null,"LAUNCH_ACTIVITY");
            .....;
    }
}

下面再來看 handlerLaunchActivity() 這個方法:

private void handleLaunchActivity(ActivityClientRecord r,Intent customIntent,String reason){
    ·····;
    Activity a = performLaunchActivity(r,customIntent);
    ......;
       handlerResumeActivity(...);
    
}

裡面這兩個很重要的方法我已經列出來了,下面我們來看看 performLaunchActivity(r,customIntent) 方法:

// 這裡面程式碼同樣很多,我們只挑選重要的
private Activity performLaunchActivity(ActivityClentRecord r,Intent customIntent){
    .......;
    // 這裡 new 出了 activity
    activity = mInstrumentation.newActivity(cl,component.getClassName(),r.intent);
    ......;
    // 呼叫了 Activity 的 attach 方法(很重要)
    activity.attach(appContext,this,getI......);
    ......;
    // 呼叫了 Activity 的 onCreate 方法
    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
    // 呼叫了 onStart 方法
    ........;
}

這段程式碼中分別呼叫了 Activity 的 attach 方法和 onCreate 方法,同時下面也呼叫了 onStart 方法,這裡省略了。

結論:在 performLaunchActivity 方法中,首先通過 mInstrumention 產生了 Activity 物件,然後呼叫了 Activity 的 attach 方法,然後通過 Instrumentation 物件呼叫了 activity 的生命週期中的方法。說了這麼多,無非是大體瞭解了這個啟動過程。知道 Activity 在執行生命週期前是先呼叫 attach 方法的。其中 attach 方法內的一些程式碼是很關鍵的,和整個 Activity 的啟動有很重要的關係,下面來看一下 attach 方法的原始碼:

// 這個方法存在於 Activity 類中
final void attach(Context context,ActivityThread aThread,Instrumentation instr,IBinder token,int ident,Application application,Intent intent,ActivityInf info,CharSequence title,Activity parent,String id,.........){
    
    .........;
    // Window 物件賦值
    mWindow = new PhoneWindow(this,window,activityConfigCallback);
    mWindow.set 許多監聽回撥 (WindowContrallerCallBack、Callback)等等;
    將各種傳遞過來的各種引數賦值給 Activity 中的成員;
                               mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),mToken,mComponent.flattenToString(),(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED)!=0);
    mWindowManager = mWindow.getWindowManager();
    
}

attach 這個方法的主要內容:首先給 Activity 中的 mWindow 成員變數賦值,具體的實現物件是 PhoneWindow,然後給 Activity 中的其他許多關鍵的成員賦值,給 mWindow 變數設定 WindowManager,然後給 Activity.mWindowManager 賦值。mWindow 是一個 Window 型別的變數,實際上一個 PhoneWindow 物件,和 Activity 的內容顯示有關係

onCreate

下面就開始執行 Activity 中的 onCreate 方法了。

我們都知道,每一個 Activity 對應一個佈局檔案,在 Activity 的 onCreate() 方法中都需要 setContentView(int res) 通過這個方法我們就可以將佈局與 Activity 關聯起來了。那麼佈局是怎麼載入的呢。這個時候就要看 setContentView() 的原始碼了:

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

getWindow() 返回的是 Window 物件,這個物件剛剛在 attach 方法中我們接觸過,其實它的真正實現是 PhoneWindow,下面來看 PhoneWindow 中的 setContentView 方法:

// 放置視窗內容的檢視,它既可以是 mDecor 本身(沒有 Title 的情況下),也可以是 mDecor 的子項, 這種情況就是 mDecor 中有 Title 的情況
ViewGroup mContentParent;
// 這是視窗的頂層檢視,包含視窗裝飾
private DecorView mDecor;

public void setContentView(int layoutResID){
    if(mContentParent == null){
        installDecor();
    }else if(!hasFeature(FEATURE_CONTENT_TRANSITIONS)){
        mContentParent.removeAllView();
    }
    // 將 佈局資源 layoutResID 填充到 mContentParent 中
    mLayoutInflater.inflate(layoutResID,mContentParent);
    ........;
}

PhoneWindow 類中有兩個和檢視相關的成員變數,一個是 DecorView mDecor,另一個是 ViewGroup mConentParent

下面再來看看 PhoneWindow 中 setContentView 中的 inStallDecor() 方法:

private void installDecor(){
    ......;
    
    if(mDecor == null){
        // 生成 mDecor;
        mDecor = generateDecor(-1);
        ......;
    }else{
        mDecor.setWindow(this);
    }
    
    if(mContnetParent == null){
        mContentParent = generateLayout(mDecor);
        .......;
        ....很多程式碼;
        // 可以認為 DecorContentParent 是操作 Title 的
        final DecorContentParent decorContentParent = (DecorContentParent)mDecor.findViewById(R.id.decor_content_parent);
        if(decorContentParent != null){
            mDecorContentParent = decorContentParent;
            .......;
            對 Title 的一系列操作;
        }
        // 也有獲取 TitleView 的程式碼
       
    }
        
}

mDecor 是通過 generateDecor() 方法來獲取的。

protected DecorView generateDecor(int featureId){
    ......;
    return new DecorView(context,featureId,this,getAttributes());
    
}

DecorView 繼承了 FrameLayout 是整個 PhoneWindow 的根檢視

再來看看 generateLayout(DecorView decor)方法:

protected ViewGroup generateLayout(DecorView decor){
    // 獲取當前主題中的一些屬性
    TypedArray a = getWindowStyle();
    // getWindowStyle() 的內容:mContext.obtainStyledAttributes(com.android.internal.R.styleable.Window);
    // 其實就是獲得定義的屬性組中的一些資訊
    ......;
    // 省略的程式碼大概就是利用 a 來獲取主題中一些預設的樣式,把這些樣式設定到 DecorView 中
    // 大概內容,類似於
    mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloading,false);
    if(mIsFloating){
        setLayout(WRAP_CONTENT,WRAP_CONTENT);
    }
    // 有沒有 windowNoTitle 等等這種屬性
    // 這裡的設定大小,可以理解為是設定 DecorView 大小,在這裡已經確定了 DecorView 的大小了
    // 根據SDK 版本來設定檢視樣式程式碼
    
    // 填充視窗的資源
    int layoutResource;
    .....根據不同的條件,給 layoutResource 賦予不同的值;
    
    mDecor.startChanging();
    // 資源填充到 mDecor 中
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); 
    // 這裡的 findViewById 就是在 mDecor 中尋找
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    ....修改 mDecor 背景顏色,設定 Title 的一些屬性;
    return contentParent;
    
}

可以看到 generateLayout() 方法裡面還是處理了許多關鍵的事情:

  • 根據各種屬性值設定了 LayoutParam 引數
  • 根據 FEATURE 的值,選擇了合適的佈局資源 id 賦值給了 layoutResource。
  • 根據 Theme 中設定的不同的風格,SDK 不同的風格來對 DecorView 進行設定,Decor 裝飾的意思,所以這個名字也很符合。
  • 把 layoutResource 填充到了 DecorView 中
  • 在 layoutResource 中有一個 id 為 ID_ANDROID_CONTENT 的 ViewGroup 即我們的 contentParent
  • DecorView 包含了 Title 和 contentParent

installDecor() 小結:

  1. 一個 Activity 對應了一個 Window,這個 Window 的實現物件是 PhoneWindow,一對一的關係
  2. PhoneWindow 管理了整個 Activity 頁面內容,不包括系統狀態列 ,PhoneWindow 是和應用的某個頁面關聯的。
  3. PhoneWindow 包含 ActionBar 和 內容。setContentViwe 方法是用來設定內容的,setTitle 是用來操作 Title 的。Window 中定義的 requestFeature() 等方法,很多與 ActionBar 屬性相關的設定。這些方法都是公共方法,是為了方便我們呼叫的。
  4. PhoneWindow 本身不是一個檢視,它的成員變數 mDecor 是整個頁面的檢視。mDecor 是在 generateLayout() 的時候填充的。Title 和 contentParent 都是通過 findViewById() 從 mDecor 中獲取的。

上面介紹的這些,只是執行完了 installDecor() 方法,這個時候,PhoneWindow 有了 DecorView,DecorView 有了自己的樣式,有了 Title 和 ContentParent。下一步就是向 ContentParent 中新增自己的佈局了。

public void setContentView(int layoutResID){
    .....;
    // 上面已經分析完了
    installDecor();
    // 向 ContentParent 中新增我們自己的佈局資源
    mLayoutInflater.inflate(layoutResID,mContentParent);
    .......;
}

到此 setContentView 算是分析完了,onCreate 也就執行完了。那麼我們可以認為上面的 ActivityThread 中的 performLaunchActivity() 執行完了,接下來就開始執行 handleResumeActivity() 方法了。

final void handleResumeActivity(IBinder token,boolean clearHide,boolean isForward,boolean reallyResume,int seq,String reason){
    ......;
    // 可以把 ActivityClientRecord 類認為是記錄 Activity 內部關鍵引數的類
    ActivityClientRecord r = performResumeActivity(token,clearHide);
    if(r!=null){
        final Activity a = r.activity;
        .....;
        if(r.window == null && !a.mFinished && willBeVisible){
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            ....;
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            .....;
            if(a.mVisibleFromClient){
                a.mWindowAdded = true;
                wm.addView(decor,l)
            }
        }
    }
    。。。。。;
    
}

performResumeActivity()方法,這個方法內部呼叫了 Activity 的 performResume() 方法(這個方法在 API 中沒有出現,需要在完整原始碼中檢視)

performResumeActivity.jpg

看到上面 mInstrumentation.callActivityOnResume(this) 就是呼叫了 Activity 的 onResume 方法。

然後再來看看 hanleResumeActivity 方法裡面的 addView 方法,wm 是上面 a.getWindowManger() 獲取到的,a 是 Activity,getWindowManager() 返回了 mWindowManager 物件,而這個物件是 WindowMangerImpl,它內部方法大部分是在 WindowManagerGlobal 內部實現

private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<>();

public void addView(View view,ViewGroup.LayoutParams params,Display display,Window parentWindow){
    .......;
    ViewRootImpl root;
    View panelParentView = null;
    ......;
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    
    root = new ViewRootImpl(view.getContext(),display);
    view.setLayoutParams(wparams);
    
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
    try{
        root.setView(view,wparams,panelParentView);
    }catch (RuntimeException e){
        .....;
    }
}

上面程式碼中,在 addView 方法中,new 了一個 ViewRootImpl 物件,然後呼叫 ViewRootImpl.setView() 方法。

/**
 * We have one child
 */
public void setView(View view,WindowManager.LayoutParams attrs,View panelParentView){
    synchronized(this){
        if(mView == null){
            mView = view;
            
            mAttachInfo.mDisplayState = mDisplay.getState();
            mDisplayManager.registerDisplayListener(mDisplayListener,mHandler);
            ....;
            requestLayout();
            .....;
            view.assignParent(this);
            .....;
        }
    }
    
}

首先將傳進來的 view 賦值給 mView,然後呼叫了 requestLayout() 方法,ViewRootImpl 不是 View 的子類,可以認為 ViewRootImpl 是這個 Activity 所對應的整個佈局的根,它來執行具體的繪製開始,view.assignParent(this),就是給自己分配了 Parent,Parent 就是 ViewRootImpl 物件。

public void requestLayout(){
    .....;
    
    scheduleTraversals();
}

最終呼叫了 performTraversals() 方法,performTraversals 方法內部程式碼很多

這裡只寫一下重要的部分

// 不傳遞引數,預設是 MATCH_PARENT
final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
// window 可以使用的寬度
int mWidth;
// window 可以使用的高度
int mHeight;

private void performTraversals(){
    int desiredWindowWidth;
    int desireWindowHeight;
    WindowManager.LayoutParams lp = mWindowAttributes;
    
    // 獲取應該給根 View 多大
    // mWidth 就是我們視窗的大小,lp.width 始終是 MATCH_PARENT
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth,lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight,lp.height);
    
    // 告訴 根 View 多大
    performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
    // perforMeasure 內部實現很簡單
    
    performLayout(lp,mWidth,mHeight);
    
    performDraw();
    
    
}

呼叫 getRootMeasureSpec(mWidth,lp.width) 得到 childWidthMeasureSpec :

getRootMeasureSpec()-1.jpg

getRootMeasureSpec()-2.jpg

通過這個方法就獲取了子 View 應該是多大,還有呈現的模式。然後把得到的數值,通過 performMeasure() 方法設定 view 大小。performMeasure 方法:

private void performMeasure(int childWidthMeasureSpec,int childHeightMeasureSpec){
   .....;
    // 這裡的 mView 就是 PhoneWindow 中的 DecorView
   mView.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}

通過這一步:mView.measure(childWidthMeasureSpec,childHeightMeasureSpec) 就是給 mView 確定大小值了,也就是根 View。

這樣整個 Activity 的佈局對應的根 View—DecorView 的大小就確定了。具體來看看 mView.measure():

// 測量一個 View 應該是多大的,引數是由它的父 View 提供的,widthMeasureSpec
// 包含了大小和約束條件
public final void measure(int widthMeasureSpec,int heightMeasureSpec){
    .......;
    onMeasure(widthMeasureSpec,heightMeasureSpec);
    .......;
}

該方法又呼叫了onMeasure 方法:

// 僅僅是 View 裡面的 onMeasure 方法,不同的 View 子類有不同的實現內容
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec));
}

// 官網文件:測量 View 的大小,這個方法通過 measure 方法來呼叫,View 的子類應該重寫此方法根據子 View 的特點。在重寫這個方法的時候,你必須呼叫 setMeasuredDimension(int,int) 來儲存測量出來的寬度和高度。

下面就是呼叫 setMeasuredDimension() 了。其中最關鍵的一步就是對 View 的兩個成員變數進行了賦值(在 setMeasuredDimensionRaw()) 方法中實現的

// 這個方法必須在 onMeasure() 方法中被呼叫用來儲存測量出的寬度和高度。
protected final void setMeasuredDimension(int measuredWidth,int measuredHeight){
    ......;
    // 為了方便,這裡直接把 setMeasuredDimensionRaw() 方法內容寫到下面了
    //給 View 的成員變數賦值
    mMeasuredWidth = measureWidth;
    mMeasuredHeight = measuredHeight;
    
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    
}

好了到此 View 中的 measure 方法也介紹完畢了。是不是覺得 onMeasure 方法裡面很簡單啊!這你就錯了,不要忘了我們分析的 onMeasure 方法是分析的類 View 中的。裡面註釋明確寫明瞭 View 的子類必須根據自身需求來重寫 onMeasure 方法 。我們上面分析的只是一個過程。不要忘了我們的根 View 是 DecorView,而 DecorView 的父類又是 FrameLayout。可以到這兩個具體的物件中看看他們的 onMeasure。感興趣的可以看一下原始碼,其實分析到這裡就可以得出結論了(這個結論僅僅是 measure 這一部分的結論) 後面再從 Activity 的啟動一塊串聯起來!

Measure 結論

Activity 有一個祖先 View 即 DecorView,通過前面的分析 DecorView 的大小是由 ViewRootImpl 中給賦值的,我們可以認為 ViewRootImpl 是所有 View 的根,我們知道我們佈局是呈現樹的結構。我這裡有一個比喻:ViewRootImpl 是這顆樹的根,是埋在地下面的,DecorView 是樹的主幹是在地上面的,ViewGroup 是枝幹,單個的 View (類似於 TextView、Button 這種)是樹葉。DecorView 這個主幹上長出許多枝幹,這裡我們的這棵樹有兩個重要的枝幹(或者一個),這個需要根據樹的種類樣式來決定。這兩個重要的枝幹就是:Title 和 mContentParent,或者只有 mContentParent。然後這個兩個枝幹又會生長出許多的枝幹,枝幹上面生長樹葉。

measure() 方法是由其父 View 來呼叫執行。從根上追溯,DecorView 的大小是由樹根(ViewRootImpl)來賦值的,然後枝幹又是由 DecorView 來呼叫的 measure 的。一層層的呼叫。

舉個簡單栗子:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:id="@+id/ll_parent"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <RelativeLayout
        android:id="@+id/rl_parent_1"
        android:layout_width="match_parent"
        android:layout_height="200dp">
        <TextView
            android:id="@+id/tv_child_1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/view"/>
        <TextView
            android:layout_below="@id/tv_child_2"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:text="@string/activity_cycle"
            />
    </RelativeLayout>
    <LinearLayout
        android:id="@+id/ll_parent_1"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <EditText
            android:id="@+id/et_child_1"
            android:inputType="text"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:hint="@string/progress_bar"/>
    </LinearLayout>
</LinearLayout>

我們這裡有這樣一棵樹,上面的佈局是我們這顆樹的 mContentParent 。首先 ViewRootImpl 會給 DecorView 大小賦值(具體大小是由 generateLayout 的時候確定的)。然後 DecorView 會呼叫 mContentParent 的 measure ,傳入 DecorView 允許它的大小。當然具體的大小是由 measure 傳入的引數和 mContentParent 共同決定的(具體細節下面介紹)然後 mContentParent 再呼叫 ll_parent.measure() 給它傳入 mContentParent 所允許它的大小。這個時候就會啟用 ll_parent 的 onMeasure 方法,在 ll_parent 的 onMeasure 方法裡面肯定會呼叫 rl_parent_1.measure 方法,然後啟用 rl_parent_1 的 onMeasure 方法,在 onMeasure 方法裡面呼叫 tv_child_1.measure ,tv_child_1 沒有孩子了,直接設定自己的大小。然後再一層層的向父佈局返回去。大體就是這樣的一個過程。

當然 measure(int widthMeasureSpec,int heightMeasureSpec) 這裡的引數包含了,子元素的大小的屬性和允許子元素的大小。具體可以看 MeasureSpec 類,很簡單。

到此每個佈局就知道自己的大小了。然後開始執行 ViewRootImpl.performLayout() 方法了

Layout()

其實這個和 measure() 方法相似。都是在父控制元件中呼叫 layout() 方法,然後在 layout 方法中會呼叫 onLayout 方法。和 measure 不同的是 onLayout 方法是給自己的子 View 來佈局的,如果不是 ViewGroup 就不需要重寫 onLayout 方法了。measure 的寬度和高度是該控制元件期望的尺寸,真正在螢幕上的位置和大小是由 layout 來決定的。接下來就是遞迴呼叫他們的 onLayout 方法。

接下來就是開始 Drawa 了,介紹到這裡整個過程算是完成了。

參考:https://www.cnblogs.com/kross/p/4029570.html

個人水平有限,如有紕漏歡迎指正!


相關文章