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

前幾天凱子哥寫的Framework層的解析文章《Activity啓動過程全解析》,反響還不錯,這說明“寫讓大家都能看懂的Framework解析文章”的思想是基本正確的。

我個人覺得,深入分析的文章必不可少,但是對於更多的Android開發者——即只想做應用層開發,不想了解底層實現細節——來說,“整體上把握,重要環節深入“是更好的學習方式。因爲這樣既可以有完整的知識體系,又不會在浩瀚的源碼世界裏迷失興趣和方向。

所以呢,今天凱子哥又帶來一篇文章,接着上一篇的結尾,重點介紹Activity開啓之後,Android系統對界面的一些操作及相關知識。

本期關鍵字

  • Window
  • PhoneWindow
  • WindowManager
  • WindowManagerImpl
  • WindowManagerGlobal
  • RootViewImpl
  • DecorView
  • Dialog
  • PopWindow
  • Toast

學習目標

  • 瞭解Android中Activity界面顯示的流程,涉及到的關鍵類,以及關鍵流程
  • 解決在開發中經常遇到的問題,並在源碼的角度弄清楚其原因
  • 瞭解Framework層與Window相關的一些概念和細節

寫作方式

老樣子,咱們還是和上次一樣,採用一問一答的方式進行學習,畢竟“帶着問題學習”纔是比較高效的學習方式。

進入正題

話說,在上次的文章中,我們解析到了從手機開機第一個zygote進程開啓,到App的第一個Activity的onCreate()結束,那麼我們這裏就接着上次留下的茬,從第一個Activity的onCreate()開始說起。

onCreate()中的setContentView()到底做了什麼?爲什麼不能在setContentView()之後設置某些Window屬性標誌?

一個最簡單的onCreate()如下:

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

通過上面幾行簡單的代碼,我們的App就可以顯示在activity_main.xml文件中設計的界面了,那麼這一切到底是怎麼做到的呢?

我們跟蹤一下源碼,然後就在Activity的源碼中找到了3個setContentView()的重載函數:

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

    public void setContentView(View view) {
        getWindow().setContentView(view);
        initWindowDecorActionBar();
    }

    public void setContentView(View view, ViewGroup.LayoutParams params) {
        getWindow().setContentView(view, params);
        initWindowDecorActionBar();
    }

我們上面用到的就是第一個方法。雖然setContentView()的重載函數有3種,但是我們可以發現,內部做的事情都是基本一樣的。首先是調用getWindow()獲取到一個對象,然後調用了這個對象的相關方法。

咱們先來看一下,getWindow()到底獲取到了什麼對象。

private Window mWindow;

public Window getWindow() {
        return mWindow;
    }

喔,原來是一個Window對象,你現在可能不知道Window到底是個什麼玩意,但是沒關係,你只要能猜到它肯定和咱們的界面顯示有關係就得了,畢竟叫“Window”麼,Windows系統的桌面不是叫“Windows”桌面麼,差不多的東西,反正是用來顯示界面的就得了。

那麼initWindowDecorActionBar()函數是做什麼的呢?

寫了這麼多程序,看名字也應該能猜出八九不離十了,init是初始化,Window是窗口,Decor是裝飾,ActionBar就更不用說了,所以這個方法應該就是”初始化裝飾在窗口上的ActionBar”,來,咱們看一下代碼實現:

/**
     * Creates a new ActionBar, locates the inflated ActionBarView,
     * initializes the ActionBar with the view, and sets mActionBar.
     */
    private void initWindowDecorActionBar() {
        Window window = getWindow();

        // Initializing the window decor can change window feature flags.
        // Make sure that we have the correct set before performing the test below.
        window.getDecorView();

        if (isChild() || !window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) {
            return;
        }

        mActionBar = new WindowDecorActionBar(this);
        mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp);

        mWindow.setDefaultIcon(mActivityInfo.getIconResource());
        mWindow.setDefaultLogo(mActivityInfo.getLogoResource());
    }

喲,沒想到這裏第一行代碼就又調用了getWindow(),接着往下調用了window.getDecorView(),從註釋中我們知道,在調用這個方法之後,Window的特徵標誌就被初始化了,還記得如何讓Activity全屏嗎?

@Override

    public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

    requestWindowFeature(Window.FEATURE_NO_TITLE); 
    getWindow().setFlags(WindowManager.LayoutParams.FILL_PARENT,                  WindowManager.LayoutParams.FILL_PARENT);

    setContentView(R.layout.activity_main);
    }

而且這兩行代碼必須在setContentView()之前調用,知道爲啥了吧?因爲在這裏就把Window的相關特徵標誌給初始化了,在setContentView()之後調用就不起作用了!

如果你還不確定的話,我們可以再看下window.getDecorView()的部分註釋

 /**
     * Note that calling this function for the first time "locks in"
     * various window characteristics as described in
     * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}
     */
    public abstract View getDecorView();

“注意,這個方法第一次調用的時候,會鎖定在setContentView()中描述的各種Window特徵”
所以說,這也同樣解釋了爲什麼在setContentView()之後設置Window的一些特徵標誌,會不起作用。如果以後遇到類似問題,可以往這方面想一下。

Activity中的findViewById()本質上是在做什麼?

在上一個問題裏面,咱們提到了一個很重要的類——Window,下面先簡單看一下這個類的幾個方法:

public abstract class Window {

    public abstract void setContentView(int layoutResID);

    public abstract void setContentView(View view);

    public abstract void setContentView(View view, ViewGroup.LayoutParams params);

    public View findViewById(int id) {
        return getDecorView().findViewById(id);
    }
}

哇塞,有個好眼熟的方法,findViewById()!

是的,你每次在Activity中用的這個方法,其實間接調用了Window類裏面的方法!

 public View findViewById(int id) {
        return getWindow().findViewById(id);
    }

不過,findViewById()的最終實現是在View及其子類裏面的,所以getDecorView()獲取到的肯定是一個View對象或者是View的子類對象:

public abstract View getDecorView();

Activity、Window中的findViewById()最終調用的,其實是View的findViewById()。

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {

    public final View findViewById(int id) {
         if (id < 0) {
                return null;
            }
            return findViewTraversal(id);
        }

        protected View findViewTraversal(int id) {
            if (id == mID) {
                return this;
            }
            return null;
        }   
    }

但是,很顯然,最終調用的肯定不是View類裏面的findViewTraversal(),因爲這個方法只會返回自身。
而且,findViewById()是final修飾的,不可被重寫,所以說,肯定是調用的被子類重寫的findViewTraversal(),再聯想到,我們的界面上有很多的View,那麼既能作爲View的容器,又是View的子類的類是什麼呢?很顯然,是ViewGroup!

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    @Override
    protected View findViewTraversal(int id) {
        if (id == mID) {
            return this;
        }

        final View[] where = mChildren;
        final int len = mChildrenCount;

        for (int i = 0; i < len; i++) {
            View v = where[i];

            if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
                v = v.findViewById(id);

                if (v != null) {
                    return v;
                }
            }
        }

        return null;
    }
}

所以說,在onCreate()中調用findViewById()對控件進行綁定的操作,實質上是通過在某個View中查找子View實現的,這裏你先記住,這個View叫做DecorView,而且它位於用戶窗口的最下面一層。

Window和PhoneWindow是什麼關係?WindowManager是做什麼的?

話說,咱們前面介紹Window的時候,只是簡單的介紹了下findViewById(),還沒有詳細的介紹下這個類,下面咱們一起學習一下。

前面提到過,Window是一個抽象類,抽象類肯定是不能實例化的,所以咱們需要使用的是它的實現類,Window的實現類有哪些呢?咱們從Dash中看下Window類的文檔

Window只有一個實現類,就是PhoneWindow!所以說這裏扯出了PhoneWindow這個類。

而且文檔還說,這個類的一個實例,也就是PhoneWindow,應該被添加到Window Manager中,作爲頂層的View,所以,這裏又扯出了一個WindowManager的概念。

除此之外,還說這個類提供了標準的UI策略,比如背景,標題區域,和默認的按鍵處理等等,所以說,咱們還知道了Window和PhoneWindow這兩個類的作用!

所以說,看文檔多重要呀!

OK,現在咱們已經知道了Window和唯一的實現類PhoneWindow,以及他們的作用。而且我們還知道了WindowManager,雖然不知道幹嘛的,但是從名字也可以猜出是管理Window的,而且還會把Window添加到裏面去,在下面的模塊中,我會詳細的介紹WindowManager這個類。

Activity中,Window類型的成員變量mWindow是什麼時候初始化的?

在每個Activity中都有一個Window類型的對象mWindow,那麼是什麼時候初始化的呢?

是在attach()的時候。

還記得attach()是什麼時候調用的嗎?是在ActivityThread.performLaunchActivity()的時候:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

     Activity activity = null;
        try {
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            } catch (Exception e) {
             ...ignore some code...
        }

    try {

        ...ignore some code...

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

        ...ignore some code...

    } catch (Exception e) {  }

     return activity;
}

在attach()裏面做了些什麼呢?

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback {

    private Window mWindow;

    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) {

             ...ignore some code...

             //就是在這裏實例化了Window對象
              mWindow = PolicyManager.makeNewWindow(this);
              //設置各種回調
            mWindow.setCallback(this);
            mWindow.setOnWindowDismissedCallback(this);
            mWindow.getLayoutInflater().setPrivateFactory(this);

             //這就是傳說中的UI線程,也就是ActivityThread所在的,開啓了消息循環機制的線程,所以在Actiivty所在線程中使用Handler不需要使用Loop開啓消息循環。
             mUiThread = Thread.currentThread();

             ...ignore some code...

            //終於見到了前面提到的WindowManager,可以看到,WindowManager屬於一種系統服務
            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();

            }

}

attach()是Activity實例化之後,調用的第一個函數,在這個時候,就實例化了Window。那麼這個PolicyManager是什麼玩意?

mWindow = PolicyManager.makeNewWindow(this);

來來來,咱們一起RTFSC(Read The Fucking Source Code)!

public final class PolicyManager {
    private static final String POLICY_IMPL_CLASS_NAME =
        "com.android.internal.policy.impl.Policy";

    private static final IPolicy sPolicy;
    static {
        // Pull in the actual implementation of the policy at run-time
        try {
            Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
            sPolicy = (IPolicy)policyClass.newInstance();
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException(
                    POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);
        } catch (InstantiationException ex) {
            throw new RuntimeException(
                    POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
        } catch (IllegalAccessException ex) {
            throw new RuntimeException(
                    POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
        }
    }

    private PolicyManager() {}

    public static Window makeNewWindow(Context context) {
        return sPolicy.makeNewWindow(context);
    }

    }

“Policy”是“策略”的意思,所以就是一個策略管理器,採用了策略設計模式。而sPolicy是一個IPolicy類型,IPolicy實際上是一個接口

public interface IPolicy {}

所以說,sPolicy的實際類型是在靜態代碼塊裏面,利用反射進行實例化的Policy類型。靜態代碼塊中的代碼在類文件加載進類加載器之後就會執行,sPolicy就實現了實例化。

那我們看下在Policy裏面實際上是做了什麼

public class Policy implements IPolicy {

    //看見PhoneWindow眼熟麼?還有DecorView,眼熟麼?這就是前面所說的那個位於最下面的View,findViewById()就是在它裏面找的
    private static final String[] preload_classes = {
        "com.android.internal.policy.impl.PhoneLayoutInflater",
        "com.android.internal.policy.impl.PhoneWindow",
        "com.android.internal.policy.impl.PhoneWindow$1",
        "com.android.internal.policy.impl.PhoneWindow$DialogMenuCallback",
        "com.android.internal.policy.impl.PhoneWindow$DecorView",
        "com.android.internal.policy.impl.PhoneWindow$PanelFeatureState",
        "com.android.internal.policy.impl.PhoneWindow$PanelFeatureState$SavedState",
    };

    //由於性能方面的原因,在當前Policy類加載的時候,會預加載一些特定的類
     static {
           for (String s : preload_classes) {
            try {
                Class.forName(s);
            } catch (ClassNotFoundException ex) {
                Log.e(TAG, "Could not preload class for phone policy: " + s);
            }
        }
    }

    //終於找到PhoneWindow了,我沒騙你吧,前面咱們所說的Window終於可以換成PhoneWindow了~
    public Window makeNewWindow(Context context) {
        return new PhoneWindow(context);
        }

}

PhoneWindow.setContentView()到底發生了什麼?

上面說了這麼多,實際上只是追蹤到了PhoneWindow.setContentView(),下面看一下到底在這裏執行了什麼:

@Override
    public void setContentView(int layoutResID) {
         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);
        }
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

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

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {

        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);
        } else {
            mContentParent.addView(view, params);
        }
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

當我們第一次調用serContentView()的時候,mContentParent是沒有進行過初始化的,所以會調用installDecor()。

爲什麼能確定mContentParent是沒有初始化的呢?因爲mContentParent就是在installDecor()裏面賦值的

private void installDecor() {

     if (mDecor == null) {
            mDecor = generateDecor();
            ...
        }

         if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
          }

}

在generateDecor()做了什麼?返回了一個DecorView對象。

    protected DecorView generateDecor() {
            return new DecorView(getContext(), -1);
        }   

還記得前面推斷出的,DecorView是一個ViewGroup的結論嗎?看下面,DecorView繼承自FrameLayout,所以咱們的推論是完全正確的。而且DecorView是PhoneWindow的私有內部類,這兩個類關係緊密!

public class PhoneWindow extends Window implements MenuBuilder.Callback {
    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {}
}

咱們再看一下在對mContentParent賦值的generateLayout(mDecor)做了什麼

protected ViewGroup generateLayout(DecorView decor) {

    ...判斷並設置了一堆的標誌位...

    //這個是我們的界面將要採用的基礎佈局xml文件的id
    int layoutResource;

    //根據標誌位,給layoutResource賦值
     if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
        } 

    ...我們設置不同的主題以及樣式,會採用不同的佈局文件...

     else {
         //我們在下面代碼驗證的時候,就會用到這個佈局,記住它哦
            layoutResource = R.layout.screen_simple;
        }

        //要開始更改mDecor啦~
        mDecor.startChanging();
        //將xml文件解析成View對象,至於LayoutInflater是如何將xml解析成View的,咱們後面再說
        View in = mLayoutInflater.inflate(layoutResource, null);
        //decor和mDecor實際上是同一個對象,一個是形參,一個是成員變量
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;
     //這裏的常量ID_ANDROID_CONTENT就是 public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
     //而且,由於是直接執行的findViewById(),所以本質上還是調用的mDecor.findViewById()。而在上面的decor.addView()執行之前,decor裏面是空白的,所以我們可以斷定,layoutResource所指向的xml佈局文件內部,一定存在一個叫做“content”的ViewGroup
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        ......

        mDecor.finishChanging();
        //最後把id爲content的一個ViewGroup返回了
        return contentParent;
}

當上的代碼執行之後,mDecor和mContentParent就初始化了,往下就會執行下面的代碼,利用LayoutInflater把咱們傳進來的layoutResID轉化成View對象,然後添加到id爲content的mContentParent中

mLayoutInflater.inflate(layoutResID, mContentParent);

所以到目前爲止,咱們已經知道了以下幾個事實,咱們總結一下:

  • DecorView是PhoneWindow的內部類,繼承自FrameLayout,是最底層的界面
  • PhoneWindow是Window的唯一子類,他們的作用就是提供標準UI,標題,背景和按鍵操作
  • 在DecorView中會根據用戶選擇的不同標誌,選擇不同的xml文件,並且這些佈局會被添加到DecorView中
  • 在DecorView中,一定存在一個叫做“content”的ViewGroup,而且我們在xml文件中聲明的佈局文件,會被添加進去

既然是事實,那麼怎麼才能驗證一下呢?

如何驗證上一個問題

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

 //主題
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,最多存放50個Toast,但是NotificationManagerService只負責管理Toast,具體的顯示工作由Toast::TN來實現

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

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

下次再見,拜拜~

參考文章


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

關注我的微博,可以獲得更多精彩內容

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章