【Android】创建窗口的过程

创建窗口的过程

从WmS的角度来看,一个窗口并不是Window类,而是一个View类.WmS收到用户消息后,需要把消息派发到窗口,View类本身并不能直接接收WmS传递过来的消息,真正接收用户消息的必须是IWindow类,而实现IWindow类的是ViewRoot.W类,每一个W内部都包含了一个View变量.

WmS并不介意该窗口(View)是属于哪个应用程序的,WmS会按一定的规则判断哪个窗口处于活动状态,然后把用户消息给W类,W类再把用户消息传递给内部的View变量,剩下的消息处理就由View对象完成.

  1. 窗口的类型

    Framework定义了三种窗口类型,三种类型的定义在WindowManager类中.

    • 第一种为应用窗口.所谓的应用窗口是指该窗口对应一个Activity,由于加载Activity是由AmS完成的,因此,对于应用程序来讲,要创建一个应用类窗口,只能在Activity内部完成.
    • 第二种是子窗口.所谓的子窗口是指,该窗口必须有一个父窗口,父窗口可以是一个应用类型窗口,也可是是任何其他类型的窗口.
    • 第三类是系统窗口.系统窗口不需要对应任何Activity,也不需要有父窗口.对于应用程序而言,理论上是无法创建系统窗口的,因为所有的应用程序都没有这个权限,然而系统进程却可以创建系统窗口.

    WindowManager类对这三种类型进行了细化,把每一种类型都用一个int常量表示,这些实际上代表了窗口对应的层(Layer).WmS在进行窗口叠加时,会按照该int常量的大小分配不同层,int值越大,代表层的位置越靠上面,即所谓的z-order.

    类型一的定义如下表所示.

    所有的Activity默认的窗口类型都是TYPE_APPLICATION,WmS在进行窗口叠加时,会动态改变应用窗口的层值,但层值不会大于99.

    类型二为子窗口,如下表所示.

    同样,创建子窗口时,客户端可以指定窗口类型介于1000~1999之间,而WmS在进行窗口叠加时,会动态调整层值.

    类型三为系统窗口,如下表所示.

    同样,当具备创建系统窗口权限时,创建系统窗口可以指定层值在2000~2999之间,WmS在进行窗口叠加时,会动态调整该层值.所不同的是,由于有些系统窗口只能出现一个,即不能添加多个,否则用户会觉得很乱,比如输入法窗口,再比如系统状态条窗口,因此,WmS在接收到创建窗口的消息时,会进行一定的检查,确保该窗口只能被创建一次.

  2. token变量的含义

    阅读本篇博文时,如果觉得不理解,可以先跳过,继续看后面的,看完再回过来看.

    token在英语中的含义为象征,符号,代表等.在创建窗口时,多处定义了和token相关的变量,而无论该变量具体的名称是什么,该变量的类型一般都是一个IBinder对象.既然是IBinder对象,其作用也就是显而易见的,即为了进行IPC调用.而与创建窗口相关的IPC对象一般只有两种,一种是指向某个W类的token,另一种是指向某个HistoryRecord的token.其中HistoryRecord对象是AmS内部为运行的每一个Activity创建的一个Binder对象,客户端的Activity可以通过Binder对象通知当前Activity的状态.

    具体来讲,token的定义出现在以下几处,如下表所示.

    后面的博文将对上表的定义进行解释.

    2.1 Activity中的mToken

    如上所述,AmS内部为每一个运行的Activity都创建了一个HistoryRecord对象,该对象的基类是一个Binder,因此mToken变量的意义正是指向了该HistoryRecord类.该变量的值是在Activity.init()函数中完成的,其流程如下图所示.

    2.2 Window中的mAppToken

    每一个Window对象中都有一个mAppToken变量,注意这里说的是Window对象,而不是窗口.

    前面说过,一个窗口本质上是一个View,而Window类却是一个应用窗口的抽象,这就好比Window侧重于一个窗口的交互,而窗口(View)则侧重于窗口的显示.所以,mAppToken并不是W类的引用,事实上正如其名称所指,它是AmS在远程为每一个Activity创建的HistoryRecord的引用.

    事实上,Window类中还有其他的Binder对象,同时由于Window并不一定要对应一个Activity,因此,如果Window类不属于某个Activity,mAppToken的变量则为空,否则mAppToken的值与Activity中的mToken值是相同的.

    2.3 WindowManager.LayoutParams中的token

    WindowManager.LayoutParams中token的意义正如其所在的类的名称,该类是在添加窗口时指定该窗口的布局参数,而token的意义正是指定该窗口对应的Binder对象,因为WmS需要该Binder对象,以便对客户端进行IPC调用.

    那么,该Binder对象应该是谁呢?你可能会认为该token应该是W类,没错,这只是一种情况而已,具体来讲,该token变量的值可以有三种.

    • 如果创建的窗口是应用窗口,token的值和Window中mAppToken值相同.
    • 如果创建的窗口为子窗口,token为其父窗口的W对象.
    • 如果创建的窗口是系统窗口,那么,token值为空.

    2.4 View中的token

    首先来看ViewRoot,客户端的每一个窗口都对应一个ViewRoot对象,该对象内部的mAttachInfo是该对象被构造时同时创建的.该变量的类型和View对象中的mAttachInfo相同.

    View类中的mAttachInfo,其含义是当该View对象被真正作为某个窗口W类的内部View时,该变量就会被赋值为ViewRoot中的mAttachInfo.在一般情况下,屏幕上所有的View对象的mAttachInfo都是被赋值过的,因为当W类中的View被添加为一个真正的窗口后,ViewRoot会调用performTraversal()方法,而该方法则会调用View或者ViewGroup的dispatchToWindow()方法.在后者调用中,会把ViewRoot中的mAttachInfo赋值给View中的mAttachInfo,所以,同一个窗口中包含的所有View对象,其内部的mAttachInfo的内容都是相同的.该变量的赋值过程如下图所示.

    mAttachInfo变量中,包含了三个Binder变量.

    • mWindowToken,如其名称所示,指的是该窗口对应的W对象.
    • mPanelParentWindowToken,如果该窗口是子窗口的,那么该变量即为父窗口中的W对象.该变量赋值和mWindowToken是互斥的,因为mWindowToken如果不为空,则意味着该窗口没有父窗口.
    • mWindow,注意,尽管该变量也是一个Binder对象,但它却更是一个IWindow对象,关于IBinder与IWindow的区别请参照前面博文中关于Binder的介绍.由此也可以看出,mWindowToken似乎是多余的,因为mWindowToken = IWindow.asBinder(),但也无所谓,只是多了一个变量而已,使用起来更方便.

    以上所有token的关系如下图所示.

  3. 创建应用窗口

    以上了解了不同类型窗口的含义,接下来介绍Framework是如何创建这些窗口的.首先来看应用窗口的创建.其总体流程如附图2所示,下面结合该图中的步骤一一介绍.

    1. 每个应用类窗口都对应一个Activity对象,因此,创建应用类窗口首先需要创建一个Activity对象.当AmS决定启动某个Activity时,会通知客户端进程,而每个客户端进程都对应一个ActivityThread类,任何Activity都必须隶属于一个应用程序,因此,启动Activity的任务最终由ActivityThread完成.

      启动某个Activity的代码本质是构造一个Activity对象,其代码如下所示:

      Activity activity = null;
      try{
          java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
          activity = mInstrumentataion.newActivity(cl,component.getClassName(),r.intent);
          r.intent.setExtrasClassLoader(cl);
          if(r.state != null){
              r.state.setClassLoader(cl);
          }
      
      }catch(Exception e){
          if(!mInstrumentation.onException(activity,e)){
              throw new RuntimeException("Unable to instantiate activity " + component + ":" + e.toString(),e);
          }
      
      }
      

      从以上代码中,使用ClassLoader从程序文件中装载指定的Activity对应的Class文件.

    2. 构造好指定的Activity对象后,接着调用Activity的attach()方法,其代码如下:

      activity.attach(appContext,this,getInstrumentation(),r.token,r.ident,app,r.intent,r.activityInfo,title,r.parent,r.embeddedID,r.lastNonConfigurationInstance,r.lastNonConfigurationChildInstances,config);
      

      attach()作用是为刚刚构造好的Activity设置内部变量,这些变量是以后进行Activity调度所必须的.这些重要的变量包括以下几项.

      • appContext : 该对象将作为Activity的BaseContext.在上篇博文中曾经讲过,Activity的本质是一个Context,而同时Activity有时继承于ContextWrapper,该类中需要一个真正的Context对象,而这就是appContext,该对象使用new ContextImpl()方法创建.
      • this : 这就是指当前ActivityThread对象,Activity对象内部可能会需要主程序的引用.
      • r.token : r是一个ActivityRecord对象,其内部变量token的含义是AmS中的一个HistoryRecord对象.
      • r.parent : 一个Activity可以有一个父Activity,这种理念是为了允许把一个Activity嵌入到另一个Activity内部执行.在应用程序使用时,常用ActivityGroup类,而ActivityGroup功能的内部支持的正是该变量.
    3. 在attach()方法内部,除了进行重要变量赋值外,另一件重要的事情就是为该Activity创建Window对象,这是通过调用PolicyManager的静态方法makeNewWindow()完成的.

      PolicyManager会根据com.android.internal.policy.impl.Policy的配置创建不同产品类型的窗口.尽管如此,这仅仅是一种程序设计的灵活性,其代码归根结底只不过是创建了一个PhoneWindow对象而已.当前的Framework中仅仅定义有两种Window的具体实现,一种是MidWindow类,另一种是PhoneWindow类,而前者基本上没有使用.从这一点也可以看出,Android的设计之初所设想的两种应用,其中一种是手机,另一种是便携上网设备(Mobile Internet Device).

      创建好Window对象后,将其赋值给Activity的内部变量mWindow,并设置该Window的Callback接口为当前的Activity对象,这就是为什么用户消息能够传递到Activity中的原因.如以下代码所示.

      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,
          Object lastNonConfiguratinInstance,
          HashMap<String,Object> lastNonConfigurationChildInstances,
          Configuration config){
      
          attachBaseContext(context);
      
          mWindow = PolicyManager.makeNewWindow(this);
          mWindow.setCallback(this);
      }
      
    4. 创建好Window对象后,需要给Window对象中的mWindowManager变量赋值,该变量的类型是WindowManager类.每一个Window内部都有一个WindowManager对象,你可能会觉得,WindowManager类是一个重量级的类,如果每个Window中都包含一个会不会是一种浪费呢?事实上,WindowManager类仅仅是一个interface类而已,真正实现该接口的有两个类,一个是Window.LocalWindowManager子类,另一个是WindowManagerImpl类.LocalWindowManager仅仅是一个壳,这就有点像ContextWrapper,本身虽然也提供了WindowManager接口的全部功能,然而真正实现这些功能的却是壳里面的WindowManager对象,这就是WindowManagerImpl类,其关系如下图所示.

      也正是这种关系,所以,每个Window内部才包含了一个WindowManager的壳以便进行其他操作,给mWindowManager赋值的代码如下:

      mWindow.setWindowManager(null,mToken,mComponent....)
      if(mParent != null){
          mWindow.setContainer(mParent.getWindow());
      }
      mWindowManager = mWindow.getWindowManager();
      

      在以上代码中,setWindowManager()方法的第一个参数为null,而在Window类该方法实现中,如果第一个参数为null,其内部就会创建一个LocalWindowManager对象.第二个参数正是AmS中Activity对应的HistoryRecord的Binder引用,该变量将作为Window中的mAppToken的值.

      每个Activity内部也有一个mWindowManager对象,其值和Window类中的同名变量相同.

    5. 配置好了Activity和Window对象后,接下来就需要给该窗口中添加真正的显示元素View或者ViewGroup.这是从performLaunchActivity()内部调用callActivityOnCreate()开始的,并会辗转调用到Activity的onCreate()方法中,该方法对于应用程序开发者来讲再熟悉不过了.

      在过去的开发经验中,大家都知道给Activity添加界面是在onCreate()方法中调用setContentView()方法,而该方法实际上却又调用到了其所对应的Window对象的setContentView()方法,如以下代码所示:

      public void setContentView(int layoutResID){
          getWindow().setContextView(layoutResID);
      }
      

      因此,下面继续分析Window中如何把一个layout.xml文件作为Window界面.

    6. PhoneWindow的setContentView()方法如以下代码所示:

      public void setContentView(int layoutResID){
          if(mContentParent == null){
              installDecor();
          }else{
              mContextParent.removeAllViews();
          }
          mLayoutInflater.inflate(layoutResID,mContentParent);
          final Callback cb = getCallback();
          if(cb != null){
              cb.onContentChanged();
          }
      }
      

      首先调用installDecor()方法为Window类安装一个窗口修饰,所谓的窗口修饰就是界面上常见的标题栏,程序中指定的layout.xml界面将被包含在窗口修饰中,称为窗口内容.窗口修饰也是一个ViewGroup,窗口修饰及其内部的窗口内容加起来就是我们所说的窗口,或者叫做Window的界面.窗口修饰及窗口内容如下图所示.

      Framework中定义了多种窗口修饰,installDecor()代码如下,该段代码主要完成三件工作.

      private void installDecor(){
          if(mDecor == null){
              mDecor = generateDecor();
              mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
              mDecor.setIsRootNamespace(true);
          }
          if(mContentParent == null){
              mContentParent = generateLayout(mDecor);
          }
      }
      
      • 使用generateDecor()创建一个DecorView对象,并赋值给mDecor变量.该变量并不完全等同于窗口修饰,窗口修饰是mDecor内部的唯一一个子视图.
      • 根据用户指定的参数选择不同的窗口修饰,并把该窗口修饰作为mDecor的子窗口,这是在generateLayout()中调用mDecor.addView()完成的.
      • 给mContentParent变量赋值,其值是通过调用ViewGroup.contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT)获得的,ID_ANDROID_CONTENT正是id = content的FrameLayout.

      不同窗口修饰的区别不大,比如是否有标题栏,是否显示左右进度条等,这些修饰窗口共同的特点是其内部必须包含一个id=content的FrameLayout,因为内容窗口正是被包含在该FrameLayout之中.常见的窗口修饰对应的XML文件如下,其路径为frameworks/base/core/res/layout.

      R.layout.dialog_ title_icons

      R.layout.screen_ title_icons

      R.layout.screen__progress

      R.layout.dialog_ custom_title

      R.layout.screen_ custom_title

      R.layout.dialog_ title

      R.layout.screen_title

      R.layout.screen_simple

      安装完窗口修饰后,就可以把用户界面layout.xml文件添加到窗口修饰中,这是通过在setContentView()中调用inflate()方法完成的,该方法的第二个参数正是mContentParent,即id=content的FrameLayout.

      最后,回调cb.onContentChanged()方法,通知应用程序窗口内容发生了改变,因为从无到有了.而cb正是Activity自身,因为Activity实现了Window.CallBack接口,并且在attach()方法中将自身作为Window对象的Callback接口实现.

      以上三件工作中的第二个要特别说明,所谓的”根据用户指定的参数”中”用户指定”有两个地方可以指定.

      第一个地方是在Activity的onCreate()方法中调用得到当前Window,然后调用requestFeature()指定.generateLayout()方法中使用getLocalFeature()获取feature值,并根据这些值选择不同的窗口修饰.

      另一个是在AndroidManifest.xml中Activity元素内部使用android:theme=”xxx”指定.generateLayout()方法中使用getWindowStyle()方法获取这些值,该方法的调用流程如下:

      • getWindowStyle() : PhoneWindow中generateLayout()
      • ->obtainStyleAttributes():Window中
      • ->getTheme().obtainStyledAttributes():Context中

      流程的最后一个是调用getTheme(),而这正是使用android:theme赋值的结果.

      以上”用户指定”及generateLayout()方法内部的流程可以总结为下图所示.

    7. 给Window类设置完其视图元素后,剩下的就是把创建的这个窗口告诉给WmS,以便WmS能够把该窗口显示在屏幕上.首先要做的是,当Activity准备好后会通知AmS,然后AmS经过各种条件的判断并最终调用到Activity的makeVisible()方法,该方法及后续的各种调用将完成真正的把窗口添加进WmS之中.其代码如下:

      void makeVisible(){
          if(!mWindowAdded){
              ViewManager wm = getWindowManager();
              wm.addView(mDecor,getWindow().getAttributes());
              mWindowAdded = true;
          }
          mDecor.setVisibility(View.VISIBLE);
      }
      
    8. 在makeVisible()方法中,首先获得该Activity内部的WindowManager对象,这实际上就是Window.LocalWindowManager对象,然后调用该对象的addView()方法,注意这不是WindowManagerImpl类的addView()方法.

      addView()方法的第一个参数mDecor是一个DecorView对象,也就是用户所能看得到的,一个Activity对应的全部界面内容;第二个参数是在构造Window对象时默认构造的WindowManager.LayoutParams对象,如以下代码所示,该代码在Window类的初始化代码中.

      //The current window attributes
      private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
      

      WindowManager.LayoutParams的构造函数如下:

      public LayoutParams(){
          super(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
          type = TYPE_APPLICATION;
          format = PixelFormat.OPAQUE;
      }
      

      以上构造函数说明,在默认情况下窗口参数的类型是一个TYPE_APPLICATION类型,即应用程序类型的窗口.

      Activity中添加窗口时为什么不直接使用WindowManagerImpl类而是使用一个LocalWindowManager类呢?其原因是,后者会检查WindowManager.LayoutParams的值,并给其内部token变量赋值,以便能够正确添加,而使用WindowManagerImpl类添加窗口时则不检查params的值,LocalWindowManager相当于说一道关卡.

      addView()的重点代码如下,即上面所说的”关卡”的作用

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

      如果添加的是子窗口,该关卡会检查params中的token,如果token为空,则把Activity对应的窗口的token赋值给params的token.

      如果添加的不是子窗口,则把mAppToken赋值给params的token.当然,如果该Activity被某个Activity包含,则把父Activity的mAppToken赋值给params的token,如以下代码所示.

      if(wp.token == null){
          wp.token = mContainer == null?mAppToken : mContainer.mAppToken;
      }
      
    9. 过了LocalWindowManager的addView()的那道关卡后,就需要来点真格的了,即调用WindowManagerImpl的addView()方法.一个应用程序内部无论有多少个Activity,但只有一个WindowManagerImpl对象,在WindowManagerImpl类中维护三个数组,用于保存该应用程序中所拥有的窗口的状态,它们分别是:

      • View[] mViews.这里的每一个View对象都将成为WmS所认为的一个窗口.
      • ViewRoot[] mRoots.所有的ViewRoot对象,mViews中每个View对象都对应的ViewRoot对象.
      • WindowManager.LayoutParams[] mParams.当把mViews中的View对象当做一个窗口添加进WmS中,WmS要求每个被添加的窗口都要对应一个LayoutParams对象,mParams正是保存了每一个窗口对应的参数对象.

      addView()的执行流程如下.

      (1) 检查所添加的窗口是否已经添加过了,不允许重复添加.

      (2) 如果所添加的窗口为子窗口类型,找到其父窗口,并保存在内部临时变量panelParentView中,该变量将作为后面调用ViewRoot的setView()的参数.

      (3) 创建一个新的ViewRoot,因为前面说过,每个窗口都对应一个ViewRoot对象.

      (4) 调用ViewRoot的setView()方法,完成最后的,真正意义上的添加工作.

    10. 完成上面所说的新建一个ViewRoot对象后,需要把新建的ViewRoot对象添加到mRoots对象中,其添加的逻辑是,新建三个长度都加1的数组,然后把原来数组mViews,mRoots,mParams的内容复制到新建数组,并把新建的View,ViewRoot及WindowManager.LayoutParams对象保存到三个数组的最后.如下图所示

    11.调用ViewRoot对象的setView(View view,WindowManager.LayoutParams attrs,View panelParent)方法,该方法将完成最后的窗口添加工作,三个参数的意义如下.

    • view : 是WindowManagerImpl中mViews数组的一个元素,也就是新建的窗口界面.
    • attrs : 即为添加窗口的参数,该参数描述该窗口的呈现风格,大小,位置等,尤其是其内部变量token,指明了该窗口和相关Activity的关系(如果有的话).
    • panelParentView : 该对象也是WindowManagerImpl中mViews数组的一个元素,仅当该窗口有父窗口时,该值才有意义.

    setView()的执行流程如下:

    1. 给ViewRoot的重要变量赋值.包括mView,mWindowAttributes及mAttachInfo,如以下代码所示.

      mView = view;
      mWindowAttributes.copyFrom(attrs);
      
      mSoftInputMode = attrs.softInputMode;
      mWindowAttributesChanged = true;
      mAttachInfo.mRootView = view;
      
      if(panelParentView != null){
          mAttachInfo.mPanelParentWindowToken = panelParentView.getApplicationWindowToken();
          mAdded = true;
      }
      

      对于mAttachInfo变量,其成员mRootView赋值为参数view.如果添加的是子窗口,那么同时给mAttachInfo的成员mPanelParentWindowToken赋值,其值为父窗口的token.

    2. 调用requestLayout(),发出界面重组请求. 该方法仅仅是发出一个异步消息,以便UI线程下一个消息处理是界面重绘,从而让该窗口在响应任何其他用户消息之前首先变得可见.

    3.调用sWindowSession.add(),通知WmS添加窗口,如以下代码所示.

    try{
        res = sWindowSession.add(mWindow,mWindowAttributes,getHostVisibility(),mAttachInfo.mContentInsets);
    }
    

    sWindowSession是ViewRoot中的一个静态变量, 每一个应用程序仅有一个sWindowSession对象,该对象类型为IWindowSession,即为一个Binder引用,该引用对应WmS中的Session子类,WmS为每一个应用程序分配一个Session对象.

    从sWindowSession.add()这段代码似乎可以看出,只要能够获得sWindowSession的引用,就可以任意创建窗口,而不需要经过以上冗长的十一个步骤,比如,可以不经过Activity,也不经过WindowManager,也不需要创建Window对象,然而,事实并非如此.因为sWindowSession这个变量的访问权限为”包内访问”,不加任何权限修饰符即为包内访问,如以下代码所示.

    static IWindowSession sWindowSession;
    

    这就意味着应用程序无法直接获取该对象,也就无法调用该对象的add()方法,而这是客户程序请求WmS添加窗口的唯一入口.那么不通过ViewRoot的sWindowSession,还有没有其他办法获得sWindowSession呢?这就要问:sWindowSession变量的值是怎么来的呢?答案是,在ViewRoot的构造函数中.构造函数中会调用getWindowSession(),而该方法内部会判断sWindowSession是否为空,如果为空则创建一个,如以下代码所示,这也是所谓的工厂模式创建对象.

    public static IWindowSession getWindowSession(Looper mainLooper){
        synchronized(mStaticInit){
            if(!mInitialized){
                try{
                    InputMethodManager imm = InputMethodManager.getInstance...
                    sWindowSession = IWindowManager.Stub.asInterface(ServiceManager.getService("window")).openSession(imm.getClient(),imm.getInput...);
                    mInitialized = true;
                }catch(RemoteException e){
    
                }
                return sWindowSession;  
            }
        }
    }
    

    由以上代码可以看出,sWindowSession是通过IWindowManager.Stub.asInterface()而来的,函数的参数实际上是一个WindowManager对象,可问题是,IWindowManager类在源码中是@hide的,所以下代码所示.

    @hide
    public interface IWindowManager extends android.os.IInterface{
        ....
    }
    

    hide意味着,SDK中将不包含该类,这再次打消了我们试图不经过ViewRoot类添加窗口的念头.看来,在标准的程序设计中,只能通过WindowManager类来创建窗口.如果产品有特别需要,可以根据以上流程改变相应的Framework代码,以达到灵活配置的需求.

    到此为止,从客户端的角度来讲,已经完成了窗口创建的全部工作.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值