【凱子哥帶你學Framework】Activity界面顯示全解析(下)

咱們接著上篇繼續講,上篇沒看的請戳:【凱子哥帶你學Framework】Activity界面顯示全解析(上)

如何驗證上一個問題

首先,說明一下運行條件:

 //主題
name="AppTheme" parent="@android:style/Theme.Holo.Light.NoActionBar"

//編譯版本
android {
    compileSdkVersion 19
    buildToolsVersion '19.1.0'

    defaultConfig {
        applicationId "com.socks.uitestapp"
        minSdkVersion 15
        targetSdkVersion 19
        versionCode 1
        versionName "1.0"
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'com.android.support:appcompat-v7:19.1.0'
}

//Activity代碼
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

//activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:text="Hello World!"
    android:textSize="20sp" />

OK,咱們的軟件已經準備好了,采用的是最簡單的布局,界面效果如下:

下面用Hierarchy看一下樹狀結構:

第一層,就是上面的DecorView,里面有一個線性布局,上面的是ViewStub,下面就是id為content的ViewGroup,是一個FrameLayout。而我們通過setContentView()設置的布局,就是TextView了。

能不能在源碼里面找到源文件呢?當然可以,這個布局就是screen_simple.xml

frameworks/base/core/res/res/layout/screen_simple.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

所以,即使你不調用setContentView(),在一個空Activity上面,也是有布局的。而且肯定有一個DecorView,一個id為content的FrameLayout。

你可以采用下面的方式獲取到DecorView,但是你不能獲取到一個DecorView實例,只能獲取到ViewGroup。

下面貼上這個圖,你就可以看明白了(轉自 工匠若水)

ViewGroup view = (ViewGroup) getWindow().getDecorView();

我們通過setContentView()設置的界面,為什么在onResume()之后才對用戶可見呢?

有開發經驗的朋友應該知道,我們的界面元素在onResume()之后才對用戶是可見的,這是為啥呢?

那我們就追蹤一下,onResume()是什么時候調用的,然后看看做了什么操作就Ok了。

這一下,我們又要從ActivityThread開始說起了,不熟悉的快去看前一篇文章《Activity啟動過程全解析》](http://blog.csdn.net/zhaokaiqiang1992/article/details/49428287)。

話說,前文說到,我們想要開啟一個Activity的時候,ActivityThread的handleLaunchActivity()會在Handler中被調用

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {

    //就是在這里調用了Activity.attach()呀,接著調用了Activity.onCreate()和Activity.onStart()生命周期,但是由于只是初始化了mDecor,添加了布局文件,還沒有把
    //mDecor添加到負責UI顯示的PhoneWindow中,所以這時候對用戶來說,是不可見的
    Activity a = performLaunchActivity(r, customIntent);

    ......
    
    if (a != null) {
    //這里面執行了Activity.onResume()
    handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed);
    
    if (!r.activity.mFinished && r.startsNotResumed) {
        try {
                    r.activity.mCalled = false;
                    //執行Activity.onPause()
                    mInstrumentation.callActivityOnPause(r.activity);
                    }
        }
    }
}

所以說,ActivityThread.handleLaunchActivity執行完之后,Activity的生命周期已經執行了4個(onCreate、onStart()、onResume、onPause())。

下面咱們重點看下handleResumeActivity()做了什么

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
            
            //這個時候,Activity.onResume()已經調用了,但是現在界面還是不可見的
            ActivityClientRecord r = performResumeActivity(token, clearHide);
            
            if (r != null) {
                final Activity a = r.activity;
                  if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                //decor對用戶不可見
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                //這里記住這個WindowManager.LayoutParams的type為TYPE_BASE_APPLICATION,后面介紹Window的時候會見到
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
               
                if (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
                    //終于被添加進WindowManager了,但是這個時候,還是不可見的
                    wm.addView(decor, l);
                }
                
                if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
                     //在這里,執行了重要的操作!
                     if (r.activity.mVisibleFromClient) {
                            r.activity.makeVisible();
                        }
                    }
            }

從上面的分析中我們知道,其實在onResume()執行之后,界面還是不可見的,當我們執行了Activity.makeVisible()方法之后,界面才對我們是可見的


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

OK,其實講到了這里,關于Activity中的界面顯示應該算是告一段落了,我們知道了Activity的生命周期方法的調用時機,還知道了一個最簡單的Activity的界面的構成,并了解了Window、PhoneWindow、DecorView、WindowManager的存在。

但是我還是感覺不過癮,因為上面只是在流程上大體上過了一遍,對于Window、WindowManager的深入了解還不夠,所以下面就開始講解Window、WindowManager等相關類的稍微高級點的知識。

前面看累了的朋友,可以上個廁所,泡個咖啡,休息下繼續往下看。

ViewManager、WindowManager、WindowManagerImpl、WindowManagerGlobal到底都是些什么玩意?

WindowManager其實是一個接口,和Window一樣,起作用的是它的實現類

public interface WindowManager extends ViewManager {

     //對這個異常熟悉么?當你往已經銷毀的Activity中添加Dialog的時候,就會拋這個異常
     public static class BadTokenException extends RuntimeException {
            public BadTokenException() {
        }

        public BadTokenException(String name) {
            super(name);
        }
    }
     
     //其實WindowManager里面80%的代碼是用來描述這個內部靜態類的
      public static class LayoutParams extends ViewGroup.LayoutParams
            implements Parcelable {
            }
}

WindowManager繼承自ViewManager這個接口,從注釋和方法我們可以知道,這個就是用來描述可以對Activity中的子View進行添加和移除能力的接口。

/** Interface to let you add and remove child views to an Activity. To get an instance
  * of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
  */
public interface ViewManager
{
    public void addView(View view, ViewGroup.LayoutParams params);
        public void updateViewLayout(View view, ViewGroup.LayoutParams params);
        public void removeView(View view);
}

那么我們在使用WindowManager的時候,到底是在使用哪個類呢?

是WindowManagerImpl。

public final class WindowManagerImpl implements WindowManager {}

怎么知道的呢?那我們還要從Activity.attach()說起

話說,在attach()里面完成了mWindowManager的初始化

 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, IVoiceInteractor voiceInteractor) {
        
            mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
            
            mWindowManager = mWindow.getWindowManager();
        
        }
        

那我們只好看下(WindowManager)context.getSystemService(Context.WINDOW_SERVICE)是什么玩意了。

這里要說明的是,context是一個ContextImpl對象,這里先記住就好,以后再細說。

class ContextImpl extends Context {

 //靜態代碼塊,完成各種系統服務的注冊
 static {
    
    ......
    
     registerService(WINDOW_SERVICE, new ServiceFetcher() {
                Display mDefaultDisplay;
                public Object getService(ContextImpl ctx) {
                    Display display = ctx.mDisplay;
                    if (display == null) {
                        if (mDefaultDisplay == null) {
                            DisplayManager dm = (DisplayManager)ctx.getOuterContext().
                                    getSystemService(Context.DISPLAY_SERVICE);
                            mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
                        }
                        display = mDefaultDisplay;
                    }
                    //沒騙你吧
                    return new WindowManagerImpl(display);
                }});
    ......
 }

@Override
    public Object getSystemService(String name) {
        ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
        return fetcher == null ? null : fetcher.getService(this);
    }
}

要注意的是,這里返回的WindowManagerImpl對象,最終并不是和我們的Window關聯的,而且這個方法是有可能返回null的,所以在Window.setWindowManager()的時候,進行了處理

 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);
        }
        //設置parentWindow,創建真正關聯的WindowManagerImpl對象
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }
    
    public final class WindowManagerImpl implements WindowManager {
        
        //最終調用的這個構造
        private WindowManagerImpl(Display display, Window parentWindow) {
            mDisplay = display;
            mParentWindow = parentWindow;
        }

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

所以說,每一個Activity都有一個PhoneWindow成員變量,并且也都有一個WindowManagerImpl,而且,PhoneWindow和WindowManagerImpl在Activity.attach()的時候進行了關聯。

插一張類圖(轉自工匠若水

知道了這些,那下面的操作就可以直接看WindowManagerImpl了。

其實WindowManagerImpl這個類也沒有什么看頭,為啥這么說呢?因為他其實是代理模式中的代理。是誰的代理呢?是WindowManagerGlobal。

public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Display mDisplay;
    private final Window mParentWindow;

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

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

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

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

}

從上面的代碼中可以看出來,WindowManagerImpl里面對ViewManager接口內方法的實現,都是通過代理WindowManagerGlobal的方法實現的,所以重點轉移到了WindowManagerGlobal這個類。

還記得前面我們的DecorView被添加到了WindowManager嗎?

wm.addView(decor, l);

其實最終調用的是WindowManagerGlobal.addView();

 public final class WindowManagerGlobal {
 
    private static IWindowManager sWindowManagerService;
        private static IWindowSession sWindowSession;
 
    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>();
 
    //WindowManagerGlobal是單例模式
    private static WindowManagerGlobal sDefaultWindowManager;
    
    public static WindowManagerGlobal getInstance() {
        synchronized (WindowManagerGlobal.class) {
            if (sDefaultWindowManager == null) {
                sDefaultWindowManager = new WindowManagerGlobal();
            }
            return sDefaultWindowManager;
        }
        }
        
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
            
              final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
             ......
                synchronized (mLock) {
            
                ViewRootImpl root;
             
                root = new ViewRootImpl(view.getContext(), display);
                view.setLayoutParams(wparams);
             
              mViews.add(view);
                mRoots.add(root);
                mParams.add(wparams);
            }
             ......
            
             try {
             //注意下這個方法,因為下面介紹ViewRootImpl的時候會用到
                root.setView(view, wparams, panelParentView);
            }catch (RuntimeException e) {
            }
            
            }
 }

我們看到,WindowManagerGlobal是單例模式,所以在一個App里面只會有一個WindowManagerGlobal實例。在WindowManagerGlobal里面維護了三個集合,分別存放添加進來的View(實際上就是DecorView),布局參數params,和剛剛實例化的ViewRootImpl對象,WindowManagerGlobal到底干嘛的呢?

其實,WindowManagerGlobal是和WindowManagerService(即WMS)通信的。

還記得在上一篇文章中我們介紹ActivityThread和AMS之間的IBinder通信的嗎?是的,這里也是IBinder通信。


 public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                try {
                        InputMethodManager imm = InputMethodManager.getInstance();
                        IWindowManager windowManager = getWindowManagerService();
                        sWindowSession = windowManager.openSession(
                         
                            ......
                            
                     } catch (RemoteException e) {
                    Log.e(TAG, "Failed to open window session", e);
                }
            }
            return sWindowSession;
        }
    }

 public static IWindowManager getWindowManagerService() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowManagerService == null) {
                  //ServiceManager是用來管理系統服務的,比如AMS、WMS等,這里就獲取到了WMS的客戶端代理對象
                sWindowManagerService = IWindowManager.Stub.asInterface(
                        ServiceManager.getService("window"));
            }
            return sWindowManagerService;
        }
    }

首先通過上面的方法獲取到IBinder對象,然后轉化成了WMS在本地的代理對象IWindowManager,然后通過openSession()初始化了sWindowSession對象。這個對象是干什么的呢?

“Session“是會話的意思,這個類就是為了實現與WMS的會話的,誰和WMS的對話呢?WindowManagerGlobal類內部并沒有用這個類呀!

是ViewRootImpl與WMS的對話。

ViewRootImpl是什么?有什么作用?ViewRootImpl如何與WMS通信

你還記得么?在前面將WindowManagerGlobal.addView()的時候,實例化了一個ViewRootImpl,然后添加到了一個集合里面,咱們先看下ViewRootImpl的構造函數吧

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
           
       public ViewRootImpl(Context context, Display display) { 
        
            mContext = context;
            //獲取WindowSession
            mWindowSession = WindowManagerGlobal.getWindowSession();
            mDisplay = display;
            
            ......
            
            mWindow = new W(this);
            //默認不可見
            mViewVisibility = View.GONE;
            //這個數值就是屏幕寬度的dp總數
            mDensity = context.getResources().getDisplayMetrics().densityDpi;
            mChoreographer = Choreographer.getInstance();
            mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
        }
 
}

在這個構造方法里面,主要是完成了各種參數的初始化,并且最關鍵的,獲取到了前面介紹的WindowSession,那么你可能好奇了,這個ViewRootImpl到底有什么作用呢?

ViewRootImpl負責管理視圖樹和與WMS交互,與WMS交互是通過WindowSession。而且ViewRootImpl也負責UI界面的布局與渲染,負責把一些事件分發至Activity,以便Activity可以截獲事件。大多數情況下,它管理Activity頂層視圖DecorView,它相當于MVC模型中的Controller。

WindowSession是ViewRootImpl獲取之后,主動和WMS通信的,但是我們在前面的文章知道,客戶端和服務器需要互相持有對方的代理引用,才能實現雙向通信,那么WMS是怎么得到ViewRootImpl的通信代理的呢?

是在ViewRootImpl.setView()的時候。

還記得不?在上面介紹WindowManagerGlobal.addView()的時候,我還重點說了下,在這個方法的try代碼塊中,調用了ViewRootImpl.setView(),下面咱們看下這個方法干嘛了:

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
        
             if (mView == null) {
                 mView = view;
                 int res;
                 requestLayout();
                
                    try {
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mInputChannel);
                        }catch (RemoteException e) {
                                  throw new RuntimeException("Adding window failed", e);
                        } finally {
                        
                     }   
                    }
                }
        }

為了突出重點,我簡化了很多代碼,從上面可以看出來,是mWindowSession.addToDisplay()這個方法把mWindow傳遞給我WMS,WMS就持有了當前ViewRootlmpl的代理,就可以調用W對象讓ViewRootlmpl做一些事情了。

這樣,雙方都有了對方的接口,WMS中的Session注冊到WindowManagerGlobal的成員WindowSession中,ViewRootImpl::W注冊到WindowState中的成員mClient中。前者是為了App改變View結構時請求WMS為其更新布局。后者代表了App端的一個添加到WMS中的View,每一個像這樣通過WindowManager接口中addView()添加的窗口都有一個對應的ViewRootImpl,也有一個相應的ViewRootImpl::W。它可以理解為是ViewRootImpl中暴露給WMS的接口,這樣WMS可以通過這個接口和App端通信。

另外源碼中很多地方采用了這種將接口隱藏為內部類的方式,這樣可以實現六大設計原則之一——接口最小原則。

從什么時候開始繪制整個Activity的View樹的?

注意前面代碼中的requestLayout();因為這個方法執行之后,我們的ViewRootImpl才開始繪制整個View樹!

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
           
            scheduleTraversals();
        }
    }
void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //暫停UI線程消息隊列對同步消息的處理
            mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
            //向Choreographer注冊一個類型為CALLBACK_TRAVERSAL的回調,用于處理UI繪制
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
           notifyRendererOfFramePending();
        }
    }

“Choreographer就是一個消息處理器,根據vsync 信號 來計算frame“

解釋起來比較麻煩,我們暫時不展開討論,你只要知道,當回調被觸發之后,mTraversalRunnable對象的run()就會被調用

 final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

doTraversal()中最關鍵的,就是調用了performTraversals(),然后就開始mesure,layout,draw了,這里面的具體邏輯本篇文章不講,因為重點是Activity的界面顯示流程,這一塊屬于View的,找時間單獨拿出來說

 void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "performTraversals");
            try {
                performTraversals();
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

來回倒騰了這么多,終于看見界面了,讓我哭會 T^T

Window的類型有幾種?分別在什么情況下會使用到哪一種?

Window的類型是根據WindowManager.LayoutParams的type屬性相關的,根據類型可以分為三類:

  • 取值在FIRST_APPLICATION_WINDOW與LAST_APPLICATION_WINDOW之間(1-99),是常用的頂層應用程序窗口,須將token設置成Activity的token,比如前面開啟Window的時候設置的類型即為TYPE_APPLICATION
  • 在FIRST_SUB_WINDOW和LAST_SUB_WINDOW(1000-1999)之間,與頂層窗口相關聯,需將token設置成它所附著宿主窗口的token,比如PopupWindow就是TYPE_APPLICATION_PANEL
  • 取值在FIRST_SYSTEM_WINDOW和LAST_SYSTEM_WINDOW(2000-2999)之間,不能用于應用程序,使用時需要有特殊權限,它是特定的系統功能才能使用,比如Toast就是TYPE_TOAST=2005,所以不需要特殊權限

下面是所有的Type說明

//WindowType:開始應用程序窗口
        public static final int FIRST_APPLICATION_WINDOW = 1;
        //WindowType:所有程序窗口的base窗口,其他應用程序窗口都顯示在它上面
        public static final int TYPE_BASE_APPLICATION  = 1;
        //WindowType:普通應用程序窗口,token必須設置為Activity的token來指定窗口屬于誰
        public static final int TYPE_APPLICATION        = 2;
        //WindowType:應用程序啟動時所顯示的窗口,應用自己不要使用這種類型,它被系統用來顯示一些信息,直到應用程序可以開啟自己的窗口為止
        public static final int TYPE_APPLICATION_STARTING = 3;
        //WindowType:結束應用程序窗口
        public static final int LAST_APPLICATION_WINDOW = 99;

        //WindowType:SubWindows子窗口,子窗口的Z序和坐標空間都依賴于他們的宿主窗口
        public static final int FIRST_SUB_WINDOW        = 1000;
        //WindowType: 面板窗口,顯示于宿主窗口的上層
        public static final int TYPE_APPLICATION_PANEL  = FIRST_SUB_WINDOW;
        //WindowType:媒體窗口(例如視頻),顯示于宿主窗口下層
        public static final int TYPE_APPLICATION_MEDIA  = FIRST_SUB_WINDOW+1;
        //WindowType:應用程序窗口的子面板,顯示于所有面板窗口的上層
        public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
        //WindowType:對話框,類似于面板窗口,繪制類似于頂層窗口,而不是宿主的子窗口
        public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;
        //WindowType:媒體信息,顯示在媒體層和程序窗口之間,需要實現半透明效果
        public static final int TYPE_APPLICATION_MEDIA_OVERLAY  = FIRST_SUB_WINDOW+4;
        //WindowType:子窗口結束
        public static final int LAST_SUB_WINDOW        = 1999;

        //WindowType:系統窗口,非應用程序創建
        public static final int FIRST_SYSTEM_WINDOW    = 2000;
        //WindowType:狀態欄,只能有一個狀態欄,位于屏幕頂端,其他窗口都位于它下方
        public static final int TYPE_STATUS_BAR        = FIRST_SYSTEM_WINDOW;
        //WindowType:搜索欄,只能有一個搜索欄,位于屏幕上方
        public static final int TYPE_SEARCH_BAR        = FIRST_SYSTEM_WINDOW+1;
        //WindowType:電話窗口,它用于電話交互(特別是呼入),置于所有應用程序之上,狀態欄之下
        public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
        //WindowType:系統提示,出現在應用程序窗口之上
        public static final int TYPE_SYSTEM_ALERT      = FIRST_SYSTEM_WINDOW+3;
        //WindowType:鎖屏窗口
        public static final int TYPE_KEYGUARD          = FIRST_SYSTEM_WINDOW+4;
        //WindowType:信息窗口,用于顯示Toast
        public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
        //WindowType:系統頂層窗口,顯示在其他一切內容之上,此窗口不能獲得輸入焦點,否則影響鎖屏
        public static final int TYPE_SYSTEM_OVERLAY    = FIRST_SYSTEM_WINDOW+6;
        //WindowType:電話優先,當鎖屏時顯示,此窗口不能獲得輸入焦點,否則影響鎖屏
        public static final int TYPE_PRIORITY_PHONE    = FIRST_SYSTEM_WINDOW+7;
        //WindowType:系統對話框
        public static final int TYPE_SYSTEM_DIALOG      = FIRST_SYSTEM_WINDOW+8;
        //WindowType:鎖屏時顯示的對話框
        public static final int TYPE_KEYGUARD_DIALOG    = FIRST_SYSTEM_WINDOW+9;
        //WindowType:系統內部錯誤提示,顯示于所有內容之上
        public static final int TYPE_SYSTEM_ERROR      = FIRST_SYSTEM_WINDOW+10;
        //WindowType:內部輸入法窗口,顯示于普通UI之上,應用程序可重新布局以免被此窗口覆蓋
        public static final int TYPE_INPUT_METHOD      = FIRST_SYSTEM_WINDOW+11;
        //WindowType:內部輸入法對話框,顯示于當前輸入法窗口之上
        public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
        //WindowType:墻紙窗口
        public static final int TYPE_WALLPAPER          = FIRST_SYSTEM_WINDOW+13;
        //WindowType:狀態欄的滑動面板
        public static final int TYPE_STATUS_BAR_PANEL  = FIRST_SYSTEM_WINDOW+14;
        //WindowType:安全系統覆蓋窗口,這些窗戶必須不帶輸入焦點,否則會干擾鍵盤
        public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
        //WindowType:拖放偽窗口,只有一個阻力層(最多),它被放置在所有其他窗口上面
        public static final int TYPE_DRAG              = FIRST_SYSTEM_WINDOW+16;
        //WindowType:狀態欄下拉面板
        public static final int TYPE_STATUS_BAR_SUB_PANEL = FIRST_SYSTEM_WINDOW+17;
        //WindowType:鼠標指針
        public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18;
        //WindowType:導航欄(有別于狀態欄時)
        public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19;
        //WindowType:音量級別的覆蓋對話框,顯示當用戶更改系統音量大小
        public static final int TYPE_VOLUME_OVERLAY = FIRST_SYSTEM_WINDOW+20;
        //WindowType:起機進度框,在一切之上
        public static final int TYPE_BOOT_PROGRESS = FIRST_SYSTEM_WINDOW+21;
        //WindowType:假窗,消費導航欄隱藏時觸摸事件
        public static final int TYPE_HIDDEN_NAV_CONSUMER = FIRST_SYSTEM_WINDOW+22;
        //WindowType:夢想(屏保)窗口,略高于鍵盤
        public static final int TYPE_DREAM = FIRST_SYSTEM_WINDOW+23;
        //WindowType:導航欄面板(不同于狀態欄的導航欄)
        public static final int TYPE_NAVIGATION_BAR_PANEL = FIRST_SYSTEM_WINDOW+24;
        //WindowType:universe背后真正的窗戶
        public static final int TYPE_UNIVERSE_BACKGROUND = FIRST_SYSTEM_WINDOW+25;
        //WindowType:顯示窗口覆蓋,用于模擬輔助顯示設備
        public static final int TYPE_DISPLAY_OVERLAY = FIRST_SYSTEM_WINDOW+26;
        //WindowType:放大窗口覆蓋,用于突出顯示的放大部分可訪問性放大時啟用
        public static final int TYPE_MAGNIFICATION_OVERLAY = FIRST_SYSTEM_WINDOW+27;
        //WindowType:......
        public static final int TYPE_KEYGUARD_SCRIM          = FIRST_SYSTEM_WINDOW+29;
        public static final int TYPE_PRIVATE_PRESENTATION = FIRST_SYSTEM_WINDOW+30;
        public static final int TYPE_VOICE_INTERACTION = FIRST_SYSTEM_WINDOW+31;
        public static final int TYPE_ACCESSIBILITY_OVERLAY = FIRST_SYSTEM_WINDOW+32;
        //WindowType:系統窗口結束
        public static final int LAST_SYSTEM_WINDOW      = 2999;


為什么使用PopWindow的時候,不設置背景就不能觸發事件?

我們在使用PopupWindow的時候,會發現如果不給PopupWindow設置背景,那么就不能觸發點擊返回事件,有人認為這個是BUG,其實并不是的。

我們以下面的方法為例,其實所有的顯示方法都有下面的流程:

public void showAtLocation(IBinder token, int gravity, int x, int y) {
        if (isShowing() || mContentView == null) {
            return;
        }

        mIsShowing = true;
        mIsDropdown = false;

        WindowManager.LayoutParams p = createPopupLayout(token);
        p.windowAnimations = computeAnimationResource();
        
        //在這里會根據不同的設置,配置不同的LayoutParams屬性
        preparePopup(p);
        if (gravity == Gravity.NO_GRAVITY) {
            gravity = Gravity.TOP | Gravity.START;
        }
        p.gravity = gravity;
        p.x = x;
        p.y = y;
        if (mHeightMode < 0) p.height = mLastHeight = mHeightMode;
        if (mWidthMode < 0) p.width = mLastWidth = mWidthMode;
        invokePopup(p);
    }

我們重點看下preparePopup()


private void preparePopup(WindowManager.LayoutParams p) {
         //根據背景的設置情況進行不同的配置
        if (mBackground != null) {
            final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
            int height = ViewGroup.LayoutParams.MATCH_PARENT;
           
           //如果設置了背景,就用一個PopupViewContainer對象來包裹之前的mContentView,并設置背景后
            PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
            PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, height
            );
            popupViewContainer.setBackground(mBackground);
            popupViewContainer.addView(mContentView, listParams);

            mPopupView = popupViewContainer;
        } else {
            mPopupView = mContentView;
        }
    }

為啥包了一層PopupViewContainer,就可以處理按鈕點擊事件了?因為PopupWindow沒有相關事件回調,也沒有重寫按鍵和觸摸方法,所以接收不到對應的信號

public class PopupWindow {}

而PopupViewContainer則可以,因為它重寫了相關方法

private class PopupViewContainer extends FrameLayout {

    @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
                if (getKeyDispatcherState() == null) {
                    return super.dispatchKeyEvent(event);
                }

                if (event.getAction() == KeyEvent.ACTION_DOWN
                        && event.getRepeatCount() == 0) {
                    KeyEvent.DispatcherState state = getKeyDispatcherState();
                    if (state != null) {
                        state.startTracking(event, this);
                    }
                    return true;
                } else if (event.getAction() == KeyEvent.ACTION_UP) {
                    //back鍵消失
                    KeyEvent.DispatcherState state = getKeyDispatcherState();
                    if (state != null && state.isTracking(event) && !event.isCanceled()) {
                        dismiss();
                        return true;
                    }
                }
                return super.dispatchKeyEvent(event);
            } else {
                return super.dispatchKeyEvent(event);
            }
        }

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
                return true;
            }
            return super.dispatchTouchEvent(ev);
        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            final int x = (int) event.getX();
            final int y = (int) event.getY();
            //觸摸在外面就消失
            if ((event.getAction() == MotionEvent.ACTION_DOWN)
                    && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
                dismiss();
                return true;
            } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                dismiss();
                return true;
            } else {
                return super.onTouchEvent(event);
            }
        }
}

在Activity中使用Dialog的時候,為什么有時候會報錯“Unable to add window -- token is not valid; is your activity running?”?

這種情況一般發生在什么時候?一般發生在Activity進入后臺,Dialog沒有主動Dismiss掉,然后從后臺再次進入App的時候。

為什么會這樣呢?

還記得前面說過吧,子窗口類型的Window,比如Dialog,想要顯示的話,比如保證appToken與Activity保持一致,而當Activity銷毀,再次回來的時候,Dialog試圖重新創建,調用ViewRootImp的setView()的時候就會出問題,所以記得在Activity不可見的時候,主動Dismiss掉Dialog。

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 this type already exists");
                        case WindowManagerGlobal.ADD_PERMISSION_DENIED:
                            throw new WindowManager.BadTokenException(
                                "Unable to add window " + mWindow +
                                " -- permission denied for this window type");
                        case WindowManagerGlobal.ADD_INVALID_DISPLAY:
                            throw new WindowManager.InvalidDisplayException(
                                "Unable to add window " + mWindow +
                                " -- the specified display can not be found");
                    }
                    throw new RuntimeException(
                        "Unable to add window -- unknown error code " + res);
                }
      }

為什么Toast需要由系統統一控制,在子線程中為什么不能顯示Toast?

首先Toast也屬于窗口系統,但是并不是屬于App的,是由系統同一控制的。
關于這一塊不想說太多,具體實現機制請參考后面的文章。

為了看下面的內容,你需要知道以下幾件事情:

  1. Toast的顯示是由系統Toast服務控制的,與系統之間的通信方式是Binder
  2. 整個Toast系統會維持最多50個Toast的隊列,依次顯示
  3. 負責現實工作的是Toast的內部類TN,它負責最終的顯示與隱藏操作
  4. 負責給系統Toast服務發送內容的是INotificationManager的實現類,它負責在Toast.show()里面把TN對象傳遞給系統消息服務,service.enqueueToast(pkg, tn, mDuration);這樣Toast服務就持有客戶端的代理,可以通過TN來控制每個Toast的顯示與隱藏。

再來張圖(轉自工匠若水

ok,現在假如你知道上面這些啦,那么我們下面就看為什么在子線程使用Toast.show()會提示

"No Looper; Looper.prepare() wasn't called on this thread."

原因很簡單,因為TN在操作Toast的時候,是通過Handler做的

@Override
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }

        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);
        }

所以說,TN初始化的線程必須為主線程,在子線程中使用Handler,由于沒有消息隊列,就會造成這個問題。

結語

上面寫了這么多,你可能看了前面忘了后面,下面,凱子哥給你總結一下,這篇文章到底講了什么東西:

  • 每個Activity,都至少有一個Window,這個Window實際類型為PhoneWindow,當Activity中有子窗口,比如Dialog的時候,就會出現多個Window。Activity的Window是我們控制的,狀態欄和導航欄的Window由系統控制。
  • 在DecorView的里面,一定有一個id為content的FraneLayout的布局容器,咱們自己定義的xml布局都放在這里面。
  • Activity的Window里面有一個DecorView,它使繼承自FrameLayout的一個自定義控件,作為整個View層的容器,及View樹的根節點。
  • Window是虛擬的概念,DecorView才是看得見,摸得著的東西,Activity.setContentView()實際調用的是PhoneWindow.setContentView(),在這里面實現了DecorView的初始化和id為content的FraneLayout的布局容器的初始化,并且會根據主題等配置,選擇不同的xml文件。而且在Activity.setContentView()之后,Window的一些特征位將被鎖定。
  • Activity.findViewById()實際上調用的是DecorView的findviewById(),這個方法在View中定義,但是是final的,實際起作用的是在ViewGroup中被重寫的findViewTraversal()方法。
  • Activity的mWindow成員變量是在attach()的時候被初始化的,attach()是Activity被通過反射手段實例化之后調用的第一個方法,在這之后生命周期方法才會依次調用
  • 在onResume()剛執行之后,界面還是不可見的,只有執行完Activity.makeVisible(),DecorView才對用戶可見
  • ViewManager這個接口里面就三個接口,添加、移除和更新,實現這個接口的有WindowManager和ViewGroup,但是他們兩個面向的對象是不一樣的,WindowManager實現的是對Window的操作,而ViewGroup則是對View的增、刪、更新操作。
  • WindowManagerImpl是WindowManager的實現類,但是他就是一個代理類,代理的是WindowManagerGlobal,WindowManagerGlobal一個App里面就有一個,因為它是單例的,它里面管理了App中所有打開的DecorView,ContentView和PhoneWindow的布局參數WindowManager.LayoutParams,而且WindowManagerGlobal這個類是和WMS通信用的,是通過IWindowSession對象完成這個工作的,而IWindowSession一個App只有一個,但是每個ViewRootImpl都持有對IWindowSession的引用,所以ViewRootImpl可以和WMS喊話,但是WMS怎么和ViewRootImpl喊話呢?是通過ViewRootImpl::W這個內部類實現的,而且源碼中很多地方采用了這種將接口隱藏為內部類的方式,這樣可以實現六大設計原則之一——接口最小原則,這樣ViewRootImpl和WMS就互相持有對方的代理,就可以互相交流了
  • ViewRootImpl這個類每個Activity都有一個,它負責和WMS通信,同時相應WMS的指揮,還負責View界面的測量、布局和繪制工作,所以當你調用View.invalidate()和View.requestLayout()的時候,都會把事件傳遞到ViewRootImpl,然后ViewRootImpl計算出需要重繪的區域,告訴WMS,WMS再通知其他服務完成繪制和動畫等效果,當然,這是后話,咱們以后再說。
  • Window分為三種,子窗口,應用窗口和系統窗口,子窗口必須依附于一個上下文,就是Activity,因為它需要Activity的appToken,子窗口和Activity的WindowManager是一個的,都是根據appToken獲取的,描述一個Window屬于哪種類型,是根據LayoutParam.type決定的,不同類型有不同的取值范圍,系統類的的Window需要特殊權限,當然Toast比較特殊,不需要權限
  • PopupWindow使用的時候,如果想觸發按鍵和觸摸事件,需要添加一個背景,代碼中會根據是否設置背景進行不同的邏輯判斷
  • Dialog在Activity不可見的時候,要主動dismiss掉,否則會因為appToken為空crash
  • Toast屬于系統窗口,由系統服務NotificationManagerService統一調度,NotificationManagerService中維持著一個集合ArrayList<ToastRecord>,最多存放50個Toast,但是NotificationManagerService只負責管理Toast,具體的現實工作由Toast::TN來實現

最后來一張Android的窗口管理框架(轉自ariesjzj

OK,關于Activity的界面顯示就說到這里吧,本篇文章大部分的內容來自于閱讀下面參考文章之后的總結和思考,想了解更詳細的可以研究下。

下次再見,拜拜~

參考文章


尊重原創,轉載請注明:From 凱子哥(<a >http://blog.csdn.net/zhaokaiqiang1992</a>) 侵權必究!

關注我的微博,可以獲得更多精彩內容:http://weibo.com/zhaokaiqiang1992

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容