Android-网络与数据存储

五种数据存储方式

  • 文件存储是一种较常用的方法,与 Java的文件存储类似,都是通过 I/O 流的形式存储数据。
  • SharedPreferences 时 Android提供的用来存储一些简单的配置信息的一种机制。
  • SQLite数据库是 Android自带的一个轻量级数据库,支持基本 SQL语法。
  • ContentProvider是 Android四大组件之一,可以将自己的数据共享给其他应用程序。
  • 网络存储是通过网络提供的存储空间来存储 / 获取数据信息。

读取各目录下的文件

  • 操作sd卡文件、读写文件
  • 操作assets目录下的文件
  • 操作raw目录下的文件
  • 操作res目录下的文件
void testAssets() throws IOException {
    // 第一种,直接读取路径
    WebView webView = new WebView(this);
    webView.loadUrl("file:///android_asset/test.html");
    
    try {
        // open的只能是文件,不能是文件夹
        InputStream inputStream = 															getResources().getAssets().open("test.html");
    } catch (IOException e) {
        e.printStackTrace();
    }
    
    // 读列表
    String[] filenames = getAssets().list("images");
    
    // 读图片
    InputStream inputStream = getAssets().open("images/test.jpg");
    Bitmap bitmap = BitmapFactory.decodeStream(inputStream);

	// 读音乐
    AssetFileDescriptor assetFileDescriptor = 
        getAssets().openFd("test.mp3");
    MediaPlayer player = new MediaPlayer();
    player.reset();
    player.setDataSource(
    	assetFileDescriptor.getFileDescriptor(),
        assetFileDescriptor.getStartOffset(),
        assetFileDescriptor.getLength());
    player.prepare();
    player.start();
}


void testResFile() {
    InputStream inputStream = getResources().openRawResource(R.id.文件名);
	
    getResources().getColor(R.color.);
    getResources().getString(R.string.);
    getResources().getDrawable(R.drawable.);
}

void testSDCard() {
    File file = new File("/sdcard/test/a.txt");
    String filePath = Environment.getExternalStorageDirectory().getAbsolutePath();
    
    Environment.getDataDirectory(); // 获取Android中的data数据目录
    Environment.getDownloadCacheDirectory(); // 获取下载的缓存
}

1、文件存储

  • 文件存储是Android中最基本的一种数据存储方式,Android中的文件存储分为内部存储和外部存储。

    • 内部存储:将应用程序中的数据以文件方式存储到设备的内部(位于data/data//files/目录),当创建的应用程序被卸载时,其内部存储文件也随之被删除。
  • 外部存储:是将文件存储到一些外部设备上,例如SD卡或者设备内嵌的存储卡(通常位于mnt/sdcard目录下,不同厂商生产的手机路径可能不同)。属于永久性存储方式。

    — 获取外部存储的权限:android.permission.WRITE_EXTERNAL_STORAGE

  • 1.1 首先创建一个Module,然后在 activity_main.xml文件中添加组件

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">
    
        <TextView
            android:id="@+id/tv_hello"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="文件读取案例..."
            />
        <Button
            android:id="@+id/btn_save"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="保存"/>
        <Button
            android:id="@+id/btn_read"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="读取"
            />
    </LinearLayout>
    
  • 1.2 在MainActivity中实现View.OnClickListener接口,并为组件实例化和设置监听事件

    public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    
        private TextView mTextView; // 声明 TextView
        private Button mSaveButton;
        private Button mReadButton;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            mTextView = findViewById(R.id.tv_hello);
            mSaveButton = findViewById(R.id.btn_save);
            mReadButton = findViewById(R.id.btn_read);
            // 设置保存和读取按钮的点击事件
            mSaveButton.setOnClickListener(this);
            mReadButton.setOnClickListener(this);
        }
    
        // 保存文件
        public void saveFileInApplication() {
            String fileName = "data.txt";
            String content = "这是我们演示的文件存储示例";
            FileOutputStream fos;
            try {
                // 以追加模式创建 data.txt文件,并写入内容保存文件到应用程序下
                // openFileOutput(文件名, 操作模式),MODE_PRIVATE:私有覆盖模式,后写入的内容会覆盖前面写入的内容
                fos = openFileOutput(fileName, MODE_PRIVATE);
                // 写入获取字的符串的字节流
                fos.write(content.getBytes());
                fos.close();
                Toast.makeText(this, "文件保存成功", Toast.LENGTH_SHORT).show();
            } catch (Exception e) {
                e.printStackTrace();
                Toast.makeText(this, "文件保存失败", Toast.LENGTH_SHORT).show();
            }
        }
    
        // 读取文件
        public void readFileInApplication() {
            String content = "";
            FileInputStream fis;
            try {
                // 读取"data.txt"文件中的字节流,并将返回的文件输入流赋值给 fis
                fis = openFileInput("data.txt");
                // 创建字节数组,通过 fis.available()方法获取 fis的可读取字节数,并作为字节数组的最大长度
                byte[] buffer = new byte[fis.available()];
                // 将读取的内容存储到字节数组当中
                fis.read(buffer);
                content = new String(buffer);
                fis.close();
                // 将读取的内容设置给TextView显示出来
                mTextView.setText(content);
                Toast.makeText(this, "读取成功", Toast.LENGTH_SHORT).show();
            } catch (Exception e) {
                e.printStackTrace();
                Toast.makeText(this, "读取失败", Toast.LENGTH_SHORT).show();
            }
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.btn_save:
                    saveFileInApplication();
                    break;
                case R.id.btn_read:
                    readFileInApplication();
                    break;
            }
        }
    }
    

    File存储核心语句:

    • openFileOutput(String name, int mode) : 保存文件内容,打开指定的私有文件输出流;
    • 返回参数类型为FileOutputStream;
    • name : 为要打开的文件名,不包含路径分隔符;
    • mode : 为文件操作模式。

    Mode的取值:

    • Environment.MODE_PRIVATE: 为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下写入的内容会覆盖原文件的内容;
    • Environment.MODE_APPEND: 检查文件是否存在,存在就往文件追加内容,否则就创建新文件。
  • 1.3 运行Module,在Android Studio的右下角Device File Explorer中的data/data/文件夹下可以找到我们新创建的data.txt文件,同样也可以打开DDMS查看,关于Android Studio3.0之后如何打开DDMS?

    在线预览

    android-FileStorage.gif


2、使用 SharedPreferences 方便的存储数据

  • SharedPreferences 是什么?

    SharedPreferences 是 Android平台上一个轻量级的存储类。

    SharedPreferences 中存储的数据是以 key/value 键值对的形式保存在XML文件中,该文件位于“data/data//shared_prefs”文件夹中。

  • SharedPreferences 适用于哪些地方?

    存储一些简单的值,应用程序的配置参数,如用户名、密码等。

  • 2.1 使用SharedPreferences 保存数据的方法 :

    • 1、使用Activity类的getSharedPreferences (String name, int mode) 方法获得SharedPreferences 对象,其中参数 name 指定存储键值对的文件名称,mode 则指定文件操作模式。
    • 2、使用SharedPreferences 的edit() 方法获得SharedPreferences.Editor对象。
    • 3、通过SharedPreferences.Editor的putXxx(String key, Xxx value) 方法写入键值对。
    • 4、通过SharedPreferences.Editor的commit()方法提交要保存的键值对。
  • 2.2 首先创建一个Module,并在activity_main.xml文件中通过约束布局快速搭建界面

    image.png

  • 2.3 创建一个SPSaveQQ类,并在该类中创建两个方法用于保存和读取账号密码

    public class SPSaveQQ {
    
        public static final String ACCOUNT = "account";
        public static final String PASSWORD = "password";
    
        // 保存QQ账号和密码到 data.xml文件中
        public static boolean saveUserInfo(Context context, String account, String password) {
            // 创建 SharedPreferences对象。参数 data是文件名,Context.MODE_PRIVATE是文件操作模式
            SharedPreferences sp = context.getSharedPreferences("data", Context.MODE_PRIVATE);
            // 创建 Editor对象
            SharedPreferences.Editor editor = sp.edit();
            // 保存用户名和密码
            editor.putString(ACCOUNT, account);
            editor.putString(PASSWORD, password);
            // 编辑结束,调用该方法提交
    //        editor.commit();
            // 和网络相关,IO操作相关的,都要用异步
            // 后台写数据,另开线程
            editor.apply();
            return true;
        }
    
        // 从 data.xml文件中获取存储的QQ账号和密码
        public static Map<String, String> getUserInfo(Context context) {
            SharedPreferences sp = context.getSharedPreferences("data", Context.MODE_PRIVATE);
            // 使用getString()方法读取用户名和密码。第一个参数为 key,第二个参数为 defValue
            String account = sp.getString(ACCOUNT, null);
            String password = sp.getString(PASSWORD, null);
            // 创建 HashMap用来存储用户登录信息
            Map<String, String> userMap = new HashMap<>();
            // 将刚才读取的用户名和密码值添加到 userMap中
            userMap.put(ACCOUNT, account);
            userMap.put(PASSWORD, password);
            return userMap;
        }
    }
    
  • 2.4 在MainActivity中初始化EditView和Button组件,并为登录按钮设置点击事件

    public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    
        private EditText mAccount;
        private EditText mPassword;
        private Button mLogin;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            // 初始化界面
            initView();
            // 调用SPSaveQQ.getUserInfo()方法获取保存在SharedPreferences文件当中的账号和密码
            Map<String, String> userInfo = SPSaveQQ.getUserInfo(this);
            if (userInfo != null) {
                mAccount.setText(userInfo.get(SPSaveQQ.ACCOUNT));
                mPassword.setText(userInfo.get(SPSaveQQ.PASSWORD));
            }
        }
    
        public void initView() {
            mAccount = findViewById(R.id.et_account);
            mPassword = findViewById(R.id.et_password);
            mLogin = findViewById(R.id.btn_login);
            // 为登录按钮设置点击事件
            mLogin.setOnClickListener(this);
        }
    
        @Override
        public void onClick(View v) {
            // 点击登录按钮时,获取文本框中的账号和密码
            String account = mAccount.getText().toString();
            String password = mPassword.getText().toString();
            // 校验用户是否输入账号和密码
            if (TextUtils.isEmpty(account)) {
                Toast.makeText(this, "请输入QQ账号", Toast.LENGTH_SHORT).show();
                return;
            }
            if (TextUtils.isEmpty(password)) {
                Toast.makeText(this, "请输入密码", Toast.LENGTH_SHORT).show();
                return;
            }
            // 登录成功
            Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();
    
            // 保存用户输入的QQ账号和密码
            boolean isSaveSuccess = SPSaveQQ.saveUserInfo(this, account, password);
            if (isSaveSuccess) {
                Toast.makeText(this, "保存成功", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(this, "保存失败", Toast.LENGTH_SHORT).show();
            }
        }
    }
    
  • 2.5 运行Module,在Android Studio的右下角Device File Explorer中的data/data//shared_prefs文件夹下可以看到我们创建的data.xml

    在线预览

    android-SharedPreferences.gif


3、SQLite数据库简介

SQLite 是一个轻量级数据库,占用资源非常低,在内存中只需占用几百KB的存储空间。
SQLite 是遵守ACID的关系型数据库管理系统,ACID是指数据库事务正确执行的四个基本要素,即原子性(Atomicity)、一致性(Consistency)、隔离性(lsolation)、持久性(Durability)。
  • 特色:轻量级、独立、隔离、跨平台、多语言接口、安全性

增删改查示例

  • 3.1 新建一个Module,并添加一个DatabaseHelper类

    /**
     * DatabaseHelper作为一个访问 SQLite的助手类,提供两个方面的功能:
     * 第一:getReadableDatabase(), getWritableDatabase()可以获得 SQLiteDatabase对象,通过该对象可以对数据库进行操作
     * 第二:提供了 onCreate()和 onUpgrade()两个回调函数,允许我们在创建和升级数据库时,进行自己的操作
     */
    public class DatabaseHelper extends SQLiteOpenHelper {
    
        public static final String DATABASE_NAME = "test.db";
    
        public DatabaseHelper(@Nullable Context context) {
            super(context, DATABASE_NAME, null, 1);
        }
    
        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL("create table tb_user(id INTEGER primary key autoincrement," +
                    "username varchar(20) not null, password varchar(20) not null)");
        }
    
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            // 当数据库的版本号增加时调用
        }
    }
    
  • 3.2 在activity_main.xml中添加一个Button按钮并设置点击事件

    <Button
            android:id="@+id/btn_index_main"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAllCaps="false"
            android:text="Hello World!"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
    • 点击按钮跳转到DatabaseButtonActivity界面

      public class MainActivity extends AppCompatActivity {
      
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_main);
              Button button = findViewById(R.id.btn_index_main);
              button.setOnClickListener(new View.OnClickListener() {
                  @Override
                  public void onClick(View v) {
                      Intent intent = new Intent();
                      intent.setClass(MainActivity.this, DatabaseButtonActivity.class);
                      startActivity(intent);
                  }
              });
          }
      }
      
  • 3.2 创建activity_database.xml和DatabaseButtonActivity,并在布局文件中添加组件

    image.png

  • 在DatabaseButtonActivity中为四个按钮设置点击事件【别忘了加入AndroidManifest.xml中

    public class DatabaseButtonActivity extends AppCompatActivity {
        private static final String TAG = "MySQLite";
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_database);
    
            initView();
        }
    
        public void initView() {
            Button insertButton = findViewById(R.id.btn_insert);
            Button deleteButton = findViewById(R.id.btn_delete);
            Button updateButton = findViewById(R.id.btn_update);
            Button selectButton = findViewById(R.id.btn_select);
    
            insertButton.setOnClickListener(new InsertListener());
            deleteButton.setOnClickListener(new DeleteListener());
            updateButton.setOnClickListener(new UpdateListener());
            selectButton.setOnClickListener(new SelectListener());
        }
    
        /**
         * 插入数据按钮的监听器
         */
        public class InsertListener implements View.OnClickListener {
            @Override
            public void onClick(View v) {
                // 1、创建 DatabaseHelper助手类对象
                DatabaseHelper databaseHelper = new DatabaseHelper(DatabaseButtonActivity.this);
                // 2、创建 SQLiteDatabase对象
                SQLiteDatabase db = databaseHelper.getWritableDatabase();
                // 3、创建 ContentValues对象
                ContentValues values = new ContentValues();
                // ContentValues类似于 Map类,通过键值对的形式存储数据
                values.put("username", "admin");
                values.put("password", "admin123");
    
                // 插入数据
                long id = db.insert("tb_user", null, values);
                db.close();
    
                if (id != -1) {
                    Log.i(TAG, "插入成功");
                }
            }
        }
    
        /**
         * 删除数据按钮的监听器
         */
        public class DeleteListener implements View.OnClickListener {
            @Override
            public void onClick(View v) {
                DatabaseHelper databaseHelper = new DatabaseHelper(DatabaseButtonActivity.this);
                SQLiteDatabase db = databaseHelper.getWritableDatabase();
                String whereClause = "id=?";
                String[] whereArgs = {"1"};
                int number = db.delete("tb_user", whereClause, whereArgs);
                db.close();
                Log.i(TAG, "删除:" + number + "行受影响");
            }
        }
    
        /**
         * 修改数据按钮的监听器
         */
        public class UpdateListener implements View.OnClickListener {
            @Override
            public void onClick(View v) {
                DatabaseHelper databaseHelper = new DatabaseHelper(DatabaseButtonActivity.this);
                SQLiteDatabase db = databaseHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("username", "zhangsan");
                String whereClause = "id=?";
                String[] whereArgs = {"1"};
                int number = db.update("tb_user", values, whereClause, whereArgs);
                db.close();
                Log.i(TAG, "修改:" + number + "行受影响");
            }
        }
    
        /**
         * 查询数据按钮的监听器
         */
        public class SelectListener implements View.OnClickListener {
            @Override
            public void onClick(View v) {
                DatabaseHelper databaseHelper = new DatabaseHelper(DatabaseButtonActivity.this);
                SQLiteDatabase db = databaseHelper.getWritableDatabase();
                String whereClause = "id=?";
                String[] whereArgs = {"1"};
                // Cursor是一个游标接口,提供了遍历查询结果的方法。query()方法返回的是一个行数集合 Cursor
                Cursor cursor = db.query("tb_user", null, whereClause,
                        whereArgs, null, null, null);
                boolean result = cursor.moveToNext();
                if (result) {
                    // 根据列名获得列索引
                    // getString()方法获取记录值,getColumnIndex()方法获取列的索引
                    String username = cursor.getString(cursor.getColumnIndex("username"));
                    String password = cursor.getString(cursor.getColumnIndex("password"));
                    cursor.close();
                    db.close();
                    Log.i(TAG,  "查询记录----用户名:" + username + ", 密码:" + password);
                } else {
                    Log.i(TAG,  "查询记录为空");
                }
            }
        }
    }
    
  • 3.3 运行效果预览在线预览

    android-SQLite.gif


4、ContentProvider

什么是Content Provider

  • 应用程序间共享数据的一种方式
  • 为存储和获取数据提供了统一的接口
  • Android为常见的一些数据提供了默认的ContentProvider
  • 四大组件之一

Google是怎么定义Content Provider的?

  • 内容提供者将一些特定的应用程序提供给其他应用程序使用。
  • 数据可以存储于文件系统、SQLite数据库或其他方式
  • 内容提供者继承于ContentProvider基类,为其他应用程序取用和存储它管理的数据实现了一套标准方法
  • 应用程序并不直接调用这些方法,而是使用ContentResolver对象,调用它的方法作为替代。
  • ContentResolver可以与任意内容提供者进行会话,与其合作来对所有相关交互通讯进行管理。

ContentProvider的实现过程

  • Database
  • Uri
  • UriMatcher
  • ContentProvider
  • query/insert/update/delete
  • AndroidManifest.xml

5、网络编程数据处理

5.1 如何请求网络数据

  • 在AndroidManifest.xml中申请权限

    <uses-permission android:name="android.permission.INTERNET"/>
    
  • 需要异步的获取数据,否则会直接闪退,因为不能在主线程中获取数据

5.2 请求百度数据,并把数据显示出来

  • 5.2.1 我们新建一个项目,在activity_main.xml中添加按钮,并在MainActivity中设置点击事件

    <Button
            android:id="@+id/btn_index_main"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAllCaps="false"
            android:text="NetWork"/>
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
         Button button = findViewById(R.id.btn_index_main);
         button.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                Intent intent = new Intent();
                intent.setClass(MainActivity.this, NetWorkActivity.class);
                startActivity(intent);
             }
         });
    }
    
  • 5.2.2 创建一个NetWorkActivity和activity_network.xml布局文件,并添加到AndroidManifest中

    image.png

    public class NetWorkActivity extends AppCompatActivity implements View.OnClickListener {
    
        private EditText mHttpEditText;
        private TextView mShowResultTextView;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_network);
    
            initView();
        }
    
        public void initView() {
            mHttpEditText = findViewById(R.id.et_http);
            Button mGetDataButton = findViewById(R.id.btn_getData);
            mShowResultTextView =findViewById(R.id.tv_showResult);
    
            mGetDataButton.setOnClickListener(this);
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.btn_getData:
                    String url = getEditTextUrl();
                    // 请求网络数据,需要在 AndroidManifest中注册网络权限
                    new RequestNetworkDataTask().execute(url);
    
                    //String content = requestData(url);
                    //mShowResultTextView.setText(content);
                    break;
            }
        }
    
        // 获取 Url地址
        public String getEditTextUrl() {
            return mHttpEditText != null ? mHttpEditText.getText().toString() : "";
        }
    
        // 返回请求数据
        public String requestData(String urlString) {
            try {
                URL url = new URL(urlString);
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                // 设置连接超时
                connection.setConnectTimeout(30000);
                // 设置请求方法
                connection.setRequestMethod("GET");
                // 建立连接
                connection.connect();
                int responseCode = connection.getResponseCode();
                String responseMessage = connection.getResponseMessage();
                InputStream inputStream = connection.getInputStream();
    
                Reader reader = new InputStreamReader(inputStream);
                char[] buffer = new char[1024];
                reader.read(buffer);
                String content = new String(buffer);
                return content;
    
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        // 异步任务处理
        public class RequestNetworkDataTask extends AsyncTask<String, Integer, String> {
    
            @Override
            protected void onPreExecute() {
                // 在后台 work之前,在主线程中
                super.onPreExecute();
                Toast.makeText(NetWorkActivity.this, "加载中...", Toast.LENGTH_SHORT).show();
            }
    
            @Override
            protected String doInBackground(String... strings) {
                String result = requestData(strings[0]);
                return result;
            }
    
            @Override
            protected void onPostExecute(String s) {
                // 执行完之后再主线程中
                super.onPostExecute(s);
                mShowResultTextView.setText(s);
                System.out.println(s);
                Toast.makeText(NetWorkActivity.this, "加载完成", Toast.LENGTH_SHORT).show();
            }
        }
    }
    
  • 5.2.3 效果预览

    image.png

注意:Genymotion需要进行网络连接哦!

5.3 数据解析

  • XML

    • SAX : 基于事件驱动的
    • PULL : android系统本身自适用的
    • DOM : 文件流的形式,会把整个文件加载出来
  • JSON

    • JSONObject

    • JSONArray

    • other…

    • GSON

    • fastjson

网络状态处理及常用网络开源库

  • 网络状态处理
    • ConnectivityManager
    • NetworkInfo
  • 常用网络开源库
    • android-async-http
    • Volley
    • OKHttp
    • Retrofit
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值