Android彈窗元件工作機制之Dialog、DialogFragment(一)

Android架構木木發表於2019-05-10

前言

Android在DialogFragment推出後,就已經不推薦繼續使用Dialog,可替換為DialogFragment,其實DialogFragment只不過是對增加一層看不到的Fragment,用於監聽生命週期,在Activity退出的時候會自動回收Dialog彈窗

基礎概念

  • Activity:活動。控制生命週期和處理事件,統籌檢視的新增與顯示,控制Window和View的互動
  • Window:視窗。在Android中是個虛擬的概念,不是View,是承載View的載體,具體實現是PhoneWindow,承載著DecorView
  • WindowManager:視窗管理者。管理Window檢視的新增或移除等,具體實現是WindowManagerService(wms)
  • DecorView:視窗根檢視。本身是FrameLayout,是Window上真正的根佈局,其包含兩部分,標題和內容
  • TitleView:標題。作為DecorView的子View,其Id為@android:id/content
  • ContentViews:內容。作為DecorView的子View,其Id為@android:id/content
  • ViewRoot:連線wms和DecorView的紐帶,View的measure、layout、draw均通過ViewRoot來完成,具體實現是ViewRootImpl

Dialog

在平時中,簡單的彈出Dialog只需要這句話

new Dialog(MainActivity.this).show();
複製程式碼

一、Dialog的顯示

1、Dialog

Dialog的構造方法有多個,但最後都會呼叫這個構造方法

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
    if (createContextThemeWrapper) {
        if (themeResId == 0) {
            //如果沒有主題,則使用預設主題
            final TypedValue outValue = new TypedValue();
            context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
            themeResId = outValue.resourceId;
        }
        //包裹主題的Context
        mContext = new ContextThemeWrapper(context, themeResId);
    } else {
        mContext = context;
    }
    //獲取windowManager服務
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    //建立新的Window
    final Window w = new PhoneWindow(mContext);
    mWindow = w;
    //設定callback
    w.setCallback(this);
    w.setOnWindowDismissedCallback(this);
    w.setWindowManager(mWindowManager, null, null);
    w.setGravity(Gravity.CENTER);
 
    mListenersHandler = new ListenersHandler(this);
}
複製程式碼

從Dialog的構造方法中可以看出,Dialog實質上是個Window,其顯示和隱藏也是藉助WindowManager去控制的

2、Dialog.show

public void show() {
    //如果之前已經show過後,就讓檢視顯示即可
    if (mShowing) {        
        if (mDecor != null) {            
            if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {                
                mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);            
            }            
            mDecor.setVisibility(View.VISIBLE);        
        }        
        return;   
    }    
    mCanceled = false;    
    
    //如果沒有create則會呼叫dispatchOncreate,該方法最終會呼叫dialog的onCreate方法
    if (!mCreated) {
        dispatchOnCreate(null);
    }
 
    //dialog的onstart回撥
    onStart();
    //獲取decorView
    mDecor = mWindow.getDecorView();
 
    //如果需要ActionBar,則建立出來
    if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
        final ApplicationInfo info = mContext.getApplicationInfo();
        mWindow.setDefaultIcon(info.icon);
        mWindow.setDefaultLogo(info.logo);
        mActionBar = new WindowDecorActionBar(this);
    }
 
    //window引數的設定
    WindowManager.LayoutParams l = mWindow.getAttributes();
    if ((l.softInputMode
            & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
        WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
        nl.copyFrom(l);
        nl.softInputMode |=
                WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
        l = nl;
    }
 
    try {
        //windowManager將decorView加入檢視
        mWindowManager.addView(mDecor, l);
        mShowing = true;
 
        sendShowMessage();
    } finally {
    }
}
複製程式碼

show()其實就是走Dialog的生命週期,然後做初始化工作,獲取Window上的DecorView後,將DecorView新增到檢視上,這裡需要注意的是在show()之後才執行onCreate()

3、Dialog.dispatchOnCreate

void dispatchOnCreate(Bundle savedInstanceState) {
    if (!mCreated) {
        onCreate(savedInstanceState); //回撥onCreate()
        mCreated = true;
    }
}

protected void onCreate(Bundle savedInstanceState) {
    //由開發者實現
}
複製程式碼

Dialog的初始化其實就是讓使用者去初始化自己的檢視,平時我們是這麼寫的

public class RxDialog extends Dialog {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //設定檢視
        setContentView(mView);
    }
}
複製程式碼

具體的邏輯還是回到**setContentView()**設定Dialog的檢視

4、Dialog.setContentView

public void setContentView(View view) {
    //呼叫mWindow進行檢視設定,mWindow實際上就是構造方法中的PhoneWindow
    mWindow.setContentView(view);
}
複製程式碼

mWindow則是在構造方法建立的PhoneWindow

5、PhoneWindow.setContentView

@Override
public void setContentView(View view) {
    //預設MATCH_PARENT
    setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}

@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
    if (mContentParent == null) {  
        installDecor();  //建立應用程式視窗檢視物件
    } else {  
        mContentParent.removeAllViews();  //重新設定應用程式視窗的檢視
    }  
 
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        view.setLayoutParams(params);
        final Scene newScene = new Scene(mContentParent, view);
        transitionTo(newScene);
    } else {
        mContentParent.addView(view, params); //將我們傳遞進來的view新增布局上
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    
    ......
}
複製程式碼

PhoneWindow.setContentView()不僅在Dialog中存在,在Activity的setContentView也是走到這裡。mContentParent指的是依附於DecorView上的R.id.content中的view。到這裡只是將Dialog設定的View載入到PhoneWindow的ContentView上,其實更主要的還是PhoneWindow新增到我們的手機螢幕上,程式碼回溯到show()mWindowManager.addView(mDecor, l)

6、WindowManagerImpl.addView

WindowManager本質上是對View進行管理,但是WindowManager顯然依然是個介面,其具體實現是WindowManagerImpl,最後還是委託給WindowManagerGlobal例項mGlobal處理

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    //委託給mGlobal來進行實現
    mGlobal.addView(view, params, mDisplay, mParentWindow);
}
複製程式碼

7、WindowManagerGlobal.addView

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    
    ......
    
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    if (parentWindow != null) {
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    } else {
        // If there's no parent, then hardware acceleration for this view is
        // set from the application's hardware acceleration setting.
        final Context context = view.getContext();
        if (context != null
                && (context.getApplicationInfo().flags
                        & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
            wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
        }
    }
 
    ViewRootImpl root;
    View panelParentView = null;
 
    synchronized (mLock) {
        // Start watching for system property changes.
        if (mSystemPropertyUpdater == null) {
            mSystemPropertyUpdater = new Runnable() {
                @Override public void run() {
                    synchronized (mLock) {
                        for (int i = mRoots.size() - 1; i >= 0; --i) {
                            mRoots.get(i).loadSystemProperties();
                        }
                    }
                }
            };
            SystemProperties.addChangeCallback(mSystemPropertyUpdater);
        }
 
        int index = findViewLocked(view, false);
        if (index >= 0) {
            if (mDyingViews.contains(view)) {
                // Don't wait for MSG_DIE to make it's way through root's queue.
                mRoots.get(index).doDie();
            } else {
                throw new IllegalStateException("View " + view
                        + " has already been added to the window manager.");
            }
            // The previous removeView() had not completed executing. Now it has.
        }
 
        // If this is a panel window, then find the window it is being
        // attached to for future reference.
        if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            final int count = mViews.size();
            for (int i = 0; i < count; i++) {
                if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                    panelParentView = mViews.get(i);
                }
            }
        }
 
        //建立ViewRootImpl,ViewRootImpl是view和window中的連線紐帶
        root = new ViewRootImpl(view.getContext(), display);
 
        view.setLayoutParams(wparams);
        
        //儲存相關View的資訊,會在Remove的時候移除,相當於快取
        mViews.add(view);//mViews:儲存的是所有Window對應的View,本質是個List
        mRoots.add(root);//mRoots:儲存的是所有Window所對應的ViewRootImpl,本質是個List
        mParams.add(wparams);//mParams:儲存所有window對應的佈局引數,本質是個List
    }
 
    // do this last because it fires off messages to start doing things
    try {
        //最終由root去實現最終的檢視顯示
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }
}
複製程式碼

將檢視新增到視窗上的工作交給root.setView(),root就是ViewRootImpl

8、ViewRootImpl.setView

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            ......
            requestLayout(); //真正完成檢視的非同步重新整理請求
            
            ......
            //這裡呼叫了mWindowSession的addToDisplay方法,在WindowManagerService層通過IPC機制完成真正的window新增
            res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                    getHostVisibility(), mDisplay.getDisplayId(),
                    mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                    mAttachInfo.mOutsets, mInputChannel);
           
        }
    }
}

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals(); //真正的去走Measure、Layout、Draw
    }
}
複製程式碼

在新增view之前,會走requestLayout(),真正實現View繪製的三部曲Measure、Layout、Draw。mWindowSession型別是IWindowSession,它是個Binder物件,真正的實現類是Session,window的新增過程實際上是一次ipc的呼叫,最後在WindowManagerService層通過IPC機制去實現的

總結

在這讀完這裡原始碼後,我們知道Window是個相對虛擬的物件,真正的操作是對Window中的DecorView進行addView()操作,而且在addView()之前,會先走onCreate()、onStart()、setContentView()操作,而在setContentView()過程中,會經過ViewRootImpl物件進行setView,並且在ViewRootImpl物件中會實現View繪製的三步曲,Measure、Layout、Draw操作,最後再將繪製好的view通過IWindowSession的ipc呼叫新增到介面上

  1. Dialog本質上是個Window,具體是通過Window的DecorView進行顯示的
  2. Dialog是在show()之後走的onCreate()、onStart()、setContentView()等回撥
    Android彈窗元件工作機制之Dialog、DialogFragment(一)

相關文章