Android 窗口添加机制系列1-Activity

参考:
1. Android应用Activity、Dialog、PopWindow、Toast窗口添加机制及源码分析
2. Android4.0窗口机制token分析以及activitiy, dialog, toast 窗口创建过程分析

我们都知道Android屏幕显示的就是Window和各种View,其中顶层的其实是DecorView,然后被添加到了PhoneWindow中,然而Activity在其中的作用主要是管理生命周期、建立窗口等。也就是说Window相关的东西对于Android屏幕来说是至关重要的

Activity Window/WindowManager关系

ActivityThread类的performLaunchActivity方法中调运了activity.attach(…)方法进行初始化。
这个是不是很熟悉,可以参考我前一篇博客《Android Context详解》 这篇博客中有说

启动一个activity的时候,执行完application相关的动作之后,会去调用handleLaunchActivity,这个方法先调用performLaunchActivity方法,创建activity,调用onCreate,onStart等方法,然后调用handleResumeActivity方法去执行onResume方法(这个执行完后视图才在屏幕上刷新,用户可见)
同时有介绍attach方法中,还会为每个activity或者service绑定mApplication对象

        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类型的mWindow对象,实际为PhoneWindow类实现了抽象Window类
        mWindow = PolicyManager.makeNewWindow(this);
        ......
        //通过抽象Window类的setWindowManager方法给Window类的成员变量WindowManager赋值实例化
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        ......
        //把抽象Window类相关的WindowManager对象拿出来关联到Activity的WindowManager类型成员变量mWindowManager
        mWindowManager = mWindow.getWindowManager();
        ......
    }

mWindow实际上是PhoneWindow对象

    public Window makeNewWindow(Context context) {
        return new PhoneWindow(context);
    }

Activity类中的attach方法又创建了Window类型的新成员变量mWindow(PhoneWindow实现类)与Activity相关联,接着在Activity类的attach方法最后又通过mWindow.setWindowManager(…)方法创建了与Window相关联的WindowManager对象,最后又通过mWindow.getWindowManager()将Window的WindowManager成员变量赋值给Activity的WindowManager成员变量mWindowManager

    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        ......
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        //实例化Window类的WindowManager类型成员mWindowManager
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
public final class WindowManagerImpl implements WindowManager {
    ......
    private WindowManagerImpl(Display display, Window parentWindow) {
        mDisplay = display;
        mParentWindow = parentWindow;
    }
    ......
    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mDisplay, parentWindow);
    }
    ......
}

这样就把Activity的Window与WindowManager关联起来了。Activity类的Window类型成员变量mWindow及WindowManager类型成员变量mWindowManager就是这么来的
接下来分析一下wm对象是怎么获取得到的

wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);

mContext其实实现类是ContextImpl,所以进入ContextImpl的getSystemService方法

    class ContextImpl extends Context {
    ......
    //静态代码块,类加载时执行一次
    static {
        ......
        //这里有一堆类似的XXX_SERVICE的注册
        ......
        registerService(WINDOW_SERVICE, new ServiceFetcher() {
                Display mDefaultDisplay;
                public Object getService(ContextImpl ctx) {
                    //搞一个Display实例
                    Display display = ctx.mDisplay;
                    if (display == null) {
                        if (mDefaultDisplay == null) {
                            DisplayManager dm = (DisplayManager)ctx.getOuterContext().
                                    getSystemService(Context.DISPLAY_SERVICE);
                            mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
                        }
                        display = mDefaultDisplay;
                    }
                    //返回一个WindowManagerImpl实例
                    return new WindowManagerImpl(display);
                }});
        ......
    }
    //这就是你在外面调运Context的getSystemService获取到的WindowManagerImpl实例
    @Override
    public Object getSystemService(String name) {
        ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
        return fetcher == null ? null : fetcher.getService(this);
    }
    //上面static代码块创建WindowManagerImpl实例用到的方法
    private static void registerService(String serviceName, ServiceFetcher fetcher) {
        if (!(fetcher instanceof StaticServiceFetcher)) {
            fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;
        }
        SYSTEM_SERVICE_MAP.put(serviceName, fetcher);
    }
}

看见没有,我们都知道Java的静态代码块是类加载是执行一次的,也就相当于一个全局的,这样就相当于每个Application只有一个WindowManagerImpl(display)实例。

就是说如果最后调用是ContextImpl的getSystemService(Context.WINDOW_SERVICE)方法,那么返回的都是同一个WindowManagerImpl对象。此时注意activity基类中覆盖了getSystemService方法。

DecorView添加WindowManager流程

还记得我在《Android View视图层次》 中说过么?

在onCreate中调用了setContentView,所以把除了decorView之外的所有的view都已经添加进去了,形成了一个视图层次结构。然后ActivityThread的handleResumeActivity方法把decorView添加到了phonewindow中去了

复习一下

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

Activity中makeVisible方法,把这个DecorView添加到wm中。。

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

看见makeVisible方法的wm变量没,这个变量就是Window类中通过调运WindowManagerImpl的createLocalWindowManager创建的WindowManagerImpl对象实例
WindowManagerImpl的addView方法如下:

public final class WindowManagerImpl implements WindowManager {
    //继承自Object的单例类
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    ......
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        //mParentWindow是上面分析的在Activity中获取WindowManagerImpl实例化时传入的当前Window
        //view是Activity中最顶层的mDecor
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
    ......
}

可以看到mGlobal是单例模式的WindowManagerGlobal成员mGlobal,addView最终调运了WindowManagerGlobal的addView

public final class WindowManagerGlobal {
    ......
    private final ArrayList<View> mViews = new ArrayList<View>();
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();
    private final ArraySet<View> mDyingViews = new ArraySet<View>();

    ......
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ......
        //获取Activity的Window的getWindow().getAttributes()的LayoutParams 
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
        //如果是Activity中调运的,parentWindow=Window,如果不是Activity的,譬如是Context的静态代码块的实例化则parentWindow为null
        if (parentWindow != null) {
            //依据当前Activity的Window调节sub Window的LayoutParams
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
            ......
        }

        ViewRootImpl root;
        ......
        synchronized (mLock) {
            ......
            //为当前Window创建ViewRoot
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            //把当前Window相关的东西存入各自的List中,在remove中会删掉
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            //把View和ViewRoot关联起来,很重要!!!
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            ......
        }
    }
    ......
}

关键的几个地方,下面一一分析
WindowManagerGlobal.addView方法中第一个重要的地方
1. parentWindow.adjustLayoutParamsForSubWindow(wparams);
wparams是个什么鬼呢?
这个参数首先是从makeVisible方法执行wm.addView(mDecor, getWindow().getAttributes());传下来的
Window.java中

    private final WindowManager.LayoutParams mWindowAttributes =
        new WindowManager.LayoutParams();
    public final WindowManager.LayoutParams getAttributes() {
        return mWindowAttributes;
    }

可以看到,LayoutParams其实是WindowManager的一个内部类。这个内部类很重要,表示的是窗口的一些布局参数
WindowManager.LayoutParams中

    public static class LayoutParams extends ViewGroup.LayoutParams
            implements Parcelable {
        //WindowType:开始应用程序窗口
        public static final int FIRST_APPLICATION_WINDOW = 1;
        //WindowType:所有程序窗口的base窗口,其他应用程序窗口都显示在它上面
        public static final int TYPE_BASE_APPLICATION   = 1;
        //WindowType:普通应用程序窗口,token必须设置为Activity的token来指定窗口属于谁
        public static final int TYPE_APPLICATION        = 2;
        ............
        //WindowType:结束应用程序窗口
        public static final int LAST_APPLICATION_WINDOW = 99;

        //WindowType:SubWindows子窗口,子窗口的Z序和坐标空间都依赖于他们的宿主窗口
        public static final int FIRST_SUB_WINDOW        = 1000;
        ...........
        //WindowType:子窗口结束
        public static final int LAST_SUB_WINDOW         = 1999;

        public int type;
        public int gravity;
        /**
         * Identifier for this window.  This will usually be filled in for
         * you.
         */
        public IBinder token = null;
        public LayoutParams() {
            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            type = TYPE_APPLICATION;
            format = PixelFormat.OPAQUE;
        }
    }

这个类里面申请了很多常量,就不贴出来了,想要具体了解,看这里吧,Android应用Activity、Dialog、PopWindow、Toast窗口添加机制及源码分析

  1. 应用程序窗口。一般应用程序的窗口,比如我们应用程序的Activity的窗口。
  2. 子窗口。一般在Activity里面的窗口,比如对话框等。
  3. 系统窗口。系统的窗口,比如输入法,Toast,墙纸等。

同时此时也说明

Activity窗体的WindowManager.LayoutParams类型是TYPE_APPLICATION。同时重点留意一下这里的type和token属性。

WindowManager.LayoutParams介绍完了,那么parentWindow.adjustLayoutParamsForSubWindow(wparams);这行代码的具体作用是什么呢?
Window.java中

    void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
        CharSequence curTitle = wp.getTitle();
        //如果是子窗口,那么就把wp的token参数设置为decorview的getWindowToken
        if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
            wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            if (wp.token == null) {
                View decor = peekDecorView();
                if (decor != null) {
                    wp.token = decor.getWindowToken();
                }
            }
            if (curTitle == null || curTitle.length() == 0) {
                String title;
                if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA) {
                    title="Media";
                } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY) {
                    title="MediaOvr";
                } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
                    title="Panel";
                } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL) {
                    title="SubPanel";
                } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG) {
                    title="AtchDlg";
                } else {
                    title=Integer.toString(wp.type);
                }
                if (mAppName != null) {
                    title += ":" + mAppName;
                }
                wp.setTitle(title);
            }
        } else { //如果不是子窗口类型,那么把wp的token设置为window的mAppToken
            if (wp.token == null) {
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;//把wp的token设置为window的mAppToken
            }
            if ((curTitle == null || curTitle.length() == 0)
                    && mAppName != null) {
                wp.setTitle(mAppName);
            }
        }
        if (wp.packageName == null) {
            wp.packageName = mContext.getPackageName();
        }
        if (mHardwareAccelerated) {
            wp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
        }
    }

上文我们知道,activity窗口的type是TYPE_APPLICATION,所以此时会执行到
wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
mContainer一般为空,那么window的mAppToken对象又是什么鬼呢?
记不记得上文中acitivty做attach的时候,把windowmanager和window绑定的时候调用的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);
    }

看到木有,mAppToken = appToken;setWindowManager方法中appToken参数其实是activity中的mToken参数,这个参数其实就是performLaunchActivity中传递下来的

所以最后的结论是:

WindowManager.LayoutParams wp中的token其实就是activity的token

WindowManagerGlobal.addView方法中第二个重要的地方
2. root = new ViewRootImpl(view.getContext(), display);
此时说明了会为每个windowmanager都创建一个ViewRootImpl对象,ViewRootImpl很熟悉吧?《Android View绘制流程》
View绘制的流程都是从ViewRootImpl发起的!!!

WindowManagerGlobal.addView方法中第三个重要的地方
3. root.setView(view, wparams, panelParentView);

ViewRootImpl.setView方法

            public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
                .......
                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();
                ......
                try {
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                }

                if (DEBUG_LAYOUT) Log.v(TAG, "Added window " + mWindow);
                if (res < WindowManagerGlobal.ADD_OKAY) {
                    mAttachInfo.mRootView = null;
                    mAdded = false;
                    mFallbackEventHandler.setView(null);
                    unscheduleTraversals();
                    setAccessibilityFocus(null, null);
                    switch (res) {
                        case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
                        case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- token " + attrs.token
                                    + " is not valid; is your activity running?");
                        case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- token " + attrs.token
                                    + " is not for an application");

                .........
            }

首先会调用requestLayout();这个方法有木有很熟悉,这个方法会依次调用scheduleTraversals,doTraversal,performTraversals,然后是测量measure,布局layout,绘画draw流程。现在终于明白了吧,为什么view绘制的流程会从ViewRootImpl的performTraversals开始。

其实requestLayout和invalidate方法最终都是会调用ViewRootImpl的performTraversals,只是前者会重新导致view及其parent view的测量,布局流程,而invalidate只会导致当前view的绘画过程

另一方面
res = mWindowSession.addToDisplay..
这里其实就是调用Session对象的addToDisplay方法,进而调用WMS(WindowManagerService)的addWindow。。。看看mWindowSession对象在哪里生成的

    public ViewRootImpl(Context context, Display display) {
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        mWindow = new W(this);
        ....
    }
    static class W extends IWindow.Stub {

WindowManagerGlobal.getWindowSession方法

    public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                try {
                    InputMethodManager imm = InputMethodManager.getInstance();
                    IWindowManager windowManager = getWindowManagerService();
                    sWindowSession = windowManager.openSession(
                            new IWindowSessionCallback.Stub() {
                                @Override
                                public void onAnimatorScaleChanged(float scale) {
                                    ValueAnimator.setDurationScale(scale);
                                }
                            },
                            imm.getClient(), imm.getInputContext());
                    ValueAnimator.setDurationScale(windowManager.getCurrentAnimatorScale());
                } catch (RemoteException e) {
                    Log.e(TAG, "Failed to open window session", e);
                }
            }
            return sWindowSession;
        }
    }

    public static IWindowManager getWindowManagerService() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowManagerService == null) {
                sWindowManagerService = IWindowManager.Stub.asInterface(
                        ServiceManager.getService("window"));
            }
            return sWindowManagerService;
        }
    }

看到没有,mWindowSession其实就是ServiceManager.getService(“window”),window系统服务的openSession方法返回的。然后可以看到,Session类中持有WindowManagerService对象的引用

    final WindowManagerService mService;
    @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets,
            InputChannel outInputChannel) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outInputChannel);
    }

所以最后其实调用的是WindowManagerService的addWindow方法,现在知道传说中WMS的作用了吧,实际上就是是用来管理窗口的,最终都是交给WMS来管理

  1. 另外注意一下,可以看到addWindow中压根就没有window对象的参数,其实到了WMS那层,压根就没有窗口这一概念,其实人家管理的是View。WMS中addView中传进去的其实是IWindow,这个其实是ViewRootImpl中的W对象,mWindow
  2. ViewRootImpl负责管理视图树和与WMS交互,与WMS交互是通过WindowSession。而且ViewRootImpl也负责UI界面的布局与渲染,负责把一些事件分发至Activity,以便Activity可以截获事件。大多数情况下,它管理Activity顶层视图DecorView,它相当于MVC模型中的Controller。
  3. 从上面可以看出来,是mWindowSession.addToDisplay()这个方法把mWindow传递给我WMS,WMS就持有了当前ViewRootlmpl的代理,就可以调用W对象让ViewRootlmpl做一些事情了。
  4. 这样,双方都有了对方的接口,WMS中的Session注册到WindowManagerGlobal的成员WindowSession中,ViewRootImpl::W注册到WindowState中的成员mClient中。前者是为了App改变View结构时请求WMS为其更新布局。后者代表了App端的一个添加到WMS中的View,每一个像这样通过WindowManager接口中addView()添加的窗口都有一个对应的ViewRootImpl,也有一个相应的ViewRootImpl::W。它可以理解为是ViewRootImpl中暴露给WMS的接口,这样WMS可以通过这个接口和App端通信。

WindowSession是ViewRootImpl获取之后,主动和WMS通信的

    public int addWindow(Session session, IWindow client, int seq,
            WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
            Rect outContentInsets, InputChannel outInputChannel) {
        ........
        boolean addToken = false;
        WindowToken token = mTokenMap.get(attrs.token);
        .......
        else if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
                AppWindowToken atoken = token.appWindowToken;
                if (atoken == null) {
                    Slog.w(TAG, "Attempted to add window with non-application token "
                          + token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
                } else if (atoken.removed) {
                    Slog.w(TAG, "Attempted to add window with exiting application token "
                          + token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_APP_EXITING;
                }
                if (type == TYPE_APPLICATION_STARTING && atoken.firstWindowDrawn) {
                    // No need for this guy!
                    if (localLOGV) Slog.v(
                            TAG, "**** NO NEED TO START: " + attrs.getTitle());
                    return WindowManagerGlobal.ADD_STARTING_NOT_NEEDED;
                }
            }
        }
        ........
    }

看到没有,此时会去判断窗口的WindowManager.LayoutParams的token是否匹配,如果不匹配就报错
WindowManager.LayoutParams attrs
AppWindowToken atoken = token.appWindowToken;
想要知道更多token相关的知识请参考
Android4.0窗口机制token分析以及activitiy, dialog, toast 窗口创建过程分析

注意一下这里:下篇文章分析Dialog机制的时候,如果context使用不当,就会
return WindowManagerGlobal.ADD_NOT_APP_TOKEN;进而导致异常

throw new WindowManager.BadTokenException( "Unable to add window -- token " + attrs.token+ " is not for an application");

到这里终于分析完了,我们现在终于明白了

  1. activity和window/WindowManager的关系: 每个activity都有一个PhoneWindow和WindowManagerImpl对象
  2. ViewRootImpl的作用: 每个activity都会有一个ViewRootImpl对象,实际功能其实都是交给ViewRootImpl来处理的
  3. WMS(WindowManagerService)的作用: ViewRootImpl实际上是通过IBinder跨进程调用通过Session,然后交给WMS来管理的
  4. View是从什么时候开始绘制的: 当setContentView加载完所有的view,包括decorview时。调用ActivityThread的handleResumeActivity方法使View可见,然后把顶层的DecorView添加到windowmanager中,最终调用ViewRootImpl的setView方法,这其中调用requestLayout方法,导致了整个View视图层次的测量布局绘画过程。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值