Android之数据存储——持久化技术

数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即时在手机或电脑关机的情况下,这些数据仍然不会丢失。
Android系统中主要提供了3种方式用于简单地实现数据持久化技术,即文件存储、SharedPreference以及数据库存储。另外,将数据保存在SD卡中也算是一种数据持久化技术,但是这种存储方式没有前三种方式安全。

一、文件存储
文件存储是Android中最基本的一种数据存储方式,它不对存储的内容进行任何的格式化处理,所有数据都是原封不动地保存到文件中的,因此比较适合用于存储一些简单的文本数据和二进制数据。

1、将数据存储到文件中
首先新建一个Android工程,在其activity_main.xml文件中为其添加一个EditText

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.studio.filepersistencetest.MainActivity">

    <EditText
        android:id="@+id/edit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="write something here"/>

</LinearLayout>

然后修改MainActivity.java代码,完成将数据存储在文件data的功能。
与Java不同的是,在Android的Context类下提供了一个openFileOutput()方法,这个方法有两个参数,第一个参数是文件名(这里的文件名不能包含路径,因为在Android中所有的文件都是默认存储到/data/data/<packagename>/files/目录下的)。第二个参数是操作模式,主要有两种操作模式可选,MODE_PRIVATE和MODE_APPEND。其中MODE_PRIVATE是默认的操作模式,表示当指定同样文件名的时候,所写入的内容将会覆盖原文件中的内容,而MODE_APPEND则表示如果该文件已存在,就往文件里追加内容,不存在就创建新文件。openFileOutput()方法返回的是一个FileOutputStream对象。

public class MainActivity extends AppCompatActivity
{

    private EditText edit;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        edit = (EditText) findViewById(R.id.edit);
    }

    /**
     * 在这个活动销毁时保存EditText中输入的数据
     */
    @Override
    protected void onDestroy()
    {
        super.onDestroy();
        String inputText=edit.getText().toString();//获取EditText中输入的数据
        save(inputText);//将数据保存起来
    }

    /**
     * 保存数据到文件中
     * @param inputText
     */
    public void save(String inputText)
    {
        FileOutputStream out=null;
        BufferedWriter writer=null;
        try
        {
            out=this.openFileOutput("data",Context.MODE_PRIVATE);//执行Context类下的openFileOutput方法,指定存储数据到文件名为data的文件中,并返回一个FileOutputStream对象
            writer=new BufferedWriter(new OutputStreamWriter(out));//将FileOutputStream字节输出流转换为OutputStreamWriter字符输出流,并让OutputStreanWriter字符输出流利用缓冲输出流
            writer.write(inputText);//利用缓冲字符输出流写入数据
        }
        catch (java.io.IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if(writer!=null)
            {
                try
                {
                    writer.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }
}

这样我们就完成了在MainActivity被Destroy的时候存储EditText中数据的功能,运行模拟器(注意:Android 7.0无法在DDMS中查看文件,因此在学习这章内容的时候请将模拟器版本选择为7.0以下的版本),在EditText中输入WindFromFarEast后,退出App,在DDMS中查看data文件内容。
这里写图片描述
由此可见,我们已经成功将数据存储到的文件data下。

2、从文件中读取数据
Context类下还提供了一个openFileInput()方法,用于从文件中读取数据,这个方法只有一个参数,就是读取数据的文件名,返回值是FileInputStream对象。
下面我们完善上面的代码,增加一个在重新启动App时将保存在文件data中的数据重新读取到EditText中的功能。

public class MainActivity extends AppCompatActivity
{

    private EditText edit;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        edit = (EditText) findViewById(R.id.edit);
        String loadText=load();//读取文件中存储的数据
        if(!TextUtils.isEmpty(loadText))//如果读取的数据不是null或者空字符串,将数据写入EditText并将输入光标放到数据最后,弹出吐司表示数据读取成功
        {
            edit.setText(loadText);
            edit.setSelection(loadText.length());
            Toast.makeText(this,"Restoring succeeded!",Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * 在这个活动销毁时保存EditText中输入的数据
     */
    @Override
    protected void onDestroy()
    {
        super.onDestroy();
        String inputText=edit.getText().toString();//获取EditText中输入的数据
        save(inputText);//将数据保存起来
    }

    /**
     * 保存数据到文件中
     * @param inputText
     */
    public void save(String inputText)
    {
        FileOutputStream out=null;
        BufferedWriter writer=null;
        try
        {
            out=this.openFileOutput("data",Context.MODE_PRIVATE);//执行Context类下的openFileOutput方法,指定存储数据到文件名为data的文件中,并返回一个FileOutputStream对象
            writer=new BufferedWriter(new OutputStreamWriter(out));//将FileOutputStream字节输出流转换为OutputStreamWriter字符输出流,并让OutputStreanWriter字符输出流利用缓冲输出流
            writer.write(inputText);//利用缓冲字符输出流写入数据
        }
        catch (java.io.IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if(writer!=null)
            {
                try
                {
                    writer.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 读取文件中的数据
     * @return
     */
    public String load()
    {
        FileInputStream in=null;
        BufferedReader reader=null;
        StringBuilder content=new StringBuilder();

        try
        {
            in=this.openFileInput("data");//指定要读取数据的文件,并返回一个FileInputStream对象
            reader=new BufferedReader(new InputStreamReader(in));//将字节输入流转换为利用缓冲的字符输入流
            String line="";
            while((line=reader.readLine())!=null)
            {
                content.append(line);
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if(reader!=null)
            {
                try
                {
                    reader.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }

        return content.toString();
    }
}

这样我们就完成了读取数据的功能,下面重新运行App。
这里写图片描述
由此可见,读取数据成功!

二、SharedPreferences存储
SharedPreferences是使用键值对的方式来存储数据的。
要想使用SharedPreferences来存储数据,首先需要获取到SharedPreferences对象,Android中主要提供了三种方法用于得到SharedPreferences对象。

  1. 利用Context类下的getSharedPreferences()方法,第一个参数指定SharedPreferences文件名称,如果指定的文件不存在则会创建一个;第二个参数指定操作模式,暂时只有MODE_PRIVATE,表示只有当前应用程序才可以对这个SharedPreferences文件进行读写。
  2. 利用Activity类下的getPreferences()方法,这个方法只接受一个操作模式参数,并自动将当前活动的类名作为SharedPreferences文件名
  3. 利用PreferenceManager类下的getDefaultSharedPreferences()方法,这个方法只接受一个Context参数,并自动使用当前应用程序的包名作为前缀来命名SharedPreferences文件

在获取到SharedPreferences对象后,就可以向SharedPreferences文件中存储数据了,主要分三步进行:

  1. 调用SharedPreferences对象的edit()方法获取一个SharedPreferences.Editor对象
  2. 向SharedPreferences.Editor对象中添加数据,添加数据的方法有putString,putInt等
  3. 调用apply()方法提交数据,完成数据存储。

下面进行实战,创建一个SharedPreferencesTest项目,修改activity_main.xml文件,为其添加一个Button用来存储数据

    <Button
        android:id="@+id/save_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Save data"/>

然后修改MainActivity中的代码

public class MainActivity extends AppCompatActivity
{

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button saveData= (Button) findViewById(R.id.save_data);
        //按下按钮就执行SharedPreferences存储操作
        saveData.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                /**
                 * 得到SharedPreferences对象的三种方式:
                 * 1、利用Context类下的getSharedPreferences()方法,第一个参数指定SharedPreferences文件名称,第二个参数指定操作模式,暂时只有MODE_PRIVATE
                 * 2、利用Activity类下的getPreferences()方法,这个方法只接受一个操作模式参数,并自动将当前活动的类名作为SharedPreferences文件名
                 * 3、利用PreferenceManager类下的getDefaultSharedPreferences()方法,这个方法只接受一个Context参数,并自动使用当前应用程序的包名作为前缀来命名SharedPreferences文件
                 */

                /**
                 * 下面是在利用Context类下的getSharedPreferences()方法得到SharedPreferences对象的前提下完成SharedPreferences存储
                 */

                //1、利用SharedPreferences对象的edit()方法得到SharedPreferences.Editor对象
                SharedPreferences.Editor editor=getSharedPreferences("data",MODE_PRIVATE).edit();

                //2、向SharedPreferences.Editor对象中添加数据
                editor.putString("name","Tom");
                editor.putInt("age",28);
                editor.putBoolean("married",false);

                //3、调用SharedPreferences.Editor类下的apply()方法提交数据,完成存储
                editor.apply();
            }
        });
    }
}

启动App,点击按钮进行数据存储,然后在DDMS中进入到/data/data/com.studio.sharedpreferencestest/shared_prefs/目录下,可以看到生成了一个data.xml文件,这里面就是我们存储的数据。我们可以发现,SharedPreferences文件是使用xml格式来对数据进行存储的。

下面学习如何从SharedPreferences文件中读取已经存储的数据
SharedPreferences对象中提供了一系列的get()方法,用于对存储的数据进行读取,例如getString(),getInt()等,这些get方法都含有两个参数,第一个参数是键,第二个参数是默认值,即表示当传入的键找不到对应的值时返回的值。

修改activity_main.xml文件,再添加一个读取数据的Button

<Button
        android:id="@+id/restore_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Restore data"/>

修改MainActivity代码,在原有代码的后面添加以下代码

        Button restoreData= (Button) findViewById(R.id.restore_data);
        restoreData.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                //1、得到SharedPreferences对象
                SharedPreferences pref=getSharedPreferences("data",MODE_PRIVATE);
                //2、从SharedPreferences文件中读取数据,get方法的第一个参数是键,第二个参数是默认值,即表示当传入的键找不到对应的值时返回的值。
                String name=pref.getString("name","");
                int age=pref.getInt("age",0);
                boolean married=pref.getBoolean("married",false);
                Log.d("MainActivity","name is "+name);
                Log.d("MainActivity","age is "+age);
                Log.d("MainActivity","married is "+married);
            }
        });

启动App,点击Restore data按钮,效果如下
这里写图片描述
说明数据读取成功。

三、SQLite数据库存储
Android系统内置了一款轻量级的关系型数据库SQLite,其遵循标准SQL语法和数据库的ACID事务,并且不用设置用户名和密码就可以使用,非常方便。

首先Android为我们提供了一个SQLiteOpenHelper类,这个类是用来创建、打开、更新数据库的,并且这个类是一个抽象类,这说明如果我们要使用这个类,就需要自定义一个类去继承SQLiteOpenHelper类。SQLiteOpenHelper类包含两个抽象方法,分别是onCreate()和onUpgrade(),前者在数据库创建时调用,后者在数据库版本更新时调用。
除了两个抽象方法外,SQLiteOpenHelper还有两个实例方法,分别是getReadableDatabase()和getWritableDatabase()。这两个方法都是用来创建一个新的数据库或者打开一个存在的数据库的,并返回一个可对数据库的数据进行CRUD操作的对象SQLiteDatabase。不同的是,当数据库不可写入的时候(如磁盘空间不足),getReadableDatabase()方法返回的对象将以只读的方式打开数据库,而getWritableDatabase()方法将会抛出异常。
SQLiteOpenHelper中有两个构造方法可供子类重写,一般使用参数少一点的那个构造方法就可以了,这个构造方法接收4个参数,第一个是Context;第二个是数据库名称;第三个参数允许我们在查询数据的时候返回一个自定义的Cursor,一般为NULL;第四个参数表示当前数据库的版本号,可用于对数据库进行升级更新操作。构建出SQLiteOpenHelper实例后,调用这个实例的getReadableDatabase或者getWritableDatabase方法就可以创建一个新的数据库了(若数据库不存在),数据库文件存放在/data/data/<package name>/databases/目录下,此时重写的onCreate()方法会得到执行。注意,如果数据库已存在,那么当版本号没有发生变化时,getReadableDatabase和getWritableDatabase方法只是单纯的打开数据库,并不会执行创建数据库的操作也不会执行onCreate()方法。如果版本号发生变化,则会更新数据库并调用onUpgrade()方法。

下面进行实战学习,首先新建一个DatabaseTest项目,然后新建MyDatabaseHelper类继承SQLiteOpenHelper类,并重写其构造方法和抽象方法。

//继承自SQLiteOpenHelper的类正如其名,是用来创建(打开)和更新数据库的类
public class MyDatabaseHelper extends SQLiteOpenHelper
{
    //创建表Book的SQL语句
    public static final String CREATE_BOOK="create table Book ("
            +"id integer primary key autoincrement,"
            +"author text,"
            +"price real,"
            +"pages integer,"
            +"name text)";

    //创建表Category的SQL语句
    public static final String CREATE_CATEGORY="create table Category ("
            +"id integer primary key autoincrement,"
            +"category_name text,"
            +"category_code integer)";

    private Context mContext;

    //构造方法的第一个参数为Context,第二个为数据库名称,第三个参数允许我们在查询数据库的时候返回一个自定义的Cursor,一般为NULL,第四个参数表示数据库版本号
    public MyDatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version)
    {
        super(context, name, factory, version);
        mContext=context;
    }

    //数据库创建时调用此方法,如果数据库已存在则不调用
    @Override
    public void onCreate(SQLiteDatabase db)
    {
        //创建数据库的同时执行建表语句创建数据表
        db.execSQL(CREATE_BOOK);
        Toast.makeText(mContext,"Create succeeded",Toast.LENGTH_SHORT).show();
    }

    //数据库更新时调用此方法
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
    {

    }

}

之后修改activity_main.xml中的代码,为其添加一个创建数据库的Button

<Button
        android:id="@+id/create_database"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Create database"/>

再修改MainActivity的代码,这里指定数据库名称为BookStore.db,数据库版本号为1,由于这个数据库之前并不存在,因此getWritableDatabase()方法在这里的作用是创建数据库,并调用onCreate()方法

public class MainActivity extends AppCompatActivity
{
    private MyDatabaseHelper dbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper=new MyDatabaseHelper(this,"BookStore.db",null,1);
        Button createDatabase= (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                dbHelper.getWritableDatabase();//创建或打开数据库BookStore.db
            }
        });
    }
}

之后我们运行App,点击Button创建数据库,弹出吐司提示创建成功,若我们再次点击,会发现吐司不再出现,说明这个数据库已经存在,因此不会再调用onCreate()方法。

但是在DDMS中是无法查看数据库的内容的,因此我们需要利用adb来查看数据库的内容,adb是Android SDK自带的一个调试工具,使用这个工具可以直接对连接在电脑上的手机或模拟器进行调试操作。它存放在sdk的platfrom_tools目录下,如果想要在命令行中使用这个工具,就需要把它的路径添加到环境变量中。

配置好环境变量后,打开命令行界面,输入adb shell就可以进入设备的控制台,然后使用cd命令进入到/data/data/com.studio.databasetest/databases/目录下,并使用ls命令查看该目录下的文件,这个目录下出现了两个数据库文件,其中一个正是BookStore.db,说明数据库创建成功。接下来我们还可以使用sqlite3 BookStore.db打开数据库,再输入.table就可以查看这个数据库中的数据表了,我们发现这个数据库中正好有我们之前放在onCreate()方法中创建的数据表Book。

之后我们还要学习如何更新升级数据库,在这里我们就利用更新数据库来为数据库新添加一个表Category。这里就需要我们之前提到的数据库的版本号。首先完成onCreate()和onUpgrade()方法

    //数据库创建时调用此方法,如果数据库已存在则不调用
    @Override
    public void onCreate(SQLiteDatabase db)
    {
        //创建数据库的同时执行建表语句创建数据表
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_CATEGORY);
        Toast.makeText(mContext,"Create succeeded",Toast.LENGTH_SHORT).show();
    }

    //数据库更新时调用此方法
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
    {
        //先删除已经存在的表的原因是因为如果在创建表时发现这张表已经存在就会报错
        db.execSQL("drop table if exists Book");
        db.execSQL("drop table if exists Category");
        //重新创建数据表
        onCreate(db);
    }

然后再修改MainActivity代码,将

dbHelper=new MyDatabaseHelper(this,"BookStore.db",null,1);

修改为

dbHelper=new MyDatabaseHelper(this,"BookStore.db",null,2);

这样版本号就从1变为了2,再执行App,点击Create database按钮后(注意这里由于数据库已经存在,并且版本号发生变化,点击按钮后执行的是onUpgrade方法),再去命令行中查看,就会发现Category表创建成功,说明数据库更新成功。

接下来学习如何对数据库中的数据进行CRUD操作。
在之前的学习中我们知道getReadableDatabase和getWritableDatabase方法会返回一个可以对数据进行CRUD操作的对象SQLiteDatabase,这个对象就是我们操作数据的关键。
1、添加数据
SQLiteDatabase中提供了一个insert()方法用于向数据表中添加数据,它接收三个参数,第一个是数据表名;第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL,一般不用这个功能,直接传入NULL即可;第三个参数是一个ContentValues对象,它提供了一系列的put()方法重载,用于向ContentValues中添加数据,只需要将表中的每个列名以及相应的值传入即可。
首先修改activity_main.xml,添加一个Button用来添加数据

<Button
        android:id="@+id/add_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Add data"/>

然后修改MainActivity代码,在原有代码后添加以下代码

        /*点击按钮Add Data则添加数据*/
        Button addData= (Button) findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                /*1、打开数据库,得到SQLiteDatabase对象,借助这个对象就可以数据库中的数据进行CRUD操作*/
                SQLiteDatabase db=dbHelper.getWritableDatabase();
                /*2、创建ContentValues对象*/
                ContentValues values=new ContentValues();
                /*3、向ContentValues对象中写入第一条数据记录*/
                values.put("name","The Da Vinci Code");
                values.put("author","Dan Brown");
                values.put("pages","454");
                values.put("price","16.96");
                /*4、将记录插入数据表Book*/
                db.insert("Book",null,values);
                /*5、清空ContentValues内部数据*/
                values.clear();
                /*6、向ContentValues对象中写入第二条数据记录,并插入到数据表Book*/
                values.put("name","The Lost Symbol");
                values.put("author","Dan Brown");
                values.put("pages","510");
                values.put("price","19.95");
                db.insert("Book",null,values);
            }
        });

这样就完成了存储两本书的数据的操作,用select * from Book可以查看详细数据
这里写图片描述

2、更新数据
SQLiteDatabase中提供了update()方法用于对数据进行更新,这个方法接收四个参数,第一个参数是数据表名,第二个参数是ContentValues对象,要把更新数据在这里组装进去,第三第四个参数用于约束更新某一行或某几行中的数据,不指定的话默认更新所有行。
首先为其添加一个Button用于更新数据

<Button
        android:id="@+id/update_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Update data"/>

然后在MainActivity中添加以下代码

//点击按钮Update data则修改数据,将达芬奇密码的价格修改为10.99
        Button updateData= (Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                SQLiteDatabase db=dbHelper.getWritableDatabase();
                ContentValues values=new ContentValues();
                values.put("price",10.99);
                db.update("Book",values,"name = ?",new String[] {"The Da Vinci Code"});
            }
        });

效果图如下
这里写图片描述

3、删除数据
SQLiteDatabase中提供了delete()方法用于删除数据,第一个参数是表名,第二个第三个参数用于约束删除某一行或某几行,不指定的话默认删除所有行
依然添加一个Button

<Button
        android:id="@+id/delete_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Delete data"/>

在MainActivity中添加代码

//点击按钮Delete data则删除页数大于500的书的记录
        Button deleteData= (Button) findViewById(R.id.delete_data);
        deleteData.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                SQLiteDatabase db=dbHelper.getWritableDatabase();
                db.delete("Book","pages > ?",new String[] {"500"});
            }
        });

效果图如下
这里写图片描述

4、查询数据
SQLiteDatabase提供了query()方法用来查询数据,这个方法的参数非常多,至少都需要七个参数,第一个参数是表名,第二个参数用于指定查询的列,第三第四个参数用于约束查询某一行或某几行的数据….懒得写了,反正这里重点不是SQL语句
query()方法会返回一个Cursor啥对象,查询到的所有数据记录都将从这个对象中取出。

继续添加Button

<Button
        android:id="@+id/query_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Query data"/>

继续添加Java代码

/*点击按钮Query data查询数据*/
        Button queryData= (Button) findViewById(R.id.query_data);
        queryData.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                SQLiteDatabase db=dbHelper.getWritableDatabase();
                Cursor cursor=db.query("Book",null,null,null,null,null,null);
                if(cursor.moveToFirst())
                {
                    do
                    {
                        /*遍历Cursor对象,取出数据并打印*/
                        String name = cursor.getString(cursor.getColumnIndex("name"));
                        String author=cursor.getString(cursor.getColumnIndex("author"));
                        int pages=cursor.getInt(cursor.getColumnIndex("pages"));
                        double price=cursor.getDouble(cursor.getColumnIndex("price"));

                        Log.d("MainActivity","name is "+name);
                        Log.d("MainActivity","author is "+author);
                    }
                    while(cursor.moveToNext());
                }
                cursor.close();
            }
        });

查询完后得到一个Cursor对象,接着我们调用他的moveToFirst方法将数据的指针移动到第一行的位置,然后进入一个循环,去遍历查询到的每一个数据。在这个循环中可以通过Cursor的getColumnIndex方法获取到某一列在表中对应位置的索引,然后将这个索引传入到相应的取值方法中,就可以得到从数据库中读取到的数据了。
效果图如下
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值