Android应用setContentView与LayoutInflater加载解析机制源码分析(超级棒!)

【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果】

1 背景

其实之所以要说这个话题有几个原因:

  1. 理解xml等控件是咋被显示的原理,通常大家写代码都是直接在onCreate里setContentView就完事,没怎么关注其实现原理。

所以接下来主要分析的就是View或者ViewGroup对象是如何添加至应用程序界面(窗口)显示的。我们准备从Activity的setContentView方法开始来说(因为默认Activity中放入我们的xml或者Java控件是通过setContentView方法来操作的,当调运了setContentView所有的控件就得到了显示)。

【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果】

2 Android5.1.1(API 22)从Activity的setContentView方法说起

2-1 Activity的setContentView方法解析

Activity的源码中提供了三个重载的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();
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

可以看见他们都先调运了getWindow()的setContentView方法,然后调运Activity的initWindowDecorActionBar方法,关于initWindowDecorActionBar方法后面准备写一篇关于Android ActionBar原理解析的文章,所以暂时跳过不解释。

2-2 关于窗口Window类的一些关系

在开始分析Activity组合对象Window的setContentView方法之前请先明确如下关系(前面分析《Android触摸屏事件派发机制详解与源码分析三(Activity篇)》时也有说过)。

这里写图片描述

看见上面图没?Activity中有一个成员为Window,其实例化对象为PhoneWindow,PhoneWindow为抽象Window类的实现类。

这里先简要说明下这些类的职责:

  1. Window是一个抽象类,提供了绘制窗口的一组通用API。

  2. PhoneWindow是Window的具体继承实现类。而且该类内部包含了一个DecorView对象,该DectorView对象是所有应用窗口(Activity界面)的根View。

  3. DecorView是PhoneWindow的内部类,是FrameLayout的子类,是对FrameLayout进行功能的修饰(所以叫DecorXXX),是所有应用窗口的根View 。

依据面向对象从抽象到具体我们可以类比上面关系就像如下:

Window是一块电子屏,PhoneWindow是一块手机电子屏,DecorView就是电子屏要显示的内容,Activity就是手机电子屏安装位置。

2-2 窗口PhoneWindow类的setContentView方法

我们可以看见Window类的setContentView方法都是抽象的。所以我们直接先看PhoneWindow类的setContentView(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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

可以看见,第五行首先判断mContentParent是否为null,也就是第一次调运);如果是第一次调用,则调用installDecor()方法,否则判断是否设置FEATURE_CONTENT_TRANSITIONS Window属性(默认false),如果没有就移除该mContentParent内所有的所有子View;接着16行mLayoutInflater.inflate(layoutResID, mContentParent);将我们的资源文件通过LayoutInflater对象转换为View树,并且添加至mContentParent视图中(其中mLayoutInflater是在PhoneWindow的构造函数中得到实例对象的LayoutInflater.from(context);)。

再来看下PhoneWindow类的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();
        }
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

看见没有,我们其实只用分析setContentView(View view, ViewGroup.LayoutParams params)方法即可,如果你在Activity中调运setContentView(View view)方法,实质也是调运setContentView(View view, ViewGroup.LayoutParams params),只是LayoutParams设置为了MATCH_PARENT而已。

所以直接分析setContentView(View view, ViewGroup.LayoutParams params)方法就行,可以看见该方法与setContentView(int layoutResID)类似,只是少了LayoutInflater将xml文件解析装换为View而已,这里直接使用View的addView方法追加道了当前mContentParent而已。

所以说在我们的应用程序里可以多次调用setContentView()来显示界面,因为会removeAllViews。

2-3 窗口PhoneWindow类的installDecor方法

回过头,我们继续看上面PhoneWindow类setContentView方法的第6行installDecor();代码,在PhoneWindow中查看installDecor源码如下:

    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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

我勒个去!又是一个死长的方法,抓重点分析吧。第2到9行可以看出,首先判断mDecor对象是否为空,如果为空则调用generateDecor()创建一个DecorView(该类是
FrameLayout子类,即一个ViewGroup视图),然后设置一些属性,我们看下PhoneWindow的generateDecor方法,如下:

    protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    }
   
   
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

可以看见generateDecor方法仅仅是new一个DecorView的实例。

回到installDecor方法继续往下看,第10行开始到方法结束都需要一个if (mContentParent == null)判断为真才会执行,当mContentParent对象不为空则调用generateLayout()方法去创建mContentParent对象。所以我们看下generateLayout方法源码,如下:

    protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.


        TypedArray a = getWindowStyle();


        //......
        //依据主题style设置一堆值进行设置


        // Inflate the window decor.


        int layoutResource;
        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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

可以看见上面方法主要作用就是根据窗口的风格修饰类型为该窗口选择不同的窗口根布局文件。mDecor做为根视图将该窗口根布局添加进去,然后获取id为content的FrameLayout返回给mContentParent对象。所以installDecor方法实质就是产生mDecor和mContentParent对象。

在这里顺带提一下:还记得我们平时写应用Activity时设置的theme或者feature吗(全屏啥的,NoTitle等)?我们一般是不是通过XML的android:theme属性或者java的requestFeature()方法来设置的呢?譬如:

通过java文件设置:


requestWindowFeature(Window.FEATURE_NO_TITLE);


通过xml文件设置:


android:theme="@android:style/Theme.NoTitleBar"
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

对的,其实我们平时requestWindowFeature()设置的值就是在这里通过getLocalFeature()获取的;而android:theme属性也是通过这里的getWindowStyle()获取的。

所以这下你应该就明白在java文件设置Activity的属性时必须在setContentView方法之前调用requestFeature()方法的原因了吧。

我们继续关注一下generateLayout方法的layoutResource变量赋值情况。因为它最终通过View in = mLayoutInflater.inflate(layoutResource, null);decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));将in添加到PhoneWindow的mDecor对象。为例验证这一段代码分析我们用一个实例来进行说明,如下是一个简单的App主要代码:

AndroidManifest.xml文件

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.yanbober.myapplication" >


    <application
        ......
        //看重点,我们将主题设置为NoTitleBar
        android:theme="@android:style/Theme.Black.NoTitleBar" >
        ......
    </application>


</manifest>
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

主界面布局文件:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <TextView android:text="@string/hello_world"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />


</RelativeLayout>
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

APP运行界面:
这里写图片描述

看见没有,上面我们将主题设置为NoTitleBar,所以在generateLayout方法中的layoutResource变量值为R.layout.screen_simple,所以我们看下系统这个screen_simple.xml布局文件,如下:

<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>
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

布局中,一般会包含ActionBar,Title,和一个id为content的FrameLayout,这个布局是NoTitle的。

再来看下上面这个App的hierarchyviewer图谱,如下:

这里写图片描述

看见了吧,通过这个App的hierarchyviewer和系统screen_simple.xml文件比较就验证了上面我们分析的结论,不再做过多解释。

然后回过头可以看见上面PhoneWindow类的setContentView方法最后通过调运mLayoutInflater.inflate(layoutResID, mContentParent);或者mContentParent.addView(view, params);语句将我们的xml或者JavaView插入到了mContentParent(id为content的FrameLayout对象)ViewGroup中。最后setContentView还会调用一个Callback接口的成员函数onContentChanged来通知对应的Activity组件视图内容发生了变化。

2-4 Window类内部接口Callback的onContentChanged方法

上面刚刚说了PhoneWindow类的setContentView方法中最后调运了onContentChanged方法。我们这里看下setContentView这段代码,如下:

    public void setContentView(int layoutResID) {
        ......
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

看着没有,首先通过getCallback获取对象cb(回调接口),PhoneWindow没有重写Window的这个方法,所以到抽象类Window中可以看到:

    /**
     * Return the current Callback interface for this window.
     */
    public final Callback getCallback() {
        return mCallback;
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这个mCallback在哪赋值的呢,继续看Window类发现有一个方法,如下:

    public void setCallback(Callback callback) {
        mCallback = callback;
    }
   
   
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

Window中的mCallback是通过这个方法赋值的,那就回想一下,Window又是Activity的组合成员,那就是Activity一定调运这个方法了,回到Activity发现在Activity的attach方法中进行了设置,如下:

    final void attach(Context context, ActivityThread aThread,
        ......
        mWindow.setCallback(this);
        ......
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

也就是说Activity类实现了Window的Callback接口。那就是看下Activity实现的onContentChanged方法。如下:

    public void onContentChanged() {
    }
   
   
  • 1
  • 2
  • 1
  • 2

咦?onContentChanged是个空方法。那就说明当Activity的布局改动时,即setContentView()或者addContentView()方法执行完毕时就会调用该方法。

所以当我们写App时,Activity的各种View的findViewById()方法等都可以放到该方法中,系统会帮忙回调。

2-5 setContentView源码分析总结

可以看出来setContentView整个过程主要是如何把Activity的布局文件或者java的View添加至窗口里,上面的过程可以重点概括为:

  1. 创建一个DecorView的对象mDecor,该mDecor对象将作为整个应用窗口的根视图。

  2. 依据Feature等style theme创建不同的窗口修饰布局文件,并且通过findViewById获取Activity布局文件该存放的地方(窗口修饰布局文件中id为content的FrameLayout)。

  3. 将Activity的布局文件添加至id为content的FrameLayout内。

至此整个setContentView的主要流程就分析完毕。你可能这时会疑惑,这么设置完一堆View关系后系统是怎么知道该显示了呢?下面我们就初探一下关于Activity的setContentView在onCreate中如何显示的(声明一下,这里有些会暂时直接给出结论,该系列文章后面会详细分析的)。

2-6 setContentView完以后Activity显示界面初探

这一小部分已经不属于sentContentView的分析范畴了,只是简单说明setContentView之后怎么被显示出来的(注意:Activity调运setContentView方法自身不会显示布局的)。

记得前面有一篇文章《Android异步消息处理机制详解及源码分析》的3-1-2小节说过,一个Activity的开始实际是ActivityThread的main方法(至于为什么后面会写文章分析,这里站在应用层角度先有这个概念就行)。

那在这一篇我们再直接说一个知识点(至于为什么后面会写文章分析,这里站在应用层角度先有这个概念就行)。

当启动Activity调运完ActivityThread的main方法之后,接着调用ActivityThread类performLaunchActivity来创建要启动的Activity组件,在创建Activity组件的过程中,还会为该Activity组件创建窗口对象和视图对象;接着Activity组件创建完成之后,通过调用ActivityThread类的handleResumeActivity将它激活。

所以我们先看下handleResumeActivity方法一个重点,如下:

    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
        // If we are getting ready to gc after going to the background, well
        // we are back active so skip it.
        ......
        // TODO Push resumeArgs into the activity for consideration
        ActivityClientRecord r = performResumeActivity(token, clearHide);


        if (r != null) {
            ......
            // If the window hasn't yet been added to the window manager,
            // and this guy didn't finish itself or start another activity,
            // then go ahead and add the window.
            ......
            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
            ......
            // The window is now visible if it has been added, we are not
            // simply finishing, and we are not starting another activity.
            if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
                ......
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
            }
            ......
        } else {
            // If an exception was thrown when trying to resume, then
            // just end this activity.
            ......
        }
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

看见r.activity.makeVisible();语句没?调用Activity的makeVisible方法显示我们上面通过setContentView创建的mDecor视图族。所以我们看下Activity的makeVisible方法,如下:

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

看见没有,通过DecorView(FrameLayout,也即View)的setVisibility方法将View设置为VISIBLE,至此显示出来。

到此setContentView的完整流程分析完毕。

【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果】

3 Android5.1.1(API 22)看看LayoutInflater机制原理

上面在分析setContentView过程中可以看见,在PhoneWindow的setContentView中调运了mLayoutInflater.inflate(layoutResID, mContentParent);,在PhoneWindow的generateLayout中调运了View in = mLayoutInflater.inflate(layoutResource, null);,当时我们没有详细分析,只是告诉通过xml得到View对象。现在我们就来分析分析这一问题。

3-1 通过实例引出问题

在开始之前我们先来做一个测试,我们平时最常见的就是ListView的Adapter中使用LayoutInflater加载xml的item布局文件,所以咱们就以ListView为例,如下:

省略掉Activity代码等,首先给出Activity的布局文件,如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">


    <ListView
        android:id="@+id/listview"
        android:dividerHeight="5dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></ListView>


</LinearLayout>
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

给出两种不同的ListView的item布局文件。

textview_layout.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="100dp"
    android:layout_height="40dp"
    android:text="Text Test"
    android:background="#ffa0a00c"/>
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

textview_layout_parent.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    xmlns:android="http://schemas.android.com/apk/res/android">


    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="100dp"
        android:layout_height="40dp"
        android:text="Text Test"
        android:background="#ffa0a00c"/>


</LinearLayout>
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

ListView的自定义Adapter文件:

public class InflateAdapter extends BaseAdapter {
    private LayoutInflater mInflater = null;


    public InflateAdapter(Context context) {
        mInflater = LayoutInflater.from(context);
    }


    @Override
    public int getCount() {
        return 8;
    }


    @Override
    public Object getItem(int position) {
        return null;
    }


    @Override
    public long getItemId(int position) {
        return 0;
    }


    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        //说明:这里是测试inflate方法参数代码,不再考虑性能优化等TAG处理
        return getXmlToView(convertView, position, parent);
    }


    private View getXmlToView(View convertView, int position, ViewGroup parent) {
        View[] viewList = {
                mInflater.inflate(R.layout.textview_layout, null),
//                mInflater.inflate(R.layout.textview_layout, parent),
                mInflater.inflate(R.layout.textview_layout, parent, false),
//                mInflater.inflate(R.layout.textview_layout, parent, true),
                mInflater.inflate(R.layout.textview_layout, null, true),
                mInflater.inflate(R.layout.textview_layout, null, false),


                mInflater.inflate(R.layout.textview_layout_parent, null),
//                mInflater.inflate(R.layout.textview_layout_parent, parent),
                mInflater.inflate(R.layout.textview_layout_parent, parent, false),
//                mInflater.inflate(R.layout.textview_layout_parent, parent, true),
                mInflater.inflate(R.layout.textview_layout_parent, null, true),
                mInflater.inflate(R.layout.textview_layout_parent, null, false),
        };


        convertView = viewList[position];


        return convertView;
    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

当前代码运行结果:
这里写图片描述

PS:当打开上面viewList数组中任意一行注释都会抛出异常(java.lang.UnsupportedOperationException: addView(View, LayoutParams) is not supported in AdapterView)。

你指定有些蒙圈了,而且比较郁闷,同时想弄明白inflate的这些参数都是啥意思。运行结果为何有这么大差异呢?

那我告诉你,你现在先别多想,记住这回事,咱们先看源码,下面会告诉你为啥。

3-2 从LayoutInflater源码实例化说起

我们先看一下源码中LayoutInflater实例化获取的方法:

    public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

看见没有?是否很熟悉?我们平时写应用获取LayoutInflater实例时不也就两种写法吗,如下:

    LayoutInflater lif = LayoutInflater.from(Context context);


    LayoutInflater lif = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   
   
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

可以看见from方法仅仅是对getSystemService的一个安全封装而已。

3-3 LayoutInflater源码的View inflate(…)方法族剖析

得到LayoutInflater对象之后我们就是传递xml然后解析得到View,如下方法:

    public View inflate(int resource, ViewGroup root) {
        return inflate(resource, root, root != null);
    }
   
   
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

继续看inflate(int resource, ViewGroup root, boolean attachToRoot)方法,如下:

    public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }


        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这个方法的第8行获取到XmlResourceParser接口的实例(Android默认实现类为Pull解析XmlPullParser)。接着看第10行inflate(parser, root, attachToRoot);,你会发现无论哪个inflate重载方法最后都调运了inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)方法,如下:

    public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");


            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context)mConstructorArgs[0];
            mConstructorArgs[0] = mContext;
            //定义返回值,初始化为传入的形参root
            View result = root;


            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
                //如果一开始就是END_DOCUMENT,那说明xml文件有问题
                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }
                //有了上面判断说明这里type一定是START_TAG,也就是xml文件里的root node
                final String name = parser.getName();


                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }


                if (TAG_MERGE.equals(name)) {
                //处理merge tag的情况(merge,你懂的,APP的xml性能优化)
                    //root必须非空且attachToRoot为true,否则抛异常结束(APP使用merge时要注意的地方,
                    //因为merge的xml并不代表某个具体的view,只是将它包起来的其他xml的内容加到某个上层
                    //ViewGroup中。)
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    //递归inflate方法调运
                    rInflate(parser, root, attrs, false, false);
                } else {
                    // Temp is the root view that was found in the xml
                    //xml文件中的root view,根据tag节点创建view对象
                    final View temp = createViewFromTag(root, name, attrs, false);


                    ViewGroup.LayoutParams params = null;


                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        //根据root生成合适的LayoutParams实例
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            //如果attachToRoot=false就调用view的setLayoutParams方法
                            temp.setLayoutParams(params);
                        }
                    }


                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }
                    // Inflate all children under temp
                    //递归inflate剩下的children
                    rInflate(parser, temp, attrs, true, true);
                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }


                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        //root非空且attachToRoot=true则将xml文件的root view加到形参提供的root里
                        root.addView(temp, params);
                    }


                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        //返回xml里解析的root view
                        result = temp;
                    }
                }


            } catch (XmlPullParserException e) {
                InflateException ex = new InflateException(e.getMessage());
                ex.initCause(e);
                throw ex;
            } catch (IOException e) {
                InflateException ex = new InflateException(
                        parser.getPositionDescription()
                        + ": " + e.getMessage());
                ex.initCause(e);
                throw ex;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;
            }


            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            //返回参数root或xml文件里的root view
            return result;
        }
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112

从上面的源码分析我们可以看出inflate方法的参数含义:

  • inflate(xmlId, null); 只创建temp的View,然后直接返回temp。

  • inflate(xmlId, parent); 创建temp的View,然后执行root.addView(temp, params);最后返回root。

  • inflate(xmlId, parent, false); 创建temp的View,然后执行temp.setLayoutParams(params);然后再返回temp。

  • inflate(xmlId, parent, true); 创建temp的View,然后执行root.addView(temp, params);最后返回root。

  • inflate(xmlId, null, false); 只创建temp的View,然后直接返回temp。

  • inflate(xmlId, null, true); 只创建temp的View,然后直接返回temp。

到此其实已经可以说明我们上面示例部分执行效果差异的原因了(在此先强调一个Android的概念,下一篇文章我们会对这段话作一解释:我们经常使用View的layout_width和layout_height来设置View的大小,而且一般都可以正常工作,所以有人时常认为这两个属性就是设置View的真实大小一样;然而实际上这些属性是用于设置View在ViewGroup布局中的大小的;这就是为什么Google的工程师在变量命名上将这种属性叫作layout_width和layout_height,而不是width和height的原因了。),如下:

  • mInflater.inflate(R.layout.textview_layout, null)不能正确处理我们设置的宽和高是因为layout_width,layout_height是相对了父级设置的,而此temp的getLayoutParams为null。
  • mInflater.inflate(R.layout.textview_layout, parent)能正确显示我们设置的宽高是因为我们的View在设置setLayoutParams时params = root.generateLayoutParams(attrs)不为空。
    Inflate(resId , parent,false ) 可以正确处理,因为temp.setLayoutParams(params);这个params正是root.generateLayoutParams(attrs);得到的。
  • mInflater.inflate(R.layout.textview_layout, null, true)与mInflater.inflate(R.layout.textview_layout, null, false)不能正确处理我们设置的宽和高是因为layout_width,layout_height是相对了父级设置的,而此temp的getLayoutParams为null。
  • textview_layout_parent.xml作为item可以正确显示的原因是因为TextView具备上级ViewGroup,上级ViewGroup的layout_width,layout_height会失效,当前的TextView会有效而已。
  • 上面例子中说放开那些注释运行会报错java.lang.UnsupportedOperationException:
    addView(View, LayoutParams) is not supported是因为AdapterView源码中调用了root.addView(temp, params);而此时的root是我们的ListView,ListView为AdapterView的子类,所以我们看下AdapterView抽象类中addView源码即可明白为啥了,如下:
    /**
     * This method is not supported and throws an UnsupportedOperationException when called.
     *
     * @param child Ignored.
     *
     * @throws UnsupportedOperationException Every time this method is invoked.
     */
    @Override
    public void addView(View child) {
        throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这里不再做过多解释。

咦?别急,到这里指定机智的人会问,我们在写App时Activity中指定布局文件的时候,xml布局文件或者我们用java编写的View最外层的那个布局是可以指定大小的啊?他们最外层的layout_width和layout_height都是有作用的啊?

是这样的,还记得我们上面的分析吗?我们自己的xml布局通过setContentView()方法放置到哪去了呢?记不记得id为content的FrameLayout呢?所以我们xml或者java的View的最外层布局的layout_width和layout_height属性才会有效果,就是这么回事而已。

3-4 LayoutInflater源码inflate(…)方法中调运的一些非public方法剖析

看下inflate方法中被调运的rInflate方法,源码如下:

    void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
            boolean finishInflate, boolean inheritContext) throws XmlPullParserException,
            IOException {


        final int depth = parser.getDepth();
        int type;
        //XmlPullParser解析器的标准解析模式
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
            //找到START_TAG节点程序才继续执行这个判断语句之后的逻辑
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            //获取Name标记
            final String name = parser.getName();
            //处理REQUEST_FOCUS的标记
            if (TAG_REQUEST_FOCUS.equals(name)) {
                parseRequestFocus(parser, parent);
            } else if (TAG_TAG.equals(name)) {
                //处理tag标记
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                //处理include标记
                if (parser.getDepth() == 0) {
                    //include节点如果是根节点就抛异常
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, parent, attrs, inheritContext);
            } else if (TAG_MERGE.equals(name)) {
                //merge节点必须是xml文件里的根节点(这里不该再出现merge节点)
                throw new InflateException("<merge /> must be the root element");
            } else {
                //其他自定义节点
                final View view = createViewFromTag(parent, name, attrs, inheritContext);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflate(parser, view, attrs, true, true);
                viewGroup.addView(view, params);
            }
        }
        //parent的所有子节点都inflate完毕的时候回onFinishInflate方法
        if (finishInflate) parent.onFinishInflate();
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

可以看见,上面方法主要就是循环递归解析xml文件,解析结束回调View类的onFinishInflate方法,所以View类的onFinishInflate方法是一个空方法,如下:

    /**
     * Finalize inflating a view from XML.  This is called as the last phase
     * of inflation, after all child views have been added.
     *
     * <p>Even if the subclass overrides onFinishInflate, they should always be
     * sure to call the super method, so that we get called.
     */
    protected void onFinishInflate() {
    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

可以看见,当我们自定义View时在构造函数inflate一个xml后可以实现onFinishInflate这个方法一些自定义的逻辑。

至此LayoutInflater的源码核心部分已经分析完毕。

4 从LayoutInflater与setContentView来说说应用布局文件的优化技巧

通过上面的源码分析可以发现,xml文件解析实质是递归控件,解析属性的过程。所以说嵌套过深不仅效率低下还可能引起调运栈溢出。同时在解析那些tag时也有一些特殊处理,从源码看编写xml还是有很多要注意的地方的。所以说对于Android的xml来说是有一些优化技巧的(PS:布局优化可以通过hierarchyviewer来查看,通过lint也可以自动检查出来一些),如下:

尽量使用相对布局,减少不必要层级结构。不用解释吧?递归解析的原因。

使用merge属性。使用它可以有效的将某些符合条件的多余的层级优化掉。使用merge的场合主要有两处:自定义View中使用,父元素尽量是FrameLayout,当然如果父元素是其他布局,而且不是太复杂的情况下也是可以使用的;Activity中的整体布局,根元素需要是FrameLayout。但是使用merge标签还是有一些限制的,具体是:merge只能用在布局XML文件的根元素;使用merge来inflate一个布局时,必须指定一个ViewGroup作为其父元素,并且要设置inflate的attachToRoot参数为true。(参照inflate(int, ViewGroup, boolean)方法);不能在ViewStub中使用merge标签;最直观的一个原因就是ViewStub的inflate方法中根本没有attachToRoot的设置。

使用ViewStub。一个轻量级的页面,我们通常使用它来做预加载处理,来改善页面加载速度和提高流畅性,ViewStub本身不会占用层级,它最终会被它指定的层级取代。ViewStub也是有一些缺点,譬如:ViewStub只能Inflate一次,之后ViewStub对象会被置为空。按句话说,某个被ViewStub指定的布局被Inflate后,就不能够再通过ViewStub来控制它了。所以它不适用 于需要按需显示隐藏的情况;ViewStub只能用来Inflate一个布局文件,而不是某个具体的View,当然也可以把View写在某个布局文件中。如果想操作一个具体的view,还是使用visibility属性吧;VIewStub中不能嵌套merge标签。

使用include。这个标签是为了布局重用。

控件设置widget以后对于layout_hORw-xxx设置0dp。减少系统运算次数。

如上就是一些APP布局文件基础的优化技巧。

5 总结

至此整个Activity的setContentView与Android的LayoutInflater相关原理都已经分析完毕。关于本篇中有些地方直接给出结论的知识点后面的文章中会做一说明。

setContentView整个过程主要是如何把Activity的布局文件或者java的View添加至窗口里,重点概括为:

  1. 创建一个DecorView的对象mDecor,该mDecor对象将作为整个应用窗口的根视图。

  2. 依据Feature等style theme创建不同的窗口修饰布局文件,并且通过findViewById获取Activity布局文件该存放的地方(窗口修饰布局文件中id为content的FrameLayout)。

  3. 将Activity的布局文件添加至id为content的FrameLayout内。

  4. 当setContentView设置显示OK以后会回调Activity的onContentChanged方法。Activity的各种View的findViewById()方法等都可以放到该方法中,系统会帮忙回调。

如下就是整个Activity的分析简单关系图:

这里写图片描述

LayoutInflater的使用中重点关注inflate方法的参数含义:

  • inflate(xmlId, null); 只创建temp的View,然后直接返回temp。

  • inflate(xmlId, parent); 创建temp的View,然后执行root.addView(temp, params);最后返回root。

  • inflate(xmlId, parent, false); 创建temp的View,然后执行temp.setLayoutParams(params);然后再返回temp。

  • inflate(xmlId, parent, true); 创建temp的View,然后执行root.addView(temp, params);最后返回root。

  • inflate(xmlId, null, false); 只创建temp的View,然后直接返回temp。

  • inflate(xmlId, null, true); 只创建temp的View,然后直接返回temp。

当我们自定义View时在构造函数inflate一个xml后可以实现onFinishInflate这个方法一些自定义的逻辑。

感谢此作者分享的此博客 ! 超级棒 ★【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值