View的绘制源码分析

平时我们加载xml文件都是直接在onCreate方法中调用setContentView,在加载Activity的onCreate方法时布局就被加载出来了,但是深入一点看,发现内容还是很多的,看了很多大神相关的博客,也写了个总结,内容可能不全,不足之处,还请多指教。

1.先从setContentView开始说起

Activity中有3个setContentView()方法,可以看到, 这三个方法都是先getWindow,获得一个Window对象,然后调用它的setContentView,那么这个Window又是什么呢?

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();
}

1.1 创建Window对象

getWindow返回的是mWindow,而这个mWindow是由PolicyManager创建的,PolicyManager提供了静态类方法(这里用到了工厂模式,PolicyManager提供工厂方法),创建了一个PhoneWindow 对象。

//创建一个Window对象
mWindow = PolicyManager.makeNewWindow(this);

最终创建Window对象的方法

//创建具体对象的接口 
public PhoneWindow makeNewWindow(Context context) { 
    return new PhoneWindow(context); 
}

Window:是一个抽象类,提供绘制窗口的通用API。
PhoneWindow :是Window的唯一的实现类,每个Activity都会有一个PhoneWindow,它是Activity和整个View交互的接口。该类内部包含了一个DecorView对象,该DectorView对象是所有应用窗口(Activity界面)的根View。
DectorView:是PhoneWindow的内部类,继承自FrameLayout。

1.2 调用PhoneWindow对象的setContentView方法

一层层下来,发现Activity的setContentView()实际上是执行的是PhoneWindow的方法,现在来看一下PhoneWindow的setContentView()

@Override 
public void setContentView(int layoutResID) {    
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window    
    // decor, when theme attributes and the like are crystalized. Do not check the feature    
    // before this happens.    
   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();    
    }
}

以上代码分析:
1 . 判断mContentParent是否为空。从名字可以看出,这是父容器,它是一个ViewGroup类型的对象,是真正的content的Parent,如果是第一次调用,会调用installDecor(),在这个方法中会先判断DectorView是否为空,为空就先创建DectorView对象mDecor,然后调用generateLayout(mDecor)得到mContentParent;如果不是第一次调用,会先清除mContentParent中的子view。

2 . mLayoutInflater.inflate(layoutResID, mContentParent);将传入的资源文件转换成View树,再添加到mContentParent中(mLayoutInflater 在PhoneWindow的构造函数中通过mLayoutInflater = LayoutInflater.from(context)得到)

再来看一下其他的两个setContentView()

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

@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    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();
    }
}

可以看到,其实做的事情都差不多,只不过多设置了LayoutParams,因为传入的View,就不需要像第一个那样还从xml文件中解析出来,可以直接将view添加到mContentParent中。

小结:比较3个setContentView

从上面的代码中,我们可以看到,第一个setContentView是通过反射解析传入的布局文件,然后添加到mContentParent,而后两个setContentView是直接将传入的View添加到mContentParent,需要注意的是,每次反射拿到的View都是重新创建的,就算两次setContentView加载的是同一个布局文件,控件的实例也是不一样的,如传入的是View/ViewGroup就能保证传入的是同一组控件。

1.3 installDecor()实例化DectorView对象

现在来看一下刚刚提到的installDecor(),初始化mDecor ,创建mContentParent,根据窗口的风格修饰,选择对应的修饰布局文件,这里内容太多,省略了。

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor();
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    }
    if (mContentParent == null) {
        //根据窗口的风格修饰,选择对应的修饰布局文件
        mContentParent = generateLayout(mDecor);
        ......
    }
}

1.4 generateLayout()创建mContentParent

接下来看generateLayout()创建mContentParent 的过程。

protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.
    TypedArray a = getWindowStyle();
    //根据当前的主题设置窗口属性
    ......
    // Inflate the window decor.

    int layoutResource;
    int features = getLocalFeatures();

    //根据当前的窗口属性选择相对应的布局
    WindowManager.LayoutParams params = getAttributes();
    ......
    //将相应的布局文件转成view添加到窗口视图对象decor中
    View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    mContentRoot = (ViewGroup) in;

    ViewGroup contentParent = (ViewGroup)findViewById(ID_Android_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn't find content container view");
    }

    ......
    return contentParent;
}

这段代码所做的事情:

  1. 根据当前的主题设置窗口属性;
  2. 根据当前的窗口属性选择相对应的布局;
  3. 将相应的布局文件转成view添加到根视图对象decor/mDecor中
    View in = mLayoutInflater.inflate(layoutResource, null); decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));,需要注意的是,这里的layoutResource并不是我们传入的资源文件,而是系统定义的。
  4. 从根布局中找到id为ID_Android_CONTENT的ViewGroup赋值给contentParent,也就是上文的mContentParent

总结:Activity,PhoneWindow,DectorView,mContentParent之间的关系

通过上面的分析,我们来看一下彼此之间的关系,有助于理解。

DecorView继承于FrameLayout,然后它有一个子view即LinearLayout,方向为竖直方向,其内有两个FrameLayout,上面的FrameLayout即为TitleBar之类的,下面的FrameLayout即为我们的ContentView,所谓的setContentView就是往这个FrameLayout里面添加我们的布局View的!

DectorView及其下层view的结构
层级关系

2.PhoneWindow的setContentView最后的回调

上面分析了加载视图到父容器mContentParent中,现在我们看一下setContentView()中的最后一步。

@Override
public void setContentView(int layoutResID) {
    ......
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

至此,已经分析完了将传入的布局文件添加到View的整个过程。接下来可以看到,它先创建了一个CallBack回调接口,在加载完以上的View到根布局之后,就会调用这个回调接口,顺便说一下,cb.onContentChanged()方法在Activity中是一个空方法,我们可以在自定义的Activity中覆写这个方法。

现在看getCallback()是由Window提供的,PhoneWindow并没有实现,继续往下看,发现Window中有一个public void setCallback(Callback callback)方法,接收到外部传入的callback,赋值给内部的mCallback 。那么这个外部方法在哪里调用呢?这个就要说一下Activity的启动了。

3.Activity的启动

在Activity加载时会先创建一个activity实例,然后调用activity的attach方法完成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, IVoiceInteractor voiceInteractor) {
    attachBaseContext(context);
    
    mFragments.attachActivity(this, mContainer, null);

     //1.创建窗口对象,是一个PhoneWindow实例
    mWindow = PolicyManager.makeNewWindow(this);

    //2.设置回调
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
    
    ......

    mToken = token;
    
    //3.将创建的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());
    }
    //4. 获取窗口管理器
    mWindowManager = mWindow.getWindowManager();
    mCurrentConfig = config;
}
  1. 在这个方法中,完成了上文提到的Window对象的创建。

  2. 为Window对象设置回调,也就是上面 setContentView()中最后获取到cb。这里设置的回调就是Activity自己,再看Activity,它实现了Window.Callback, KeyEvent.Callback两个回调接口。其中 KeyEvent.Callback接口中声明了处理手势事件的方法(onKeyDown按下,onKeyUp抬起,onKeyLongPress长按...),而Window.Callback声明了一些事件分发的函数,关于View的事件分发,可以看这个 View的事件分发机制 ,从这里可以看出activity本身不具备处理用户的事件的能力。

  3. 为mWindow设置WindowManagerWindowManager主要用来管理窗口的一些状态、属性、view增加、删除、更新、窗口顺序、消息收集和处理等.

  4. 获取Window的WindowManager的实现类WindowManagerImpl保存在activity的mWindowManager中。

3.1 关于mWindow.setWindowManager()方法

从setWindowManager方法中可以发现,这里返回的是WindowManagerImpl对象,WindowManager是一个接口,而WindowManagerImpl是它的实现类,这里调用createLocalWindowManager()

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);
}

WindowManagerImpl中的createLocalWindowManager()方法

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

3.2 将生成的窗口视图对象是添加到手机屏幕

我们知道,Activity的视图是在Activity的生命周期onResume()方法执行之后才会显示的,这是因为在ActivityThread的handleResumeActivity方法中调用了Activity的onResume()方法,关于Activity的启动这一部分内容我还没有看过,“老罗的Android之旅”中有这一部分内容的详细分析,有兴趣的可以看一下。在这个函数中,会调用activity的makeVisible方法经WindowManagerImpl将DecorView展示出来。这里的getWindowManager()就是之前设置的WindowManager。最后就Activity就可以请求WindowManagerService将视图绘制到屏幕上了。

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

总结

总结一下整个View的加载过程:

  1. 首先在Activity启动时,在attach方法中先创建Activity的窗口对象,它是PhoneWindow类型,每个Activity都有一个窗口对象,然后为这个窗口设置各种事件的回调,还要注册其对应的窗口管理器,用来管理窗口的一些状态,属性,view的更新等。

  2. 当调用Activity的onCreat方法时,会调用设置布局文件,Activity的setContentView其实调用的是Activity的窗口对象PhoneWindow的setContentView,PhoneWindow有一个内部类DectorView(FrameLayout的子类),它是整个窗口下的根View,内部包含两个FrameLayout,一个根据主题样式来进行TitleBar之类设置,一个就是用来装我们传入的布局文件中的view,这个就是mContetntParent。第一次调用PhoneWindowsetcontentView方法会先创建DectorView,进行一些初始化的设置,然后解析系统的资源文件到DectorView中,接着会找到id为ID_Android_CONTENTFrameLayout,将其赋值给mContetntParent,用来放我们传入的资源文件解析出来的view.

  3. 这些初始化的设置完成之后,就是处理我们调用setContentView时传入的布局文件了,如果传入的资源文件id,会调用反射机制解析xml文件,再把解析出来的各个view加到mContetntParent,如果传入的是View,那么直接加到mContetntParent就可以,最后就是系统在调用onResume之后,经之前设置的WindowManager将整个DecorView展示出来。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容