View工作原理之Activity,Window和View的关系

1.概述

    Window做为一个窗口的概念,在Android的日常开发中很常见,但是一般不直接操作Window,而是操作View。Window其实是高于View的一个层次了,View都是由Window接管的,具体实现是WindowManagerService,外界通过WindowManager访问Window。Activity(视图层),Dialog,Toast本质上都是通过Window来展示的,它们都附加在Window之上,Window是View的管理者。就连点击事件也是从Window传给DecorView,最后再由DecorView传给我们的View。

2.从setContentView()源码看起

  • 2.1 Activity::setContentView()
 public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
 }
 public Window getWindow() {
    return mWindow;
 }
  • 2.2 mWindow的初始化是在Activity的attach方法中执行的。了解过Activity启动流程,我们知道:attach方法是在Activity启动流程中,ActivityThread::performLaunchActivity()方法中被调用的,是在Activity被创建后,完成初始化操作的。
 //android.app.Activity#attach()
 final void attach(Context context, ActivityThread aThread,   ... Window window ...) {
       ...
      //这里进行mWindow的初始化,可以看到Activity中的Window实现类是PhoneWindow,
      //目前为止,PhoneWindow也是Window的唯一实现类
      mWindow = new PhoneWindow(this, window, activityConfigCallback);
      //这里是给Window设置了WindowManager,WindowManager是通过IPC获取的系统服务,
      //WindowManager只是一个接口类型,具体实现是WindowManagerImpl类,
      //当然WindowManagerImpl又将实际的逻辑实现交给了WindowManagerGlobal类
      mWindow.setWindowManager( (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
       mToken, mComponent.flattenToString(),
      (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
      //Activity持有的WindowManager也是从Window中拿过来的
      mWindowManager = mWindow.getWindowManager();
       ....
 }
  • 2.3 继续看PhoneWindow的setContentView()实现代码
    @Override
    public void setContentView(int layoutResID) {
       //mContentParent其实就是android.R.id.content布局对应的实际展示的内容
       if (mContentParent == null) {
           //这个方法时构造一个顶层的DecorView对象,其实是直接通过new DecorView产生的实例对象,并赋值给mDecor变量
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
           //第二次调用setContentView()方法时走这里,会先remove掉所有的子View再通过inflate进行加载布局
            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();
        }
        //这个变量,会在requestFeature()方法调用时判断时机是否正确,
        //如果实在setContentView之后调用的,会抛出"requestFeature() must be called before adding content"的异常
        mContentParentExplicitlySet = true;
 }
  //installDecorView():构造Activity视图框架的根视图,并通过LayoutInflater加载mContentParent,
 //我们一般操作的setContentView其实就是将布局展示到了mContentParent中
  private void installDecor() {
        ...
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
           //DecorView已经构造好了,可以从DecorView中通过findViewById的方式实例化mContentParent对象了
            mContentParent = generateLayout(mDecor);
        }
        ...
 }  
  • 2.4 generateLayout(mDecor)也是获取mContentParent对象的关键方法
protected ViewGroup generateLayout(DecorView decor) {
       ...
       // Inflate the window decor.
        int layoutResource;
        int features = getLocalFeatures();
         // System.out.println("Features: 0x" + Integer.toHexString(features));
        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;
        }
        ....
        //mDecorView虽然已经初始化了,但是他的布局还未加载,通过上面对features变量值的一堆if-else判断,
        //获取到对应的feature值的布局文件,再通过inflater对象加载布局
        mDecor.startChanging();
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
        //这里获取的就是mContentParent对象,findViewById是View的方法,这里是间接调用了DecorView的findViewById方法,
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        ...
        return contentParent;
}
  • 2.5 查找ID_ANDROID_CONTENT变量,可以看到:其值是android.R.id.content
/**
     * The ID that the main layout in the XML layout file should have.
     */
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
  • 2.6 另外: DecorView加载布局文件资源的方法是:onResourcesLoaded()
 void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
   ...
    //这里inflate方法的root直接传null,的确这已经是跟布局了,肯定是没有父View可以传了
   final View root = inflater.inflate(layoutResource, null);
   ...
}

3.源码分析结论

由以上2.1~2.6对源码的分析,可以得到以下结论:

  • 3.1 Activity、Window和View的依赖关系:


    Activity、Window和View的依赖关系
  • 3.2 setContentView()执行的序列图:


    setContentView()执行的序列图

    Activity展示的其实是PhoneWindow上的内容。那么其实 setContentView 实际上是调用的 PhonwWindow的setContentView。可以看出Window是做为中介者连接了Activity和View。

  • 3.3.Activity的视图框架结构


    Activity的视图逻辑
  • 3.4 解释一下DecorView的布局资源的加载逻辑,features变量是怎么来的:

  • 再观察generateLaout()的实现如下:

protected ViewGroup generateLayout(DecorView decor) {
    ...
    int layoutResource;
    int features = getLocalFeatures(); //获取的是mLocalFeatures值
     //下面是一堆用feastures值来判断改用哪个布局文件的逻辑
     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);
    } 
    ...
    mDecor.startChanging();
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    ...
}
  • 那么,mLocalFeatures的值是怎么来的?直接搜索mLocalFeatures被赋值的地方,发现是通过getDefaultFeatures()方法拿到的,但是这个只是拿到默认的features值,这是不够的。反推,上面都是位操作,我们要搜索mFeatures和mLocalFeaures的位操作地方了,源码里面用一个int值代表一堆标记位的事情也不少了。果然,发现,是在PhonwWindow的requestFeature()中设置的标记位:
 public boolean requestFeature(int featureId) {
     if (mContentParentExplicitlySet) {
            throw new AndroidRuntimeException("requestFeature() must be called before adding content");
     }
    final int features = getFeatures();
    final int newFeatures = features | (1 << featureId);
    if ((newFeatures & (1 << FEATURE_CUSTOM_TITLE)) != 0 &&
            (newFeatures & ~CUSTOM_TITLE_COMPATIBLE_FEATURES) != 0) {
           throw new AndroidRuntimeException(
                    "You cannot combine custom titles with other title features");
     }
    ...
    //Window的requestFeature()会将featureId通过按位或运算记录到mFeatures变量中
    return super.requestFeature(featureId);
 }

这样也能解释的通了,为什么在Activity中使用requestFeature()要在setContentView()前调用了,以为这个值是给Activity的DecorView选择加载那种样式的布局文件用的。如果在setContentView之后调用,这个时候PhoneWindow::generateLayout(DecorView)已经执行完成了,并不会生效。

4.小结

  • 阅读源码不能死嗑细节,要带着目的去阅读。Android源码不同于一般的业务代码(迭代版本多了的业务代码照样可怕),随便一个类可能就动辄上万行,如果要搞懂每一个细节不现实。
  • 带着问题阅读,效果效率都会更好。比如,
    • setContentView是怎么把我们写的layout文件内容加载到Activity上面?
    • 为啥要转调到mWindow的setContentView上去了,抽象类是不能直接构造实例对象的,一定是有个实现类去实现,他的实现类是谁?
    • ...类似的问题很多,这些问题能够引导我们一步步“破局”
    • 阅读完毕,要整理结论,不能看完就没有下文了,一堆代码谁都能看的懂,只有总结整理成自己的东西才是真正有用的东西
  • 阅读过程中,联想从源码中可以避开哪些工作上的坑。
    • requestFeature必须要在setContentView前执行。
      如果setContentView在同一个Activity中第二次执行,即使在第二次setContentView前调用requestFeature方法也是会抛出异常的, 因为第一次 调用setContentView,mContentParentExplicitlySet变量已经做了标记,并且mContenParent也是已经初始化好了,不会因为调用setContentView就重新初始化。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271