Android窗口管理分析(3):窗口分組及Z-order的確定

在Android系統(tǒng)中,窗口是有分組概念的,例如,Activity中彈出的所有PopupWindow會(huì)隨著Activity的隱藏而隱藏肩榕,可以說(shuō)這些都附屬于Actvity的子窗口分組垮抗,對(duì)于Dialog也同樣如此氏捞,只不過(guò)Dialog與Activity屬于同一個(gè)分組。之間已經(jīng)簡(jiǎn)單介紹了窗口類型劃分:應(yīng)用窗口冒版、子窗口液茎、系統(tǒng)窗口,Activity與Dialog都屬于應(yīng)用窗口辞嗡,而PopupWindow屬于子窗口捆等,Toast、輸入法等屬于系統(tǒng)窗口续室。只有應(yīng)用窗口與系統(tǒng)窗口可以作為父窗口栋烤,子窗口不能作為子窗口的父窗口,也就說(shuō)Activity與Dialog或者系統(tǒng)窗口中可以彈出PopupWindow挺狰,但是PopupWindow不能在自己內(nèi)部彈出PopupWindow子窗口明郭。日常開(kāi)發(fā)中,一些常見(jiàn)的問(wèn)題都同窗口的分組有關(guān)系丰泊,比如為什么新建Dialog的時(shí)候必須要用Activity的Context薯定,而不能用Application的;為什么不能以PopupWindow的View為錨點(diǎn)彈出子PopupWindow趁耗?其實(shí)這里面就牽扯都Android的窗口組織管理形式沉唠,本文主要包含以下幾點(diǎn)內(nèi)容:

  • 窗口的分組管理 :應(yīng)用窗口組、子窗口組苛败、系統(tǒng)窗口組
  • Activity满葛、Dialg應(yīng)用窗口及PopWindow子窗口的添加原理跟注意事項(xiàng)
  • 窗口的Z次序管理:窗口的分配序號(hào)、次序調(diào)整等
  • WMS中窗口次序分配如何影響SurfaceFlinger服務(wù)

WMS窗口添加一文中分析過(guò),窗口的添加是通過(guò)WindowManagerGlobal.addView()來(lái)完成 函數(shù)原型如下

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

前三個(gè)參數(shù)是必不可少的罢屈,view嘀韧、params、display缠捌,其中display表示要輸出的顯示設(shè)備锄贷,先不考慮。view 就是APP要添加到WindowManagerGlobal管理的View曼月,而 params是WindowManager.LayoutParams谊却,主要用來(lái)描述窗口屬性,WindowManager.LayoutParams有兩個(gè)很重要的參數(shù)type與token哑芹,

public static class LayoutParams extends ViewGroup.LayoutParams
        implements Parcelable {
  ...
  public int type;
  ...
  public IBinder token = null;
  
  }

type用來(lái)描述窗口的類型炎辨,而token其實(shí)是標(biāo)志窗口的分組,token相同的窗口屬于同一分組聪姿,后面會(huì)知道這個(gè)token其實(shí)是WMS在APP端對(duì)應(yīng)的一個(gè)WindowToken的鍵值碴萧。這里先看一下type參數(shù)乙嘀,之前曾添加過(guò)Toast窗口,它的type值是TYPE_TOAST破喻,標(biāo)識(shí)是一個(gè)系統(tǒng)提示窗口虎谢,下面先簡(jiǎn)單看下三種窗口類型的Type對(duì)應(yīng)的值,首先看一下應(yīng)用窗口

窗口TYPE值 窗口類型
FIRST_APPLICATION_WINDOW = 1 開(kāi)始應(yīng)用程序窗口
TYPE_BASE_APPLICATION=1 所有程序窗口的base窗口曹质,其他應(yīng)用程序窗口都顯示在它上面
TYPE_APPLICATION =2 普通應(yīng)用程序窗口婴噩,token必須設(shè)置為Activity的token
TYPE_APPLICATION_STARTING =3 應(yīng)用程序啟動(dòng)時(shí)所顯示的窗口
LAST_APPLICATION_WINDOW = 99 結(jié)束應(yīng)用程序窗口

一般Activity都是TYPE_BASE_APPLICATION類型的,而TYPE_APPLICATION主要是用于Dialog咆繁,再看下子窗口類型

窗口TYPE值 窗口類型
FIRST_SUB_WINDOW = 1000 SubWindows子窗口讳推,子窗口的Z序和坐標(biāo)空間都依賴于他們的宿主窗口
TYPE_APPLICATION_PANEL =1000 面板窗口,顯示于宿主窗口的上層
TYPE_APPLICATION_MEDIA =1001 媒體窗口(例如視頻)玩般,顯示于宿主窗口下層
TYPE_APPLICATION_SUB_PANEL =1002 應(yīng)用程序窗口的子面板银觅,顯示于所有面板窗口的上層
TYPE_APPLICATION_ATTACHED_DIALOG = 1003 對(duì)話框,類似于面板窗口坏为,繪制類似于頂層窗口究驴,而不是宿主的子窗口
TYPE_APPLICATION_MEDIA_OVERLAY =1004 媒體信息,顯示在媒體層和程序窗口之間匀伏,需要實(shí)現(xiàn)半透明效果
LAST_SUB_WINDOW=1999 結(jié)束子窗口

最后看幾個(gè)系統(tǒng)窗口類型洒忧,

窗口TYPE值 窗口類型
FIRST_SYSTEM_WINDOW = 2000 系統(tǒng)窗口
TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW 狀態(tài)欄
TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3 系統(tǒng)提示,出現(xiàn)在應(yīng)用程序窗口之上
TYPE_TOAST = FIRST_SYSTEM_WINDOW+5 顯示Toast

了解窗口類型后够颠,我們需要面對(duì)的首要問(wèn)題是:窗口如何根據(jù)類型進(jìn)行分組歸類的熙侍?Dialog是如何確定附屬Activity,PopupWindow如何確定附屬父窗口履磨?蛉抓。

窗口的分組原理

如果用一句話概括窗口分組的話:Android窗口是以token來(lái)進(jìn)行分組的,同一組窗口握著相同的token剃诅,什么是token呢巷送?在 Android WMS管理框架中,token一個(gè)IBinder對(duì)象矛辕,IBinder在實(shí)體端與代理端會(huì)相互轉(zhuǎn)換笑跛,這里只看實(shí)體端,它的取值只有兩種:ViewRootImpl中ViewRootImpl.W聊品,或者是ActivityRecord中的IApplicationToken.Stub對(duì)象飞蹂,其中ViewRootImpl.W的實(shí)體對(duì)象在ViewRootImpl中實(shí)例化,而IApplicationToken.Stub在ActivityManagerService端實(shí)例化翻屈,之后被AMS添加到WMS服務(wù)中去陈哑,作為Activity應(yīng)用窗口的鍵值標(biāo)識(shí)。之前說(shuō)過(guò)Activity跟Dialog屬于同一分組,現(xiàn)在就來(lái)看一下Activity跟Dialog的token是如何復(fù)用的芥颈,這里的復(fù)用分為APP端及WMS服務(wù)端,關(guān)于窗口的添加流程之前已經(jīng)分析過(guò)赚抡,這里只跟隨窗口token來(lái)分析窗口的分組爬坑,我們知道在WMS端页徐,WindowState與窗口的一一對(duì)應(yīng)疲吸,而WindowToken與窗口分組曙求,這可以從兩者的定義看出如下:

class WindowToken {

    final WindowManagerService service;
    final IBinder token;
    final int windowType;
    final boolean explicit;
    <!--當(dāng)前窗口對(duì)應(yīng)appWindowToken蛉艾,是不是同Activity存在依附關(guān)系-->
    AppWindowToken appWindowToken;
    <!--關(guān)鍵點(diǎn)1 當(dāng)前WindowToken對(duì)應(yīng)的窗口列表-->
    final WindowList windows = new WindowList();
    ...
}

final class WindowState implements WindowManagerPolicy.WindowState {
    static final String TAG = "WindowState";

    final WindowManagerService mService;
    final WindowManagerPolicy mPolicy;
    final Context mContext;
    final Session mSession;
    <!--當(dāng)前WindowState對(duì)應(yīng)IWindow窗口代理-->
    final IWindow mClient;
    <!--當(dāng)前WindowState對(duì)應(yīng)的父窗口-->
    final WindowState mAttachedWindow;
    ...
    <!--當(dāng)前WindowState隸屬的token-->
    WindowToken mToken;
    WindowToken mRootToken;
    AppWindowToken mAppToken;
    AppWindowToken mTargetAppToken;
    ...
    }

可以看到WindowToken包含一個(gè) WindowList windows = new WindowList()醇坝,其實(shí)就是WindowState列表延蟹;而WindowState有一個(gè)WindowToken mToken窍仰,也就是WindowToken包含一個(gè)WindowState列表将硝,而每個(gè)WindowState附屬一個(gè)WindowToken窗口組岩四,示意圖如下:

WindowToken與WindowState關(guān)系.jpg

Activity對(duì)應(yīng)token及WindowToken(AppWindowToken)的添加

AMS在為Activity創(chuàng)建ActivityRecord的時(shí)候哭尝,會(huì)新建IApplicationToken.Stub appToken對(duì)象,在startActivity之前會(huì)首先向WMS服務(wù)登記當(dāng)前Activity的Token剖煌,隨后材鹦,通過(guò)Binder通信將IApplicationToken傳遞給APP端,在通知ActivityThread新建Activity對(duì)象之后耕姊,利用Activity的attach方法添加到Activity中桶唐,先看第一步AMS將Activity的token加入到WMS中,并且為Activity創(chuàng)建APPWindowToken茉兰。

<!--AMS ActivityStack.java中代碼 -->
 final void startActivityLocked(ActivityRecord r, boolean newTask,
            boolean doResume, boolean keepCurTransition, Bundle options) {
    ...<!--關(guān)鍵點(diǎn)1  添加Activity token到WMS-->
    mWindowManager.addAppToken(task.mActivities.indexOf(r), r.appToken,XXX);
   }
  @Override
    public void addAppToken(int addPos, IApplicationToken token, int taskId, int stackId,
            int requestedOrientation, boolean fullscreen, boolean showForAllUsers, int userId,
            int configChanges, boolean voiceInteraction, boolean launchTaskBehind) {
             synchronized(mWindowMap) {
             <!--新建AppWindowToken-->
            AppWindowToken atoken = findAppWindowToken(token.asBinder());
            atoken = new AppWindowToken(this, token, voiceInteraction);
            ...
            <!--將AppWindowToken以IApplicationToken.Stub為鍵值放如WMS的mTokenMap中-->
            mTokenMap.put(token.asBinder(), atoken);
            <!--開(kāi)始肯定是隱藏狀態(tài)尤泽,因?yàn)檫€沒(méi)有resume-->
            atoken.hidden = true;
            atoken.hiddenRequested = true;
        }
    }

也就是說(shuō)Activity分組的Token其實(shí)是早在Activity顯示之前就被AMS添加到WMS中去的,之后AMS才會(huì)通知App端去新建Activity规脸,并將Activity的Window添加到WMS中去坯约,接著看下APP端的流程:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    <!--關(guān)鍵點(diǎn)1 新建Activity-->
    Activity activity = null;
    try {
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
      ...
   try {
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);
        if (activity != null) {
        <!--關(guān)鍵點(diǎn)2 新建appContext-->
            Context appContext = createBaseContextForActivity(r, activity);
            CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
            Configuration config = new Configuration(mCompatConfiguration);
        <!--關(guān)鍵點(diǎn)3 attach到WMS-->
           activity.attach(appContext, this, getInstrumentation(), r.token,XXX);
        ...
      } 

關(guān)鍵點(diǎn)1燃辖,新建一個(gè)Activity鬼店,之后會(huì)為Activiyt創(chuàng)建一個(gè)appContext,這個(gè)Context主要是為了activity.attach使用的黔龟,其實(shí)就是單純new一個(gè)ContextImpl妇智,之后Activity會(huì)利用attach函數(shù)將ContextImpl綁定到自己身上。

static ContextImpl createActivityContext(ActivityThread mainThread,
        LoadedApk packageInfo, int displayId, Configuration overrideConfiguration) {
    return new ContextImpl(null, mainThread, packageInfo, null, null, false,
            null, overrideConfiguration, displayId);
}

 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) {
        <!--關(guān)鍵點(diǎn)1 為Activity綁定ContextImpl 因?yàn)锳ctivity只是一個(gè)ContextWraper-->
        attachBaseContext(context);
        mFragments.attachHost(null /*parent*/);
        <!--關(guān)鍵點(diǎn)2 new一個(gè)PhoneWindow 并設(shè)置回調(diào)-->
        mWindow = new PhoneWindow(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        ...
        <!--關(guān)鍵點(diǎn)3 Token的傳遞-->
        mToken = token;
        mIdent = ident;
        mApplication = application;
        ...
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        <!--將Window的WindowManager賦值給Activity-->
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;
    }

mWindow.setWindowManager并不是直接為Window設(shè)置WindowManagerImpl氏身,而是利用當(dāng)前的WindowManagerImpl重新為Window創(chuàng)建了一個(gè)WindowManagerImpl巍棱,并將自己設(shè)置此WindowManagerImpl的parentWindow:

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
        boolean hardwareAccelerated) {
    mAppToken = appToken;
    mAppName = appName;
    mHardwareAccelerated = hardwareAccelerated
            || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
    if (wm == null) {
        wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
    }
    mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

 public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
    return new WindowManagerImpl(mDisplay, parentWindow);
}

之后將Window的WindowManagerImpl傳遞給Activity,作為Activity的WindowManager將來(lái)Activity通過(guò)getSystemService獲取WindowManager服務(wù)的時(shí)候蛋欣,其實(shí)是直接返回了Window的WindowManagerImpl航徙,

@Override
public Object getSystemService(String name) {

    if (WINDOW_SERVICE.equals(name)) {
        return mWindowManager;
    } else if (SEARCH_SERVICE.equals(name)) {
        ensureSearchManager();
        return mSearchManager;
    }
    return super.getSystemService(name);
}

之后看一下關(guān)鍵點(diǎn)3,這里傳遞的token其實(shí)就是AMS端傳遞過(guò)來(lái)的IApplicationToken代理陷虎,一個(gè)IBinder對(duì)象到踏。之后利用ContextImpl的getSystemService()函數(shù)得到一個(gè)一個(gè)WindowManagerImpl對(duì)象杠袱,再通過(guò)setWindowManager為Activity創(chuàng)建自己的WindowManagerImpl。到這一步窝稿,Activity已經(jīng)準(zhǔn)備完畢楣富,剩下的就是在resume中通過(guò)addView將窗口添加到到WMS,具體實(shí)現(xiàn)在ActivityThread的handleResumeActivity函數(shù)中:

 final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
        ActivityClientRecord r = performResumeActivity(token, clearHide);
        
        if (r != null) {
            final Activity a = r.activity;
            ...
            if (r.window == null && !a.mFinished && willBeVisible) {
               <!--關(guān)鍵點(diǎn)1-->
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                <!--關(guān)鍵點(diǎn)2 獲取WindowManager-->
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
                <!--關(guān)鍵點(diǎn)3 添加到WMS管理-->
                    wm.addView(decor, l);
                }
             ...
             }   

關(guān)鍵點(diǎn)1是為了獲取Activit的Window及DecorView對(duì)象伴榔,如果用戶沒(méi)有通過(guò)setContentView方式新建DecorView纹蝴,這里會(huì)利用PhoneWindow的getDecorView()新建DecorView,

@Override
public final View getDecorView() {
    if (mDecor == null) {
        installDecor();
    }
    return mDecor;
}

之后通過(guò)Activity的getWindowManager()獲取WindowManagerImpl對(duì)象踪少,這里獲取的WindowManagerImpl其實(shí)是Activity自己的WindowManagerImpl塘安,

private WindowManagerImpl(Display display, Window parentWindow) {
    mDisplay = display;
    mParentWindow = parentWindow;
}

它的mParentWindow 是非空的,獲取WindowManagerImpl之后援奢,便利用 addView(decor, l)將DecorView對(duì)應(yīng)的窗口添加到WMS中去兼犯,最后調(diào)用的是

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

可以看到這里會(huì)傳遞mParentWindow給WindowManagerGlobal對(duì)象,作為調(diào)整WindowMangaer.LayoutParams 中token的依據(jù):

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    <!--調(diào)整wparams的token參數(shù)-->
    if (parentWindow != null) {
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    } 
     ViewRootImpl root;
     View panelParentView = null;
         ..
        <!--新建ViewRootImpl ,并利用wparams參數(shù)添加窗口-->
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
       ..
      <!--新建ViewRootImpl -->
      root.setView(view, wparams, panelParentView);
     }

parentWindow.adjustLayoutParamsForSubWindow是一個(gè)很關(guān)鍵的函數(shù)萝究,從名字就能看出免都,這是為了他調(diào)整子窗口的參數(shù):

   void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
        CharSequence curTitle = wp.getTitle();
        <!--如果是子窗口如何處理-->
        if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
            wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            <!--后面會(huì)看到,其實(shí)PopupWindow類的子窗口的wp.token是在上層顯示賦值的-->
            if (wp.token == null) {
                View decor = peekDecorView();
                if (decor != null) {
                    // 這里其實(shí)是父窗口的IWindow對(duì)象 Window只有Dialog跟Activity才有
                    wp.token = decor.getWindowToken();
                }
            }
             
        } else {
        <!--這里其實(shí)只對(duì)應(yīng)用窗口有用 Activity與Dialog都一樣-->
            if (wp.token == null) {
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
            }
        }
    }

對(duì)于Activity來(lái)說(shuō)帆竹,wp.token = mContainer == null ? mAppToken : mContainer.mAppToken绕娘,其實(shí)就是AMS端傳過(guò)來(lái)的IApplicationToken,之后在ViewRootImpl中setView的時(shí)候栽连,會(huì)利用IWindowSession代理與WMS端的Session通信险领,將窗口以及token信息傳遞到WMS端,其中IApplicationToken就是該Activity所處于的分組秒紧,在WMS端绢陌,會(huì)根據(jù)IApplicationToken IBinder鍵值,從全局的mTokenMap中找到對(duì)應(yīng)的AppWindowToken熔恢。既然說(shuō)分組脐湾,就應(yīng)該有其他的子元素,下面看一下Activity上彈出Dialog的流程叙淌,進(jìn)一步了解為什么Activity與它彈出的Dialog是統(tǒng)一分組(復(fù)用同一套token)秤掌。

Dialg分組及顯示原理:為什么Activity與Dialog算同一組?

在添加到WMS的時(shí)候鹰霍,Dialog的窗口屬性是WindowManager.LayoutParams.TYPE_APPLICATION闻鉴,同樣屬于應(yīng)用窗口,因此茂洒,必須使用Activity的AppToken才行孟岛,換句話說(shuō),必須使用Activity內(nèi)部的WindowManagerImpl進(jìn)行addView才可以。Dialog和Activity共享同一個(gè)WindowManager(也就是WindowManagerImpl)渠羞,而WindowManagerImpl里面有個(gè)Window類型的mParentWindow變量斤贰,這個(gè)變量在Activity的attach中創(chuàng)建WindowManagerImpl時(shí)傳入的為當(dāng)前Activity的Window,而Activity的Window里面的mAppToken值又為當(dāng)前Activity的token次询,所以Activity與Dialog共享了同一個(gè)mAppToken值腋舌,只是Dialog和Activity的Window對(duì)象不同,下面用代碼確認(rèn)一下:

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
<!--關(guān)鍵點(diǎn) 1 根據(jù)theme封裝context-->
    if (createContextThemeWrapper) {
        ...
        mContext = new ContextThemeWrapper(context, themeResId);
    } else {
        mContext = context;
    }
     <!--獲取mWindowManager-->
     
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    <!--創(chuàng)建PhoneWindow-->
    final Window w = new PhoneWindow(mContext);
    mWindow = w;
    w.setCallback(this);
    w.setOnWindowDismissedCallback(this);
    w.setWindowManager(mWindowManager, null, null);
    w.setGravity(Gravity.CENTER);
    mListenersHandler = new ListenersHandler(this);
}

以上代碼先根據(jù)Theme調(diào)整context,之后利用context.getSystemService(Context.WINDOW_SERVICE)渗蟹,這里Dialog是從Activity彈出來(lái)的,所以context是Activity赞辩,如果你設(shè)置Application雌芽,會(huì)有如下error,至于為什么辨嗽,后面分析會(huì)看到世落。

 android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
       at android.view.ViewRootImpl.setView(ViewRootImpl.java:563)
       at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:269)
       at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)

接著看Activity的getSystemService,上文分析過(guò)這種方法獲取的其實(shí)是Activity中PhoneWindow的WindowManagerImpl糟需,所以后面利用WindowManagerImpl addView的時(shí)候屉佳,走的流程與Activity一樣≈扪海看一下show的代碼:

public void show() {
    ...
    onStart();
    mDecor = mWindow.getDecorView();
    ...
    <!--關(guān)鍵點(diǎn) WindowManager.LayoutParams的獲取-->
    WindowManager.LayoutParams l = mWindow.getAttributes();
    ...
    try {
        mWindowManager.addView(mDecor, l);
        mShowing = true;
        sendShowMessage();
    } finally {
    }
}

Window在創(chuàng)建的時(shí)候武花,默認(rèn)新建WindowManager.LayoutParams mWindowAttributes

private final WindowManager.LayoutParams mWindowAttributes =
    new WindowManager.LayoutParams();

采用的是無(wú)參構(gòu)造方法,

    public LayoutParams() {
        super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        type = TYPE_APPLICATION;
        format = PixelFormat.OPAQUE;
    }

因此這里的type = TYPE_APPLICATION杈帐,也就是說(shuō)Dialog的窗口類型其實(shí)是應(yīng)用窗口体箕。因此在addView走到上文的adjustLayoutParamsForSubWindow的時(shí)候,仍然按照Activity的WindowManagerImpl addView的方式處理挑童,并利用Activity的PhoneWindow的 adjustLayoutParamsForSubWindow調(diào)整參數(shù)累铅,賦值給WindowManager.LayoutParams token的值仍然是Activity的IApplicationToken,同樣在WMS端站叼,對(duì)應(yīng)就是APPWindowToken娃兽,也就是Activity與Dialog屬于同一分組。

   void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
        CharSequence curTitle = wp.getTitle();
                <!--這里其實(shí)只對(duì)應(yīng)用窗口有用 Activity與Dialog都一樣-->
            if (wp.token == null) {
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
        }
    }

回到之前遺留的一個(gè)問(wèn)題尽楔,為什么Dialog用Application作為context不行呢投储?Dialog的窗口類型屬于應(yīng)用窗口,如果采用Application作為context翔试,那么通過(guò)context.getSystemService(Context.WINDOW_SERVICE)獲取的WindowManagerImpl就不是Activity的WindowManagerImpl轻要,而是Application,它同Activity的WindowManagerImpl的區(qū)別是沒(méi)有parentWindow垦缅,所以adjustLayoutParamsForSubWindow函數(shù)不會(huì)被調(diào)用冲泥,WindowManager.LayoutParams的token就不會(huì)被賦值,因此ViewRootImpl在通過(guò)setView向WMS在添加窗口的時(shí)候會(huì)失敗:

public int addWindow(Session session, IWindow client, XXX )
        ...
        <!--對(duì)于應(yīng)用窗口 token不可以為null-->
        WindowToken token = mTokenMap.get(attrs.token);
        if (token == null) {
            if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
                Slog.w(TAG, "Attempted to add application window with unknown token "
                      + attrs.token + ".  Aborting.");
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }

WMS會(huì)返回WindowManagerGlobal.ADD_BAD_APP_TOKEN的錯(cuò)誤給APP端凡恍,APP端ViewRootImpl端收到后會(huì)拋出如下異常

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
                   ....
                    case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                        throw new WindowManager.BadTokenException(
                                "Unable to add window -- token " + attrs.token
                                + " is not for an application");

以上就為什么不能用Application作為Dialog的context的理由(不能為Dialog提供正確的token)志秃,接下來(lái)看一下PopupWindow是如何處理分組的。

PopupWindow類子窗口的添加流程及WindowToken分組

PopupWindow是最典型的子窗口嚼酝,必須依附父窗口才能存在浮还,先看下PopupWindow一般用法:

     View root = LayoutInflater.from(AppProfile.getAppContext()).inflate(R.layout.pop_window, null);
    PopupWindow popupWindow = new PopupWindow(root, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, true);
    popupWindow.setBackgroundDrawable(new BitmapDrawable());
    popupWindow.showAsDropDown(archorView);

PopupWindow的構(gòu)造函數(shù)很普通,主要是一些默認(rèn)入場(chǎng)闽巩、出廠動(dòng)畫的設(shè)置钧舌,如果在新建PopupWindow的時(shí)候已經(jīng)將根View傳遞到構(gòu)造函數(shù)中去,PopupWindow的構(gòu)造函數(shù)會(huì)調(diào)用setContentView涎跨,如果在show之前洼冻,沒(méi)有調(diào)用setContentView,則拋出異常隅很。

public PopupWindow(View contentView, int width, int height, boolean focusable) {
    if (contentView != null) {
        mContext = contentView.getContext();
        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    }
    setContentView(contentView);
    setWidth(width);
    setHeight(height);
    setFocusable(focusable);
}

下面主要看PopupWindow的showAsDropDown函數(shù)

public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
    <!--關(guān)鍵點(diǎn)1  利用通過(guò)View錨點(diǎn)所在窗口顯性構(gòu)建PopupWindow的token-->
    final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
    <!--關(guān)鍵點(diǎn)2-->
    preparePopup(p);
    ...
    <!--關(guān)鍵點(diǎn)3-->
    invokePopup(p);
}

showAsDropDown有3個(gè)關(guān)鍵點(diǎn)撞牢,關(guān)鍵點(diǎn)1是生成WindowManager.LayoutParams參數(shù),WindowManager.LayoutParams參數(shù)里面的type叔营、token是非常重要參數(shù)屋彪,PopupWindow的type是TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW,是一個(gè)子窗口绒尊。關(guān)鍵點(diǎn)2是PopupDecorView的生成畜挥,這個(gè)View是PopupWindow的根ViewGroup,類似于Activity的DecorView婴谱,關(guān)鍵3利用WindowManagerService的代理砰嘁,將View添加到WMS窗口管理中去顯示,先看關(guān)鍵點(diǎn)1:

private WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
    final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
    p.gravity = computeGravity();
    p.flags = computeFlags(p.flags);
    p.type = mWindowLayoutType;
    <!--顯性賦值token-->
    p.token = token;
    p.softInputMode = mSoftInputMode;
    p.windowAnimations = computeAnimationResource();
    if (mBackground != null) {
        p.format = mBackground.getOpacity();
    } else {
        p.format = PixelFormat.TRANSLUCENT;
    }
    ..
    p.privateFlags = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH
            | PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;
    return p;
}

上面的Token其實(shí)用的是anchor.getWindowToken()勘究,如果是Activity中的View矮湘,其實(shí)用的Token就是Activity的ViewRootImpl中的IWindow對(duì)象,如果這個(gè)View是一個(gè)系統(tǒng)窗口中的View口糕,比如是Toast窗口中彈出來(lái)的缅阳,用的就是Toast ViewRootImpl的IWindow對(duì)象,歸根到底景描,PopupWindow自窗口中的Token是ViewRootImpl的IWindow對(duì)象十办,同Activity跟Dialog的token(IApplicationToken)不同,該Token標(biāo)識(shí)著PopupWindow在WMS所處的分組超棺,最后來(lái)看一下PopupWindow的顯示:

private void invokePopup(WindowManager.LayoutParams p) {
    if (mContext != null) {
        p.packageName = mContext.getPackageName();
    }
    final PopupDecorView decorView = mDecorView;
    decorView.setFitsSystemWindows(mLayoutInsetDecor);
    setLayoutDirectionFromAnchor();
    <!--關(guān)鍵點(diǎn)1-->
    mWindowManager.addView(decorView, p);
    if (mEnterTransition != null) {
        decorView.requestEnterTransition(mEnterTransition);
    }
}

主要是調(diào)用了WindowManager的addView添加視圖并顯示向族,這里首先需要關(guān)心一下mWindowManager,

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

這的context 可以是Activity棠绘,也可以是Application件相,因此WindowManagerImpl也可能不同再扭,不過(guò)這里并沒(méi)有多大關(guān)系,因?yàn)镻opupWindow的token是顯性賦值的夜矗,就是是就算用Application泛范,也不會(huì)有什么問(wèn)題,對(duì)于PopupWindow子窗口紊撕,關(guān)鍵點(diǎn)是View錨點(diǎn)決定其token罢荡,而不是WindowManagerImpl對(duì)象:

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

之后利用ViewRootImpl的setView函數(shù)的時(shí)候,WindowManager.LayoutParams里的token其實(shí)就是view錨點(diǎn)獲取的IWindow對(duì)象对扶,WindowManagerService在處理該請(qǐng)求的時(shí)候区赵,

public int addWindow(Session session, IWindow client, XXX ) {

      <!--關(guān)鍵點(diǎn)1,必須找到子窗口的父窗口浪南,否則添加失敗-->
       WindowState attachedWindow = null;
        if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
            attachedWindow = windowForClientLocked(null, attrs.token, false);
            if (attachedWindow == null) {
                return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
            }
        }
        <!--關(guān)鍵點(diǎn)2 如果Activity第一次添加子窗口 惧笛,子窗口分組對(duì)應(yīng)的WindowToken一定是null-->
        boolean addToken = false;
        WindowToken token = mTokenMap.get(attrs.token);
        AppWindowToken atoken = null;
        if (token == null) {
        ...
            token = new WindowToken(this, attrs.token, -1, false);
            addToken = true;
        }           
        <!--關(guān)鍵點(diǎn)2 新建窗口WindowState對(duì)象 注意這里的attachedWindow非空-->
       WindowState win = new WindowState(this, session, client, token,
                attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);
       ...
        <!--關(guān)鍵點(diǎn)4 添加更新全部map,-->
        if (addToken) {
            mTokenMap.put(attrs.token, token);
         }
        mWindowMap.put(client.asBinder(), win);
        }

從上面的分析可以看出逞泄,WMS會(huì)為PopupWindow窗口創(chuàng)建一個(gè)子窗口分組WindowToken,每個(gè)子窗口都會(huì)有一個(gè)指向父窗口的引用拜效,因?yàn)槭抢酶复翱诘腎Window作為鍵值喷众,父窗口可以很方便的利用自己的IWindow獲取WindowToken,進(jìn)而得到全部的子窗口紧憾,

關(guān)于系統(tǒng)窗口到千,前文層分析過(guò)Toast系統(tǒng)窗口,Toast類系統(tǒng)窗口在WMS端只有一個(gè)WindowToken赴穗,鍵值是null憔四,這個(gè)比較奇葩,不過(guò)還沒(méi)驗(yàn)證過(guò)般眉。

窗口的Z次序管理:窗口的分配序號(hào)了赵、次序調(diào)整等

雖然我們看到的手機(jī)屏幕只是一個(gè)二維平面X*Y,但其實(shí)Android系統(tǒng)是有隱形的Z坐標(biāo)軸的甸赃,其方向與手機(jī)屏幕垂直柿汛,與我們的實(shí)現(xiàn)平行,所以并不能感知到埠对。

Z order.jpg

前面分析了窗口分組的時(shí)候涉及了兩個(gè)對(duì)象WindowState與Windtoken络断,但僅限分組,分組無(wú)法決定窗口的顯示的Z-order项玛,那么再WMS是怎么管理所有窗口的Z-order的貌笨? 在WMS中窗口被抽象成WindowState,因此WindowState內(nèi)部一定有屬性來(lái)標(biāo)志這個(gè)窗口的Z-order襟沮,實(shí)現(xiàn)也確實(shí)如此锥惋,WindowState采用三個(gè)個(gè)int值mBaseLayer+ mSubLayer + mLayer 來(lái)標(biāo)志窗口所處的位置昌腰,前兩個(gè)主要是根據(jù)窗口類型確定窗口位置,mLayer才是真正的值净刮,定義如下:

final class WindowState implements WindowManagerPolicy.WindowState {
    
    final WindowList mChildWindows = new WindowList();
    final int mBaseLayer;
    final int mSubLayer;
     <!--最終Z次序的賦值-->
   int mLayer;
    
    }

從名字很容知道m(xù)BaseLayer是標(biāo)志窗口的主次序剥哑,面向的是一個(gè)窗口組,而mSubLayer主要面向單獨(dú)窗口淹父,要來(lái)標(biāo)志一個(gè)窗口在一組窗口中的位置株婴,對(duì)兩者來(lái)說(shuō)值越大,窗口越靠前暑认,從此final屬性知道困介,兩者的值是不能修改的,而mLayer可以修改蘸际,對(duì)于系統(tǒng)窗口座哩,一般不會(huì)同時(shí)顯示兩個(gè),因此粮彤,可以用主序決定根穷,比較特殊的就是Activity與子窗口,首先子窗口的主序肯定是父窗口決定的导坟,子窗口只關(guān)心次序就行屿良。而父窗口的主序卻相對(duì)麻煩,比如對(duì)于應(yīng)用窗口來(lái)說(shuō)惫周,他們的主序都是一樣的尘惧,因此還要有一個(gè)其他的維度來(lái)作為參考,比如對(duì)于Activity递递,主序都是一樣的喷橙,怎么定他們真正的Z-order呢?其實(shí)Activity的順序是由AMS保證的登舞,這個(gè)順序定了贰逾,WMS端Activity窗口的順序也是定了,這樣下來(lái)次序也方便定了菠秒。

WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token,
           WindowState attachedWindow, int appOp, int seq, WindowManager.LayoutParams a,
           int viewVisibility, final DisplayContent displayContent) {
        ...
            <!--關(guān)鍵點(diǎn)1  子窗口類型的Z order-->
        if ((mAttrs.type >= FIRST_SUB_WINDOW &&
                mAttrs.type <= LAST_SUB_WINDOW)) {
            mBaseLayer = mPolicy.windowTypeToLayerLw(
                    attachedWindow.mAttrs.type) * WindowManagerService.TYPE_LAYER_MULTIPLIER
                    + WindowManagerService.TYPE_LAYER_OFFSET;
            mSubLayer = mPolicy.subWindowTypeToLayerLw(a.type);
            mAttachedWindow = attachedWindow;               final WindowList childWindows = mAttachedWindow.mChildWindows;
            final int numChildWindows = childWindows.size();
            if (numChildWindows == 0) {
                childWindows.add(this);
            } else {
             ...
        } else {
            <!--關(guān)鍵點(diǎn)2  普通窗口類型的Z order-->
            mBaseLayer = mPolicy.windowTypeToLayerLw(a.type)
                    * WindowManagerService.TYPE_LAYER_MULTIPLIER
                    + WindowManagerService.TYPE_LAYER_OFFSET;
            mSubLayer = 0;
            mAttachedWindow = null;
            mLayoutAttached = false;
        }
       ...
    }

由于窗口所能選擇的類型是確定的似踱,因此mBaseLayer與mSubLayer所能選擇的值只有固定幾個(gè),很明顯這兩個(gè)參數(shù)不能精確的確定Z-order稽煤,還會(huì)有其他微調(diào)的手段核芽,也僅限微調(diào),在系統(tǒng)層面酵熙,決定了不同類型窗口所處的位置轧简,比如系統(tǒng)Toast類型的窗口一定處于所有應(yīng)用窗口之上,不過(guò)我們最關(guān)心的是Activity類的窗口如何確定Z-order的匾二,在new WindowState之后哮独,只是粗略的確定了Activity窗口的次序拳芙,看一下添加窗口的示意代碼:

addWindow(){
    <!--1-->
    new WindowState
    <!--2-->
    addWindowToListInOrderLocked(win, true);
    <!--3-->
    assignLayersLocked(displayContent.getWindowList());
    }

新建state對(duì)象之后,Z-order還要通過(guò)addWindowToListInOrderLocked及assignLayersLocked才能確定皮璧,addWindowToListInOrderLocked主要是根據(jù)窗口的Token找到歸屬舟扎,插入到對(duì)應(yīng)Token的WindowState列表,如果是子窗口還要插入到父窗口的對(duì)應(yīng)位置中:

次序確定.jpg

插入到特定位置后其實(shí)Z-order就確定了悴务,接下來(lái)就是通過(guò)assignLayersLocked為WindowState分配真正的Z-order mLayer,

   private final void assignLayersLocked(WindowList windows) {
        int N = windows.size();
        int curBaseLayer = 0;
        int curLayer = 0;
        int i;

        boolean anyLayerChanged = false;
            for (i=0; i<N; i++) {
            final WindowState w = windows.get(i);
            final WindowStateAnimator winAnimator = w.mWinAnimator;
            boolean layerChanged = false;
            int oldLayer = w.mLayer;
            if (w.mBaseLayer == curBaseLayer || w.mIsImWindow
                    || (i > 0 && w.mIsWallpaper)) {
                <!--通過(guò)偏移量-->
                curLayer += WINDOW_LAYER_MULTIPLIER;
                w.mLayer = curLayer;
            } else {
                curBaseLayer = curLayer = w.mBaseLayer;
                w.mLayer = curLayer;
            }
            if (w.mLayer != oldLayer) {
                layerChanged = true;
                anyLayerChanged = true;
            }
            ...
    }

mLayer最終確定后睹限,窗口的次序也就確定了,這個(gè)順序要最終通過(guò)后續(xù)的relayout更新到SurfaceFlinger服務(wù)讯檐,之后羡疗,SurfaceFlinger在圖層混排的時(shí)候才知道如何處理。

WMS中窗口次序分配如何影響SurfaceFlinger服務(wù)

SurfaceFlinger在圖層混排的時(shí)候應(yīng)該不會(huì)混排所有的窗口别洪,只會(huì)混排可見(jiàn)的窗口叨恨,比如有多個(gè)全屏Activity的時(shí)候,SurfaceFlinger只會(huì)處理最上面的挖垛,那么SurfaceFlinger如何知道哪些窗口可見(jiàn)哪些不可見(jiàn)呢痒钝?前文分析了WMS分配Z-order之后,要通過(guò)setLayer更新到SurfaceFlinger痢毒,接下來(lái)看具體流程送矩,創(chuàng)建SurfaceControl之后,會(huì)創(chuàng)建一次事務(wù)闸准,確定Surface的次序:

   SurfaceControl.openTransaction();
            try {
                mSurfaceX = left;
                mSurfaceY = top;
                    try {
                    mSurfaceControl.setPosition(left, top);
                    mSurfaceLayer = mAnimLayer;
                    final DisplayContent displayContent = w.getDisplayContent();
                    if (displayContent != null) {
                        mSurfaceControl.setLayerStack(displayContent.getDisplay().getLayerStack());
                    }
                    <!--設(shè)置次序-->
                    mSurfaceControl.setLayer(mAnimLayer);
                    mSurfaceControl.setAlpha(0);
                    mSurfaceShown = false;
                } catch (RuntimeException e) {
                    mService.reclaimSomeSurfaceMemoryLocked(this, "create-init", true);
                }
                mLastHidden = true;
            } finally {
                SurfaceControl.closeTransaction();
            }
        }

這里通過(guò)openTransaction與closeTransaction保證一次事務(wù)的完整性,中間就Surface次序的調(diào)整梢灭,closeTransaction會(huì)與SurfaceFlinger通信夷家,通知SurfaceFlinger更新Surface信息,這其中就包括Z-order敏释。

總結(jié)

本文簡(jiǎn)要分析了Android窗口的分組库快,以及WMS窗口次序的確定,最后簡(jiǎn)單提及了一下窗口次序如何更新到SurfaceFlinger服務(wù)的钥顽,也方便將來(lái)理解圖層合成义屏。

作者:看書的小蝸牛
原文鏈接:Android窗口管理分析(3):窗口分組及Z-order的確定
僅供參考,歡迎指正

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蜂大,一起剝皮案震驚了整個(gè)濱河市闽铐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌奶浦,老刑警劉巖兄墅,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異澳叉,居然都是意外死亡隙咸,警方通過(guò)查閱死者的電腦和手機(jī)沐悦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)五督,“玉大人藏否,你說(shuō)我怎么就攤上這事〕浒” “怎么了副签?”我有些...
    開(kāi)封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)误证。 經(jīng)常有香客問(wèn)我继薛,道長(zhǎng),這世上最難降的妖魔是什么愈捅? 我笑而不...
    開(kāi)封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任遏考,我火速辦了婚禮,結(jié)果婚禮上蓝谨,老公的妹妹穿的比我還像新娘灌具。我一直安慰自己,他們只是感情好譬巫,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布咖楣。 她就那樣靜靜地躺著,像睡著了一般芦昔。 火紅的嫁衣襯著肌膚如雪诱贿。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天咕缎,我揣著相機(jī)與錄音珠十,去河邊找鬼。 笑死凭豪,一個(gè)胖子當(dāng)著我的面吹牛焙蹭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嫂伞,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼孔厉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了帖努?” 一聲冷哼從身側(cè)響起撰豺,我...
    開(kāi)封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拼余,沒(méi)想到半個(gè)月后郑趁,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡姿搜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年寡润,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了捆憎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡梭纹,死狀恐怖躲惰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情变抽,我是刑警寧澤础拨,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站绍载,受9級(jí)特大地震影響诡宗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜击儡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一塔沃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧阳谍,春花似錦蛀柴、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至训貌,卻和暖如春制肮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背递沪。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工豺鼻, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人区拳。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓拘领,卻偏偏與公主長(zhǎng)得像意乓,于是被迫代替她去往敵國(guó)和親樱调。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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