Android Window機(jī)制

Window的簡(jiǎn)單使用

public void addView(View view){
        mFloatingButton = new Button(this);
        mFloatingButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()){
                    case MotionEvent.ACTION_MOVE:
                        mWindowParams.x = (int) event.getRawX();
                        mWindowParams.y = (int) event.getRawY();
                        //window更新操作
                        mWindowManager.updateViewLayout(mFloatingButton,mWindowParams);
                        break;
                        default:
                            break;
                }
                return true;
            }
        });
        mFloatingButton.setText("bottom");
        mWindowParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT
        ,WindowManager.LayoutParams.WRAP_CONTENT,0,0, PixelFormat.TRANSPARENT);
        mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
        | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
        mWindowParams.gravity = Gravity.LEFT | Gravity.TOP;
        mWindowParams.x = 100;
        mWindowParams.y = 300;
        //8.0以上使用TYPE_APPLICATION_OVERLAY  8.0以下使用Error
        mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY ;
        //新增window操作
        mWindowManager.addView(mFloatingButton,mWindowParams);
    }
  //移出window操作
   public void removeView(View view){
        if (null != mWindowManager){
            mWindowManager.removeView(mFloatingButton);
        }
    }

應(yīng)用Window層級(jí)分為1-99 除呵,子window層級(jí)范圍是1000-1999,系統(tǒng)window層級(jí)范圍是2000-2999.
通過一下代碼設(shè)置:

  mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY ;

需要配置系統(tǒng)彈窗權(quán)限:

 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

Window內(nèi)部機(jī)制

addView方法

通過下面代碼獲取到WindowManager對(duì)象隅肥。

WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);

WindowManager是接口類,繼承ViewManager接口竿奏。提供addView updateViewLayout removeView方法。實(shí)現(xiàn)類為WindowManagerImpl腥放。

mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);

所以我們看一下WindowManagerImpl的相關(guān)操作方法:

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

@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
   applyDefaultToken(params);
   mGlobal.updateViewLayout(view, params);
}

@Override
public void removeView(View view) {
   mGlobal.removeView(view, false);
}

可以發(fā)現(xiàn)WindowManagerImpl并沒有直接實(shí)現(xiàn)Window的相關(guān)操作方法泛啸,而是通過WindowManagerGlobal類來處理的。WindowManagerrGlobaladdView主要有以下幾個(gè)操作:

1.檢測(cè)參數(shù)是否合法秃症,如果是子Window則需要調(diào)整布局參數(shù)候址,不是則針對(duì)是否硬件加速做處理
if (view == null) {
    throw new IllegalArgumentException("view must not be null");
   }
if (display == null) {
   throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
   throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
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;
     }
}
2.觀測(cè)系統(tǒng)屬性變化
// 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);
}
3.判斷添加的view是否是之前刪除的view,如果是立即調(diào)用dodie方法刪除
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.
    }
4.創(chuàng)建ViewRootImpl并將View添加到列表中

在WindowManagerGlobal中有幾個(gè)重要的列表:

private final ArrayList<View> mViews = new ArrayList<View>();

private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();

private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingViews = new ArraySet<View>();

其中mViews存儲(chǔ)的是所以Window對(duì)應(yīng)的view种柑,mRoots存儲(chǔ)的是Window對(duì)應(yīng)的ViewRootImpl岗仑,mParams存儲(chǔ)的是Window對(duì)應(yīng)的布局參數(shù),mDyingViews存儲(chǔ)的是正在被刪除的View對(duì)象聚请,也就是那些調(diào)用了removeView方法但是刪除操作還沒完成的Window對(duì)象荠雕。在addView方法中將Window一系列對(duì)象存儲(chǔ)到列表中:

root = new ViewRootImpl(view.getContext(), display);

view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
5.通過ViewRootImpl更新界面并完成Window的添加過程:
root.setView(view, wparams, panelParentView);

通過ViewRootImpl.setView方法來完成。在setView內(nèi)部通過requestLayout來完成異步刷新請(qǐng)求驶赏。下面的代碼中scheduleTraversals實(shí)際上是view繪制的入口:

public void requestLayout() {
  f (!mHandlingLayoutInLayoutRequest) {
       checkThread();
       mLayoutRequested = true;
       scheduleTraversals();
    }
}

然后通過WindowSession完成Window的添加過程炸卑。

mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
         getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
         mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
         mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
setFrame(mTmpFrame);

上面的代碼中,mWindowSession類型是IWindowSession煤傍,它是一個(gè)binder對(duì)象盖文,實(shí)現(xiàn)類是Session,也就是說Window添加過程其實(shí)是一次IPC的過程蚯姆。
Session內(nèi)部通過WindowManagerService來實(shí)現(xiàn)Window的添加代碼如下:

@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
            Rect outStableInsets, Rect outOutsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
                outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel,
                outInsetsState);
    }

這樣一來五续,Window添加請(qǐng)求就交給了WindowManagerService處理了,至于WindowManagerService是如何處理的龄恋,本文先不做分析疙驾,有興趣的朋友可以自行查看WindowManagerService源碼,本文主要是分析Window添加的整體流程篙挽。

Window刪除過程:

Window刪除和Window添加一樣都是通過WindowManagerImpl調(diào)用WindowManagerGlobal來實(shí)現(xiàn)的:

public void removeView(View view, boolean immediate) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            View curView = mRoots.get(index).getView();
            removeViewLocked(index, immediate);
            if (curView == view) {
                return;
            }

            throw new IllegalStateException("Calling with view " + view
                    + " but the ViewAncestor is attached to " + curView);
        }
    }

代碼邏輯很簡(jiǎn)單荆萤,從mViews數(shù)組中找到View的索引,然后調(diào)用removeViewLocked進(jìn)行刪除:

private void removeViewLocked(int index, boolean immediate) {
        ViewRootImpl root = mRoots.get(index);
        View view = root.getView();

        if (view != null) {
            InputMethodManager imm = view.getContext().getSystemService(InputMethodManager.class);
            if (imm != null) {
                imm.windowDismissed(mViews.get(index).getWindowToken());
            }
        }
        boolean deferred = root.die(immediate);
        if (view != null) {
            view.assignParent(null);
            if (deferred) {
                mDyingViews.add(view);
            }
        }
    }

removeViewLocked這邊的邏輯也非常明確,直接調(diào)用ViewRootImpl的die方法:

boolean die(boolean immediate) {
        // Make sure we do execute immediately if we are in the middle of a traversal or the damage
        // done by dispatchDetachedFromWindow will cause havoc on return.
        if (immediate && !mIsInTraversal) {
            doDie();
            return false;
        }

        if (!mIsDrawing) {
            destroyHardwareRenderer();
        } else {
            Log.e(mTag, "Attempting to destroy the window while drawing!\n" +
                    "  window=" + this + ", title=" + mWindowAttributes.getTitle());
        }
        mHandler.sendEmptyMessage(MSG_DIE);
        return true;
    }

die方法中針對(duì)immediate字段進(jìn)行同步異步處理铣卡,immediate為false链韭,說明異步刪除,發(fā)送消息MSG_DIE煮落,ViewRootImpl內(nèi)部Handler接受到消息后調(diào)用dodie方法敞峭,同步則直接調(diào)用dodie方法。其余并無差別蝉仇。

    void doDie() {
        checkThread();
        if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
        synchronized (this) {
            if (mRemoved) {
                return;
            }
            mRemoved = true;
            if (mAdded) {
                dispatchDetachedFromWindow();
            }
            ...
            //destroy相關(guān)回收操作

            mAdded = false;
        }
        WindowManagerGlobal.getInstance().doRemoveView(this);
    }

doDie方法中調(diào)用了dispatchDetachedFromWindow方法旋讹,然后進(jìn)行一些資源回收操作,最后刪除WindowManagerGlobal中列表相關(guān)的數(shù)據(jù)轿衔。下面看一下dispatchDetachedFromWindow方法:

void dispatchDetachedFromWindow() {
        mFirstInputStage.onDetachedFromWindow();
        if (mView != null && mView.mAttachInfo != null) {
            mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
            //調(diào)用view的方法
            mView.dispatchDetachedFromWindow();
        }
        //...資源回收
        try {
            mWindowSession.remove(mWindow);
        } catch (RemoteException e) {
        }
        unscheduleTraversals();
    }

在dispatchDetachedFromWindow中主要有一些操作:

  • 1.調(diào)用View的dispatchDetachedFromWindow方法沉迹,這個(gè)方法內(nèi)部會(huì)調(diào)用View的onDetachedFromWindow等方法,而onDetachedFromWindow大家肯定不會(huì)陌生害驹,當(dāng)view從window中移出是都會(huì)回調(diào)鞭呕,里面通常做一些資源回收,動(dòng)效回收的操作
  • 2.資源回收相關(guān)操作
  • 3.通過IPC調(diào)用Session的remove方法刪除數(shù)據(jù)宛官。

Window的更新過程:

與上面兩個(gè)方法一樣更新的方法也是通過WindowManagerImpl調(diào)用WindowManagerGlobal來實(shí)現(xiàn)的:

    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;

        view.setLayoutParams(wparams);

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            ViewRootImpl root = mRoots.get(index);
            mParams.remove(index);
            mParams.add(index, wparams);
            root.setLayoutParams(wparams, false);
        }
    }

WindowManagerGlobal的updateViewLayout方法會(huì)更新view的LayoutParams葫松,然后替換列表中的LayoutParams,然后更新ViewRootImpl中的LayoutParams底洗。這一步操作是通過ViewRootImpl.setLayoutParams方法來完成的腋么。setLayoutParams方法會(huì)調(diào)用ViewRootImpl的scheduleTraversals方法來對(duì)view進(jìn)行重新布局,包括測(cè)量布局和重繪亥揖。同時(shí)ViewRootImpl會(huì)通過Session來更新視圖珊擂,這個(gè)操作也是一個(gè)IPC操作,最終的調(diào)用是WindowManagerServicerelayoutWindow方法费变。
到這里Window的三大操作整體流程就介紹完了摧扇。

Window創(chuàng)建過程:

通過上面的分析,View是Android中視圖的呈現(xiàn)方式胡控,但是View不能單獨(dú)存在扳剿,它必須附著在Window這個(gè)抽象的概率上面,因此有視圖的地方就有Window昼激。那么那些地方有視圖呢庇绽?
Android中可以提供視圖的地方有Activity、Dialog橙困、Toast和其他一些依托window實(shí)現(xiàn)的視圖比如PopupWindow瞧掺、菜單等。下面我們分析一下這些視圖元素的簡(jiǎn)單調(diào)用:

Activity的Window創(chuàng)建過程:

Activity的setContentView方法:

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

getWindow返回mWindow成員變量

public Window getWindow() {
   return mWindow;
}

這邊就需要引入Activity啟動(dòng)流程相關(guān)的知識(shí)凡傅,在Activity啟動(dòng)中會(huì)調(diào)用performLaunchActivity方法通過類加載的方式創(chuàng)建Activity實(shí)例辟狈,然后調(diào)用Activity.attach方法:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

        ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }
        ...

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

        return activity;
    }

mWindow變量在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, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        if (info.uiOptions != 0) {
            mWindow.setUiOptions(info.uiOptions);
        }
        mUiThread = Thread.currentThread();

        mMainThread = aThread;
        mInstrumentation = instr;
        mToken = token;
        mAssistToken = assistToken;
        mIdent = ident;
        mApplication = application;
        mIntent = intent;
        mReferrer = referrer;
        mComponent = intent.getComponent();
        mActivityInfo = info;
        mTitle = title;
        mParent = parent;
        mEmbeddedID = id;
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
        if (voiceInteractor != null) {
            if (lastNonConfigurationInstances != null) {
                mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
            } else {
                mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
                        Looper.myLooper());
            }
        }

        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;

        mWindow.setColorMode(info.colorMode);

        setAutofillOptions(application.getAutofillOptions());
        setContentCaptureOptions(application.getContentCaptureOptions());
    }

在attach方法中,創(chuàng)建Activity所屬的window對(duì)象,并設(shè)置回調(diào)接口。Activity實(shí)現(xiàn)了Window的callback接口哼转,因此當(dāng)Window接受到外界的狀態(tài)改變時(shí)會(huì)回調(diào)Activity的方法明未,mWindow變量是PhoneWindow類型,所以我們來看一下PhoneWindow.setContentView方法:

public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            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 {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }
setContentView大致有如下步驟:
1.如果mContentParent為空壹蔓,通過installDecor趟妥,完成Decorview的裝載工作。其中mContentParent是Decorview承載的View佣蓉。

創(chuàng)建的方法在installDecor方法中:

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);

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

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

            //標(biāo)題相關(guān)UI處理

            if (mDecor.getBackground() == null && mBackgroundFallbackDrawable != null) {
                mDecor.setBackgroundFallback(mBackgroundFallbackDrawable);
            }

            // Only inflate or create a new TransitionManager if the caller hasn't
            // already set a custom one.
            ....動(dòng)畫相關(guān)...
        }
    }

installDecor中判斷Decorview是否為空披摄,為空則調(diào)用generateDecor(-1)創(chuàng)建。然后判斷mContentParent是否為空勇凭,為空則generateLayout(mDecor)創(chuàng)建疚膊。

protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use
        // the context we have. Otherwise we want the application context, so we don't cling to the
        // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
    }

generateDecor方法總只有主活動(dòng)界面使用ApplicationContext,其余界面使用上下文context虾标。然后返回DecorView實(shí)例寓盗。

protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.

        //window主題樣式設(shè)置并設(shè)置布局

        mDecor.startChanging();
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        ...

        mDecor.finishChanging();

        return contentParent;
    }

generateLayout中針對(duì)styleable設(shè)置不同的flags,然后依據(jù)flags設(shè)置不同的layout布局夺巩。最后通過DecorView的onResourcesLoaded方法完成布局渲染贞让。然后返回com.android.internal.R.id.content的view,該View是窗體的內(nèi)容view柳譬。這時(shí)候mContentParent就被設(shè)置成這個(gè)view了喳张。

2.將View添加到PhoneWindow的mContentParent中,mContentParent是DecorView布局中的id為com.android.internal.R.id.content的View

回到PhoneWindow的setContentView方法中:

public void setContentView(int layoutResID) {
        ...上述初始化view方法...

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

setContentView后續(xù)代碼中美澳,判斷是否有動(dòng)畫销部,有則完成動(dòng)畫,然后在Scene中將layoutResID對(duì)應(yīng)的布局添加到mContentParent布局中制跟。沒有則直接通過下面代碼完成布局添加的操作舅桩。

mLayoutInflater.inflate(layoutResID, mContentParent);
3.回調(diào)Activity的onContentChanged方法通知Activity視圖完成改變。

Activity實(shí)現(xiàn)了Window.Callback接口雨膨,并在ActivityThread對(duì)Window完成綁定擂涛。此時(shí)的回調(diào)是告知Activity的布局文件已經(jīng)被添加到DecorView中了,需要Activity做相應(yīng)的處理聊记。這是一個(gè)空方法撒妈,需要Activity自己處理這個(gè)回調(diào)。

經(jīng)過上面的步驟排监,DecorView已經(jīng)完成初始化狰右,Activity的布局也被添加到DecorView中對(duì)應(yīng)content的View中,但是這時(shí)候DecorView還沒有被WindowManager正式添加到Window中舆床。
Window更多的表示的是一種抽象的功能集合棋蚌,雖然在Activity的attach方法中就已經(jīng)被創(chuàng)建了嫁佳,但是因?yàn)闆]有被WindowManager識(shí)別,所以這個(gè)Window無法提供具體的功能谷暮,因?yàn)樗鼰o法接受到外界的輸入信息蒿往。在ActivityThreadhandleResumeActivity方法中,首先會(huì)調(diào)用ActivityonResume方法,然后會(huì)調(diào)用r.activity.makeVisible(),makeVisible方法控制這DecorView正在完成添加與顯示的過程坷备,到這邊Activity才會(huì)被用戶看到:

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

到這邊Activity的分析已經(jīng)完成了熄浓,下面我們做一下總結(jié):


ActivitysetContentView.png

Dialog的創(chuàng)建過程

應(yīng)用層代碼
 AlertDialog.Builder builder = new AlertDialog.Builder(this);
 builder.setView(view);
 builder.setNegativeButton();
 builder.setPositiveButton();
 builder.setTitle()
 AlertDialog alertDialog = builder.create();
 alertDialog.show();

屬性設(shè)置到AlertDialog.Builder中情臭,builder.create時(shí)通過P.apply(dialog.mAlert)方法配置到AlertController上省撑。調(diào)用alertDialog.show()方法:

 public AlertDialog create() {
    // Context has already been wrapped with the appropriate theme.
    final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);
    P.apply(dialog.mAlert);
    ...
    return dialog;
 }

 public void 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;

     if (!mCreated) {
         dispatchOnCreate(null);
     } else {
         // Fill the DecorView in on any configuration changes that
         // may have occured while it was removed from the WindowManager.
         final Configuration config = mContext.getResources().getConfiguration();
         mWindow.getDecorView().dispatchConfigurationChanged(config);
     }

     ...
     mWindowManager.addView(mDecor, l);
     ...
     }

在show方法中判斷是否創(chuàng)建,如果未創(chuàng)建的話調(diào)用dispatchOnCreate方法:

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

在這個(gè)方法中判斷未創(chuàng)建調(diào)用onCreate方法俯在,onCreate是個(gè)空方法竟秫,供子類調(diào)用,也就是AlertDialog跷乐,因此我們來看一下AlertDialog.onCreate:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mAlert.installContent();
}

在此方法中調(diào)用AlertController的installContent方法肥败,在此方法中完成Dialog內(nèi)部view的設(shè)置:

public void installContent() {
    int contentView = selectContentView();
    mWindow.setContentView(contentView);
    setupView();
}

可以看到最終還是通過mWindow.setContentView來設(shè)置的。在Dialog的構(gòu)造方法中mWindow完成初始化:

Dialog構(gòu)造方法:

  Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
            if (themeResId == Resources.ID_NULL) {
                final TypedValue outValue = new TypedValue();
                context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
                themeResId = outValue.resourceId;
            }
            mContext = new ContextThemeWrapper(context, themeResId);
        } else {
            mContext = context;
        }

        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setOnWindowSwipeDismissedCallback(() -> {
            if (mCancelable) {
                cancel();
            }
        });
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);

        mListenersHandler = new ListenersHandler(this);
    }

和Activity一樣Window是PhoneWindow類型愕提。所以Dialog也是通過PhoneWindow來設(shè)置布局的馒稍。在show方法的后續(xù)代碼中完成Window在WinddowManager的注冊(cè):

mWindowManager.addView(mDecor, l);

移出代碼則是在dialog消失的時(shí)候調(diào)用下面代碼完成:

mWindowManager.removeViewImmediate(mDecor);

Dialog與Activity展示基本上一直,只不過有個(gè)特別需要注意的就是普通的Dialog必須采用Activity的context浅侨,如果采用Application的context則報(bào)錯(cuò)纽谒。

Caused by: android.view.WindowManager$BadTokenException: 
Unable to add window -- token null is not valid; is your activity running?

這個(gè)問題是沒有應(yīng)用token導(dǎo)致的,而應(yīng)用token一般只有Activity有如输,所以這里需要使用Activity的context鼓黔。我們知道系統(tǒng)dialog是不需要token的,因此可以設(shè)置dialog為系統(tǒng)彈窗不见,這樣就可以正常彈出了澳化。之前講過WindowManager.LayoutParams中的type表示window的類型,而系統(tǒng)window層級(jí)范圍是2000-2999.因此我們需要將type改成系統(tǒng)層級(jí)稳吮,本例采用TYPE_APPLICATION_OVERLAY來指定對(duì)話框window類型為系統(tǒng)window缎谷。
當(dāng)然要注意權(quán)限:

 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

Toast Window機(jī)制

Toast使用
Toast.makeText(this,"sss",Toast.LENGTH_LONG).show();

參數(shù)有context、提示內(nèi)容灶似、和顯示時(shí)長(zhǎng)列林。
Toast.LENGTH_LONG展示時(shí)間長(zhǎng) 3.5s
Toast.LENGTH_SHORT展示時(shí)間短 2s

Toast Window創(chuàng)建過程

Toast與dialog不同,它工作過程相對(duì)比較復(fù)雜喻奥。Toast是基于Window實(shí)現(xiàn)的但是因?yàn)門oast具有定時(shí)取消功能席纽,所以采用了Handler。在Toast內(nèi)部有兩種IPC過程撞蚕,第一種是Toast訪問NotificationManagerService润梯,第二種是NotificationManagerService回調(diào)Toast里的TN接口。
Toast屬性系統(tǒng)Window,內(nèi)部View有兩種方式指定纺铭,一種是系統(tǒng)自帶寇钉,另一種是通過setView方式指定一個(gè)自定義View。但是不管如何它們都對(duì)應(yīng)一個(gè)屬性mNextView舶赔。
Toast提供showcancel方法來控制Toast展示和隱藏扫倡。這兩種操作內(nèi)部都是一個(gè)IPC過程:

public void show() {
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }

    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
    final int displayId = mContext.getDisplayId();

    try {
        service.enqueueToast(pkg, tn, mDuration, displayId);
    } catch (RemoteException e) {
        // Empty
    }
}

通過IPC機(jī)制,調(diào)用NotificationManagerService的enqueueToast方法竟纳,參數(shù)為包名撵溃、TN、時(shí)長(zhǎng)和屏幕ID锥累。其中TN是遠(yuǎn)程Binder對(duì)象缘挑,在NotificationManagerService中控制Window展示與隱藏方法觸發(fā)后會(huì)通過TN再次觸發(fā)IPC操作,回調(diào)TN本地的展示隱藏方法桶略,從而控制Window的添加與隱藏语淘。下面我們來分析下enqueueToast方法:

public void enqueueToast(String pkg, ITransientNotification callback, int duration,
                int displayId)
    {
        ...參數(shù)判斷..

        synchronized (mToastQueue) {
            int callingPid = Binder.getCallingPid();
            long callingId = Binder.clearCallingIdentity();
            try {
                ToastRecord record;
                int index = indexOfToastLocked(pkg, callback);
                // If it's already in the queue, we update it in place, we don't
                // move it to the end of the queue.
                if (index >= 0) {
                    record = mToastQueue.get(index);
                    record.update(duration);
                } else {
                    // Limit the number of toasts that any given package except the android
                    // package can enqueue.  Prevents DOS attacks and deals with leaks.
                    if (!isSystemToast) {
                        int count = 0;
                        final int N = mToastQueue.size();
                        for (int i=0; i<N; i++) {
                             final ToastRecord r = mToastQueue.get(i);
                             if (r.pkg.equals(pkg)) {
                                 count++;
                                 if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                     Slog.e(TAG, "Package has already posted " + count
                                            + " toasts. Not showing more. Package=" + pkg);
                                     return;
                                 }
                             }
                        }
                    }

                    Binder token = new Binder();
                    mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, displayId);
                    record = new ToastRecord(callingPid, pkg, callback, duration, token,
                            displayId);
                    mToastQueue.add(record);
                    index = mToastQueue.size() - 1;
                    keepProcessAliveIfNeededLocked(callingPid);
                }
                // If it's at index 0, it's the current toast.  It doesn't matter if it's
                // new or just been updated.  Call back and tell it to show itself.
                // If the callback fails, this will remove it from the list, so don't
                // assume that it's valid after this.
                if (index == 0) {
                    showNextToastLocked();
                }
            } finally {
                Binder.restoreCallingIdentity(callingId);
            }
        }
    }
enqueueToast分為以下幾個(gè)步驟:
  • 1.參數(shù)判斷
  • 2.判斷消息是否已經(jīng)在Toast列表中,存在則更新际歼。不存在判斷是否達(dá)到上限沒有達(dá)到則加入Toast列表中
  • 3.showNextToastLocked展示Toast

我們接著看showNextToastLocked方法:

void showNextToastLocked() {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
        if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
        try {
            record.callback.show(record.token);
            scheduleDurationReachedLocked(record);
            return;
        } catch (RemoteException e) {
            Slog.w(TAG, "Object died trying to show notification " + record.callback
                    + " in package " + record.pkg);
            // remove it from the list and let the process die
            int index = mToastQueue.indexOf(record);
            if (index >= 0) {
                mToastQueue.remove(index);
            }
            keepProcessAliveIfNeededLocked(record.pid);
            if (mToastQueue.size() > 0) {
                record = mToastQueue.get(0);
            } else {
                record = null;
            }
        }
    }
}

showNextToastLocked方法獲取Toast列表中第一位展示惶翻。展示是通過ToastRecord的callback完成的,這個(gè)callback就是TN對(duì)象的遠(yuǎn)程Binder鹅心,通過跨進(jìn)程方式來完成TN方法的調(diào)用吕粗,最終被調(diào)用的TN方法運(yùn)行在發(fā)現(xiàn)Toast請(qǐng)求的引用的Toast的Binder線程池中。因此就需要Handler來將其切換到當(dāng)前線程中巴帮。所以意味著Toast不能在沒有Looper的線程中彈出溯泣。彈窗展示之后會(huì)設(shè)置一個(gè)延時(shí)取消的消息,其他的延時(shí)時(shí)長(zhǎng)取決于Toast展示時(shí)長(zhǎng)榕茧。

private void scheduleDurationReachedLocked(ToastRecord r)
  {
    mHandler.removeCallbacksAndMessages(r);
    Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
    int delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
    // Accessibility users may need longer timeout duration. This api compares original delay
    // with user's preference and return longer one. It returns original delay if there's no
    // preference.
    delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
            AccessibilityManager.FLAG_CONTENT_TEXT);
    mHandler.sendMessageDelayed(m, delay);
  }

在Handler中調(diào)用handleDurationReached方法處理延時(shí)消息:

public void handleMessage(Message msg)
    {
        switch (msg.what)
        {
            case MESSAGE_DURATION_REACHED:
                handleDurationReached((ToastRecord) msg.obj);
                break;
                ....
         }

    }

handleDurationReached判斷當(dāng)前Toast是否在列表中垃沦,如果是的話則調(diào)用cancelToastLocked方法:

private void handleDurationReached(ToastRecord record)
{
    if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
    synchronized (mToastQueue) {
        int index = indexOfToastLocked(record.pkg, record.callback);
        if (index >= 0) {
            cancelToastLocked(index);
        }
    }
}

在cancelToastLocked中會(huì)調(diào)用record.callback.hide方法,從而通過IPC機(jī)制調(diào)用TN中hide方法用押,同時(shí)將Toast從列表中刪除肢簿,然后判斷當(dāng)前列表是否為空,不過不為空的話調(diào)用showNextToastLocked方法重復(fù)之前的展示取消邏輯蜻拨。

void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        try {
            record.callback.hide();
        } catch (RemoteException e) {
            Slog.w(TAG, "Object died trying to hide notification " + record.callback
                    + " in package " + record.pkg);
            // don't worry about this, we're about to remove it from
            // the list anyway
        }

        ToastRecord lastToast = mToastQueue.remove(index);

        mWindowManagerInternal.removeWindowToken(lastToast.token, false /* removeWindows */,
                lastToast.displayId);
        // We passed 'false' for 'removeWindows' so that the client has time to stop
        // rendering (as hide above is a one-way message), otherwise we could crash
        // a client which was actively using a surface made from the token. However
        // we need to schedule a timeout to make sure the token is eventually killed
        // one way or another.
        scheduleKillTokenTimeout(lastToast);

        keepProcessAliveIfNeededLocked(record.pid);
        if (mToastQueue.size() > 0) {
            // Show the next one. If the callback fails, this will remove
            // it from the list, so don't assume that the list hasn't changed
            // after this point.
            showNextToastLocked();
        }
    }

在TN中show與hide方法池充,是通過Handler將線程切換到當(dāng)前線程:

public void show(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "SHOW: " + this);
    mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}

/**
 * schedule handleHide into the right thread
 */
@Override
public void hide() {
    if (localLOGV) Log.v(TAG, "HIDE: " + this);
    mHandler.obtainMessage(HIDE).sendToTarget();
}

在Handler中分別調(diào)用方法handleShow與handleHide處理Window的添加與移出工作:

public void handleShow(IBinder windowToken) {
    ...
    mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    ....
    try {
        mWM.addView(mView, mParams);
        trySendAccessibilityEvent();
    } catch (WindowManager.BadTokenException e) {
        /* ignore */
    }
}

public void handleHide() {
    if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
    if (mView != null) {
        // note: checking parent() just to make sure the view has
        // been added...  i have seen cases where we get here when
        // the view isn't yet added, so let's try not to crash.
        if (mView.getParent() != null) {
            if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
            mWM.removeViewImmediate(mView);
        }

        // Now that we've removed the view it's safe for the server to release
        // the resources.
        try {
            getService().finishToken(mPackageName, this);
        } catch (RemoteException e) {
        }

        mView = null;
    }
}

到這里Toast的工作機(jī)制分析完成了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末缎讼,一起剝皮案震驚了整個(gè)濱河市收夸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌血崭,老刑警劉巖卧惜,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厘灼,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡咽瓷,警方通過查閱死者的電腦和手機(jī)设凹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茅姜,“玉大人闪朱,你說我怎么就攤上這事∽耆鳎” “怎么了奋姿?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)航唆。 經(jīng)常有香客問我胀蛮,道長(zhǎng),這世上最難降的妖魔是什么糯钙? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮退腥,結(jié)果婚禮上任岸,老公的妹妹穿的比我還像新娘。我一直安慰自己狡刘,他們只是感情好享潜,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嗅蔬,像睡著了一般剑按。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上澜术,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天艺蝴,我揣著相機(jī)與錄音,去河邊找鬼鸟废。 笑死猜敢,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的盒延。 我是一名探鬼主播缩擂,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼添寺!你這毒婦竟也來了胯盯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤计露,失蹤者是張志新(化名)和其女友劉穎博脑,沒想到半個(gè)月后楞捂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡趋厉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年寨闹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片君账。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡繁堡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出乡数,到底是詐尸還是另有隱情椭蹄,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布净赴,位于F島的核電站绳矩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏玖翅。R本人自食惡果不足惜翼馆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望金度。 院中可真熱鬧应媚,春花似錦、人聲如沸猜极。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)跟伏。三九已至丢胚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間受扳,已是汗流浹背携龟。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留辞色,地道東北人骨宠。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像相满,于是被迫代替她去往敵國(guó)和親层亿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

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