Android四大组件

前言

这篇博客总结了一些Android开发的基础知识,作为一个开发多年的Android狗,我认为隔一段时间来巩固所学跟接触的知识很有必要,于是便有了这篇博客随记。

一、四大组件

四大组件是Android世界的重要基石,分别是Activity,Service服务,Content Provider内容提供者,BroadcastReceiver广播接收器。

1.Activity

Activity的作用

Activity是一种展示型组件,主要是向用户展示一个界面,并且可以接收用户的输入信息从而和用户进行交互。对用户来说,Activity就是Android应用的全部,因为其他三大组件对用户来说是不可感知的。
Activity的启动由Intent触发,其中Intent分为显式启动和隐式启动。
Activity组件的主要作用是展示一个界面并和用户交互,它扮演的是一种前台界面的角色。

Activity的生命周期

生命周期
运行状态:当Activity位于任务栈的栈顶时,则Activity处于运行状态。
暂停状态:当Activity不再处于栈顶位置,但依然可见时,则Activity就进入了暂停状态。
停止状态:当Activity不再处于栈顶位置,并且完全不可见的时候,这时Activity就进入了停止状态。
销毁状态:当Activity从任务栈中移除后就变成了销毁状态。
Activity的生命周期回调:
1.onCreate():表示Activity正在被创建,这是Activity生命周期的第一个方法。可以做一些初始化的工作(加载布局资源、初始化所需要的数据等),但是不要做耗时的工作。
2.onStart():Activity在屏幕上由不可见变为可见的时候该方法被调用。这时界面是不可交互的。
3.onResume():表明Activity已经创建完成,并且可以开始活动了,这个时候用户已经可以看到界面了,并且即将与用户交互。
4.onPause():在Activity处于可见但不可见交互时候该方法被调用。可以在这个方法中将一些消耗CPU的资源释放掉,以及保存活动状态。
5.onStop():表示Activity即将停止,可以做一些稍微重量级的回收工作,同样也不能太耗时。
6.onRestart():表示Activity正在重新启动。一般情况下,在Activity从不可见重新变为可见的状态时onRestart就会被调用。这种情形通常是由于用户的行为所导致的,比如用户按下Home键切换到桌面或者打开了一个新的Activity(这时当前Activity会暂停,也就是onPause和onStop被执行),接着用户又回到了这个Activity,就会调用onRestart()。
7.onDestroy():表示Activity即将被销毁,这是Activity生命周期的最后一个回调,可以做一些回收工作和资源释放。

各种情况下执行生命周期的情况:
1.当启动OneActivity执行生命周期的情况:第一次启动的时候,会依次执行onCreate()–>onStart()–>onResume()
2.关闭OneActivity时情况:关闭OneActivity,依次执行onPause()–>onStop()–>onDestroy()
3.在OneActivity界面中按home键,然后在进入OneActivity时执行生命周期情况:onPause()–>onStop() –>onRestart()–>onStart()–>onResume()
4.从OneActivity启动TwoActivity执行情况:OneActivity的onPause()–>然后执行TwoActivity的onCreate()–>onStart()–>onResume()–>最后再执行OneActivity的onStop()
5.从TwoActivity中返回到OneActivity时执行情况:执行TwoActivity的onPause()–>然后执行OneActivity的onRestart()–>onStart()–>onResume()–>再执行TwoActivity的onStop()–>onDestroy()
6.横竖屏切换的情况:onSaveInstance()–>onStop()–>onDestroy()–>onCreate()–>onStart()–>onRestoreInstance()–>onResume()

Activity的启动模式

  1. Standard 标准模式
    说明: Android创建Activity时的默认模式,假设没有为Activity设置启动模式的话,默认标准模式。每次启动一个Activity都会又一次创建一个新的实例入栈,无论这个实例是否存在。

生命周期:如上所看到的,每次被创建的实例Activity 的生命周期符合典型情况,它的onCreate、onStart、onResume都会被调用。

举例:此时Activity 栈中以此有A、B、C三个Activity,此时C处于栈顶,启动模式为Standard 模式。

若在C Activity中加入点击事件,须要跳转到还有一个同类型的C Activity。结果是还有一个C Activity进入栈中,成为栈顶。

  1. SingleTop 栈顶复用模式
    说明:分两种处理情况:须要创建的Activity已经处于栈顶时,此时会直接复用栈顶的Activity。不会再创建新的Activity;若须要创建的Activity不处于栈顶,此时会又一次创建一个新的Activity入栈,同Standard模式一样。

生命周期:若情况一中栈顶的Activity被直接复用时,它的onCreate、onStart不会被系统调用,由于它并没有发生改变。可是一个新的方法 onNewIntent会被回调(Activity被正常创建时不会回调此方法)。

举例:此时Activity 栈中以此有A、B、C三个Activity,此时C处于栈顶,启动模式为SingleTop 模式。情况一:在C Activity中加入点击事件,须要跳转到还有一个同类型的C Activity。

结果是直接复用栈顶的C Activity。

情况二:在C Activity中加入点击事件,须要跳转到还有一个A Activity。结果是创建一个新的Activity入栈。成为栈顶。

  1. SingleTask 栈内复用模式
    说明:若须要创建的Activity已经处于栈中时,此时不会创建新的Activity,而是将存在栈中的Activity上面的其他Activity所有销毁,使它成为栈顶。

生命周期:同SingleTop 模式中的情况一同样。仅仅会又一次回调Activity中的 onNewIntent方法

举例:此时Activity 栈中以此有A、B、C三个Activity。此时C处于栈顶,启动模式为SingleTask 模式。

情况一:在C Activity中加入点击事件,须要跳转到还有一个同类型的C Activity。结果是直接用栈顶的C Activity。情况二:在C Activity中加入点击事件,须要跳转到还有一个A Activity。

结果是将A Activity上面的B、C所有销毁,使A Activity成为栈顶。

  1. SingleInstance 单实例模式
    说明: SingleInstance比較特殊,是全局单例模式,是一种加强的SingleTask模式。它除了具有它所有特性外,还加强了一点:具有此模式的Activity仅仅能单独位于一个任务栈中。

这个经常使用于系统中的应用,比如Launch、锁屏键的应用等等,整个系统中仅仅有一个!所以在我们的应用中一般不会用到。了解就可以。

举例:比方 A Activity是该模式,启动A后。系统会为它创建一个单独的任务栈,由于栈内复用的特性。新的请求均不会创建新的Activity,除非这个独特的任务栈被系统销毁。

Activity可能会遇到的问题

你后台的Activity被系统 回收怎么办:onSaveInstanceState

简单来说,onSaveInstanceState作用就是在activity可能被销毁时被调用来存储activity的状态,然后可以在onCreate或者onRestoreInstanceState中恢复这些信息。

onSaveInstanceState()回调方法会携带一个Bundle类型的参数,键值对形式存储数据(什么是Bundle,本文附件会进行解释),Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法保存字符串等等,这些方法需要传入两个参数,一个是键,这个键用于后面从Bundle中取值,第二个参数是我们真正要存储的数据。
@override
protected void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
//这里将我们临时输入的一些数据存储起来
String tempData = “something you just typed”;
outState.putString(“data_key”, tempData);
}
取的时候如下
@Override
protected void onCreate(Bundle sacedInstanceState){
super.onCreate(savedInstanceState);
Log.i(“MainActivity”, “onCreate()”);
setContentView(R.layout.activity_main);
//判断saveInstanceState是否为空,如果不为空则将对应数据取出赋值给tempData的引用
if(savedInstanceState != null){
String tempData = savedInstanceState.getString(“data_key”);
Log.i(“MainActivity”, tempData);
}
}
总而言之,onSaveInstanceState的调用遵循一个重要原则,即当系统“未经你许可”时销毁了你的activity,则onSaveInstanceState会被系统调用,这是系统的责任,因为它必须要提供一个机会让你保存你的数据

2.Service

Service的作用

Service主要用于在后台处理一些耗时的逻辑,或者去执行某些需要长期运行的任务。必要的时候我们甚至可以在程序退出的情况下,让Service在后台继续保持运行状态。

Service的两种状态

启动状态:
当应用组件(如 Activity)通过调用 startService() 启动服务时,服务即处于“启动”状态。一旦启动,服务即可在后台无限期运行,即使启动服务的组件已被销毁也不受影响,除非手动调用才能停止服务, 已启动的服务通常是执行单一操作,而且不会将结果返回给调用方。

绑定状态:
当应用组件通过调用 bindService() 绑定到服务时,服务即处于“绑定”状态。绑定服务提供了一个客户端-服务器接口,允许组件与服务进行交互、发送请求、获取结果,甚至是利用进程间通信 (IPC) 跨进程执行这些操作。 仅当与另一个应用组件绑定时,绑定服务才会运行。 多个组件可以同时绑定到该服务,但全部取消绑定后,该服务即会被销毁。

在这里插入图片描述

Service的两种状态

两种服务:Service 和 IntentService

Service

Service: 在后台不断运行知道你停止服务或者解绑。
创建Service类
public class MyService extends Service {

private Disposable subscribe;

private int i = 0;

@Override
public void onCreate() {
    super.onCreate();
    Log.e("===cjw", "onCreate");

    //计时器
    subscribe = Observable.interval(0, 1, TimeUnit.SECONDS)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(aLong -> {
                i++;
                Log.e("===cjw", "i = " + i + "," + Thread.currentThread().getName());
            });
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    Log.e("===cjw", "onStartCommand");
    return super.onStartCommand(intent, flags, startId);
}

@Override
public void onDestroy() {
    super.onDestroy();
    Log.e("===cjw", "onDestroy");
    subscribe.dispose();
}

}
启动Service
Intent intent = new Intent(MainActivity.this, MyService.class);
启动服务
startService(intent);

IntentService

IntentService: 一次性执行任务,使用IntentService。

无论哪种服务都需要在AndroidManifest中声明服务

创建IntentService类
public class MyIntentService extends IntentService {

public MyIntentService(String name) {
    super(name);
    Log.e("===cjw", "MyIntentService");
}

public MyIntentService() {
    super("MyIntentService");
}

//该方法在会在一个单独的线程中执行,来完成工作任务。任务结束后,该Service自动停止
@Override
protected void onHandleIntent(@Nullable Intent intent) {
    Log.e("===cjw", "onHandleIntent");
}

@Override
public void onDestroy() {
    super.onDestroy();
    Log.e("===cjw", "onDestroy");
}

}
启动IntentService
Intent intent = new Intent(MainActivity.this, MyService.class);
启动服务
startService(intent);

绑定服务(BindService)
这个是Activity与Service之间的绑定,当Activity与Service绑定后,Activity销毁后Service也会销毁。startService宿主Activity销毁后Service不会销毁。
1.Binder
在Service类中创建Binder对象,用于提供给Activity交换数据
public class MyBinder extends Binder {
//提供给客户端调用
MyService getService() {
// 返回当前对象Service
return MyService.this;
}
}
在onBind中返回Binder
public IBinder onBind(Intent intent) {
Log.e("===cjw", “onBind”);
return binder;
}
在Activity中绑定

绑定服务
bindService(intent, conn, Service.BIND_AUTO_CREATE);
ServiceConnection conn;
conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
MyService.MyBinder service1 = (MyService.MyBinder) service;
MyService service2 = service1.getService();
service2.test();
}

        //意外销毁执行
        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

3.BroadCast广播

广播接收器用于响应来自其他应用程序或者系统的广播消息。这些消息有时被称为事件或者意图。例如,应用程序可以初始化广播来让其他的应用程序知道一些数据已经被下载到设备,并可以为他们所用。这样广播接收器可以定义适当的动作来拦截这些通信。

有以下两个重要的步骤来使系统的广播意图配合广播接收器工作。

创建广播接收器
广播接收器需要实现为BroadcastReceiver类的子类,并重写onReceive()方法来接收以Intent对象为参数的消息。

 public class MyReceiver extends BroadcastReceiver {
   @Override
   public void onReceive(Context context, Intent intent) {
      Toast.makeText(context, "Intent Detected.", Toast.LENGTH_LONG).show();
   }
}

注册广播接收器
应用程序通过在AndroidManifest.xml中注册广播接收器来监听制定的广播意图。假设我们将要注册MyReceiver来监听系统产生的ACTION_BOOT_COMPLETED事件。该事件由Android系统的启动进程完成时发出。

<application
   android:icon="@drawable/ic_launcher"
   android:label="@string/app_name"
   android:theme="@style/AppTheme" >
   <receiver android:name="MyReceiver">

      <intent-filter>
         <action android:name="android.intent.action.BOOT_COMPLETED">
         </action>
      </intent-filter>

   </receiver>
</application>

现在,无论什么时候Android设备被启动,都将被广播接收器MyReceiver所拦截,并且在onReceive()中实现的逻辑将被执行。
有许多系统产生的事件被定义为类Intent中的静态常量值。
事件常量 描述
android.intent.action.BATTERY_CHANGED 持久的广播,包含电池的充电状态,级别和其他信息。
android.intent.action.BATTERY_LOW 标识设备的低电量条件。
android.intent.action.BATTERY_OKAY 标识电池在电量低之后,现在已经好了。
android.intent.action.BOOT_COMPLETED 在系统完成启动后广播一次。
android.intent.action.BUG_REPORT 显示报告bug的活动。
android.intent.action.CALL 执行呼叫数据指定的某人。
android.intent.action.CALL_BUTTON 用户点击"呼叫"按钮打开拨号器或者其他拨号的合适界面。
android.intent.action.DATE_CHANGED 日期发生改变。
android.intent.action.REBOOT 设备重启。

广播自定义意图
如果你想要应用程序中生成并发送自定义意图,你需要在活动类中通过sendBroadcast()来创建并发送这些意图。如果你使用sendStickyBroadcast(Intent)方法,则意图是持久的(sticky),这意味者你发出的意图在广播完成后一直保持着。

public void broadcastIntent(View view)
{
   Intent intent = new Intent();
   intent.setAction("com.runoob.CUSTOM_INTENT");
   sendBroadcast(intent);
}

com.runoob.CUSTOM_INTENT的意图可以像之前我们注册系统产生的意图一样被注册。

<application
   android:icon="@drawable/ic_launcher"
   android:label="@string/app_name"
   android:theme="@style/AppTheme" >
   <receiver android:name="MyReceiver">

      <intent-filter>
         <action android:name="com.runoob.CUSTOM_INTENT">
         </action>
      </intent-filter>

   </receiver>
</application>

4.ContentProvider内容提供者

内容提供者组件通过请求从一个应用程序向其他的应用程序提供数据。这些请求由类 ContentResolver 的方法来处理。内容提供者可以使用不同的方式来存储数据。数据可以被存放在数据库,文件,甚至是网络。
在这里插入图片描述
你可以查询,编辑它的内容,使用 insert(), update(), delete() 和 query() 来添加或者删除内容。多数情况下数据被存储在 SQLite 数据库。

步骤 描述
1 使用 Android Studio 创建 Android 应用程序并命名为 Content Provider,在包com.runoob.contentprovider 下,并建立空活动。
2 修改主要活动文件 MainActivity.java 来添加两个新的方法 onClickAddName() 和 onClickRetrieveStudents()。
3 在包 com.runoob.contentprovider 下创建新的 Java 文件 StudentsProvider.java 来定义实际的提供者,并关联方法。
4 使用<provider…/>标签在 AndroidManifest.xml 中注册内容提供者。
5 修改 res/layout/activity_main.xml 文件的默认内容来包含添加学生记录的简单界面。
6 无需修改 strings.xml,Android Studio 会注意 strings.xml 文件。
7 启动 Android 模拟器来运行应用程序,并验证应用程序所做改变的结果。

package com.runoob.contentprovider;

import android.net.Uri;
import android.os.Bundle;
import android.app.Activity;
import android.content.ContentValues;
import android.content.CursorLoader;
import android.database.Cursor;
import android.view.Menu;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import com.runoob.contentprovider.R;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    public void onClickAddName(View view) {
        // Add a new student record
        ContentValues values = new ContentValues();

        values.put(StudentsProvider.NAME,
                ((EditText)findViewById(R.id.editText2)).getText().toString());

        values.put(StudentsProvider.GRADE,
                ((EditText)findViewById(R.id.editText3)).getText().toString());

        Uri uri = getContentResolver().insert(
                StudentsProvider.CONTENT_URI, values);

        Toast.makeText(getBaseContext(),
                uri.toString(), Toast.LENGTH_LONG).show();
    }

    public void onClickRetrieveStudents(View view) {

        // Retrieve student records
        String URL = "content://com.example.provider.College/students";

        Uri students = Uri.parse(URL);
        Cursor c = managedQuery(students, null, null, null, "name");

        if (c.moveToFirst()) {
            do{
                Toast.makeText(this,
                        c.getString(c.getColumnIndex(StudentsProvider._ID)) +
                                ", " +  c.getString(c.getColumnIndex( StudentsProvider.NAME)) +
                                ", " + c.getString(c.getColumnIndex( StudentsProvider.GRADE)),
                        Toast.LENGTH_SHORT).show();
            } while (c.moveToNext());
        }
    }
}

在包com.runoob.contentprovider下创建新的文件StudentsProvider.java。以下是src/com.runoob.contentprovider/StudentsProvider.java的内容。

public class StudentsProvider extends ContentProvider {

    static final String PROVIDER_NAME = "com.example.provider.College";
    static final String URL = "content://" + PROVIDER_NAME + "/students";
    static final Uri CONTENT_URI = Uri.parse(URL);

    static final String _ID = "_id";
    static final String NAME = "name";
    static final String GRADE = "grade";

    private static HashMap<String, String> STUDENTS_PROJECTION_MAP;

    static final int STUDENTS = 1;
    static final int STUDENT_ID = 2;

    static final UriMatcher uriMatcher;
    static{
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(PROVIDER_NAME, "students", STUDENTS);
        uriMatcher.addURI(PROVIDER_NAME, "students/#", STUDENT_ID);
    }

    /**
     * 数据库特定常量声明
     */
    private SQLiteDatabase db;
    static final String DATABASE_NAME = "College";
    static final String STUDENTS_TABLE_NAME = "students";
    static final int DATABASE_VERSION = 1;
    static final String CREATE_DB_TABLE =
            " CREATE TABLE " + STUDENTS_TABLE_NAME +
                    " (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                    " name TEXT NOT NULL, " +
                    " grade TEXT NOT NULL);";

    /**
     * 创建和管理提供者内部数据源的帮助类.
     */
    private static class DatabaseHelper extends SQLiteOpenHelper {
        DatabaseHelper(Context context){
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db)
        {
            db.execSQL(CREATE_DB_TABLE);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            db.execSQL("DROP TABLE IF EXISTS " +  STUDENTS_TABLE_NAME);
            onCreate(db);
        }
    }

    @Override
    public boolean onCreate() {
        Context context = getContext();
        DatabaseHelper dbHelper = new DatabaseHelper(context);

        /**
         * 如果不存在,则创建一个可写的数据库。
         */
        db = dbHelper.getWritableDatabase();
        return (db == null)? false:true;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        /**
         * 添加新学生记录
         */
        long rowID = db.insert( STUDENTS_TABLE_NAME, "", values);

        /**
         * 如果记录添加成功
         */

        if (rowID > 0)
        {
            Uri _uri = ContentUris.withAppendedId(CONTENT_URI, rowID);
            getContext().getContentResolver().notifyChange(_uri, null);
            return _uri;
        }
        throw new SQLException("Failed to add a record into " + uri);
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder) {
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        qb.setTables(STUDENTS_TABLE_NAME);

        switch (uriMatcher.match(uri)) {
            case STUDENTS:
                qb.setProjectionMap(STUDENTS_PROJECTION_MAP);
                break;

            case STUDENT_ID:
                qb.appendWhere( _ID + "=" + uri.getPathSegments().get(1));
                break;

            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }

        if (sortOrder == null || sortOrder == ""){
            /**
             * 默认按照学生姓名排序
             */
            sortOrder = NAME;
        }
        Cursor c = qb.query(db, projection, selection, selectionArgs,null, null, sortOrder);

        /**
         * 注册内容URI变化的监听器
         */
        c.setNotificationUri(getContext().getContentResolver(), uri);
        return c;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        int count = 0;

        switch (uriMatcher.match(uri)){
            case STUDENTS:
                count = db.delete(STUDENTS_TABLE_NAME, selection, selectionArgs);
                break;

            case STUDENT_ID:
                String id = uri.getPathSegments().get(1);
                count = db.delete( STUDENTS_TABLE_NAME, _ID +  " = " + id +
                        (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""), selectionArgs);
                break;

            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }

        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        int count = 0;

        switch (uriMatcher.match(uri)){
            case STUDENTS:
                count = db.update(STUDENTS_TABLE_NAME, values, selection, selectionArgs);
                break;

            case STUDENT_ID:
                count = db.update(STUDENTS_TABLE_NAME, values, _ID + " = " + uri.getPathSegments().get(1) +
                        (!TextUtils.isEmpty(selection) ? " AND (" +selection + ')' : ""), selectionArgs);
                break;

            default:
                throw new IllegalArgumentException("Unknown URI " + uri );
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)){
            /**
             * 获取所有学生记录
             */
            case STUDENTS:
                return "vnd.android.cursor.dir/vnd.example.students";

            /**
             * 获取一个特定的学生
             */
            case STUDENT_ID:
                return "vnd.android.cursor.item/vnd.example.students";

            default:
                throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
    }
}

 以下是修改后的AndroidManifest.xml文件。这里添加了<provider.../>标签来包含我们的内容提供者:



 <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.runoob.contentprovider"
        android:versionCode="1"
        android:versionName="1.0" >
    
        <uses-sdk
            android:minSdkVersion="8"
            android:targetSdkVersion="22" />
    
        <application
            android:allowBackup="true"
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name"
            android:theme="@style/AppTheme" >
    
            <activity
                android:name="com.runoob.contentprovider.MainActivity"
                android:label="@string/app_name" >
    
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
    
            </activity>
    
            <provider android:name="StudentsProvider"
                android:authorities="com.example.provider.College" >
            </provider>
    
        </application>
    
    </manifest>

DOWN

  • 11
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值