Android组件之Activity

Activity总结

 

序言:Activity作为Android四大组件之一,其重要性不言而喻。


本文目录结构:

1.        Acticity是什么

2.        Activity的创建

3.        Activity的生命周期

4.        Activity的启动方式

5.        Activity 之间通信

6.        Activity 的 IntentFilter

7.        Activity的launchMode

 

一、Acticity是什么

Activity是这样一个程序组件,它为用户提供一个用于任务交互的画面。例如,拨打电话,拍照,发邮件。或者查看地图。每一个activity都被分配一个窗口。在这个窗口里,你可以绘制用户交互的内容。这个窗口通常占满屏幕,但也有可能比屏幕小,并且浮在其它窗口的上面。


二、Activity的创建

创建一个activity,你必须继承Activity类或者Activity的子类。在你的创建的Activity中,你需要实现系统回调的回调方法,以便当activity被创建、停止、恢复或摧毁时调用。两个最重要的回调方法是:

       1onCreate()

你必须实现这个方法。系统调用它当创建你的activity的时候。在你的实现中,你应该初始化你的activity的基本的组件。更重要的是,这里就是你必须调用setContentView()来定义activity用户接口而已的地方。

       2onPause()

       系统调用这个方法当用户离开你的activity(虽然不总是意味着activity被摧毁)。这通常是你应该提交任何变    化,那此将会超越user session而存在的(因为用户可能不再回来)。

 


android 中创建一个 Activity 是很简单的事情,编写一个继承自 android.app.Activity Java类并在 AndroidManifest.xml声明即可。

Activity 文件:

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //使用setContentView用来显示某个视图
        setContentView(R.layout.main);
    }
}


AndroidManifest.xml:

<activity
         android:name=". MainActivity "
         android:label="@string/app_name">
      <intent-filter>
          <actionandroid:name="android.intent.action.MAIN" />
          <categoryandroid:name="android.intent.category.LAUNCHER" />
      </intent-filter>
</activity>  


<action> 元素指定程序的入口。 <category> 指出该activity应该被列如系统的启动器(launcher)(允许用户启动它)

<!--
1
、一个应用程序可以有多个Activity,每个Activity是同级别的,那么在启动程序时,最先启动哪个Activity呢?
有些程序可能需要显示在程序列表里,有些不需要。怎么定义呢?
android.intent.action.MAIN
决定应用程序最先启动的Activity
android.intent.category.LAUNCHER
决定应用程序是否显示在程序列表里
2
、因为你的程序可能有很多个activity只要xml配置文件中有这么一个intent-filter,而且里面有这个launcher,那么这个activity就是点击程序时最先运行的那个activity
3
、现在只有一个activity,那么加不加就没有关系了。用于模拟器启动时设置为默认打开为的activity
-->

 

三、Activity的生命周期 


public classActivity extends ApplicationContext {

        protected void onCreate(Bundle savedInstanceState);

        protected void onStart();

        protected void onRestart();

        protected void onResume();

        protected void onPause();

        protected void onStop();

        protected void onDestroy();
        
}<span style="font-family: Arial, Helvetica, sans-serif; font-size: 12px; background-color: rgb(255, 255, 255);"> </span>

由图可知:

   在一个Activity正常启动过程中,这些方法调用的顺序是onCreate -> onStart -> onResume;

     在Activity被kill掉的时候方法顺序是onPause -> onStop -> onDestroy,此为一个完整的Lifecycle。

     那么对于中断处理(比如电话来了),则是onPause -> onStop,恢复时onStart -> onResume;

     如果当前应用程序的是一个Theme为Translucent(半透明) 或者Dialog 的Activity那么中断就是onPause ,恢复的时候onResume。

     假设在当前的ActivityA中启动ActivityB,调用顺序为onPause(A)-> onCreate(B)-> onStart(B) -> onResume(B)-> onStop(A).


各种方法在系统中的作用及我们应该做什么:

  onCreate:在这里创建界面,做一些数据的初始化工作;

  onStart: 到这一步变成“用户可见不可交互”的状态;

  onResume:变成和用户可交互的,(在Activity栈系统通过栈的方式管理这些Activity,即当前Activity在栈的最上端,运行完弹出栈,则回到上一个Activity);

  onPause:到这一步是可见但不可交互的,系统会停止动画等消耗CPU的事情。从上文的描述已经知道,应该在这里保存你的一些数据,因为这个时候你的程序的优先级降低,有可能被系统收回。在这里保存的数据,应该在onResume里读出来。

  onStop:变得不可见 ,被下一个activity覆盖了

  onDestroy:这是Activity被kill前最后一个被调用方法了,可能是其他类调用finish方法或者是系统为了节省空间将它暂时性的干掉,可以用isFinishing()来判断它,如果你有一个Progress Dialog在线程中运行,请在onDestroy里把他cancel掉,不然等线程结束的时候,调用Dialog的cancel方法会抛异常。

onPause,onstop, onDestroy,三种状态下 activity都有可能被系统kill 掉。

 

四、Activity的启动方式

启动一个Activity


你可以通过调用startActivity()启动一个其他的activity, 并传递一个 Intent , 它用于描述activity。 intent指定了你想要启动的activity,或者指定了你想展现的动作(系统帮你选择合适的activity,它可能来自于其他的程序)。 intent也可以携带比较小量的数据,用于启动acitivity。

在你自己的应用中,你经常会简单地启动一个已知的activity,通过创建一个明确的intent。这个intent指定了activity的类名。 例如下面演示了如何启动一个叫SignInActivity的activity:

Intent intent = new Intent(this, SignInActivity.class);
startActivity(intent);

然而,你的程序可能想要展示某些动作,例如发邮件,短信,微博,或者使用你activity中的数据。这时候,你就不应该使用自己的activity来做这些工作。你应该调用系统中其他程序提供的响应功能。这是intent真正体现其价值的地方。你可以创建一个描述了响应动作的intent,然后系统来为你挑选完成任务的程序。如果有多个选择,系统会提示用户进行选择。例如你想让用户发邮件,你可以创建下面的intent:

Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_EMAIL, recipientArray);
startActivity(intent);

      EXTRA_EMAIL 是一个邮件intent中添加的额外字符串数组,它指定了邮件该发给哪些邮件地址。当一个邮件程序响应了这个intent,它将读取这些地址,并把他们放置到邮件表单的被发送人栏。这时,邮件程序被启动。当用户完成了发送操作,你的activity会被恢复。


启动一个带返回结果的activity

有时候,你想要启动一个activity,并从这个activty获得一个结果。这时,要通过 startActivityForResult()(取代startActivity())来启动activity。然后通过实现onActivityResult()回调方法来获得返回后的结果。当这个后续的activity被关闭,它将发送一个 Intent 给 onActivityResult() 方法。


例如,你可能想要取一个联系人的信息。下面介绍怎么创建intent并处理结果:

private void pickContact() {
    // Create an intent to "pick" a contact, as defined by the content provider URI
    Intent intent = new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI);
    startActivityForResult(intent, PICK_CONTACT_REQUEST);
}
 
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // If the request went well (OK) and the request was PICK_CONTACT_REQUEST
    if (resultCode == Activity.RESULT_OK && requestCode == PICK_CONTACT_REQUEST) {
        // Perform a query to the contact's content provider for the contact's name
        Cursor cursor = getContentResolver().query(data.getData(),
        new String[] {Contacts.DISPLAY_NAME}, null, null, null);
        if (cursor.moveToFirst()) { // True if the cursor is not empty
            int columnIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
            String name = cursor.getString(columnIndex);
            // Do something with the selected contact's name...
        }
    }
}

这个例子展示了使用onActivityResult() 来获取结果的基本方法。第一步要判断请求是否被成功响应,通过判断resultCode 是不是RESULT_OK,然后判断这个响应是不是针对相应的请求,此时只要判断requestCode 和发送时提供的第二个参数 startActivityForResult() 是否相匹配。最后,查询 Intent中的data信息。 (data 参数)。


这个过程中,ContentResolver 开启了一个查询而不是content provider, 它返回一个 Cursor ,这将允许数据被读取。更多content provider相关信息,请查阅ContentProviders 文档。


关于intent的更多信息,查看 Intentsand Intent Filters 文档。


关闭Activity


你可以通过调用finish() 来终止activity。你也可以调用finishActivity() 来终止你之前启动了的一个独立activity。

注意: 多数情况下,你不应该明确地通过这些方式来关闭acitivity。就像下面要讨论的activity的生命周期。系统会为你管理。所以你不必关闭他们。调用这些方法将有悖于用户体验。它们仅用于你绝对不想让用户再返回这个activity的实例。

 

五、Activity之间的通信

使用 Intent通信

在 Android 中,不同的 Activity 实例可能运行在一个进程中,也可能运行在不同的进程中。因此我们需要一种特别的机制帮助我们在 Activity 之间传递消息。Android 中通过 Intent 对象来表示一条消息,一个 Intent 对象不仅包含有这个消息的目的地,还可以包含消息的内容,这好比一封 Email,其中不仅应该包含收件地址,还可以包含具体的内容。对于一个 Intent 对象,消息“目的地”是必须的,而内容则是可选项。


这里拿发送,接受邮件做例子:

如果我们想要给“收件人”Activity 说点什么的话,那么可以通过下面这封“e-mail”来将我们消息传递出去:

Intent intent =new Intent(CurrentActivity.this,OtherActivity.class);
  // 创建一个带“收件人地址”的 email 
 Bundle bundle =new Bundle();// 创建 email 内容
 bundle.putBoolean("boolean_key", true);// 编写内容
 bundle.putString("string_key", "string_value"); 
 intent.putExtra("key", bundle);// 封装 email 
 startActivity(intent);// 启动新的 Activity

那么收件人该如何收信呢?在 OtherActivity类的 onCreate()或者其它任何地方使用下面的代码就可以打开这封“e-mail”阅读其中的信息:

 Intent intent =getIntent();// 收取 email 
 Bundle bundle =intent.getBundleExtra("key");// 打开 email 
 bundle.getBoolean("boolean_key");// 读取内容
 bundle.getString("string_key");


上面我们通过 bundle对象来传递信息,bundle维护了一个 HashMap<String,Object>对象,将我们的数据存贮在这个HashMap中来进行传递。但是像上面这样的代码稍显复杂,因为Intent内部为我们准备好了一个 bundle,所以我们也可以使用这种更为简便的方法:

Intent intent =new Intent(EX06.this,OtherActivity.class); 
 intent.putExtra("boolean_key", true); 
 intent.putExtra("string_key", "string_value"); 
 startActivity(intent);

接收: 

Intent intent=getIntent(); 
 intent.getBooleanExtra("boolean_key",false); 
 intent.getStringExtra("string_key");

使用 SharedPreferences

SharedPreferences使用 xml格式为 Android应用提供一种永久的数据存贮方式。对于一个Android应用,它存贮在文件系统 /data/data/your_app_package_name/shared_prefs/目录下,可以被处在同一个应用中的所有

Activity 访问。Android提供了相关的 API来处理这些数据而不需要程序员直接操作这些文件或者考虑数据同步问题

// 写入 SharedPreferences 
 SharedPreferences preferences = g
 etSharedPreferences("name", MODE_PRIVATE); 
 Editor editor = preferences.edit(); 
 editor.putBoolean("boolean_key", true); 
 editor.putString("string_key", "string_value"); 
 editor.commit(); 
        
 // 读取 SharedPreferences 
 SharedPreferences preferences = getSharedPreferences("name", MODE_PRIVATE); 
 preferences.getBoolean("boolean_key", false); 
 preferences.getString("string_key", "default_value");

其它方式

Android 提供了包括SharedPreferences在内的很多种数据存贮方式,比如SQLite,文件等,程序员可以通过这些API实现Activity之间的数据交换。如果必要,我们还可以使用IPC方式。

 

六、Activity Intent Filter

IntentFilter描述了一个组件愿意接收什么样的Intent对象,Android将其抽象为android.content.IntentFilter类。在AndroidAndroidManifest.xml配置文件中可以通过<intent-filter >节点为一个Activity指定其Intent Filter,以便告诉系统该Activity可以响应什么类型的Intent
当程序员使用startActivity(intent)来启动另外一个Activity时,如果直接指定intent了对象的Component属性,那么Activity Manager将试图启动其Component属性指定的Activity。否则Android将通过Intent的其它属性从安装在系统中的所有Activity中查找与之最匹配的一个启动,如果没有找到合适的Activity,应用程序会得到一个系统抛出的异常。这个匹配的过程如下:
 Activity 中 Intent Filter 的匹配过程

Action 匹配

Action 是一个用户定义的字符串,用于描述一个 Android应用程序组件,一个 Intent Filter可以包含多个 Action。在 AndroidManifest.xml Activity定义时可以在其 <intent-filter >节点指定一个 Action列表用于标示 Activity所能接受的动作,例如:

<intent-filter > 
 <action android:name="android.intent.action.MAIN" /> 
 <action android:name="com.zy.myaction" /> 
 </intent-filter>

如果我们在启动一个 Activity时使用这样的 Intent对象:

Intent intent =new Intent(); 
intent.setAction("com.zy.myaction");

那么所有的 Action列表中包含了com.zy.myaction Activity都将会匹配成功。

Android 预定义了一系列的 Action分别表示特定的系统动作。这些 Action通过常量的方式定义在 android.content. Intent中,以ACTION_开头。我们可以在 Android提供的文档中找到它们的详细说明。

 

URI 数据匹配

一个 Intent可以通过 URI携带外部数据给目标组件。在 <intent-filter >节点中,通过 <data/>节点匹配外部数据。

mimeType 属性指定携带外部数据的数据类型,scheme指定协议,hostportpath指定数据的位置、端口、和路径。如下:

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

如果在 Intent Filter中指定了这些属性,那么只有所有的属性都匹配成功时 URI数据匹配才会成功。

Category 类别匹配

<intent-filter >节点中可以为组件定义一个 Category类别列表,当 Intent中包含这个列表的所有项目时 Category类别匹配才会成功.

 

action、category、data的匹配规则:

a)      action : 能够过滤匹配到任何一个action 即可

b)      category : 如果有指定category,不管有几个都要能够与过滤规则中的任何一个category相同。

c)      data : 与action 相似,能完全匹配到过滤规则中的某一个,注意使用setDataAndType()方法,如果单独使用setData()或者SetType()会擦除之前设置的data或type.

 

七、Activity的 launchMode

使用很简单,只需要在manifest中对应的Activity元素加入 android:launchMode 属性即可。如下述代码

<activity
            android:name=".SingleTaskActivity"
            android:label="singleTask launchMode"
            android:launchMode="singleTask">

接下来就是介绍launchMode的四个值的时刻了。

standard

这是launchMode的默认值,Activity不包含android:launchMode或者显示设置为standard的Activity就会使用这种模式。

一旦设置成这个值, 每当有一次Intent请求,就会创建一个新的Activity实例 。举个例子,如果有10个撰写邮件的Intent,那么就会创建10个ComposeMailActivity的实例来处理这些Intent。结果很明显,这种模式会创建某个Activity的多个实例。

Android 5.0之前的表现

这种Activity新生成的实例会放入发送Intent的Task的栈的顶部。下图为启动同一程序内的Activity。 

standardtopstandard

下面的图片展示跨程序之间调用,新生成的Activity实例会放入发送IntentTask的栈的顶部,尽管它们属于不同的程序。 

standardgallery2

但是当我们打开任务管理器,则会有一点奇怪,应该显示的任务是Gallery,展示的界面却是另一个程序的Activity(因为其位于Task的栈顶)。

gallerystandard

这时候如果我们从Gallery应用切换到拨号应用,再返回到Gallery,看到的还是这个非GalleryActivity,如果我们想要对Gallery进行操作,必须按Back键返回到Gallery界面才可以。确实有点不太合理。

Android 5.0及之后表现

对于同一应用内部Activity启动和5.0之前表现一样,变化的就是不同应用之间Activity启动变得合理了。

跨应用之间启动Activity,会创建一个新的Task,新生成的Activity就会放入刚创建的Task中。如下图

standardgalleryl

同时任务管理器查看任务也显得更加合理了。

gallerystandardl1

假设之前存在我们的测试程序,然后从Gallery又分享文件到我们的测试程序,则对应的任务管理器展示效果如下。

gallerystandardl2

使用场景:standard这种启动模式适合于撰写邮件Activity或者社交网络消息发布Activity。如果你想为每一个intent创建一个Activity处理,那么就是用standard这种模式。

singleTop

singleTop其实和standard几乎一样,使用singleTopActivity也可以创建很多个实例。唯一不同的就是, 如果调用的目标Activity已经位于调用者的Task的栈顶,则不创建新实例,而是使用当前的这个Activity实例,并调用这个实例的onNewIntent方法  

singletop

 在singleTop这种模式下,我们需要处理应用这个模式的Activity的onCreate()和onNewIntent()两个方法,确保逻辑正常。

使用场景

关于singleTop一个典型的使用场景就是搜索功能。假设有一个搜索框,每次搜索查询都会将我们引导至SearchActivity查看结果,为了更好的交互体验,我们在结果页顶部也放置这样的搜索框。

假设一下,SearchActivity启动模式为standard,那么每一个搜索都会创建一个新的SearchActivity实例,10次查询就是10个Activity。当我们想要退回到非SearchActivity,我们需要按返回键10次,这显然太不合理了。

但是如果我们使用singleTop的话,如果SearchActivity在栈顶,当有了新的查询时,不再重新创建SearchAc实例,而是使用当前的SearchActivity来更新结果。当我们需要返回到非SearchActivity只需要按一次返回键即可。使用了singleTop显然比之前要合理。

总结

·        只有在调用者和目标Activity在同一Task中,并且目标Activity位于栈顶,才使用现有目标Activity实例,否则创建新的目标Activity实例。

·        如果是外部程序启动singleTop的Activity,在Android 5.0之前新创建的Activity会位于调用者的Task中,5.0及以后会放入新的Task中。

singleTask

singleTask这个模式和前面提到的standard和singleTop截然不同。 使用singleTask启动模式的Activity在系统中只会存在一个实例 。如果这个实例已经存在,intent就会通过onNewIntent()方法传递到这个Activity。否则新的Activity实例被创建。

同一程序内

如果系统中不存在singleTaskActivity的实例,那么就需要创建这个Activity的实例,并且将这个实例放入和调用者相同的Task中并位于栈顶。

singleTask1

如果singleTaskActivity实例已然存在,那么在Activity回退栈中,所有位于该Activity上面的Activity实例都将被销毁掉(销毁过程会调用Activity生命周期回调),这样使得singleTask Activity实例位于栈顶。与此同时,Intent会通过onNewIntent传递到这个SingleTask Activity实例。

singleTaskD

然而在Google关于singleTask 文档 有这样一段描述

The system createsa new task and instantiates the activity at the root of the new task.

意思为 系统会创建一个新的Task,并创建Activity实例放入这个新的Task的底部。

然而实际并非如此,在我的例子中,singleTaskActivity并创建并放入了调用者所在的Task,而不是放入新的Task,使用 adbshell dumpsys activity 便可以进行验证。

Task id #239
  TaskRecord{428efe30 #239 A=com.thecheesefactory.lab.launchmode U=0 sz=2}
  Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.thecheesefactory.lab.launchmode/.StandardActivity }
    Hist #1: ActivityRecord{429a88d0 u0 com.thecheesefactory.lab.launchmode/.SingleTaskActivity t239}
      Intent { cmp=com.thecheesefactory.lab.launchmode/.SingleTaskActivity }
      ProcessRecord{42243130 18965:com.thecheesefactory.lab.launchmode/u0a123}
    Hist #0: ActivityRecord{425fec98 u0 com.thecheesefactory.lab.launchmode/.StandardActivity t239}
      Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.thecheesefactory.lab.launchmode/.StandardActivity }
      ProcessRecord{42243130 18965:com.thecheesefactory.lab.launchmode/u0a123}

然而想要实现文档的描述也并非不可能,我们需要在设置launchModesingleTask的同时,再加上taskAffinity属性即可。

<activity
            android:name=".SingleTaskActivity"
            android:label="singleTask launchMode"
            android:launchMode="singleTask"
            android:taskAffinity="">

完成上面的修改,我们看一下效果,Task的变化如下图。

singleTaskTaskAffinity

同时,系统中的任务管理器效果也会相应变化。

screenshot17

跨应用之间

在跨应用Intent传递时,如果系统中不存在singleTask Activity的实例,那么讲创建一个新的Task,然后创建SingleTask Activity的实例,将其放入新的Task中。Task变化如下。

singleTaskAnotherApp1

系统的任务管理器也会如下变化。

singletaskfromapp2

如果singleTaskActivity所在的应用进程存在,但是singleTask Activity实例不存在,那么从别的应用启动这个Activity,新的Activity实例会被创建,并放入到所属进程所在的Task中,并位于栈顶位置。

singleTaskAnotherApp2

更复杂的一种情况,如果singleTaskActivity实例存在,从其他程序被启动,那么这个Activity所在的Task会被移到顶部,并且在这个Task中,位于singleTask Activity实例之上的所有Activity将会被正常销毁掉。如果我们按返回键,那么我们首先会回退到这个Task中的其他Activity,直到当前TaskActivity回退栈为空时,才会返回到调用者的Task

singleTaskAnotherApp3

在上图中,当Task2中的相册启动分享调用Task1中的singleTask Activity,而该Activity实例存在,并位于Task1中回退栈中的第三个位置(从上到下顺序),那么位于该Activity上面的两个Activity实例将会被销毁掉,使得该Activity实例位于栈顶。此时Task1中的回退栈只剩两个Activity,如果点击返回,那么会退到的不是相册应用,而是singleTaskActivity栈位置下面的Activity,再次点击返回方可返回相册应用。

使用场景

该模式的使用场景多类似于邮件客户端的收件箱或者社交应用的时间线Activity。上述两种场景需要对应的Activity只保持一个实例即可,但是也要谨慎使用这种模式,因为它可以在用户未感知的情况下销毁掉其他Activity

singleInstance

这个模式和singleTask差不多,因为他们在系统中都只有一份实例。唯一不同的就是存放singleInstance Activity实例的Task只能存放一个该模式的Activity实例。如果从singleInstance Activity实例启动另一个Activity,那么这个Activity实例会放入其他的Task中。同理,如果singleInstance Activity被别的Activity启动,它也会放入不同于调用者的Task中。

singleInstance

 虽然是两个task,但是在系统的任务管理器中,却始终显示一个,即位于顶部的Task中。

singleInstances

另外当我们从任务管理器进入这个应用,是无法通过返回键会退到Task1的。

好在有办法解决这个问题,就是之前提到的 taskAffinity="" ,为launchModesingleInstanceActivity加入这个属性即可。

<activity
            android:name=".SingleInstanceActivity"
            android:label="singleInstance launchMode"
            android:launchMode="singleInstance"
            android:taskAffinity="">

再次运行修改的代码,查看任务管理器,这样的结果就合理了。

screenshot18

使用情况

这种模式的使用情况比较罕见,在Launcher中可能使用。或者你确定你需要使Activity只有一个实例。建议谨慎使用。

IntentFlags

除了在manifest文件中设置launchMode之外,还可以在Intnet中设置flag达到同样的效果。如下述代码就可以让StandardActivitysingleTop模式启动。

Intent intent = new Intent(StandardActivity.this, StandardActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);
关于Intent Flags这里暂不做重点介绍,具体可以参考 官方文档

原文信息

·        UnderstandAndroid Activity’s launchMode: standard,singleTop, singleTask and singleInstance


参考文献:
http://www.android-doc.com/guide/components/activities.html
http://www.ibm.com/developerworks/cn/opensource/os-cn-android-actvt/
http://blog.csdn.net/qq_25184739/article/details/51347403
http://www.kejik.com/article/21538.html
http://www.cnblogs.com/lyp3314/archive/2011/11/10/2244971.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值