【凱子哥帶你學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文件中聲明的布局文件,會被添加進去

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

咱們下篇再說~


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

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

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

推薦閱讀更多精彩內容