安卓学习笔记之理解Window与WindowManager

Window的概念

对于Android的Window而言,实际上是一个相对抽象的概念。从Window的定义可以看出,Window是对窗体的一种抽象,是顶级Window的外观与行为策略。目前仅有的实现类是PhoneWindow,可以使用它来创建Window(对系统而言)。

 public abstract class Window {
   // Window 样式相关静态常量等public interface Callback {    // Window 回调接口
       …
    }

  // Window相关抽象方法
       …

}

View的事件分发机制中的事件传递:单击事件由Activity内部的Window -> Decor View -> View

WindowManager
从字面意思上理解,它是Window的管理者,更切确的说,它是Window中View的管理者。因为事实上,Window所需要展示的内容是由View来承载的(也就是DecorView),并且创建一个Window需要通过WindowManager来协助完成。它的具体实现是WindowManagerImpl,通常可以使用getSystemService(Context.WINDOW_SERVICE)来得到。

Android中基本上所有的View都是Window来呈现的,不管是Activity、Toast还是Dialog,它们的视图都是附加到Window上的,因此可以将Window理解为View的承载者与直接管理者。

WindowManager是外界访问Window的入口,通常使用定义于ViewManager的相关方法来操作的:

public interface ViewManager{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

Window与WindowManager继承体系图如下:
这里写图片描述

通常创建的WindowManager实际上是WindowManagerImpl
在SystemServiceRegistry类中有如下代码,可知是静态创建的WindowManagerImpl:

static {
 …. 
registerService(Context.WINDOW_SERVICE, WindowManager.class,
                new CachedServiceFetcher<WindowManager>() {
            @Override
            public WindowManager createService(ContextImpl ctx) {
                return new WindowManagerImpl(ctx);
            }});
 …
}

获取WindowManager实际上获取的是WindowManagerImpl实例:

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

WindowManagerService
它是系统全局Window的管理者,负责协调Window的层级、显示及事件派发等。可以这样理解,WindowManager是本地端的管理者,负责与远程服务的WindowManagerService进行交互,从而使Window能层次分明的显示出来。WindowManager与WindowManagerService的交互是一个IPC过程。

WindowManagerService继承体系的继承体系如下,可以发现它实际上是一个Binder子类:

这里写图片描述

在SystemServer启动的时候,会注册WindowManagerService到ServiceManager中:

private void startOtherServices() {
...
    wm = WindowManagerService.main(context, inputManager,  mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL,!mFirstBoot, mOnlyCore);
    ServiceManager.addService(Context.WINDOW_SERVICE, wm);
…}

通过 ServiceManager.getService(“window”)); 可以获得WindowManagerService,这通常在系统内部使用 。

WindowManagerGlobal
我们知道WindowManagerImpl是WindowManager的实现类,但实际上它的工作基本委托给了WindowManagerGlobal类来完成。 WindowManagerGlobal实现了WindowManagerImpl的功能,并对View、ViewRootImpl以及LayoutParams进行管理:

// view集合
private final ArrayList<View> mViews = new ArrayList<View>();  // ViewRootImpl集合
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();// 参数集合
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
// 即将移除的view集合
private final ArraySet<View> mDyingViews = new ArraySet<View>();

ViewRootImpl
ViewRootImpl不是View,实际上是顶级View的管理者。每一个ViewRootImpl 都对应着一个ViewTree ,通过它来完成View的绘制及显示过程。下图展示了它与WM、WMS之间的关系:
这里写图片描述


WindowManager

1. 添加view到Window示例

使用WindowManager添加一个view到Window
自定义浮窗 需要权限android.permission.SYSTEM_ALERT_WINDOW

    /**
     * 显示浮窗
     * @param content   要填充的文本内容
     * @param layoutId   用于创建窗体View的布局
     */
    public  void show(String content,int layoutId) {
          wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
        params = new WindowManager.LayoutParams();
        screenHeight = AppInfoUtils.getScreenSize(context).height;
        screenWidth = AppInfoUtils.getScreenSize(context).width;
        // 加载布局
        view = View.inflate(mContext, layoutId, null);
        // 设置浮窗params属性
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        params.type = WindowManager.LayoutParams.TYPE_PHONE;
        params.gravity = Gravity.TOP+Gravity.LEFT; // 将重心设置为左上方
        params.format = PixelFormat.TRANSLUCENT;    // 半透明
        params.x = sp.getInt("startX", 0);      // 设置显示位置
        params.y = sp.getInt("startY", 0);
        TextView tvLocation =  (TextView) view.findViewById(R.id.tv_toast_location);
        tvLocation.setText(content);
        // 将View添加到窗体管理器
        wm.addView(view, params);
    }

2. 参数解析

1、LayoutParams.Flags参数表示Window的属性,通过设置它的选项可以控制Window的显示特性。如下几种常见选项:

FLAG_NOT_FOCUSABLE

不许获得焦点

FLAG_NOT_TOUCHABLE

不接受触摸屏事件

FLAG_NOT_TOUCH_MODAL

当窗口可以获得焦点(没有设置 FLAG_NOT_FOCUSALBE 选项)时,仍然将窗口范围之外的点设备事件(鼠标、触摸屏)发送给后面的窗口处理。否则它将独占所有的点设备事件,而不管它们是不是发生在窗口范围内。

FLAG_SHOW_WHEN_LOCKED

当屏幕锁定时,窗口可以被看到。这使得应用程序窗口优先于锁屏界面。可配合FLAG_KEEP_SCREEN_ON选项点亮屏幕并直接显示在锁屏界面之前。可使用FLAG_DISMISS_KEYGUARD选项直接解除非加锁的锁屏状态。此选项只用于最顶层的全屏幕窗口。

FLAG_DIM_BEHIND

  窗口之后的内容变暗

FLAG_BLUR_BEHIND

窗口之后的内容变模糊。

2、Type参数表示Window的类型,有3种主要类型:
1)Application_windows (应用Window):

    值在 FIRST_APPLICATION_WINDOW 和 LAST_APPLICATION_WINDOW 之间。
    是通常的、顶层的应用程序窗口。必须将 token 设置成 activity 的 token 。  

2)Sub_windows (子Window):

    取值在 FIRST_SUB_WINDOW 和 LAST_SUB_WINDOW 之间。与顶层窗口相关联,token 必须设置为它所附着的宿主窗口的 token。

3)System_windows (系统Window):

    取值在 FIRST_SYSTEM_WINDOW 和 LAST_SYSTEM_WINDOW 之间。

3、Window的层次

每个Window都有对应的z-ordered,层次大的会覆盖到层次小的Window上面。在三类Window中应用Window的层级范围在1~99之间,子Window的范围在1000~1999之间,系统Window的层级范围在2000~2999之间。

要使Window位于所有Window的最顶层,采用较大的层级即可,系统Window的层级是最大的,一般选用TYPE_SYSTEM_OVERLAY或TYPE_SYSTEM_ERROR,同时要声明权限android.permission.SYSTEM_ALERT_WINDOW。如下示例

    params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;

4、WindowManager提供的常用方法
WindowManager继承自ViewManager,提供了添加view、删除view和更新view,这三个方法都是定义在ViewManager。

public interface ViewManager
{
    /**
     * @param view The view to be added to this window.
     * @param params The LayoutParams to assign to view.
     */
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

Window的内部机制

Window是一个抽象的概念,每一个Window都对应着一个view和ViewRootImpl,Window与View通过ViewRootImpl建立起联系,Window是以View作为实体存在,实际使用WindowManager访问来Window,外部无法直接访问Window。WindowManager提供了三个针对View的接口方法addView、updateViewLayout和removeView,分析Window的内部机制从Window的添加、更新和删除开始。

Window的添加过程

Window的添加依赖于WindowManager,而WindowManager是一个接口,它的具体实现类是WindowManagerImpl,在WindowManagerImpl中实现了如下几个操作view的方法
@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
@Override
    public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.updateViewLayout(view, params);
    }
@Override
    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }

@Override
     public void removeViewImmediate(View view) {
        mGlobal.removeView(view, true);
    }

由上可知,WindowManagerImpl将操作view的实现都委托给了WindowManagerGlobal(即mGlobal),下面来看一下WindowManagerGlobal的addView方法,完整代码如下

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
// 1、---
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (display == null) {
            throw new IllegalArgumentException("display must not be null");
        }
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
            // If there's no parent, then hardware acceleration for this view is
            // set from the application's hardware acceleration setting.
            final Context context = view.getContext();
            if (context != null
                    && (context.getApplicationInfo().flags
                            & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
                wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
            }
        }

        ViewRootImpl root;
        View panelParentView = null;
// 2、---
        synchronized (mLock) {
            // Start watching for system property changes.
            if (mSystemPropertyUpdater == null) {
                mSystemPropertyUpdater = new Runnable() {
                    @Override public void run() {
                        synchronized (mLock) {
                            for (int i = mRoots.size() - 1; i >= 0; --i) {
                                mRoots.get(i).loadSystemProperties();
                            }
                        }
                    }
                };
                SystemProperties.addChangeCallback(mSystemPropertyUpdater);
            }
// 3、---
            int index = findViewLocked(view, false);
            if (index >= 0) {
                if (mDyingViews.contains(view)) {
                    // Don't wait for MSG_DIE to make it's way through root's queue.
                    mRoots.get(index).doDie();
                } else {
                    throw new IllegalStateException("View " + view
                            + " has already been added to the window manager.");
                }
                // The previous removeView() had not completed executing. Now it has.
            }
// 4、---
            // If this is a panel window, then find the window it is being
            // attached to for future reference.
            if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
                final int count = mViews.size();
                for (int i = 0; i < count; i++) {
                    if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                        panelParentView = mViews.get(i);
                    }
                }
            }

// 5、---
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }
// 6、---
        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }

上面addView方法大概做了如下几件事:
1、检查参数是否合法,并判断当前添加的是否为子Window(parentWindow是否为空),若为子Window则为其做相关调整,否则为其开启硬件加速
2、监视系统属性的变化
3、通过findViewLocked获取mViews中view的索引,看添加的view是否在mViews的集合里,如果获取的index>=0,此view存在,接着判断要删除的集合是否包含此view,若包含则直接执行doDie()删除当前view,若不包含则会抛出异常(此view正在被删除,还没有完成)
4、判断添加的是否为panel window,若是则找出以备后查
5、将Window的一系列参数添加到集合中,几种集合如下:

    mViews:存储了所有Window所对应的View
    mRoots:存储了所有Window所对应的ViewRootImpl
    mParams:存储了所有Window所对应的布局参数
    mDyingViews:存储的是即将被删除的View对象或正在被删除的View对象

6、通过ViewRootImpl的setView方法来完成界面的更新,并完成Window的添加。

在setView内部会通过requestLayout方法来完成异步刷新请求,scheduleTraversals是View的绘制入口函数。

@Override
public void requestLayout() {、
   if (!mHandlingLayoutInLayoutRequest) {
      checkThread();  // 检查线程,通常检查是否为主线程(禁止非主线程更新UI)           
      mLayoutRequested = true; 
      scheduleTraversals(); 
 }   
}

接着调用了scheduleTraversals方法

void scheduleTraversals() {        
        if (!mTraversalScheduled) {            
            mTraversalScheduled = true;            
            ///  向底层注册一个绘制事件,下次Vsync信号来时会执行相关事件             
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);   ...      
            }  
  }

下次垂直刷新信号来时,会回调mTraversalRunnable来执行绘制操作,实际会调用doTraversal方法,进一步会调用performTraversals方法,它是真正绘制的入口:

final class TraversalRunnable implements Runnable {
    @Override       
      public void run() {
          doTraversal();        
      }    
}


void doTraversal() {       
    if (mTraversalScheduled) {            
    mTraversalScheduled = false;   
    mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);         
    if (mProfile) {               
        Debug.startMethodTracing(“ViewAncestor”);            
    }            
    performTraversals();           
… 
  }
}

performTraversal函数很长,下面列出了主要做的工作:

  • dispatchAttachedToWindow -> onAttachedToWindow ,第一次添加时调用
  • executeActions , 执行attach view中post的Runnable action
  • relayoutWindow ,请求WindowManagerService来计算窗体大小,内容区域等。
  • performMeasure -> measure -> onMeasure ,递归执行测量
  • performLayout -> layout -> onLayout , 递归执行布局
  • performDraw -> draw -> onDraw , 递归执行绘制

在setView方法之后会接着执行如下代码,WindowSession最终完成Window的添加,mWindowSession的类型是IWindowSession,它是一个Binder对象,真正的实现类是Session,因此Window的添加的过程是一个IPC调用

try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                } catch (RemoteException e) {
                    mAdded = false;
                    mView = null;
                    mAttachInfo.mRootView = null;
                    mInputChannel = null;
                    mFallbackEventHandler.setView(null);
                    unscheduleTraversals();
                    setAccessibilityFocus(null, null);
                    throw new RuntimeException("Adding window failed", e);
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }

在Session内部会调用WindowManagerService的addWindow方法进行Window方法添加,具体的过程在WindowManagerService中实现了。WindowManagerService会为每个应用保留一个单独的Session。

  • 这个方法的IWindow 参数是一个Binder对象,用来回调应用的ViewRootImpl相关方法。
  • mService即WindowManagerService,最终调用了它的addWindow方法来完成最终的添加,将 Window的状态保存到了一个叫做WindowState 的对象中。之后涉及到了具体的Window参数, 层级计算等细节问题就略过了。
  • 可以发现,这个方法并没有传递View给WindowManagerService,因为它并不关心具体的UI内容,而只关心Window的层级、大小等信息
@Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
            InputChannel outInputChannel) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outStableInsets, outInputChannel);
    }

到此Window的添加就完成了。大致走了如下流程

WindowManager -> WindowManagerImpl -> WindowManagerGlobal>addView -> ViewRootImpl>setView>requestLayout ->
 (IPC)Session>addToDisplay -> WindowManagerService>addWindow

Window的删除过程

删除的过程与添加类似,通过WindowManagerGlobal来实现删除,下面看它的removeView方法

public void removeView(View view, boolean immediate) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            View curView = mRoots.get(index).getView();
            removeViewLocked(index, immediate);
            if (curView == view) {
                return;
            }

            throw new IllegalStateException("Calling with view " + view
                    + " but the ViewAncestor is attached to " + curView);
        }
    }

上述方法在要移除的view不为空的情况下,通过findViewLocked查找view在mViews(上述)中的索引,然后通过removeViewLocked进行删除。看一下这两个方法:

private int findViewLocked(View view, boolean required) {
        final int index = mViews.indexOf(view);
        if (required && index < 0) {
            throw new IllegalArgumentException("View=" + view + " not attached to window manager");
        }
        return index;
    }
private void removeViewLocked(int index, boolean immediate) {
        ViewRootImpl root = mRoots.get(index);
        View view = root.getView();

        if (view != null) {
            InputMethodManager imm = InputMethodManager.getInstance();
            if (imm != null) {
                imm.windowDismissed(mViews.get(index).getWindowToken());
            }
        }
        boolean deferred = root.die(immediate);
        if (view != null) {
            view.assignParent(null);
            if (deferred) {
                mDyingViews.add(view);
            }
        }
    }

从removeViewLocked方法可以看出,删除操作是由ViewRootImpl来完成的,删除分为两种,分别为同步删除(removeViewImmediate)和异步删除(removeView),在ViewRootImpl的die(immediate)方法中进行判断。如果为同步则直接调用doDie方法进行删除,否则会发送一个消息进行异步处理,同时执行mDyingViews.add(view)

**
     * @param immediate True, do now if not in traversal. False, put on queue and do later.
     * @return True, request has been queued. False, request has been completed.
     */
    boolean die(boolean immediate) {
        // Make sure we do execute immediately if we are in the middle of a traversal or the damage
        // done by dispatchDetachedFromWindow will cause havoc on return.
        if (immediate && !mIsInTraversal) {
            doDie();
            return false;
        }

        if (!mIsDrawing) {
            destroyHardwareRenderer();
        } else {
            Log.e(TAG, "Attempting to destroy the window while drawing!\n" +
                    "  window=" + this + ", title=" + mWindowAttributes.getTitle());
        }
        mHandler.sendEmptyMessage(MSG_DIE);
        return true;
    }

在doDie方法内部调用dispatchDetachedFromWindow()方法删除Window,最后调用WindowManagerGlobal的doRemoveView方法进行数据刷新,包括mRoots,mViews,mParams和mDyingViews,需要将当前Window所关联的这三类对象从集合中删除

void doDie() {
        checkThread();
        synchronized (this) {
            if (mRemoved) {
                return;
            }
            mRemoved = true;
            if (mAdded) {
                dispatchDetachedFromWindow();
            }

            ... 
        WindowManagerGlobal.getInstance().doRemoveView(this);
    }

dispatchDetachedFromWindow方法如下:

void dispatchDetachedFromWindow() {
        if (mView != null && mView.mAttachInfo != null) {
            mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
            mView.dispatchDetachedFromWindow();  // 回调
        }

        mAccessibilityInteractionConnectionManager.ensureNoConnection();
        mAccessibilityManager.removeAccessibilityStateChangeListener(
                mAccessibilityInteractionConnectionManager);
        mAccessibilityManager.removeHighTextContrastStateChangeListener(
                mHighContrastTextManager);
        removeSendWindowContentChangedCallback();  // 清除一些监听与回调

        destroyHardwareRenderer();   // 销毁硬件渲染相关资源

        setAccessibilityFocus(null, null);

        mView.assignParent(null);
        mView = null;
        mAttachInfo.mRootView = null;

        mSurface.release();

        ...
        try {
            mWindowSession.remove(mWindow); //将mWindow从Window中删除
        } catch (RemoteException e) {
        }

      ...
        unscheduleTraversals();
    }

在dispatchDetachedFromWindow方法中真正执行删除操作,内部作了如下几件事:
1、调用Wiew的dispatchDetachedFromWindow方法,它的方法内部会调用onDetachedFromWindow()方法,当view从Window被移除,此方法就会被调用,可以在此方法中做一些资源回收工作,诸如终止动画、线程
2、垃圾回收的相关工作,如清理数据和消息、移除回调和监听。
3、通过Session的remove方法移除Window:mWindowSession.remove(mWindow),此过程是一个IPC过程,最终会调用WindowManagerService的removeWindow方法。
4、取消计划任务,主要是一些View绘制操作(测量、布局及绘制)
到此,Window的删除过程就已经完成了,大致流程

WindowManager -> (实现类)WindowManagerImpl ->(委托类) WindowManagerGlobal>removeView>removeViewLocked  ->
 ViewRootImpl>doDie>dispatchDetachedFromWindow  ->  (IPC) Session>remove  ->  WindowManagerService>removeWindow

Window的更新过程

同创建、删除Window类似,更新Window的实施者依然是WindowManagerGlobal,下面看它的updateViewLayout方法

public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;

        view.setLayoutParams(wparams);

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            ViewRootImpl root = mRoots.get(index);
            mParams.remove(index);
            mParams.add(index, wparams);
            root.setLayoutParams(wparams, false);
        }
    }

更新过程首先要替换旧的params,接着通过ViewRootImpl的setLayoutParams方法进行更新ViewRootImpl中的params,在setLayoutParams方法内通scheduleTraversals进行Vew的重新布局(测量、布局、绘制),并会通过如下流程来更新Window的视图

scheduleTraversals-> doTraversal -> performTraversals-> relayoutWindow-> mWindowSession.relayout -> mService.relayoutWindow

到此Window的更新就完成了,大致流程如下:

WindowManager -> WindowManagerImpl  ->  WindowManagerGlobal>updateViewLayout -> ViewRootImpl>setLayoutParams>scheduleTraversals>doTraversal>performTraversals>relayoutWindow -> (IPC)Session>relayout  -> WindowManagerService>relayoutWindow

后记:此篇参考了安卓开发艺术探索,融入个人总结所成,如有错误请不吝赐教。特此说明。更多细节可查阅android源码。
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值