天天看點

Android 從源碼角度分析View、Activity、Window之間的關系

對于 setContentView(layoutResId) 方法,相信大家再熟悉不過了,那麼對于該方法的内部實作呢?對我個人而言,從來沒研究過,前期學習過程中隻要不報錯,程式能跑起來,就ok,這也就是每次看到别人文章的時候,總是表現出一種我要是這麼牛B就行了,哈哈,然而在群演的角色演繹上永無止境的大步向前走,要想改變這種鹹魚現狀,隻能強迫自己搞點事情啦——閱讀操蛋的源碼,好了,廢話不多說,開始今天的主題。

一、從源碼角度分析 setContentView(layoutResId)

先點進 Activity 的 setContentView(layoutResId) 瞧一瞧,代碼如下:

public void setContentView(@LayoutRes int layoutResID) {

   getWindow().setContentView(layoutResID);
   initWindowDecorActionBar();
}
           

代碼很少,第1行,可以看出通過 getWindow 擷取到 Window 執行個體,然後調用該執行個體的 setContentView(layoutResID) 方法,第2行,從英文意思中可以看到是初始化 ActionBar 之類的東西,暫不做深究。

接着看 getWindow 方法,代碼如下:

public Window getWindow() {
   return mWindow;
}
           

該方法直接傳回一個 Window 對象,看到這裡,目前能肯定的就是在 setContentView 方法執行之前 mWindow 對象就已經被初始化了,這個應該沒有什麼分歧吧?要不然會報空指針異常的。

那麼 mWindow 執行個體到底是在哪裡被初始化?接着找會發現在 Activity 中的 attach 方法中發現了 mWindow 的初始化,代碼如下:

mWindow = new PhoneWindow(this, window, activityConfigCallback);
           

PhoneWindow 又是個什麼鬼?繼續查閱源碼:

public class PhoneWindow extends Window implements MenuBuilder.Callback
           

可以看出 PhoneWindow 是 Window 的一個子類,并且是唯一的子類。接着可以得出這樣一個結論:mWindow 其實是一個 PhoneWindow 類對象,那麼也就可以看出,在最上面的源碼中,其實是調用了 PhoneWindow 類中的 setContentView 方法。

按照上面的分析,直接在 PhoneWindow 類中尋找 setContentView 方法,查到的結果總共有三個重載方法,代碼如下:

1、public void setContentView(int layoutResID) 
2、public void setContentView(View view)
3、public void setContentView(View view, ViewGroup.LayoutParams params)
           

根據上面傳參,我們可以确定,調用的是第1個方法,接着展開方法,代碼如下:

@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);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }
           

mContentParent 是一個 ViewGroup 類型的全局變量,剛進入方法中,mContentParent 對象為 null,是以會執行 installDecor 方法,由于 installDecor 方法代碼過多,這裡隻展示關鍵代碼,代碼如下:

private void installDecor() {
       
        if (mDecor == null) {
        
            mDecor = generateDecor(-1);
        } else {
        
            mDecor.setWindow(this);
        }
        
        if (mContentParent == null) {
        
            mContentParent = generateLayout(mDecor);
        }
    }
           

mDecor 是 DecorView 的一個執行個體對象,DecorView 繼承于 Fragment 類,它被認為是 Android 系統中 View 視圖的根節點。方法開始的時候 mDecor 并沒有被初始化,是以會執行方法 generateDecor,接着看該方法的具體實作,代碼如下:

protected DecorView generateDecor(int featureId) {

        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext().getResources());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
    }
           

該方法主要是初始化 DecorView 對象,繼續看?上面?的 installDecor 方法,接下來會執行一個 generateLayout 方法,繼續點進去看,代碼太多,精簡一番,代碼如下:

protected ViewGroup generateLayout(DecorView decor) {
             
        
        int layoutResource;
        int features = getLocalFeatures();
       
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
            setCloseOnSwipeEnabled(true);
        } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
            if (mIsFloating) {
                TypedValue res = new TypedValue();getContext().getTheme().resolveAttribute(R.attr.dialogTitleIconsDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else {
               
                layoutResource = R.layout.screen_title_icons;
            }
            
            removeFeature(FEATURE_ACTION_BAR);
            
        } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0 && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
            
            layoutResource = R.layout.screen_progress;
        } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
           
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(R.attr.dialogCustomTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else {
                
                layoutResource = R.layout.screen_custom_title;
            }
           
            removeFeature(FEATURE_ACTION_BAR);
        } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
           
            if (mIsFloating) {
               
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(R.attr.dialogTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
                layoutResource = a.getResourceId(R.styleable.Window_windowActionBarFullscreenDecorLayout,R.layout.screen_action_bar);
            } else {
               
                layoutResource = R.layout.screen_title;
            }
          
        } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
           
            layoutResource = R.layout.screen_simple_overlay_action_mode;
        } else {
           
            layoutResource = R.layout.screen_simple;
        } 
        
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
        
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
       
        if (contentParent == null) {
           
            throw new RuntimeException("Window couldn't find content container view");
        }
        
        
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            registerSwipeCallbacks(contentParent);
        }
  
        mDecor.finishChanging();
        
        return contentParent;
    }
           

由上往下看,我們首先會根據不同的視窗特性,來篩選與之對應的布局檔案,下面會依次列出 8 種布局檔案,分析到這裡說個題外話,如果想去掉标題欄 TitleBar,那麼我們必須在 setContentView 方法之前調用 requestWindowFeature 方法,否則無效。 然後緊接着會調用 DecorView 中的 onResourcesLoaded 方法,看看該方法到底做了什麼事,删減之後,代碼如下:

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {

        mDecorCaptionView = createDecorCaptionView(inflater);
        final View root = inflater.inflate(layoutResource, null);
       
        if (mDecorCaptionView != null) {
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            
            mDecorCaptionView.addView(root,new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {
        
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT,MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        initializeElevation();
    }
           

上面提到 8 種布局檔案

  • screen_swipe_dismiss
  • screen_title_icons
  • screen_progress
  • screen_action_bar
  • screen_custom_title
  • screen_title
  • screen_simple_overlay_action_mode
  • screen_simple

先看 onResourcesLoaded 方法,很明顯,傳入的布局檔案 layoutResource 通過 inflate 獲得了 View 對象 root,然後調用 addView 方法,将 root 添加到 DecorView 中,換句話說,将傳入的布局檔案 layoutResource 添加到 FrameLayout 布局中。再回到 generateLayout 方法中,緊接着又定義了一個 ViewGroup 對象 contentParent,contentParent 是擷取布局檔案 layoutResource 中 id 為 content 的控件,且該控件為FrameLayout,最後将 contentParent 作為方法的傳回值傳回。

這 8 種布局檔案路徑是 frameworks / base / core / res / res / layout,并且這些布局中有一個公共點,全部有一個 id 為 content 的 FrameLayout 控件 ( ID_ANDROID_CONTENT 是Window 中定義的 int 型常量,值為com.android.internal.R.id.content )。

重頭梳理一下 generateLayout 方法可以得出這樣一個流程:

  1. 根據系統設定的視窗特性找到與之對應的布局
  2. 将改布局檔案添加到 DecorView 中
  3. contentParent 擷取布局中 id 為 content 的 FrameLayout 控件

分析完了 generateLayout 方法,回到 setContentView 方法中,繼續執行一個 if 判斷語句,代碼如下:

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
 
         final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext());
         transitionTo(newScene);
      } else {
 
         mLayoutInflater.inflate(layoutResID, mContentParent);
      }
           

最後一步,将我們設定的布局 layoutResID 添加到頂級視圖 ( DecorView ) 中 id 為 content 的 FrameLayout 控件中,正好也就解釋了,設定布局的方法名叫做 setContentView 而不是 setView。

二、DecorView 如何與 Window 關聯

經過上面對 setContentView 的分析,PhoneWindow 和 DecorView 執行個體被建立完成,那麼 DecorView 是何時被添加到主視窗 PhoneWindow 的呢?這個過程是在 onResume 後完成的,ActivityThread 在執行完 performLaunchActivity 後,便會執行 handlerResumeActivity() 代碼精簡後見下方:

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {

    ActivityClientRecord r = performResumeActivity(token, clearHide);
 
    if (r != null) {
        final Activity a = r.activity;
        boolean willBeVisible = !a.mStartedActivity;

        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (a.mVisibleFromClient) {
                a.mWindowAdded = true;
                wm.addView(decor, l);
            }
        }

        if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
                
            mNumVisibleActivities++;
            if (r.activity.mVisibleFromClient) {
              
                r.activity.makeVisible(); 
            }
        }
}
           

分析 handleResumeActivity 方法内部執行流程:

  • handleResumeActivity 方法中調用 performResumeActivity 方法
  • performResumeActivity 方法中調用 performResume 方法
  • performResume 方法中執行 activity.performResume 方法
  • activity.performResume 方法中調用mInstrumentation.callActivityOnResume
  • mInstrumentation.callActivityOnResume 中調用 activity.onResume()

經過上述前 4 步,就會執行到 onResume 方法, 回到 handleResumeActivity 方法中接着往下看,通過 getDecorView 擷取到 DecorView 對象執行個體 decor,這個沒什麼多說的,此時 decor 就是我們在上面分析 setContentView 中擷取到的 DecorView,通過調用方法 getWindowManager 擷取 ViewManager 執行個體 wm。

  • 在開發過程中盡量不要在 onResume 方法中去執行太多複雜的邏輯,不然頁面加載會有一種卡頓現象。

通過流程圖的方式直覺描述一下整體流程:

Android 從源碼角度分析View、Activity、Window之間的關系

圖檔是盜的,不糾結 ~ _~

當獲得DecorView對象後,先執行了一次setVisibility(View.INVISIBLE)操作,執行完addView()操作後才會重新設定為VISIBLE,此處的做法類似于SurfaceView繪制過程對Canvas的鎖操作,頁面的顯示需要由過渡動畫管理器TranslateManager進行控制,如果直接在可見狀态下進行頁面繪制,給使用者一種頁面加載卡頓的感覺,而等待頁面全部繪制完畢後再整體展示給使用者可以有效的避免這個問題。

Activity 中的 getWindowManager 方法:

public WindowManager getWindowManager() {
        return mWindowManager;
    }
           

繼續找會發現 mWindowManager 在 Activity 中的 attach 方法中被初始化,關鍵代碼如下:

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
            
        attachBaseContext(context);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);

        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    }
           

正是在 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);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }
           

繼續看 createLocalWindowManager 方法,代碼如下:

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

傳回的是一個 WindowManagerImpl 對象,是以可以得出 ViewManager 執行個體 wm 其實是一個 WindowManagerImpl 對象,回到 handleResumeActivity 方法中接着往下看,關鍵的一句 wm.addView ,那麼直接在 WindowManagerImpl 中查詢 addView方法,代碼如下:

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

可以看到,WindowManagerImpl 其内部方法始終持有 WindowManagerGlobal 的引用 mGlobal,實際也是調用了 WindowManagerGlobal 的 addView,在 WindowManagerGlobal 中查找對應的 addView 方法,代碼精簡如下:

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;

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

         view.setLayoutParams(wparams);

         mViews.add(view);
         mRoots.add(root);
         mParams.add(wparams);

         try {
             root.setView(view, wparams, panelParentView);
         } catch (RuntimeException e) {
            
          if (index >= 0) {
                 removeViewLocked(index, true);
          }
             throw e;
          }
        }
    }
           

ViewRootImpl 就是一個 ViewTree 管理類,DecorView 作為 ViewTree 根布局。每個傳進來的 DecorView 都會建立一個對應的 ViewRootImpl 來管理。ViewRootImpl 将實際控制着 DecorView 的繪制周期,同時還可以與WMS進行Binder通信。ViewRootImpl 在調用 setView 後,即向 WMS 發起了添加請求,WMS 便會将目前的 PhoneWindow 放入自身管理的 Window 清單中,将 DecorView 添加到 PhoneWindow 上,并且通知 ViewRootImpl 進行繪制操作,這樣 View 和 Window 就關聯起來了。這裡關于通信方面的知識,就不多說了,畢竟自己還不是很懂,是以就引用别人的結論。之後,要是弄清楚通信相關的内容會及時更新。

  • 分析到這裡也可以解釋一個問題,在 onCreate 方法中開啟一個子線程去更新 UI,系統并不會報錯,那是因為更新 UI 時,ViewRootImpl 類還沒有被建立,是以根本不會調用 checkThread 方法去判斷目前 Thread 是否是 MainThread

三、推薦相關文章

  • 全面解析Activity、Window、View三者關系,看了都說好

本篇文章是對 View、Activity、Window 三者關系相關知識點的一個歸納、總結,一方面鞏固知識,另一方面也要學會從源碼角度去分析、處理問題,後續如有相關知識,會持續更新内容,文章中若有錯誤之處、或不足之處,歡迎指正!

繼續閱讀