Activity的生命周期和启动模式

1.1俩种状态下生命周期分析

1.正常状况****生命周期分析

1.七个生命周期

1,onCreate : 表示正在Activity创建

2,onRestart: 表示Activity正在重新启动,比如打开新的Activity返回到本Activity中时(由不可见变为可见状态时会调用)

3,onStart : 表示Activity正在启动,即创建后开始启动(此时活动不一定可见)

4,onResume : 表示Activity已经可见,可以在前台发生活动

5,onPause : 表示该Activity正在停止,一般会马上调用onStop,在调用onStop之前,如果可以返回Activity,那么他会调用前面的onResume方法,当该方法执行完毕新Activtiy才会调用onResume

6, onStop : 表示该Activity即将停止,

7,onDestroy : 表示当前活动马上就会被销毁

在这里插入图片描述

2.部分源码分析

活动A的onResume和活动A的下一个活动B的onPuse那个先执行?

//dontWaitForPause用于表示是否需要等待当前Activity暂停完成

boolean dontWaitForPause =(next.info.flags&ActivityInfo.FLAG RESUME_WHILE_PAUSING)!=0;

//pausing表示是否正在暂停Activity堆栈,   ActivityStack的管理者,用于管理Activity堆栈。
//userLeaving 是一个参数,用于指示是否是用户主动离开当前Activity
//pauseBackStacks 方法似乎是用来对 Activity Stack 中的 Activity 进行暂停操作

boolean pausing =mStackSupervisor.pauseBackStacks(userLeaving, true,dontWaitForPause);

//当前处于resumed状态的Activity

if(mResumedActivity !=null) {
	pausing I= startPausingLocked (userLeaving, false, true, dontWaitForPause);
if (DEBUG STATES) Slog.d(TAG,"resumeTopActivityLocked: Pausing " +mResumedActivity);
} 

Acitvity的启动涉及 Instrumentation,ActivityThread和ActivityMangerService(AMS) 三个部分,启动Acitivity的请求由 Instrumentation处理,他通过Binder向AMS发送请求,而AMS内部有一个ActivityStack负责栈内的Activty同步,AMS通过ACtivityThread来同步Activty的状态,实现生命周期方法的调用。上面的代码来自,ActivityStack中的resumeTopActivityInnerLocked方法

上面的代码

  • 首先,根据下一个Activity的标志位判断是否需要等待当前Activity暂停完成,将结果存储在dontWaitForPause变量中。
  • 接下来,调用pauseBackStacks方法暂停Activity堆栈,传入用户是否正在离开以及是否需要等待暂停完成等参数,将结果存储在pausing变量中。

如果当前有Activity处于resumed状态(处于前台可以与用户交互)(mResumedActivity != null),则继续执行以下操作:

  1. 调用startPausingLocked方法**,开始暂停当前Activity,**传入用户是否正在离开、false、true和dontWaitForPause等参数,并将结果存储在I变量中。
  2. 如果开启了DEBUG STATES模式,则输出日志信息表示正在暂停当前Activity

从上面我们可以知道,dontWaitForPause是会判断下一个活动的状态来进行判断,影响他的下一步操作,所以只有活动A的onPause执行后新的活动才会启动

不可以在onPause中进行重量级操作(耗时),onPause和onStep都不可以进行耗时操作,如果无法避免应该在onStep中操作

2.异常状况****生命周期分析

1. 资源相关的系统配置发生改变导致Activity被杀死并重新创建

在处理Activity之前,了解一下资源加载机制,比如图片,我们是将图片放置在drawable目录,然后通过Resources获取图片,为了适配不同的设备我们需要在其他文件夹也准备图片,如drawable-mdpi等,当程序启动是,会根据设备加载合适的Resources资源,比如手机横屏和竖屏手机得到是俩张不同的图片(如果有提前设置的话),在手机Activity从竖屏转换为横屏的时候,默认情况下它会将Activity杀死,然后重新创建
在这里插入图片描述

在系统配置改变的时候,原有活动会被正常杀死,onPause,onStop,onDestroy,都会被正常执行,然后因为是异常终止,所以系统会通过onSavelnstanceState来保存当前Activty的状态,他会在onStop之前执行,与onPause无硬性执行先后,在新的Activity被创建之后,系统会调用onRestorelnstanceState,前面销毁时onSavelnstanceState会保存一个Bundle对象,将其传给onRestorelnstanceState来重建Activity,在执行顺序上说onRestorelnstanceState在onStart之后执行

在这个过程中,系统会自动保存和恢复Activity的视图结构,比如用户输入的数据,ListView的位置。和Activity一样View 也有上面提到的俩个方法

在保存和恢复VIew时,系统的工作流程是:

  • Activity被异常终止
  • Activity会调用onSaveInstanceState保存数据,
  • 然后Activty会委托Window保存数据,
  • Windows委托他上面的顶级容器保存数据,顶级容器可以是一个ViewGroup,一般来说可能是DecorVeiw,
  • 最后由顶级容器一一通知子元素来保存数据

Window(窗口) : 不是Windows操作系统在Android中,每个Activity对应一个Window对象,Window负责管理Activity的布局、显示等操作。Window是一个抽象的概念,实际上是WindowManagerService在底层的实现。Activity与用户交互都是通过Window来实现的

ViewGroup(视图容器):ViewGroup是View的子类,用于容纳和管理其他View对象。在Android布局中,大部分的布局都是通过ViewGroup来实现的,比如LinearLayout、RelativeLayout等

DecorView(装饰视图):DecorView是Window中的顶级视图,是整个Activity布局的根视图。它包含了系统标题栏、内容区域、底部导航栏等元素,可以看作是整个Activity布局的容器。

部分源码分析

书上给出了TextView相关部分的部分源码

@Override
//保存当前 TextView 的状态,并返回一个 Parcelable 对象
public Parcelable onSaveInstanceState() {
//用于保存父类的状态信息
        Parcelable superState = super.onSaveInstanceState();
// 是否需要保存文本内容的状态。
        boolean save = mFreezesText;
//分别表示文本内容中选择文本的起始和结束位置。
        int start = 0;
        int end = 0;
//如果文本不为空,获取文本起始和结束位置
        if (mText != null) {
        start = getSelectionStart();
        end = getSelectionEnd();
//经过判断需要保存文本
            if (start => 0 || end => 0) {
               save = true;
             }
        }
        if (save) {
//个自定义的 Parcelable 子类,用于保存 TextView 的状态信息,目前保存了父类的数据
        SavedState ss = new SavedState(superState);
        ss.selStart = start;
        ss.selEnd = end;
//Spanned 是一个接口,SpannableStringBuilder 是其实现类,用于处理带有样式文本的 CharSequence。
//检查 mText 是否是 Spanned 的实例,
        if (mText instanceof Spanned) {
        Spannable sp = new SpannableStringBuilder(mText);
            if (mEditor != null) {
            //个自定义方法,用于移除文本中的拼写错误样式。
             removeMisspelledSpans(sp);
            sp.removeSpan(mEditor.mSuggestionRangeSpan);
            }
           ss.text = sp;
        }
// 如果 mText 不是 Spanned 的实例,直接将 mText 转换为字符串后赋值给 SavedState 对象 ss 中的 text 属性
        else {
        ss.text = mText.toString();
        }
//查 TextView 是否获得焦点,并且选择的文本起始和结束位置大于等于 0
        if (isFocused() && start => 0 && end => 0) {
        ss.frozenWithFocus = true;
        }
           ss.error = getError();
            return ss;
          }
        return superState;
        }

首先判断是否需要保存文本内容的状态,如果文本不为空且选择文本起始和结束位置大于等于0,则需要保存。接着创建一个自定义的 Parcelable 子类 SavedState 来保存 TextView 的状态信息,包括选择文本的起始和结束位置、文本内容以及焦点信息。如果文本包含样式信息(Spanned 实例),则处理样式后保存;否则直接将文本转换为字符串保存。最后,根据是否获得焦点和选择文本的情况设置相应的状态标志,保存错误信息,最终返回保存的状态信息。这里有一点就是他的TextView不只是保存了字符串,还保存了其他状态,比如是否处于焦点状态等

2.资源内存不足导致低优先级的Activity被杀死

​ Activty是有不同的优先级的,这个优先级由高到低分为三个情况:

  1. 前台activity:正在和用户进行交互,用户直接解除,优先级是最高的
  2. 可见但是非前台activty:可见但是在其之上还有其他的控件导致其无法直接与用户交互,比如弹出对话框时底部的activity
  3. 后台Activity:已经被暂停的Activity,优先级最低

在内存不足的时候,系统会依次按照优先级一个个杀死Activity,后续通过onSavelnstanceState和onRestorelnstanceState来恢复数据,如果一个进程不在四大组件中运行,那么他可能很快就会被系统杀死,所以最好将要处理的后台服务放到Service中。

上面都是说当系统配置发生改变时,Activity会被自动重新创建,如何使其不重新创建呢,在Activity中是由属性可以完成这个操作的,

android:configChanges="orientation"

这个是在旋转时的相关属性,它还有很多相关属性

<activity 
android:name="com.ryg.chapter_1.MainActivity" 
android:configChanges="orientation|screenSize" 
android:label="@string/app_name" > 
<intent-filter>

这样Activity确实没有重新创建,活动没有调用onSavelnstanceState和onRestorelnstanceState,调用了onContigurationChanged方法

1.2Activity的启动模式

默认情况,我们多次启动同一个Activity,系统会创建多个实例然后一一放入任务栈,当我们回退的时候,他会一个一个退出,这样的情况,多次启动同一个活动会创建多个实例,所以在设计的时候,提供了启动方式来处理这个问题

四种启动模式

1,standard:标准模式

系统的默认模式,每次启动都创建新的实例,在这种情况下,谁启动这个Activity,这个Activity就会运行在启动他的Activity所在栈中,在这种情况下我们使用ApplicationContext启动standard模式下的Activity,会发生报错,因为非Activity的Context是没有任务栈的,为了处理这种情况我们可以为待启动Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就会为它创建一个新的任务栈,这个时候待启动Activity实际上是以singleTask模式启动的

2,singleTop:栈顶复用模式

如果新的Activity已经位于任务栈的栈顶那么,该Activity就不会被重新创建,同时它的onNewIntent方法会被回调,通过此方法的参数我们可以取出当前请求的信息。如果新Activity的实例已存在但不是位于栈顶,那么新Activity仍然会重新重建。

和名字一样只有栈顶Activity会被复用,其他和标准模式一样

3,singleTask:栈内复用模式

这是一种单实例模式,在这种模式下,只要Activity在一个栈中存在,那么多次启动此Activity都不会重新创建实例,和singleTop一样,系统也会回调其onNewIntent。

  • 比如目前任务栈S1中的情况为ABC,这个时候Activity D以singleTask模式请求启动,其所需要的任务栈为S2,由于S2和D的实例均不存在,所以系统会先创建任务栈S2,然后再创建D的实例并将其入栈到S2。
  • 另外一种情况,假设D所需的任务栈为S1,其他情况如上面例子1所示,那么由于S1已经存在,所以系统会直接创建D的实例并将其入栈到S1。
  • 如果D所需的任务栈为S1,并且当前任务栈S1的情况为ADBC,根据栈内复用的原则,此时D不会重新创建,系统会把D切换到栈顶并调用其onNewIntent方法,同时由于singleTask默认具有clearTop的效 果,会导致栈内所有在D上面的Activity全部出栈,于是最终S1中的情况为AD。

4,singleInstance:单实例模式

这是一种加强的singleTask模式,它除了具有singleTask模式的所有特性外,还加强了一点,那就是具有此种模式的Activity只能单独地位于一个任务栈中,换句话说,比如Activity A是singleInstance模式,当A启动后,系统会为它创建一个新的任务栈,然后A独自在这个新的任务栈中,由于栈内复用的特性,后续的请求均不会创建新Activity,除非这个独特的任务栈被系统 销毁了。

核心是singleInstance 模式确保了该 Activity 独自存在于一个任务栈中,并且不会与其他 Activity 共享任务栈。如果多次启动他会一直复用同一个实例。

这里需要指出一种情况,我们假设目前有2个任务栈,前台任务栈的情况为AB,而后台任务栈的情况为CD,这里假设CD的启动模式均为singleTask。现在请求启动D,那么

整个后台任务栈都会被切换到前台,这个时候整个后退列表变成了ABCD。当用户按back键的时候,列表中的Activity会一一出栈,

在这里插入图片描述

这里的执行顺序,打开了D,他会将先将D回退,他会将后台任务栈切换为前台任务栈,然后优先回退前台任务栈,所以会在回退C,然后这个栈执行完毕,执行下一个栈,AB所在栈,所以上面ABCD是执行顺序

如果是启动活动C

在这里插入图片描述

任务栈是什么

在singleTask启动模式中,多次提到某个Activity所需的任务栈,什么是Activity所需要的任务栈呢?这要从一个参数说起:TaskAffinity,可以翻译为任务相关性。这个参数标识了一个 Activity所需要的任务栈的名字,默认情况下,所有Activity所需的任务栈的名字为应用的包名。当然,我们可以为每个Activity都单独指定TaskAffinity属性,这个属性值必须不能和包名相同,否则就相当于没有指定。TaskAffinity属性主要和singleTask启动模式或者allowTaskReparenting属性配对使用,在其他情况下没有意义。另外,任务栈分为前台任务栈和后台任务栈,后台任务栈中的Activity位于暂停状态,用户可以通过切换将后台任务栈再次调到前台。

TaskAffinity 就像是为特定项目组指定的办公室名,而任务栈则像是不同部门的集合,系统会根据 TaskAffinity 来管理和分配不同项目组的办公室(任务栈)

当TaskAffinity和singleTask启动模式配对使用的时候,它是具有该模式的Activity的目前任务栈的名字,待启动的Activity会运行在名字和TaskAffinity相同的任务栈中。

TaskAffinity和allowTaskReparenting结合

当TaskAffinity和allowTaskReparenting结合的时候,这种情况比较复杂,会产生特殊的效果。当一个应用A启动了应用B的某个Activity后,如果这个Activity的allowTaskReparenting属性为true的话,那么当应用B被启动后,此Activity会直接从应用A的任务栈转移到应用B的任务栈中。这还是很抽象,再具体点,比如现在有2个应用A和B,A启动了B的一个Activity C,然后按Home键回到桌面,然后再单击B的桌 面图标,这个时候并不是启动了B的主Activity,而是重新显示了已经被应用A启动的Activity C,或者说,C从A的任务栈转移到了B的任务栈中。可以这么理解,由于A启动了C,这个时候C只能运行在A的 任务栈中,但是C属于B应用,正常情况下,它的TaskAffinity值肯定不可能和A的任务栈相同(因为包名不同)。所以,当B被启动后,B会创建自己的任务栈,这个时候系统发现C原本所想要的任务栈已经 被创建了,所以就把C从A的任务栈中转移过来了。

应用A是一个社交软件,应用B是一个新闻客户端。在应用A中,用户点击了一个新闻链接,这时应用A启动了应用B的某个阅读新闻的Activity(Activity C),现在,Activity C 被启动后,用户按下 Home 键回到桌面。然后用户再次点击应用B的图标,期望重新打开应用B。在这种情况下,由于 Activity C 具有 allowTaskReparenting 为 true 的属性,系统会特殊处理。系统不会重新启动应用B的主 Activity,而是将先前从应用A启动的 Activity C 直接转移到应用B的任务栈中进行显示。

如何给Activity指定启动模式

第一种是通过AndroidMenifest为Activity指定启动模式,

<activity 
android:name="com.ryg.chapter_1.SecondActivity" 
android:configChanges="screenLayout" //配置改变是是否销毁活动
android:launchMode="singleTask" //指定活动启动模式
android:label="@string/app_name" />//应用程序的界面元素中显示该 Activity 的名称,

另一种情况是通过在Intent中设置标志位来为Activity指定启动模式

Intent intent = new Intent(); 
intent.setClass(MainActivity.this,SecondActivity.class); 
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 
startActivity(intent);

这两种方式都可以为Activity指定启动模式,但是二者还是有区别的。首先,优先级上,第二种方式的优先级要高于第一种,当两种同时存在时,以第二种方式为准;其次,上述两种方式在限定范围上有所不同,比如,第一种方式无法直接为Activity设定FLAG_ACTIVITY_CLEAR_TOP标识,而第二种方式无法为Activity指定singleInstance模式。

Activty的Flags

Activity的Flags有很多,书上分析一些比较常用的标记位。标记位的作用很多,有的标记位可以设定Activity的启动模式,还有的标记位可以影响Activity的运行状态

  • FLAG_ACTIVITY_NEW_TASK (flag_activity_new _task)

这个标记位的作用是为Activity指定“singleTask”启动模式,其效果和在XML中指定该启动模式相同。

  • FLAG_ACTIVITY_SINGLE_TOP (flag_activity_single_top)

这个标记位的作用是为Activity指定“singleTop”启动模式,其效果和在XML中指定该启动模式相同。

  • FLAG_ACTIVITY_CLEAR_TOP(flag_activity_clear_top)

具有此标记位的Activity,当它启动时,在同一个任务栈中所有位于它上面的Activity都要出栈。这个模式一般需要和FLAG_ACTIVITY_NEW_TASK配合使用,在这种情况下,被启动Activity的实例如果已经存在,那么系统就会调用它的onNewIntent。如果被启动的Activity采用standard模式启动,那么它连同它之上的Activity都要出栈,系统会创建新的Activity实例并放入栈顶

  • FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS(flag_activity_exclude_from_recents)

具有这个标记的Activity不会出现在历史Activity的列表中,当某些情况下我们不希望用户通过历史列表回到我们的Activity的时候这个标记比较有用。它等同于在XML中指定Activity的属性android:excludeFromRecents=“true”。

这些标记位的使用一般是结合Intent的方式来使用

Intent intent = new Intent(this, TargetActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);

1.3IntentFilter的匹配规则

我们知道,启动Activity分为两种,显式调用和隐式调用。二者的区别这里就不多说了,显式调用需要明确地指定被启动对象的组件信息,包括包名和类名,而隐式调用则不需要明确指定组件信息原则上一个Intent不应该既是显式调用又是隐式调用,如果二者共存的话以显式调用为主。显式调用很简单,这里主要介绍一下隐式调用。隐式调用需要Intent能够匹配目标组件的IntentFilter中所设置的过滤信息,如果不匹配将无法启动目标Activity。IntentFilter中的过滤信息有action、category、data,下面是一个过滤规则的示例:

<activity
android:name="com.ryg.chapter_1.ThirdActivity"//该活动的类名
        android:configChanges="screenLayout"//配置文件改变是否销毁活动
        android:label="@string/app_name"//在管理器中展示的名字
        android:launchMode="singleTask"//Activity的启动方式
        android:taskAffinity="com.ryg.task1">//任务栈名称
        <intent-filter>//定义可以响应的Intent
            //action 表示INtent的活动名称
            <action android:name="com.ryg.charpter_1.c"/>
            <action android:name="com.ryg.charpter_1.d"/>
            //Intent的类别,表示Intent的额外类型
            <category android:name="com.ryg.category.c"/>
            <category android:name="com.ryg.category.d"/>
            <category android:name="android.intent.category.DEFAULT"/>
            //可以指定Intent的携带数据类型,这里指定为text/plain
            <data android:mimeType="text/plain"/>
        </intent-filter>
</activity>

为了匹配过滤列表,需要同时匹配过滤列表中的action、category、data信息,否则匹配失败。一个过滤列表中的action、category和data可以有多个,所有的action、category、data分别构成不同类别,同一类别的信息共同约束当前类别的匹配过程。只有一个Intent同时匹配action类别、category类别、data类别才算完全匹配,只有完全匹配才能成功启动目标Activity。另外一点,一个Activity中可以有多个intentfilter,一个Intent只要能匹配任何一组intent-filter即可成功启动对应的Activity。

如果不需要特定的自定义行为或匹配规则,可以不手动设置这些属性,让系统根据默认值进行处理。系统会根据Intent的内容和其他条件自动选择合适的Activity进行匹配和启动。

各种属性的匹配规则

  1. action的匹配规则

action是一个字符串,系统预定义了一些action,同时我们也可以在应用中定义自己的action。action的匹配规则是Intent中的action必须能够和过滤规则中的action匹配,这里说的匹配是指action的字符串值 完全一样。一个过滤规则中可以有多个action,那么只要Intent中的action能够和过滤规则中的任何一个action相同即可匹配成功。针对上面的过滤规则,只要我们的Intent中action值为“com.ryg.charpter_1.c”或 者“com.ryg.charpter_1.d”都能成功匹配。需要注意的是,Intent中如果没有指定action,那么匹配失败。总结一下,action的匹配要求Intent中的action存在且必须和过滤规则中的其中一个action相同,这里需要注意它和category匹配规则的不同。另外,**action区分大小写,**大小写不同字符串相同的action会匹配失败。

  1. category的匹配规则

category是一个字符串,系统预定义了一些category,同时我们也可以在应用中定义自己的category。category的匹配规则和action不同,它要求Intent中如果含有category,那么所有的category都必须和过滤 规则中的其中一个category相同。换句话说,Intent中如果出现了category,不管有几个category,对于每个category来说,它必须是过滤规则中已经定义了的category。当然,Intent中可以没有category,如果没有category的话,按照上面的描述,这个Intent仍然可以匹配成功。

这里要注意下它和action匹配过程的不同,action是要求Intent中必须有一个action且必须能够和过滤规则中的某个action相同,而category要求Intent可以没有category,但是如果你一旦有category,不管有几个,每个都要能够和过滤规则中的任何一个category相同。

  1. data的匹配规则

data的匹配规则和action类似,如果过滤规则中定义了data,那么Intent中必须也要定义可匹配的data。

data的结构

<data android:scheme="string" 
    android:host="string" 
    android:port="string" 
    android:path="string" 
    android:pathPattern="string" 
    android:pathPrefix="string" 
    android:mimeType="string" />

data由两部分组成,mimeType和URI。mimeType指媒体类型,比如image/jpeg、 audio/mpeg4-generic和video/*等,可以表示图片、文本、视频等不同的媒体格式。

而URI中包含的数据就比较多了,URI的结构:

<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern> 
http://www.baidu.com:80/search/info

Scheme(方案):URI的模式,比如http、file、content等,如果URI中没有指定scheme,那么整个URI的其他参数无效,这也意味着URI是无效的。

Host(主机):URI的主机名,比如www.baidu.com,如果host未指定,那么整个URI中的其他参数无效,这也意味着URI是无效的。

Port(端口):URI中的端口号,比如80,仅当URI中指定了scheme和host参数的时候port参数才是有意义的

Path(路径):表示完整的路径信息,路径部分可以指定资源的具体位置或标识。例如,/folder/subfolder/etc 表示具体的路径。

PathPattern(路径模式):表示路径的模式,可能包含通配符或模式匹配规则,用于匹配多个路径。这种模式可以用来指定一类路径。

PathPrefix(路径前缀):表示路径的前缀,指定路径的起始部分。可以用来匹配以特定前缀开头的路径。

<data android:mimeType="image/*" />

这种规则指定了媒体类型为所有类型的图片,那么Intent中的mimeType属性必须为“image/*”才能匹配,这种情况下虽然过滤规则没有指定URI,但是却有默认值,URI的默认值为content和file

Intent的匹配事例

intent.setDataAndType(Uri.parse("file://abc"),"image/png")

另外,如果要为Intent指定完整的data,必须要调用setDataAndType方法,不能先调用setData再调用setType,因为这两个方法彼此会清除对方的值

<intent-filter> 
    <data android:mimeType="video/mpeg" android:scheme="http" ... /> 
    <data android:mimeType="audio/mpeg" android:scheme="http" ... /> 
    ... 
</intent-filter>

这种规则指定了两组data规则,且每个data都指定了完整的属性值,既有URI又有mimeType。为了匹配(2)中规则,我们可以写出如下示例:

intent.setDataAndType(Uri.parse("http://abc"),"video/mpeg")

对应一开始给出的过滤规则他的Intent可以是

Intent intent = new Intent("com.ryg.charpter_1.c"); 
intent.addCategory("com.ryg.category.c"); 
intent.setDataAndType(Uri.parse("file://abc"),"text/plain"); 
startActivity(intent);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值