美文网首页View绘制流程Android开发Android技术知识
Android视图加载流程(1)之SetContent( )

Android视图加载流程(1)之SetContent( )

作者: 你需要一台永动机 | 来源:发表于2017-08-18 15:12 被阅读227次

    关键类:window,PhoneWindow,DecorView
    关键方法:setContentView(int layoutResID)及其重载方法

    简单介绍:

    1. Window是一个抽象类,提供了绘制窗口的一组通用API
    2. PhoneWindow是Window的具体继承实现类。
    3. DecorView原为PhoneWindow的内部类,后独立出来。DecorView是所有窗口的根View (FrameLayout的子类)

    关系图:


    源码解读:

    Step1:Activity类

    setContentView()是非常关键的方法,其重载方法有3个

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

    可以清楚的看到3个重载方法都调用了getWindow()中相应的setContentView方法

    Step2:PhoneWindow类

    由于Window类为抽象类,所以我们要看实现类PhoneWindow里的setContent(int layoutResID)

    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. 判断mCntentParent是否null,
      1. 为空:调用installDecor(),初始化DecorView
      2. 不为空:判断是否设置FEATURE_CONTENT_TRANSITIONS(默认false)
        • 若没设置:清除mContentParent的子视图
    2. mLayoutInflater.inflate(layoutResID, mContentParent)将资源文件转换为View并加载至mContentParent

    Step3:PhoneWindow类

    • setContentView(int layoutResID)
    • setContentView(View view)
    • setContentView(View view, ViewGroup.LayoutParams params)`

    后两个方法与第一个方法大同小异

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

    setContentView(View view)实际上是调用了setContentView(View view, ViewGroup.LayoutParams params)只是LayoutParams设置为了MATCH_PARENT而已。

    setContentView(View view, ViewGroup.LayoutParams params)方法调用addView()加载视图至mContentParent

    Step4:PhoneWindow类

    到这里,大家都知道我们所加载的布局最终都是add到mContentParent,那mContentParent从哪里来呢?我们回头看一下installDecor()方法

    ViewGroup mContentParent;//内容视图
    private DecorView mDecor;//所有视图的根视图
        
    //实例化DecorView
    protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    }
        
    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) {
            //根据窗口的风格修饰,选择对应的修饰布局文件,并且将id为content的FrameLayout赋值给mContentParent
            mContentParent = generateLayout(mDecor);
            //......
            //初始化一堆属性值
        }
    } 
    
    1. 判断mDecor是否为null,为空调用generateDecor()创建一个DecorView
    2. 判断mContentParent是否为null,为空调用generateLayout(mDecor)创建一个mContentParent对象

    此时我们看到了前几步所需的mContentParent

    Step5:PhoneWindow类

    接着我们看mContentParent是具体如何创建的

    protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
        
        TypedArray a = getWindowStyle();
        
        //......
        //依据主题style设置一堆值进行设置
        
        // Inflate the window decor.
        
        int layoutResource;
            
        //要在setContentView之前调用requesetFeature的原因
        int features = getLocalFeatures();//获取本地特性
        //......
        //根据设定好的features值选择不同的窗口修饰布局文件,得到layoutResource值
        
        //把选中的窗口修饰布局文件添加到DecorView对象里,并且指定contentParent值
        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");
        }
        
        //......
        //继续一堆属性设置,完事返回contentParent
        return contentParent;
    }   
    
    1. 根据app所设置的不用类型主题获取相对应的系统根布局文件(这些布局都有一个以id为content的帧布局:内容布局)
    2. 解析这些布局文件并加载至mDecor视图
    3. 接着通过mDecor获取id为content的帧布局返回给contentParent对象(前几步的关注对象)

    主题的话以后有空的话专门写一篇文章介绍,这里大概认识一下~

    当我们使用(Window.FEATURE_NO_TITLE)主题,则它对应的系统布局为R.layout.screen_simple

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

    有没有看到R.id.content的FrameLayout么?这就是上面讲的系统布局内的内容视图

    Step6:结束 Finish

    此时我们的布局文件文件已经转换为视图并加载到DecorView当中,但是此时布局并没有显示!那视图是如何显示出来呢?接下去的文章会分析这一块。

    总结 Summary

    通过以上6步我们大致可以理解setContentView的整个过程:

    1. 创建DecorView的对象,并将此对象作为窗口的根视图
    2. 根据主题及其样式的不同获取对应的布局文件,解析并加载至mDecorView
    3. 通过mDecorView获取id为content的帧布局contentParent,
    4. 将Activity的布局文件解析并加载至contentParent
    理解图:

    额外 Extra

    Step1 PhoneWindow类

    这里我们聊一下与setContent有关的回调,我们回看一下刚才的setContentView

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

    当我们setContentView执行完加载视图后,接着获取回调CallBack,并且执行改回调的方法onContentChanged()。此回调从哪里来呢?

    Step2 Window类

    private Callback mCallback;
        
    public final Callback getCallback() {
        return mCallback;
    }
        
     public void setCallback(Callback callback) {
        mCallback = callback;
    }
        
    public interface Callback{
        ......
         public void onContentChanged();
        ......
    }
    

    从这里我们可以清楚的看到Window类中包含了Callback和所对应的get( )和set( ),
    getCallback()我们已经知道在哪里调用,那setCallBack呢?

    Step3 Activity类

    //此类为简化版
    public class Activity implements Window.Callback {
        
        final void attach(Context context, ActivityThread aThread){
        ......
        mWindow.setCallback(this);
        ......
         }
         //接口所调用的方法
         public void onContentChanged() {}
    }
    

    我们可以看出Activity实现了Callback的接口,且设置setCallback为this。所以setContent()执行完毕后就会调用此方法。setContent()竟然是空方法

    疑问 Wonder

    此文只讲到DecorView由PhoneWindow生成并加载的。我们知道phoneWindow是从Activity的getWindow()获取的。那PhoneWindow跟Activity是如何产生关联的呢?

    Android视图加载流程(2)之Window和WindowManager的创建与Activity


    PS:本文整理自以下文章,若有发现问题请致邮caoyanglee92@gmail.com
    工匠若水 Android应用setContentView与LayoutInflater加载解析机制源码分析
    Hohohong Android窗口机制

    相关文章

      网友评论

        本文标题:Android视图加载流程(1)之SetContent( )

        本文链接:https://www.haomeiwen.com/subject/ztsirxtx.html