總結UI原理和高級的UI優化方式

不知道UI原理如何作UI優化?html

本文內容分爲三個部分,UI原理、LayoutInflater原理、UI優化,篇幅有點長,能夠選擇本身喜歡的章節進行閱讀,每個部分最後都有小結。java

相信你們多多少少看過一些Activity啓動源碼分析的文章,也能大概說出Activity啓動流程,例如這種回答:node

AMS負責管理系統全部Activity,因此應用startActivity 最終會經過Binder調用到AMS的startActivity方法,AMS啓動一個Activity以前會作一些檢查,例如權限、是否在清單文件註冊等,而後就能夠啓動了,AMS是一個系統服務,在單獨進程,因此要將生命週期告訴應用,又涉及到跨進程調用,這個跨進程一樣採用Binder,媒介是經過ActivityThread的內部類ApplicationThread,AMS將生命週期跨進程傳到ApplicationThread,而後ApplicationThread 再分發給ActivityThread內部的Handler,這時候生命週期已經回調到應用主線程了,回調Activity的各個生命週期方法。android

還能夠細分,好比Activity、Window、DecorView之間的關係,這個其實也應該難度不大,又忽然想到,setContentView爲何要放在onCreate中?,放在其它方法裏行不行,能不能放在onAttachBaseContext方法裏?其實,這些問題能夠在源碼中找到答案。git

本文參考Android 9.0源碼,API 28。github

正文

寫一個Activity,咱們通常都是經過在onCreate方法中調用setContentView方法設置咱們的佈局,有沒有想過這個問題,設置完佈局,界面就會開始繪製嗎?web

1、從生命週期源碼分析UI原理

Activity啓動流程大體以下:面試

  1. Context -> startActivity
  2. AMS -> startActivity
  3. 進程不存在則通知Zygote啓動進程,啓動完進程,執行ActivityThread的main方法,進入loop循環,經過Handler分發消息。
  4. ApplicationThread -> scheduleLaunchActivity
  5. ActivityThread -> handleLaunchActivity
  6. 其它生命週期回調

從第4點開始分析canvas

1.1 ActivityThread

1.1.1 內部類 ApplicationThread

AMS暫且先不分析,AMS啓動Activity會經過ApplicationThread通知到ActivityThread,啓動Activity從ApplicationThread開始提及,看下 scheduleLaunchActivity 方法api

1.1.2 ApplicationThread#scheduleLaunchActivity

public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
            ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
            CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
            int procState, Bundle state, PersistableBundle persistentState,
            List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
            boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {

        ActivityClientRecord r = new ActivityClientRecord();
        r.token = token;
        r.ident = ident;
        r.intent = intent;
        ...
        sendMessage(H.LAUNCH_ACTIVITY, r);
    }
複製代碼

sendMessage 最終封裝一個ActivityClientRecord對象到msg,調用mH.sendMessage(msg);,mH 是一個Handler,直接看處理部分吧,

1.2 ActivityThread的內部類H

private class H extends Handler {

	public void handleMessage(Message msg) {
		switch (msg.what) {
            case LAUNCH_ACTIVITY: {
                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

                r.packageInfo = getPackageInfoNoCheck(
                        r.activityInfo.applicationInfo, r.compatInfo);
				//一、從msg.obj獲取ActivityClientRecord 對象,調用handleLaunchActivity 處理消息
                handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            } break;
			...

	}


}
複製代碼

上面是Handler很基礎的東西,應該都能看懂,註釋1,啓動Activity直接進入 handleLaunchActivity 方法

1.3 ActivityThread#handleLaunchActivity

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
    ...
    // Make sure we are running with the most recent config.
    handleConfigurationChanged(null, null);

    // Initialize before creating the activity //初始化WindowManagerGlobal
    WindowManagerGlobal.initialize();

    //1. 啓動一個Activity,涉及到建立Activity對象,最終返回Activity對象
    Activity a = Activity a = performLaunchActivity(r, customIntent);

    if (a != null) {
        r.createdConfig = new Configuration(mConfiguration);
        reportSizeConfigurations(r);
        Bundle oldState = r.state;
	//2. Activity 進入onResume方法
        handleResumeActivity(r.token, false, r.isForward,
                !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);

        if (!r.activity.mFinished && r.startsNotResumed) {
	    //3. 若是沒能在前臺顯示,就進入onPuse方法
            performPauseActivityIfNeeded(r, reason);

        }
    } else {
        // If there was an error, for any reason, tell the activity manager to stop us.
	//4. activity啓動失敗,則通知AMS finish掉這個Activity
        try {
            ActivityManagerNative.getDefault()
                .finishActivity(r.token, Activity.RESULT_CANCELED, null,
                        Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }
}
複製代碼

註釋1:performLaunchActivity,開始啓動Activity了
註釋2:Activity進入Resume狀態,handleResumeActivity
註釋3:若是沒能在前臺顯示,那麼進入pause狀態,performPauseActivityIfNeeded
註釋4,若是啓動失敗,通知AMS去finishActivity。

主要看performLaunchActivity(1.3.1) 和 handleResumeActivity(1.3.2)

1.3.1 ActivityThread#performLaunchActivity

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

	//1 建立Activity對象
	activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);

	//2 調用Activity的attach方法
	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);

	//3.回調onCreate
	mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
	
	//4.回調onStart
	activity.performStart();
	
	
	if (r.state != null || r.persistentState != null) {
	//5.若是有保存狀態,則調用onRestoreInstanceState 方法,例如Activity被異常殺死,重寫onSaveInstanceState保存的一些狀態
        mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
                                r.persistentState);
    }

	//6.回調 onPostCreate,這個方法基本沒用過
	mInstrumentation.callActivityOnPostCreate(activity, r.state);

}
複製代碼

從這裏能夠看出Activity幾個方法調用順序:

  1. Activity#attach
  2. Activity#onCreate
  3. Activity#onStart
  4. Activity#nnRestoreInstanceState
  5. Activity#onPostCreate

咱們主要來分析 attach 方法 和 最熟悉的onCreate方法

1.Activity#attach
final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window) {
	//一、回調 attachBaseContext
    attachBaseContext(context);

	//二、建立PhoneWindow
    mWindow = new PhoneWindow(this, window);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    ...
    
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    if (mParent != null) {
        mWindow.setContainer(mParent.getWindow());
    }
    mWindowManager = mWindow.getWindowManager();
    mCurrentConfig = config;
複製代碼

attach方法主要關注兩點:
1.會調用attachBaseContext 方法;
2.建立PhoneWindow,賦值給mWindow,這個後面會常常遇到。

2. Activity#onCreate

你們有沒有思考過,爲何 setContentView 要放在onCreate方法中?
不能放在onStart、onResume中咱們大概是知道的,由於按Home鍵再切回來會回調生命週期onStart、onResume,setContentView屢次調用是不必的。 那若是是放在attachBaseContext 裏面行不行?
看下 setContentView 裏面的邏輯

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

public Window getWindow() {
    return mWindow;
}
複製代碼

getWindow 返回的是mWindow,mWindow是在 Activity 的 attach 方法初始化的,上面剛剛分析過attach方法,mWindow是一個PhoneWindow對象。因此setContentView 必須放在attachBaseContext以後。

4. PhoneWindow#setContentView
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
	//一、 建立DecorView
        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 {
	//二、mContentParent是DecorView中的FrameLayout,將咱們的佈局添加到這個FrameLayout裏
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    ...
}
複製代碼

installDecor 方法是建立DecorView,看一下主要代碼

註釋1:installDecor,建立根佈局 DecorView,
註釋2:mLayoutInflater.inflate(layoutResID, mContentParent); 將xml佈局渲染到mContentParent裏,第二節會重點分析LayoutInflater原理。

先看註釋1

5. PhoneWindow#installDecor
private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
	//1.建立DecorView
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } else {
	// 2. mDecor 不爲空,就是建立過,只需設置window
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
	//3.找到DecorView 內容部分,findViewById(ID_ANDROID_CONTENT)
        mContentParent = generateLayout(mDecor);

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

	//4.找到DecorView的根View,給標題欄部分,設置圖標和標題啥的
        final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                R.id.decor_content_parent);

	//5.設置標題圖標啥的
        if (decorContentParent != null) {
            mDecorContentParent = decorContentParent;
            mDecorContentParent.setWindowCallback(getCallback());
            if (mDecorContentParent.getTitle() == null) {
                mDecorContentParent.setWindowTitle(mTitle);
            }

            ...
            mDecorContentParent.setUiOptions(mUiOptions);

            if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0 ||
                    (mIconRes != 0 && !mDecorContentParent.hasIcon())) {
                mDecorContentParent.setIcon(mIconRes);
            } else if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) == 0 &&
                    mIconRes == 0 && !mDecorContentParent.hasIcon()) {
                mDecorContentParent.setIcon(
                        getContext().getPackageManager().getDefaultActivityIcon());
                mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK;
            }
            if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0 ||
                    (mLogoRes != 0 && !mDecorContentParent.hasLogo())) {
                mDecorContentParent.setLogo(mLogoRes);
            }

            ...
        } 
        
		...
    }
}
複製代碼

installDecor 主要作了兩件事,一個是建立DecorView,一個是根據主題,填充DecorView中的一些屬性,好比默認就是標題欄+內容部分

先看註釋1:generateDecor 建立DecorView

6. PhoneWindow#generateDecor
protected DecorView generateDecor(int featureId) {
    ...
    return new DecorView(context, featureId, this, getAttributes());
}
複製代碼
7. DecorView 構造方法
DecorView(Context context, int featureId, PhoneWindow window,
        WindowManager.LayoutParams params) {
    super(context);
    ...

    updateAvailableWidth();

    //設置window
    setWindow(window);
}
複製代碼

建立DecorView傳了一個PhoneWindow進去 。

installDecor 方法裏面註釋2,若是判斷DecorView已經建立過的狀況下,直接調用mDecor.setWindow(this);

再看 installDecor 方法註釋3

if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
		...
複製代碼

給 mContentParent 賦值,看下 generateLayout 方法

8. PhoneWindow#generateLayout
protected ViewGroup generateLayout(DecorView decor) {

    // 1.窗口各類屬性設置,例如 requestFeature(FEATURE_NO_TITLE);
	...
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
        requestFeature(FEATURE_NO_TITLE);
    }
	...
	if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
        setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
    }
	...
	
	//2.獲取 windowBackground 屬性,默認窗口背景
	if (mBackgroundDrawable == null) {
            if (mBackgroundResource == 0) {
                mBackgroundResource = a.getResourceId(
                        R.styleable.Window_windowBackground, 0);
            }
			...
	}

	...
	// 3.獲取DecorView裏面id爲 R.id.content的佈局
	ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
	...
	if (getContainer() == null) {
	//4.在註釋2的時候已經獲取了窗口背景屬性id,這裏轉換成drawable,而且設置給DecorView
        final Drawable background;
        if (mBackgroundResource != 0) {
            background = getContext().getDrawable(mBackgroundResource);
        } else {
            background = mBackgroundDrawable;
        }
        mDecor.setWindowBackground(background);

		

	}


	return contentParent;

}
複製代碼

註釋1: 首先是窗口屬性設置,例如咱們的主題屬性設置了沒有標題,則走:requestFeature(FEATURE_NO_TITLE);, 全屏則走setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));

還有其餘不少屬性判斷,這裏只列出兩個表明性的,爲何說表明性呢,由於我咱們平時要讓Activity全屏,去掉標題欄,能夠在主題裏設置對應屬性,還能夠經過代碼設置,也就是在onCreate方法裏setContentView以前調用

getWindow().requestFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN );
複製代碼

註釋2:獲取窗口背景屬性id,這個id就是style.xml 裏面咱們定義的主題下的一個屬性

<style name="AppThemeWelcome" parent="Theme.AppCompat.Light.DarkActionBar">
	<item name="colorPrimary">@color/colorPrimary</item>
    ...

    <item name="android:windowBackground">@mipmap/logo</item> //就是這個窗口背景屬性

</style>
複製代碼

註釋3,這個ID_ANDROID_CONTENT 定義在Window中,
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content; 找到id對應的ViewGroup叫contentParent,最終是要返回回去。

註釋4:在註釋2的時候已經獲取了窗口背景屬性id,這裏轉換成drawable,而且設置給DecorView,DecorView是一個FrameLayout,因此咱們在主題中設置的默認背景圖,最終是設置給DecorView

generateLayout 方法分析完了,主要作的事情是讀取主題裏配置的一堆屬性,而後給DecorView 設置一些屬性,例如背景,還有獲取DecorView 裏的內容佈局,做爲方法返回值。

回到 installDecor 方法註釋4,填充DecorContentParent 內容

9. 填充 DecorContentParent
//這個是DecorView的外層佈局,也就是titlebar那一層
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                R.id.decor_content_parent);

        if (decorContentParent != null) {
	
            mDecorContentParent = decorContentParent;
            mDecorContentParent.setWindowCallback(getCallback());
            
			//有該屬性的話就進行相關設置,例如標題、圖標
			mDecorContentParent.setWindowTitle(mTitle);
			...
			mDecorContentParent.setIcon(mIconRes);
			...
			mDecorContentParent.setIcon(
                        getContext().getPackageManager().getDefaultActivityIcon());
			...
			mDecorContentParent.setLogo(mLogoRes);
			...
複製代碼

這是installDecor 的最後一步,設置標題、圖標等屬性。

setContentView若是發現DecorView不存在則建立DecorView,如今DecorView 建立完成,那麼要將咱們寫的佈局添加到DecorView中去了,怎麼作呢,看 PhonwWPhonwWindowindow#setContentView 方法的註釋2,mLayoutInflater.inflate(layoutResID, mContentParent);,這個mContentParent 上面已經分析過了,是 generateLayout 方法返回的,就是DecorView裏面id爲content 的佈局。

因此接下來的一步LayoutInflater.inflate就是將咱們寫的佈局添加到DecorView的內容部分,怎麼添加?這個就涉及到解析xml,轉換成對應的View對象了,怎麼轉換?

LayoutInflater 原理放在第二節,這樣章節比較清晰。 mLayoutInflater.inflate(layoutResID, mContentParent);執行完,xml佈局就被解析成View對象,添加到DecorView中去了。畫一張圖

Activity、Window、DecorView層級關係

此時DecorView並不能顯示到屏幕,由於PhoneWindow並無被使用,只是準備好了而已。1.3 繼續分析第二點

1.3.2 ActivityThread#handleResumeActivity

final void handleResumeActivity(IBinder token,
        boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
	ActivityClientRecord r = mActivities.get(token);	        
	...
	//一、先回調Activity的onResume方法
    r = performResumeActivity(token, clearHide, reason);

    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();
            decor.setVisibility(View.INVISIBLE);
			//二、wm是當前Activity的WindowManager
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            ...
            if (a.mVisibleFromClient && !a.mWindowAdded) {
                a.mWindowAdded = true;
				// 3.添加decor到WindowManager
                wm.addView(decor, l);
            }
複製代碼

主要關注的就是上面的代碼,
註釋1:performResumeActivity,主要是回調Activity的onResume方法。
註釋2: 從Activity開始跟蹤這個wm,實際上是WindowManagerImpl,
註釋3: 調用WindowManagerImpl的addView方法,最終調用的是WindowManagerGlobal的addView方法,將DecorView添加到 WindowManagerGlobal

從這個順序能夠看出,在Activity的onResume方法回調以後,纔會將decorView添加到window,看下addView 的邏輯

1 WindowManagerImpl#addView
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
...    
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
複製代碼

WindowManagerGlobal#addView

2 WindowManagerGlobal#addView
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {


	ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
        ... 
	//一、建立 ViewRootImpl
        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

	//二、添加到ArrayList進行管理
        mViews.add(view);
	//3.mRoots 存放ViewRootImpl對象
        mRoots.add(root);
        mParams.add(wparams);
    }

    // do this last because it fires off messages to start doing things
    try {
	// 4.調用ViewRootImpl的setView
        root.setView(view, wparams, panelParentView);
    }
複製代碼

入參view是DecorView
註釋1:建立 ViewRootImpl,
註釋2:將decorView添加進到ViewRootImpl中去,ViewRootImpl 和 DecorView 的關係創建。
註釋3:將ViewRootImpl保存到 ArrayList
註釋4:調用ViewRootImpl的setView 方法,這個很關鍵。

ViewRootImpl的setView 方法,第一個參數是DecorView,第二個參數是PhoneWindow的屬性,第三個參數是父窗口,若是當前是子窗口,那麼須要傳父窗口,不然傳null,默認null便可。

看暈了沒,從新梳理一下resume的整個流程:

  1. ActivityThread 調用 handleResumeActivity(這個方法源頭是AMS回調的)
  2. 回調 Activity 的 onResume
  3. 將 DecorView添加到 WindowManagerGlobal
  4. 建立 ViewRootImpl,調用setView方法,跟DecorView綁定

接下來主要關注 ViewRootImpl 的setView方法

1.4 ViewRootImpl 分析

看下構造方法

1.4.1 ViewRootImpl 構造

public ViewRootImpl(Context context, Display display) {
    mContext = context;
    //一、WindowSession 是IWindowSession,binder對象,可跨進程跟WMS通訊
    mWindowSession = WindowManagerGlobal.getWindowSession();
    mDisplay = display;
    ...
    // 二、建立window,是一個binder對象
    mWindow = new W(this);
    ...
    //三、mAttachInfo 很熟悉吧,在這裏建立的,View.post跟這個息息相關
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
    ...
    //四、Choreographer在這裏初始化
    mChoreographer = Choreographer.getInstance();
    mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
    loadSystemProperties();
}
複製代碼

註釋1:建立 mWindowSession,這是一個Binder對象,能夠跟WMS通訊。
註釋2:mWindow 實例化,主要是接收窗口狀態改變的通知。
註釋3:mAttachInfo 建立,view.post(runable) 能獲取寬高的問題跟這個mAttachInfo的建立有關。
註釋4:mChoreographer 初始化,以前文章講過 Choreographer,再也不重複講。

mWindowSession 的建立簡單來看一下,由於接下來不少地方都要用到它來跟WMS通訊。

1.4.2 mWindowSession 是幹嗎的

mWindowSession = WindowManagerGlobal.getWindowSession();
複製代碼

看下WindowManagerGlobal 是怎麼建立Session的

1. WindowManagerGlobal.getWindowSession()
public static IWindowSession getWindowSession() {
    synchronized (WindowManagerGlobal.class) {
        if (sWindowSession == null) {
            try {
                InputMethodManager imm = InputMethodManager.getInstance();
				// 這裏獲取的是WMS的Binder代理
                IWindowManager windowManager = getWindowManagerService();
				// 經過代理調用 WMS 的openSession 方法返回一個 Session(也是一個Binder代理)
                sWindowSession = windowManager.openSession(
                        new IWindowSessionCallback.Stub() {
                            @Override
                            public void onAnimatorScaleChanged(float scale) {
                                ValueAnimator.setDurationScale(scale);
                            }
                        },
                        imm.getClient(), imm.getInputContext());
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
        return sWindowSession;
    }
}
複製代碼

拿到WMS的binder代理,經過代理調用WMS 的 openSession 方法。看下WMS的 openSession 方法

2. WindowManagerService#openSession
public IWindowSession openSession(IWindowSessionCallback callback, IInputMethodClient client,
        IInputContext inputContext) {
    if (client == null) throw new IllegalArgumentException("null client");
    if (inputContext == null) throw new IllegalArgumentException("null inputContext");
	//建立一個 Session,把WMS傳給Session,Session就有WMS的引用
    Session session = new Session(this, callback, client, inputContext);
    return session;
}
複製代碼

建立一個Session返回, 它也是一個Binder對象,擁有跨進程傳輸能力

3. Session
final class Session extends IWindowSession.Stub
    implements IBinder.DeathRecipient {
複製代碼

經過分析,mWindowSession 是一個 擁有跨進程能力的 Session 對象,擁有WMS的引用,後面ViewRootImpl跟WMS交互都是經過這個mWindowSession

看下ViewRootImpl的setView方法

1.4.2 ViewRootImpl#setView

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
	    // 1.DecorView賦值給mView
            mView = view;
            ...
	    //2.會調用requestLayout
            requestLayout();
            ...
            try {
                mOrigWindowType = mWindowAttributes.type;
                mAttachInfo.mRecomputeGlobalAttributes = true;
                collectViewAttributes();
		//3.WindowSession,將window添加到屏幕,有返回值
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(),
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mInputChannel);
            } 
            ...
	    //4.添加window失敗處理
            if (res < WindowManagerGlobal.ADD_OKAY) {
                ...
                switch (res) {
                    case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
                    case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                        throw new WindowManager.BadTokenException(
                                "Unable to add window -- token " + attrs.token
                                + " is not valid; is your activity running?");
                    case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                        throw new WindowManager.BadTokenException(
                                "Unable to add window -- token " + attrs.token
                                + " is not for an application");
                    case WindowManagerGlobal.ADD_APP_EXITING:
                        throw new WindowManager.BadTokenException(
                                "Unable to add window -- app for token " + attrs.token
                                + " is exiting");
                    case WindowManagerGlobal.ADD_DUPLICATE_ADD:
                        throw new WindowManager.BadTokenException(
                                "Unable to add window -- window " + mWindow
                                + " has already been added");
                    case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
                        // Silently ignore -- we would have just removed it
                        // right away, anyway.
                        return;
                    case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
                        throw new WindowManager.BadTokenException("Unable to add window "
                                + mWindow + " -- another window of type "
                                + mWindowAttributes.type + " already exists");
                    case WindowManagerGlobal.ADD_PERMISSION_DENIED:
                        throw new WindowManager.BadTokenException("Unable to add window "
                                + mWindow + " -- permission denied for window type "
                                + mWindowAttributes.type);
                    case WindowManagerGlobal.ADD_INVALID_DISPLAY:
                        throw new WindowManager.InvalidDisplayException("Unable to add window "
                                + mWindow + " -- the specified display can not be found");
                    case WindowManagerGlobal.ADD_INVALID_TYPE:
                        throw new WindowManager.InvalidDisplayException("Unable to add window "
                                + mWindow + " -- the specified window type "
                                + mWindowAttributes.type + " is not valid");
                }
                throw new RuntimeException(
                        "Unable to add window -- unknown error code " + res);
            }

            ...
			//5.這裏,給DecorView設置parent爲ViewRootImpl
            view.assignParent(this);
            ...
        }
    }
}
複製代碼

setView 整理以下:
註釋1. 給mView賦值爲DecorView
註釋2. 調用 requestLayout,主動發起繪製請求
註釋3. WindowSession 是一個Binder對象,mWindowSession.addToDisplay(...),將PhoneWindow添加到WMS去了
註釋4. 添加window失敗的處理
註釋5: view.assignParent(this); 給DecorView設置parent爲ViewRootimpl

註釋2調用requestLayout,會請求vsyn信號,而後下一次vsync信號來的時候,會調用 performTraversals 方法,這個上一篇文章分析過了,performTraversals 主要是執行View的measure、layout、draw。

註釋3,mWindowSession.addToDisplay 這一句要重點分析,

1.4.2.1 mWindowSession.addToDisplay

mWindowSession 的建立上面已經分析過了。

1.4.2.2 Session#addToDisplay
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
        int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
        Rect outOutsets, InputChannel outInputChannel) {
	//調用WMS 的 addWindow
    return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
            outContentInsets, outStableInsets, outOutsets, outInputChannel);
}
複製代碼

Session裏面有WMS的應用,這裏能夠看到 addToDisplay 方法就是調用了 WMS 的 addWindow 方法。

1.4.2.3 WindowManagerService#addWindow

WMS 的 addWindow 方法代碼比較多,不打算詳細分析,內部涉及到窗口權限校驗、token校驗、建立窗口對象、添加窗口到Windows列表、肯定窗口位置等。讀者能夠自行查看源碼。

到這裏能夠知道的是,ViewRootImpl跟WMS通訊的媒介就是Session:

ViewRootImpl -> Session -> WMS

回來ViewRootImpl的 setView 方法,註釋2調用 requestLayout方法,最終會請求vsync信號,而後等下一個vsync信號來臨,就會經過Handler執行performTraversals方法。因此按照執行順序,performTraversals方法是在添加Window以後才調用。

1.5 ViewRootImpl#performTraversals

private void performTraversals() {

	... // 一、relayoutWindow 方法是將mSurface跟WMS關聯
	relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
	 ...
	performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
	...
	performLayout(lp, mWidth, mHeight);
	 ...
	performDraw();
}
複製代碼

performTraversals 裏面執行View的三個方法,這個你們可能都知道,但是怎麼跟屏幕關聯上的呢,performDraw 方法裏面能找到答案~

看下performDraw

private void performDraw() {      
        draw(fullRedrawNeeded);
...
}


private boolean draw(boolean fullRedrawNeeded) {
        //這個 mSurface是在定義的時候new的 
        Surface surface = mSurface;
        // 只分析軟件繪製
        if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
                    return false;
         }
        return useAsyncReport;
    }
複製代碼
  1. mSurface 在定義的時候就new了public final Surface mSurface = new Surface();,可是隻是一個空殼,其實在 performTraversals 的註釋1裏面作了一件事,調用 relayoutWindow 這個方法
relayoutWindow
private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
            boolean insetsPending) throws RemoteException {

        ...

        int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params,
                (int) (mView.getMeasuredWidth() * appScale + 0.5f),
                (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
                insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
                mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
                mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
                mPendingMergedConfiguration, mSurface);

        return relayoutResult;
    }
複製代碼

裏面調用了mWindowSession.relayout 方法,將mSurface傳過去,直接看Session的relayout 方法

public int relayout(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int requestedWidth, int requestedHeight, int viewFlags, int flags, long frameNumber,
            Rect outFrame, Rect outOverscanInsets, Rect outContentInsets, Rect outVisibleInsets,
            Rect outStableInsets, Rect outsets, Rect outBackdropFrame,
            DisplayCutout.ParcelableWrapper cutout, MergedConfiguration mergedConfiguration,
            Surface outSurface) {
        //調用WMS的relayoutWindow方法
        int res = mService.relayoutWindow(this, window, seq, attrs,
                requestedWidth, requestedHeight, viewFlags, flags, frameNumber,
                outFrame, outOverscanInsets, outContentInsets, outVisibleInsets,
                outStableInsets, outsets, outBackdropFrame, cutout,
                mergedConfiguration, outSurface);
        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
        if (false) Slog.d(TAG_WM, "<<<<<< EXITING relayout to "
                + Binder.getCallingPid());
        return res;
    }
複製代碼

最終調用WMS的relayoutWindow方法,WMS會對這個mSurface作一些事情,使得這個mSurface跟當前的PhoneWindow關聯起來。

performDraw -> draw -> drawSoftware

繼續看ViewRootImpl的drawSoftware方法

ViewRootImpl#drawSoftware
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {

        // Draw with software renderer.
        final Canvas canvas;
        //一、從mSurface獲取一個Canvas
        canvas = mSurface.lockCanvas(dirty);
        //二、View繪製
        mView.draw(canvas);
        //三、繪製完提交到Surfece,而且釋放畫筆
        surface.unlockCanvasAndPost(canvas);

    }
複製代碼

drawSoftware 方法的核心就三句代碼:

  1. lockCanvas從Surface 獲取一個canvas。
  2. draw,用這個canvas去執行View的繪製。
  3. unlockCanvasAndPost,將繪製的內容提交到Surface。

mView.draw(canvas); 你們都比較熟悉了,mView 是DecorView,是一個FrameLayout,這裏就是遍歷子view,調用draw方法了,最終就是調用Canvas.draw。
整個View樹draw完,就會將繪製的內容提交到Surface,以後這個Surface會被SurfaceFlinger 管理。

軟件繪製流程
解讀一下這種圖:

  • Surface :每一個View都是由某一個窗口管理,每一個窗口關聯一個Surface
  • Canvas:經過Surface的lockCanvas方法獲取一個Canvas,Canvas能夠理解爲Skia底層接口的封裝,調用Canvas 的draw方法,至關於調用Skia庫的繪製方法。
  • Graphic Buffer:SurfaceFlinger 會幫咱們託管一個Buffer Queue,咱們從Buffer Queue獲取一個Graphic Buffer,而後經過Canvas以及Skia將繪製的數據柵格化到上面。
  • SurfaceFlinger:經過Swap Buffer 把前臺Graphic Buffer 提交給SurfaceFlinger,最後經過硬件合成器 Hardware Composer合成並輸出到顯示屏。

UI原理總結

  1. Activity的attach 方法裏建立PhoneWindow。
  2. onCreate方法裏的 setContentView 會調用PhoneWindow的setContentView方法,建立DecorView而且把xml佈局解析而後添加到DecorView中。
  3. 在onResume方法執行後,會建立ViewRootImpl,它是最頂級的View,是DecorView的parent,建立以後會調用setView方法。
  4. ViewRootImpl 的 setView方法,會將PhoneWindow添加到WMS中,經過 Session做爲媒介。 setView方法裏面會調用requestLayout,發起繪製請求。
  5. requestLayout 一旦發起,最終會調用 performTraversals 方法,裏面將會調用View的三個measure、layout、draw方法,其中View的draw 方法須要一個傳一個Canvas參數。
  6. 最後分析了軟件繪製的原理,經過relayoutWindow 方法將Surface跟當前Window綁定,經過Surface的lockCanvas方法獲取Surface的的Canvas,而後View的繪製就經過這個Canvas,最後經過Surface的unlockCanvasAndPost 方法提交繪製的數據,最終將繪製的數據交給SurfaceFlinger去提交給屏幕顯示。

軟件繪製流程

2、LayoutInflater 原理

前面分析setContentView的時候,最終會調用 mLayoutInflater.inflate(layoutResID, mContentParent); 將佈局文件的View添加到DecorView的content部分。這一節就來分析LayoutInflater的原理~

LayoutInflater 是在 PhoneWindow 構造方法裏建立的

public PhoneWindow(Context context) {
    super(context);
    mLayoutInflater = LayoutInflater.from(context);
}
複製代碼

LayoutInflater是一個系統服務

public static LayoutInflater from(Context context) {
    LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    return LayoutInflater;
}
複製代碼

inflate 方法有不少重載,看帶有資源id參數的

2.1 LayoutInflater#inflate(@LayoutRes int resource, @Nullable ViewGroup root)

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}


//root 不爲空,attachToRoot就爲true,表示最終解析出來的View要add到root中去。
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
	//1.將資源id轉換成 XmlResourceParser   
    final XmlResourceParser parser = res.getLayout(resource);
    try {
		//2.調用另外一個重載方法
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}	
複製代碼

註釋1是將資源id轉換成 XmlResourceParser,XmlResourceParser 是啥?

public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable {
    /**
     * Close this interface to the resource.  Calls on the interface are no
     * longer value after this call.
     */
    public void close();
}
複製代碼

繼承 XmlPullParser ,這是一個標準XmlPullParser接口,同時繼承AttributeSet接口以及當用戶完成資源讀取時調用的close接口,簡單來講就是一個xml資源解析器,用於解析xml資源。

關於 XmlPullParser 原理能夠參考文末連接。

2.2 【擴展】XML解析有哪些方式

SAX(Simple API XML)

是一種基於事件的解析器,事件驅動的流式解析方式是,從文件的開始順序解析到文檔的結束,不可暫停或倒退

DOM

即對象文檔模型,它是將整個XML文檔載入內存(因此效率較低,不推薦使用),每個節點當作一個對象

Pull

Android解析佈局文件所使用的方式。Pull與SAX有點相似,都提供了相似的事件,如開始元素和結束元素。不一樣的是,SAX的事件驅動是回調相應方法,須要提供回調的方法,然後在SAX內部自動調用相應的方法。而Pull解析器並無強制要求提供觸發的方法。由於他觸發的事件不是一個方法,而是一個數字。它使用方便,效率高。

沒用過不要緊,可是要知道Pull解析的優勢:
Pull解析器小巧輕便,解析速度快,簡單易用,很是適合在Android移動設備中使用,Android系統內部在解析各類XML時也是用Pull解析器,Android官方推薦開發者們使用Pull解析技術。

參考 https://www.cnblogs.com/guolingyun/p/6148462.html

上面inflate方法註釋1解析完xml,獲得XmlResourceParser 對象,

解析一個xml,咱們能夠直接使用 getContext().getResources().getLayout(resource);,返回一個 XmlResourceParser 對象。

註釋2,調用另外一個inflate 的重載方法,XmlResourceParser 做爲入參

2.3 LayoutInflater#inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {

        final Context inflaterContext = mContext;
	//獲取xml中的屬性,存到一個Set裏
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;

        try {
            // Look for the root node.
            int type;
	    //一、尋找開始標籤
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }

	    //2.遍歷一遍沒找到開始標籤,拋異常
            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }

	    //3.開始標籤的名字,例如根佈局是個LinearLayout,那麼這個name就是LinearLayout
            final String name = parser.getName();
            
	    //四、若是根佈局是merge標籤
            if (TAG_MERGE.equals(name)) {
				//merge標籤必需要有附在一個父佈局上,rootView是空,那麼拋異常
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }

		// 遇到merge標籤調用這個rInflate方法(下面會分析)
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
				//五、不是merge標籤,調用createViewFromTag,經過標籤建立一個根View
                // Temp is the root view that was found in the xml
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    
		    //這個方法裏面是解析layout_width和layout_height屬性,建立一個LayoutParams
                    // Create layout params that match root, if supplied
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                    //若是隻是單純解析,不附加到一個父View去,那麼根View設置LayoutParams,不然,使用外部View的寬高屬性
                        temp.setLayoutParams(params);
                    }
                }

		//6.調用這個方法,解析子View
                // Inflate all children under temp against its context.
                rInflateChildren(parser, temp, attrs, true);


                // 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);
                }

		//若是不附加到root,那麼返回這個temp,result 默認是 root
                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        }


        return result;
    }
}
複製代碼

這個方法按順序來分析,不是很複雜,一步步看下,

註釋1到3:從XmlPullParser 裏面解析出開始標籤,例如,而後取出標籤的名字 LinearLayout

註釋4:若是根標籤是 merge標籤,那麼再判斷,merge標籤必需要依附到rootview,不然拋異常。merge標籤的使用就不用說了,必須放在根佈局。

根佈局merge標籤的解析是調用 rInflate(parser, root, inflaterContext, attrs, false);方法,看一下

2.4 merge標籤解析:rInflate

void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

    final int depth = parser.getDepth();
    int type;

    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        final String name = parser.getName();
        
		
        if (TAG_REQUEST_FOCUS.equals(name)) { 
	    // 一、requestFocus 標籤
            parseRequestFocus(parser, parent);
        } else if (TAG_TAG.equals(name)) {     
	    // 二、tag 標籤
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {  
	    // 三、include 標籤
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {  
	    //四、merge 標籤下不能有merge標籤
            throw new InflateException("<merge /> must be the root element");
        } else {                             
	    //五、其它正常標籤,調用createViewFromTag 建立View對象
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
	    //六、遞歸解析子View
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

    if (finishInflate) {
        parent.onFinishInflate();
    }
}
複製代碼

merge標籤的解析,主要是如下這些狀況:

<?xml version="1.0" encoding="utf-8"?>
<merge
    xmlns:android="http://schemas.android.com/apk/res/android">

    <requestFocus></requestFocus>  //requestFocus

    <tag android:id="@+id/tag"></tag>  //tag

    <include layout="@layout/activity_main"/>  //include

    <merge>  //錯誤,merge標籤必須在根佈局
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:textColor="@color/colorAccent"
            android:text="123"/>
    </merge>

	<TextView  //正常標籤
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:textColor="@color/colorAccent"
        android:text="123"/>

</merge>
複製代碼

2.4.1 解析requestFocus標籤

調用父View的 requestFocus 方法

private void parseRequestFocus(XmlPullParser parser, View view)
        throws XmlPullParserException, IOException {
    view.requestFocus(); //這個view是parent,也就是父佈局請求聚焦

    consumeChildElements(parser);  //跳過requestFocus標籤
}


final static void consumeChildElements(XmlPullParser parser)
        throws XmlPullParserException, IOException {
    int type;
    final int currentDepth = parser.getDepth();
    //遇到 END_TAG 退出循環,在此處也就是跳過 requestFocus 標籤
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) {
        // Empty
    }
}
複製代碼

2.4.2 解析tag標籤

給父佈局設置tag

private void parseViewTag(XmlPullParser parser, View view, AttributeSet attrs)
        throws XmlPullParserException, IOException {
    final Context context = view.getContext();
    final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ViewTag);
    final int key = ta.getResourceId(R.styleable.ViewTag_id, 0);
    final CharSequence value = ta.getText(R.styleable.ViewTag_value);
    view.setTag(key, value); //這個view是parent,給父佈局設置tag
    ta.recycle();

    consumeChildElements(parser);  //跳過tag標籤
}
複製代碼

能夠看到requestFocus標籤和tag標籤只是給父View設置屬性。

2.4.3 解析include標籤

private void parseInclude(XmlPullParser parser, Context context, View parent,
        AttributeSet attrs) throws XmlPullParserException, IOException {
    int type;

	// 父View必須是ViewGroup
    if (parent instanceof ViewGroup) {
       
        ...
	//一、解析layout屬性值id,解析不到默認0
        int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
        ...
	//二、檢查layout屬性值,是0就拋異常
        if (layout == 0) {
            final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
            throw new InflateException("You must specify a valid layout "
                    + "reference. The layout ID " + value + " is not valid.");
        } else {
	    //三、解析layout屬性對應的佈局
            final XmlResourceParser childParser = context.getResources().getLayout(layout);
	    //以後就跟解析前面同樣,判斷是否有merge標籤,有就執行 rInflate,沒有就執行 createViewFromTag
            try {
                final AttributeSet childAttrs = Xml.asAttributeSet(childParser);

                while ((type = childParser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty.
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(childParser.getPositionDescription() +
                            ": No start tag found!");
                }

                final String childName = childParser.getName();

                if (TAG_MERGE.equals(childName)) {
                    // The <merge> tag doesn't support android:theme, so
                    // nothing special to do here.
                    rInflate(childParser, parent, context, childAttrs, false);
                } else {
                    final View view = createViewFromTag(parent, childName,
                            context, childAttrs, hasThemeOverride);
		    //父View,最終解析子View會添加到父View裏。
                    final ViewGroup group = (ViewGroup) parent;

                    final TypedArray a = context.obtainStyledAttributes(
                            attrs, R.styleable.Include);
                    final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
		    //獲取visibility屬性
                    final int visibility = a.getInt(R.styleable.Include_visibility, -1);
                    a.recycle();
                    ViewGroup.LayoutParams params = null;
                    try {
                        params = group.generateLayoutParams(attrs);
                    } catch (RuntimeException e) {
                        // Ignore, just fail over to child attrs.
                    }
                    if (params == null) {
                        params = group.generateLayoutParams(childAttrs);
                    }
                    view.setLayoutParams(params);

                    //解析子View
                    rInflateChildren(childParser, view, childAttrs, true);

                    if (id != View.NO_ID) {
                        view.setId(id);
                    }
		    //設置 visibility 屬性
                    switch (visibility) {
                        case 0:
                            view.setVisibility(View.VISIBLE);
                            break;
                        case 1:
                            view.setVisibility(View.INVISIBLE);
                            break;
                        case 2:
                            view.setVisibility(View.GONE);
                            break;
                    }

                    group.addView(view);
                }
            } finally {
                childParser.close();
            }
        }
    } else {
        throw new InflateException("<include /> can only be used inside of a ViewGroup");
    }

    LayoutInflater.consumeChildElements(parser);
}
複製代碼

解析include標籤步驟比較多,以 <include layout="@layout/activity_main"/>爲例:

首先,include標籤的父View必須是ViewGroup,
註釋1和註釋2是檢查include標籤有沒有layout屬性,屬性值不能是0,屬性值對應layout/activity_main這個佈局id,

註釋3,xml解析,解析layout/activity_main佈局,context.getResources().getLayout(layout)很熟悉了, 而後就跟最開始的解析佈局差很少了,若是遇到merge標籤,就調用rInflate方法去解析merge標籤,不然,就跟inflate方法邏輯同樣,調用createViewFromTag建立一個View對象,而後再調用rInflateChildren解析子View,還有就是給View設置visibility屬性。 這部分邏輯跟inflate方法裏面同樣,因此放到後面分析。

2.4.4 子標籤是merge

merge標籤下不能有merge標籤,merge標籤必須在xml根節點

2.4.5 遞歸解析子View,rInflateChildren

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
        boolean finishInflate) throws XmlPullParserException, IOException {
    //遞歸調用解析merge標籤的方法,parent換了而已
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
複製代碼

merge標籤解析完了,回到 2.3 LayoutInflater#inflate 看註釋5

2.5 LayoutInflater#inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)

//五、不是merge標籤,調用createViewFromTag,經過標籤建立一個根View
                // Temp is the root view that was found in the xml
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    
		    //這個方法裏面是解析layout_width和layout_height屬性,返回一個LayoutParams
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        //若是隻是單純解析,不附加到一個父View去,那麼根View設置LayoutParams,不然,使用外部View的寬高屬性
                        temp.setLayoutParams(params);
                    }
                }

		//六、調用這個方法,解析子View
                // Inflate all children under temp against its context.
                rInflateChildren(parser, temp, attrs, true);


                // 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);
                }

		//若是不附加到root,那麼返回這個temp,result 默認是 root
                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                if (root == null || !attachToRoot) {
                    result = temp;
                }
複製代碼

註釋5,這裏的邏輯跟解析include標籤裏的layout邏輯基本是同樣的,經過createViewFromTag 方法,傳View的名稱,去建立一個View對象

2.6 LayoutInflater#createViewFromTag

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {

    //一、若是是view標籤,取出class屬性
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }

    ...

    //二、TAG_1995 :blink,若是是blink標籤,就返回一個 BlinkLayout,會閃爍的意思
    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }

    try {
        View view;
        if (mFactory2 != null) {
	    // 三、經過 mFactory2 建立View
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
	    // 四、經過 mFactory 建立View
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }

        if (view == null && mPrivateFactory != null) {
	    // 五、經過 mPrivateFactory 建立View
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
		// 六、經過 onCreateView 方法建立View
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }

        return view;
    } 
}
複製代碼

註釋1:若是是view標籤,例如<view class="android.widget.TextView"></view>,則取出class屬性,也就是android.widget.TextView

註釋2: 若是標籤是blink,則建立 BlinkLayout 返回,看下BlinkLayout 是啥?

2.6.1 BlinkLayout

private static class BlinkLayout extends FrameLayout {
    private static final int MESSAGE_BLINK = 0x42;
    private static final int BLINK_DELAY = 500;

    private boolean mBlink;
    private boolean mBlinkState;
    private final Handler mHandler;

    public BlinkLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mHandler = new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                if (msg.what == MESSAGE_BLINK) {
                    if (mBlink) {
                        mBlinkState = !mBlinkState;
                        makeBlink(); //循mBlink爲true則循環調用
                    }
                    invalidate();
                    return true;
                }
                return false;
            }
        });
    }

    private void makeBlink() {
        Message message = mHandler.obtainMessage(MESSAGE_BLINK);
        mHandler.sendMessageDelayed(message, BLINK_DELAY); //延時500毫秒
    }

	@Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        mBlink = true; //標誌位true
        mBlinkState = true;

        makeBlink();
    }

	@Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        mBlink = false;
        mBlinkState = true;

        mHandler.removeMessages(MESSAGE_BLINK);
    }
複製代碼

內部經過Handler,onAttachedToWindow的時候mBlink是true,調用makeBlink方法後,500毫秒以後Handler處理,由於mBlink爲true,因此又會調用makeBlink方法。因此這是一個每500毫秒就會改變狀態刷新一次的FrameLayout,表示沒用過這玩意。

繼續看 createViewFromTag方法的註釋三、經過 mFactory2 建立View

2.6.2 Factory2

public interface Factory2 extends Factory {
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}

public interface Factory {
    public View onCreateView(String name, Context context, AttributeSet attrs);
}
複製代碼

Factory2是一個接口,mFactory2 不爲空,那麼最終會經過這個接口的實現類去建立View。

AppCompatDelegateImpl 中實現了這個Factory2接口,何時使用 AppCompatDelegateImpl?還有就是mFactory2 是何時在哪裏賦值的?

看下 AppCompatActivity 源碼就明白了

2.6.3 AppCompatActivity

protected void onCreate(@Nullable Bundle savedInstanceState) {
    AppCompatDelegate delegate = this.getDelegate(); //一、獲取代理
    delegate.installViewFactory(); //二、初始化Factory
    delegate.onCreate(savedInstanceState);
    ..
}
複製代碼

註釋1:獲取代理

public AppCompatDelegate getDelegate() {
    if (this.mDelegate == null) {
        this.mDelegate = AppCompatDelegate.create(this, this);
    }

    return this.mDelegate;
}
複製代碼

AppCompatDelegate.create 其實返回的就是 AppCompatDelegateImpl

public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
    return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
}
複製代碼

onCreate 註釋2:delegate.installViewFactory(),看 AppCompatDelegateImpl 裏面

public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
	//若是 getFactory 空,就設置Factory2
    if (layoutInflater.getFactory() == null) {
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    }

}
複製代碼

LayoutInflaterCompat#setFactory2

public static void setFactory2(@NonNull LayoutInflater inflater, @NonNull Factory2 factory) {
    inflater.setFactory2(factory); //設置factory2
    //AppCompact 是兼容包,sdk 21如下應該是沒有提供setFactory2 這個方法,須要強制經過反射設置factory2,
    if (VERSION.SDK_INT < 21) {
        Factory f = inflater.getFactory();
        if (f instanceof Factory2) {
	    //強制設置Factory2
            forceSetFactory2(inflater, (Factory2)f);
        } else {
            forceSetFactory2(inflater, factory);
        }
    }

}
複製代碼

LayoutInflaterCompat#forceSetFactory2

只看關鍵代碼以下:

private static void forceSetFactory2(LayoutInflater inflater, Factory2 factory) {
    //反射拿到LayoutInflater 的mFactory2 字段
    sLayoutInflaterFactory2Field = LayoutInflater.class.getDeclaredField("mFactory2");
    sLayoutInflaterFactory2Field.setAccessible(true);

	//強制設置
	sLayoutInflaterFactory2Field.set(inflater, factory);

    
}
複製代碼

這裏簡單說一下AppCompact 是幹嗎的,簡單來講就是高版本的一些特性,正常狀況下低版本設備是看不到的,因此谷歌提供兼容包,v四、v七、以及如今推薦替換androidx,目的是爲了讓高api的一些特性,在低版本也能享受到,只要繼承AppCompactActivity,低版本設備也能有高版本api的特性。

通過上面分析,針對 view = mFactory2.onCreateView(parent, name, context, attrs); 這句代碼,
咱們來看 AppCompatDelegateImplonCreateView 方法就對了,有理有據,你不要說你寫界面都是繼承Activity,我會鄙視你。

2.6.4 AppCompatDelegateImpl#onCreateView

public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    return this.createView(parent, name, context, attrs);
}

public View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {
    if (this.mAppCompatViewInflater == null) {
        TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
	//一、咱們能夠在主題中配置自定義的 Inflater
        String viewInflaterClassName = a.getString(styleable.AppCompatTheme_viewInflaterClass);
        if (viewInflaterClassName != null && !AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
            try {
                Class viewInflaterClass = Class.forName(viewInflaterClassName);
                this.mAppCompatViewInflater = (AppCompatViewInflater)viewInflaterClass.getDeclaredConstructor().newInstance();
            } catch (Throwable var8) {
                Log.i("AppCompatDelegate", "Failed to instantiate custom view inflater " + viewInflaterClassName + ". Falling back to default.", var8);
                this.mAppCompatViewInflater = new AppCompatViewInflater();
            }
        } else {
	    // 二、若是沒有指定,默認建立 AppCompatViewInflater
            this.mAppCompatViewInflater = new AppCompatViewInflater();
        }
    }
	...
    //三、最後經過AppCompatViewInflater 的 createView 方法建立View
    return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());
}
複製代碼

註釋1和註釋2,說明咱們能夠在主題中配置Inflater,能夠自定義建立View的規則

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="viewInflaterClass">android.support.v7.app.AppCompatViewInflater</item> //指定viewInflaterClass
</style>
複製代碼

註釋3 AppCompatViewInflater 的 createView 方法就是真正去建立View對象了。

2.6.5 AppCompatViewInflater#createView、

final View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
    ...
    switch(name.hashCode()) {
	...
    case -938935918:
        if (name.equals("TextView")) {
            var12 = 0;
        }
        break;

    case 2001146706:
        if (name.equals("Button")) {
            var12 = 2;
        }
    }
	...省略不少case

    //上面已經對var12賦值,走對應的方法
    switch(var12) {
    case 0:
	// 0 是 TextView標籤
        view = this.createTextView(context, attrs);
        this.verifyNotNull((View)view, name);
        break;
    case 1:
        view = this.createImageView(context, attrs);
        this.verifyNotNull((View)view, name);
        break;
    case 2:
        view = this.createButton(context, attrs);
        this.verifyNotNull((View)view, name);
        break;
    ...省略不少case

    default:
        view = this.createView(context, name, attrs);
    }

    if (view == null && originalContext != context) {
        //若是上面都沒有建立,createViewFromTag 挽救一些
        view = this.createViewFromTag(context, name, attrs);
    }

    if (view != null) {
        this.checkOnClickListener((View)view, attrs);
    }

    return (View)view;
}
複製代碼

對應View走對應的建立方法,遇到TextView,調用 createTextView 方法,

protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
    return new AppCompatTextView(context, attrs);
}
複製代碼

看到這裏就明白了,只要使用AppCompactActivity,佈局裏寫TextView,最終建立的是 AppCompatTextView。

其它ImageView、Button同理,就再也不分析,讀者能夠本身查看源碼。

若是走完全部case沒有建立成功,不要緊,還能夠挽救一下,調用 createViewFromTag,添加前綴,而後經過反射建立,看一下

2.6.6 AppCompatViewInflater#createViewFromTag

private View createViewFromTag(Context context, String name, AttributeSet attrs) {
    //一、若是是view標籤,獲取class屬性,就是類的全路徑
    if (name.equals("view")) {
        name = attrs.getAttributeValue((String)null, "class");
    }

    View view;
    try {
        this.mConstructorArgs[0] = context;
        this.mConstructorArgs[1] = attrs;
        View var4;
        //若是是API提供的控件,這個條件會成立
        if (-1 == name.indexOf(46)) {
	    //sClassPrefixList 定義: private static final String[] sClassPrefixList = new String[]{"android.widget.", "android.view.", "android.webkit."};
            for(int i = 0; i < sClassPrefixList.length; ++i) {
		//createViewByPrefix,傳View標籤名和前綴
                view = this.createViewByPrefix(context, name, sClassPrefixList[i]);
                if (view != null) {
                    View var6 = view;
                    return var6;
                }
            }

            var4 = null;
            return var4;
        }

	//自定義的控件,不須要前綴,傳null
        var4 = this.createViewByPrefix(context, name, (String)null);
        return var4;
    } catch (Exception var10) {
        view = null;
    } finally {
        this.mConstructorArgs[0] = null;
        this.mConstructorArgs[1] = null;
    }

    return view;
}
複製代碼

在佈局xml裏寫的 TextView,類全路徑是android.widget.TextView,因此要添加前綴 android.widget.才能經過全路徑反射建立TextView對象, sClassPrefixList 中定義的前綴有三個: private static final String[] sClassPrefixList = new String[]{"android.widget.", "android.view.", "android.webkit."};,原生控件定義在這幾個目錄下。

經過View名字加上前綴(若是是咱們的自定義View,前綴null)來建立View,調用createViewByPrefix方法

2.6.7 AppCompatViewInflater#createViewByPrefix

private View createViewByPrefix(Context context, String name, String prefix) throws ClassNotFoundException, InflateException {
    Constructor constructor = (Constructor)sConstructorMap.get(name);

    try {
	//若是沒有緩存
        if (constructor == null) {
	    //最終經過ClassLoader去加載一個類,類有前綴的話會在這裏作拼接
            Class<? extends View> clazz = context.getClassLoader().loadClass(prefix != null ? prefix + name : name).asSubclass(View.class);
            constructor = clazz.getConstructor(sConstructorSignature);
	    //緩存類構造器
            sConstructorMap.put(name, constructor);
        }

        constructor.setAccessible(true);
        return (View)constructor.newInstance(this.mConstructorArgs);
    } catch (Exception var6) {
        return null;
    }
}
複製代碼

最終經過ClassLoader去加載一個類,asSubclass(View.class); 是強轉換成View對象,

這裏用到反射去建立一個View對象,考慮到反射的性能,因此緩存了類的構造器,key是類名,也就是說第一次須要反射加載Class對象,以後建立相同的View,直接從緩存中取出該View的構造器,調用 newInstance 方法實例化。

看到這裏,可能你們會有一個問題了,爲何只分析AppCompat,若是主題不使用AppCompat,那麼流程又是怎樣的?

這裏之因此分析AppCompat,由於它其實已經包含了默認的處理在裏面,好比TextView,若是建立AppCompatTextView失敗,則經過反射區建立android.widget.TextView,默認流程也差很少是這樣的,不信?

再回到 LayoutInflater#createViewFromTag

2.7 LayoutInflater#createViewFromTag

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {

    //一、若是是view標籤,取出class屬性
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }

    ...

    //二、TAG_1995 :blink,若是是blink標籤,就返回一個 BlinkLayout,會閃爍的意思
    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }

    try {
        View view;
        if (mFactory2 != null) {
	    // 三、經過 mFactory2 建立View
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
	    // 四、經過 mFactory 建立View
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }

        if (view == null && mPrivateFactory != null) {
	    // 五、經過 mPrivateFactory 建立View
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
		// 六、經過 onCreateView 方法建立View
                if (-1 == name.indexOf('.')) {
                    //原生控件沒有.  調用這個 onCreateView方法建立
                    view = onCreateView(parent, name, attrs);
                } else {
                    //自定義控件全名有. 因此前綴傳null
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }

        return view;
    } 
}
複製代碼

註釋三、四、5 都是經過代理類去建立,分析了mFactory2,mFactory也是相似的,這裏就再也不分析了,
若是沒有代理類或者代理類沒能力建立,那麼走默認的建立方法,也就就是 註釋6,LayoutInflater#onCreateView(String name, AttributeSet attrs),簡單看下吧

2.7.1 LayoutInflater#onCreateView(String name, AttributeSet attrs)

protected View onCreateView(String name, AttributeSet attrs)
        throws ClassNotFoundException {
    //一、傳前綴,不考慮support包,android提供的原生控件都是android.view. 開頭
    return createView(name, "android.view.", attrs);
}

public final View createView(String name, String prefix, AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    //二、讀取緩存
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    Class<? extends View> clazz = null;

    try {

        //沒有有緩存,經過反射建立View
        if (constructor == null) {
            // Class not found in the cache, see if it's real, and try to add it
            clazz = mContext.getClassLoader().loadClass(
                    prefix != null ? (prefix + name) : name).asSubclass(View.class);
            
            if (mFilter != null && clazz != null) {
                boolean allowed = mFilter.onLoadClass(clazz);
                if (!allowed) {
                    failNotAllowed(name, prefix, attrs);
                }
            }
            constructor = clazz.getConstructor(mConstructorSignature);
            constructor.setAccessible(true);
            sConstructorMap.put(name, constructor);
        } else {
            //有緩存的邏輯
			...

        }

        Object[] args = mConstructorArgs;
        args[1] = attrs;

        final View view = constructor.newInstance(args);
	//若是是 ViewStub
        if (view instanceof ViewStub) {
            // Use the same context when inflating ViewStub later.
            final ViewStub viewStub = (ViewStub) view;
            viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
        }
        return view;

    }
}
複製代碼

一、傳前綴android.view. ,原生控件在這個包下。
二、經過反射建立View,反射耗性能,因此採用緩存機制。
三、若是遇到viewStub,則給它設置一個 LayoutInflater

上面分析的是一層View的建立流程,第二層View怎麼建立的呢?

看 rInflateChildren(parser, temp, attrs, true)

2.8 rInflateChildren(parser, temp, attrs, true);

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
        boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
複製代碼

rInflateChildren 方法內部調用 rInflate,rInflate方法上面其實已經分析過了,跟解析merge標籤流程是同樣的。到這裏基本把整個xml解析成View對象的流程分析完了,須要簡單小結一下,否則都忘了前面講啥了。

LayoutInflater 小結

xml佈局的解析,文字總結以下:

一、 採用 Pull解析,將佈局id解析成 XmlResourceParser 對象,簡單介紹幾種xml解析方式:SAX、DOM和Pull解析。
二、 由於第一步已經將xml解析出來,因此直接取出標籤的View名字,若是是merge標籤,調用rInflate 方法,若是是普通的正常的View,就經過createViewFromTag 這個方法建立一個View對象。
三、介紹rInflate這個方法是如何解析各個標籤的

  • requestFocus標籤
  • tag 標籤
  • include 標籤
  • view 標籤
  • blink 標籤
  • merge 標籤

主要是對以上這些標籤(不分前後)的處理,處理完

xml中第二層View的建立最終也是調用rInflate這個方法,屬於遞歸建立,建立View的順序是深度優先。

三、 介紹 createViewFromTag 方法建立View的方式,若是是blink標籤,直接new new BlinkLayout() 返回,簡單介紹了BlinkLayout,雖然沒用過它。

四、 正常狀況下,createViewFromTag 方法建立View的幾個邏輯

  • mFactory2 不爲空就調用mFactory2.onCreateView 方法建立
  • mFactory 不爲空就調用 mFactory.onCreateView 方法建立
  • 上述建立失敗,就用默認 mPrivateFactory 建立
  • 上述依然沒建立成功,就調用LayoutInflater的CreateView方法,給View添加 android.view. 前綴,而後經過反射去建立一個View對象,考慮到反射耗性能,因此第一次建立成功就會緩存類的構造器 Constructor,以後建立相同View只要調用構造器的newInstance方法。

五、介紹 Factory2 接口的實現類 AppCompatDelegateImpl,AppCompatActivity 內部幾乎全部生命週期方法都交給AppCompatDelegateImpl這個代理去作,而且給LayoutInflater設置了Factory2,因此建立View的時候優先經過 Factory2 接口去建立,也就是經過AppCompatDelegateImpl 這個實現類去建立。

六、介紹 AppCompatDelegateImpl 建立View的方式,若是佈局文件是TextView,則建立的是兼容類AppCompactTextView,其它控件同理。建立失敗則回退到正常的建立流程,添加前綴,例如 android.widget.TextView,而後經過反射去建立。 七、若是沒有使用兼容包,例如繼承Activity,那麼也是經過反射去建立View對象,自定義View不加前綴。

3、UI 優化

所謂UI優化,就是拆解渲染過程的耗時,找到瓶頸的地方,加以優化。

前面分析了UI原理,Activity、Window、DecorView、ViewRootImpl之間的關係,以及XML佈局文件是如何解析成View對象的。

耗時的地方:

  • View的建立在主線程,包括measure、layout、draw,界面複雜的時候,這一部分可能會很耗時。
  • 解析XML,反射建立VIew對象,這一過程的耗時。

下面介紹一些經常使用的UI優化方式~

3.1 常規方式

  1. 減小UI層級、使用merge、Viewstub標籤優化
  2. 優化layout開銷、RelativeLayout和帶有weight的Linearlayout會測量屢次,能夠嘗試使用ConstraintLayout 來代替。
  3. 背景優化,分析DecorView建立的時候,發現DecorView會設置一個默認背景,能夠統一將DecorView背景設置爲通用背景,其它父控件就無需設置背景,避免重複繪製。

3.2 xml轉換成代碼

使用xml編寫佈局,很方便,可是最終要經過LayoutInflater的inflate方法,將xml解析出來並遞歸+反射去建立View對象,佈局比較複雜的時候,這一部分會很是耗時。

使用代碼建立能夠減小xml遞歸解析和反射建立View的這部分耗時。 固然,若是將xml都換成代碼來寫,開發效率將不忍直視,而且代碼可讀性也是個問題。

掌閱開源的一個庫,編譯期自動將xml轉換成java代碼,X2C

它的原理是採用APT(Annotation Processor Tool)+ JavaPoet技術來完成編譯期間【註解】-【解註解】->【翻譯xml】->【生成java】整個流程的操做

即在編譯生成APK期間,將須要翻譯的layout翻譯生成對應的java文件,這樣對於開發人員來講寫佈局仍是寫原來的xml,但對於程序來講,運行時加載的是對應的java文件。

編譯期xml轉java代碼

侵入性極低,去除註解則回退到原生的運行時解析方式。固然,有些狀況是不支持轉換的,好比merge標籤,編譯期無法肯定它的parent。

3.3 異步建立View

經過子線程建立View,減小主線程耗時。

private void threadNewView() {
        new Thread(){
            @Override
            public void run() {

                mSplashView = LayoutInflater.from(MainActivity.this).inflate(R.layout.activity_splash,null);
            }
        }.start();
    }
複製代碼

固然,這種方式須要處理同步問題,而且沒有從源頭上解決建立View耗時,只是將這部分耗時放到線程去作。UI更新的操做仍是要切換到主線程,否則會觸發ViewRootImpl的checkThread檢測。

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }
複製代碼

3.4 複用View

複用View這個應該比較常見了,像RecyclerView的四級緩存,目的就是經過複用View減小建立View的時間。

咱們能夠在onDestroy方法將View的狀態清除,而後放入緩存。在onCreate的時候去命中緩存,設置狀態。

View緩存池

3.5 異步佈局: Litho

正常狀況下measure、layout、draw都是在主線程執行的,最終繪製操做是在draw方法,而measure、layout只是作一些數據準備,徹底能夠放到子線程去作。

Litho 的原理就是將measure、layout 放到子線程: github.com/facebook/li…

Litho

優勢:

  1. 將measure、layout、放到子線程去作,減小主線程耗時。
  2. 使用本身的佈局引擎,減小View層級,界面扁平化。
  3. 優化RecyclerView,提升緩存命中率。

缺點:

  1. 沒法在AS中預覽。
  2. 使用本身的佈局引擎,有一點的使用成本。

3.6 Flutter:自繪引擎

Flutter 是一個跨平臺UI框架,內部集成了Skia圖像庫,本身接管了圖像繪製流程,性能直逼原生,是時候制定計劃學習一波了~

UI優化總結

前面解析了UI原理以後,咱們知道UI渲染主要涉及到xml的解析、View的measure、layout、draw這些耗時的階段。UI優化方式總結以下:

  • 常規方式。
  • xml的解析和反射建立View耗時,採用new 代替xml。
  • 複用View
  • 異步建立View,將建立View的這部分時間放到子線程。
  • 將measure、layout放到子線程,表明: Litho

全文總結

本文分爲三節:

  1. 第一節經過源碼分析了UI原理,主要是介紹Activity、Window、DecorView、ViewRootImpl之間的關係,以及建立的流程。
  2. 第二節分析LayoutInflater解析xml佈局的原理,反射建立View是耗時的,內部使用了緩存。
  3. 第三節在前兩節的原理基礎上,介紹了了幾種UI優化方式。

UI優化參考:
Android開發高手課21 | UI 優化(下):如何優化 UI 渲染?

我在掘金髮布的其它文章:
面試官:說說多線程併發問題
面試官又來了:你的app卡頓過嗎?
面試官:今日頭條啓動很快,你以爲多是作了哪些優化?

有問題評論區留言。

更多文章,敬請期待。

相關文章
相關標籤/搜索