【适配】AndroidQ分区存储简单示例

背景

在适配分区存储之前,有三个问题需要了解下

1. 什么是分区存储?

分区存储是Android10中提出的文件存储策略,用于更好的管理私有数据和共享数据,减少之前的文件和数据存放混乱的问题。

2. 为什么要适配分区存储?

从Android10开始的分区存储策略,可能会导致之前的文件存储功能,在之后的版本使用中出现问题,如:读取和写入文件失败、文件出现异常等,因此需要适配分区存储。

3. 如何适配分区存储?

请看下面。

导航

简介

在这里插入图片描述
分区存储主要将APP的存储空间分为私有存储空间共享存储空间。私有存储空间存放只允许APP自身进行读取的文件和数据,如APP的配置信息、图片缓存等;共享存储空间存放需要提供给其他APP使用的文件,如office文档、相册图片等。

使用分区存储,主要便于保护APP的隐私减少卸载残留

内容

1.自身APP操作

下列操作都是由okhttp下载图片到内部存储空间/files/test/Test.jpg中,然后将图片写入到对应的存储空间

1.1内部存储空间和外部存储空间

内部目标文件位置位于 内部存储/files/Test.jpg
外部目标文件位置位于 外部存储空间/Pictures/Test.jpg

因为内部存储和外部存储的操作基本一致,不同的是操作的路径,因此将内部存储和外部存储的操作方法合并。获取内部存储的路径方法为:getFilesDir(),获取外部存储的路径方法为:getExternalFilesDir()。

(1)写文件

//写入文件到内/外部存储
private void writeImgToInternalOrExternal(String path){
    //判断文件是否存在
    File newFile = new File(path);
    if (newFile.exists()){
        newFile.delete();
    }

    //写入文件
    try {
        InputStream is = new FileInputStream(downloadPath);
        OutputStream os = new FileOutputStream(internalPath);

        byte[] bytes = new byte[1024];
        int len = 0;

        while ((len = is.read(bytes))!=-1){
            os.write(bytes,0,len);
        }

        os.close();
        is.close();

        Bitmap bitmap = BitmapFactory.decodeFile(path);
        ivShow.setImageBitmap(bitmap);
        tvShow.setText("保存到内/外部存储成功,路径:"+path);
    }catch (IOException e){
        tvShow.setText("保存到内/外部存储失败:"+e.getMessage());
        ivShow.setImageResource(0);
    }
}

(2)读文件

//读取文件从内/外部存储
private void readImgFromInternalOrExternal(String path){
    File newFile= new File(path);
    if (!newFile.exists()){
        tvShow.setText("内/外部存储无文件");
        ivShow.setImageResource(0);
        return;
    }
    Bitmap bitmap = BitmapFactory.decodeFile(path);
    ivShow.setImageBitmap(bitmap);
    tvShow.setText("内/外部存储文件路径:"+path);
}

(3)更新文件

这里使用OkHttp从网络中获取图片数据并写入

建议:这里仅供参考,请删除原先的文件后重新创建文件写入,推荐使用写文件的操作

//更新文件从内/外存储
private void updateImgFromExternal(InputStream is){
    try {
        OutputStream os = new FileOutputStream(externalPath);
        byte[] bytes = new byte[1024];
        int len = 0;

        while ((len = is.read(bytes))!=-1){
            os.write(bytes,0,len);
        }

        os.close();
        tvShow.setText("修改内/外存储文件成功");
    }catch (Exception e){
        tvShow.setText("修改内/外存储文件失败:"+e.getMessage());
    }
}

(4)删除文件

//删除文件从内/外部存储
private void deleteImgFromInternal(String path){
    File file = new File(path);
    if (!file.exists()){
        tvShow.setText("内/外部存储文件不存在");
        ivShow.setImageResource(0);
        return;
    }

    file.delete();
    tvShow.setText("删除内/外部存储文件成功");
    ivShow.setImageResource(0);
}
1.2 共享存储空间

目标文件位置位于 Pictures/Test.jpg

(1)写文件

//写入文件到共享存储
private void writeImgToShareStorage(){
    //检查文件是否存在(这里使用这种方式速度太慢,待后期进行改进)
    try {
        //判断条件
        String selection = MediaStore.Images.Media.RELATIVE_PATH+"=? and "+MediaStore.Images.Media.DISPLAY_NAME+"=?";
        //判断结果
        String[] selectionArgs = new String[]{Environment.DIRECTORY_PICTURES+File.separator,"Test.jpg"};

        Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    null,
                    selection,
                    selectionArgs,
                    null,
                    null);
        if (cursor!=null&&cursor.moveToFirst()){
                //文件存在
            int id = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
            Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,id);
            int raw = getContentResolver().delete(uri,null,null);
            cursor.close();
        }

        //保存文件
        ContentValues contentValues = new ContentValues();
            contentValues.put(MediaStore.Images.Media.DISPLAY_NAME,"Test.jpg");
            contentValues.put(MediaStore.Images.Media.MIME_TYPE,"image/*");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
                contentValues.put(MediaStore.Images.Media.RELATIVE_PATH,Environment.DIRECTORY_PICTURES);
        }else {
            //创建文件
            String dataPath = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+Environment.DIRECTORY_PICTURES+File.separator+"Test.jpg";
            File file = new File(dataPath);
            if (file.exists()){
                file.delete();
            }else {
                //这里根据实际路径判断是否创建父类的文件夹
                file.createNewFile();
            }
                contentValues.put(MediaStore.Images.Media.DATA,dataPath);
        }
        Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,contentValues);

        //写入
        ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri,"w");
        InputStream is = new FileInputStream(downloadPath);
        OutputStream os = new FileOutputStream(pfd.getFileDescriptor());

        byte[] bytes = new byte[1024];
        int len = 0;

        while ((len = is.read(bytes))!=-1){
            os.write(bytes,0,len);
        }

        os.close();
        is.close();

        String bitmapPath = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+Environment.DIRECTORY_PICTURES+File.separator+"Test.jpg";
        ivShow.setImageURI(uri);
        tvShow.setText("保存到共享存储成功,路径为:"+bitmapPath);
    }catch (Exception e){
        tvShow.setText("保存文件到共享存储失败:"+e.getMessage());
        ivShow.setImageResource(0);
    }
}

(2)读文件

//读取文件从共享存储
private void readImgFromShareStorage(){
    try {
        //检查是否存在
        //判断条件
        String selection = MediaStore.Images.Media.RELATIVE_PATH+"=? and "+MediaStore.Images.Media.DISPLAY_NAME+"=?";
        //判断结果
        String[] selectionArgs = new String[]{Environment.DIRECTORY_PICTURES+File.separator,"Test.jpg"};
        //回调结果
        String[] projection = new String[]{MediaStore.Images.Media._ID,MediaStore.Images.Media.DISPLAY_NAME,MediaStore.Images.Media.RELATIVE_PATH,MediaStore.Images.Media.DATA};

       Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    projection,
                    selection,
                    selectionArgs,
                    null,
                    null);
        if (cursor!=null&&cursor.moveToFirst()){
            //文件存在
            String name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME));
            String path = null;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
                path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.RELATIVE_PATH))+File.separator+name;
            }else {
                path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));
            }
            int id = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
            Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,id);
            ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri,"r");
            Bitmap bitmap = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
            ivShow.setImageBitmap(bitmap);
            tvShow.setText("读取共享存储的文件成功,路径为:"+path);

            cursor.close();
        }else {
            //文件不存在
            tvShow.setText("共享存储文件不存在");
            ivShow.setImageResource(0);
        }
    }catch (Exception e){
        tvShow.setText("从共享存储读取文件失败:"+e.getMessage());
        ivShow.setImageResource(0);
    }
}

(3)更新文件

更新文件名称

//更新文件名称从共享存储
private void updateImgNameFromShareStorage(){
    try {
        //判断条件
        String selection = MediaStore.Images.Media.RELATIVE_PATH+"=? and "+MediaStore.Images.Media.DISPLAY_NAME+"=?";
        //判断结果
        String[] selectionArgs = new String[]{Environment.DIRECTORY_PICTURES+File.separator,"Test.jpg"};

        Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    null,
                    selection,
                    selectionArgs,
                    null,
                    null);
        if (cursor!=null&&cursor.moveToFirst()){
            //文件存在
            int id = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
            Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,id);
            //更新数据
            ContentValues contentValues = new ContentValues();
                contentValues.put(MediaStore.Images.Media.DISPLAY_NAME,"Test123.jpg");
                contentValues.put(MediaStore.Images.Media.MIME_TYPE,"image/*");
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
                    contentValues.put(MediaStore.Images.Media.RELATIVE_PATH,Environment.DIRECTORY_PICTURES);
            }else {
                //创建文件
                String dataPath = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+Environment.DIRECTORY_PICTURES+File.separator+"Test123.jpg";
                File file = new File(dataPath);
                if (file.exists()){
                    file.delete();
                }else {
                    file.createNewFile();
                }
                    contentValues.put(MediaStore.Images.Media.DATA,dataPath);
            }

            int raw = getContentResolver().update(uri,contentValues,null,null);

            String bitmapPath = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+Environment.DIRECTORY_PICTURES+File.separator+"Test123.jpg";
            tvShow.setText("更新共享存储的文件成功,路径为:"+bitmapPath);
            ivShow.setImageResource(0);
            return;
        }

        tvShow.setText("未找到共享存储的文件");
        ivShow.setImageResource(0);
    }catch (Exception e){
        tvShow.setText("更新共享存储的文件出错:"+e.getMessage());
    }
}

更新文件内容

非常不建议这种操作,仅供参考。
建议:1.将原来的数据读出来,追加需要添加的数据,2.删除此文件,重新创建并写入

//更新文件内容从共享存储
private void updateImgContentFromShareStorage(InputStream is){
    //保存文件
    try {
        //这里只测试android10及以上的,android10及以下的暂未测试
        //检查是否存在
        //判断条件
        String selection = MediaStore.Images.Media.RELATIVE_PATH+"=? and "+MediaStore.Images.Media.DISPLAY_NAME+"=?";
        //判断结果
        String[] selectionArgs = new String[]{Environment.DIRECTORY_PICTURES+File.separator,"Test.jpg"};
        //回调结果
        String[] projection = new String[]{MediaStore.Images.Media._ID,MediaStore.Images.Media.DISPLAY_NAME,MediaStore.Images.Media.RELATIVE_PATH,MediaStore.Images.Media.DATA};

        Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    projection,
                    selection,
                    selectionArgs,
                    null,
                    null);
        if (cursor!=null&&cursor.moveToFirst()){
            //存在
            int id = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
            Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,id);

                //更新文件
            ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri,"w");
            OutputStream os = new FileOutputStream(pfd.getFileDescriptor());
            byte[] bytes = new byte[1024];
            int len = 0;

            while ((len = is.read(bytes))!=-1){
                os.write(bytes,0,len);
            }

            os.close();
            tvShow.setText("修改共享存储文件完成:"+Environment.DIRECTORY_PICTURES);
            ivShow.setImageURI(uri);
            return;
        }

        tvShow.setText("共享存储文件不存在");
        ivShow.setImageResource(0);
    }catch (Exception e){
        tvShow.setText("更新共享存储文件内容异常:"+e.getMessage());
    }
}

(4)删除文件

//删除文件从共享存储
private void deleteImgFromShareStorage(){
    try {
        //判断条件
        String selection = MediaStore.Images.Media.RELATIVE_PATH+"=? and "+MediaStore.Images.Media.DISPLAY_NAME+"=?";
        //判断结果
        String[] selectionArgs = new String[]{Environment.DIRECTORY_PICTURES+File.separator,"Test.jpg"};

        Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    null,
                    selection,
                    selectionArgs,
                    null,
                    null);
        if (cursor!=null&&cursor.moveToFirst()){
            //文件存在
            int id = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
            Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,id);
            int raw = getContentResolver().delete(uri,null,null);
            cursor.close();
            tvShow.setText("删除共享存储的文件成功");
            ivShow.setImageResource(0);
            return;
        }

        tvShow.setText("未找到共享存储的文件");
        ivShow.setImageResource(0);
    }catch (Exception e){
        tvShow.setText("删除共享存储的文件出错:"+e.getMessage());
    }
}
2.其他APP操作

这里展示读取、编辑和删除其他APP在外部存储空间和共享存储空间中写入的文件。根据Android10和Android11中的描述,内部存储空间是无法被其他应用查看和使用的,因此不进行读取操作。

2.1 外部存储空间

如果使用SAF框架无法找到外部存储空间的文件,请点击右上角,选择“显示内部存储设备”,选择Android/data/包名/file下查找文件

(1)写文件

//创建文件在存储中
//通过onActivityResult获取到写入的文件的uri,这里的文件是空的,需要将数据写入
private void writeImgToOtherExternal(){
    Intent intent = new Intent();
    intent.setAction(Intent.ACTION_CREATE_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("*/*");
    intent.putExtra(Intent.EXTRA_TITLE,"newTest.jpg");
    startActivityForResult(intent,2000);
}

(2)读文件

//读取文件从其他app的外部存储
//通过onActivityResult获取到文件的uri
private void readImgFromOtherExternal(){
    Intent intent = new Intent();
    intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("*/*");
    startActivityForResult(intent,2001);
}

(3)修改文件

这里的操作方式为:使用SAF框架获取uri之后,将修改之后的数据写入

注意:这里写入的是txt文件,如果需要追加数据而不是覆盖数据,则需要将数据读取后进行追加写入

//这里获取到uri之后,直接写入修改后的数据
Uri updateUri = data.getData();
try {
     OutputStream outputStream = getContentResolver().openOutputStream(updateUri);
     outputStream.write("测试外部存储,使用另一个app更新的".getBytes());
     outputStream.close();

     tvShow.setText("更新其他app的外部存储空间文件完成");
}catch (Exception e){
     tvShow.setText("更新其他app的外部存储的文本异常:"+e.getMessage());
}

(4)删除文件

//这里在获取到文件的uri之后,针对文件进行删除
Uri deleteUri = data.getData();
try {
     boolean delete = DocumentsContract.deleteDocument(getContentResolver(),deleteUri);
     if (delete){
           tvShow.setText("删除存储空间的文件成功");
     }else {
           tvShow.setText("删除存储空间的文件失败");
     }
}catch (Exception e){
    tvShow.setText("删除存储空间的文件失败");
}
2.2 共享存储空间

其实SAF框架也可以操作共享存储空间的文件,但是需要用户自行选择沐浴露和目标文件。这里采用Media api操作文件主要是为了减少用户操作,直接使用默认的逻辑处理文件。

(1)写文件

因为存储空间所有APP共用,当写入文件后,该APP自动获取此文件的读写权限,所以不存在写入其他APP的共享存储空间

(2)读文件

获取单个图片

读文件的操作和读取自身的共享存储空间的文件方式一致

使用Media api获取其他APP的共享存储空间的文件需要请求权限

这里使用的已知的图片名称和图片位置进行测试,如果不知道图片位置,可以使用SAF框架获取图片

查询所有的图片

//获取所有的图片文件
private void getAllImg(){
    StringBuilder builder = new StringBuilder();

    try {
        Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,null,null,null,null);
        if (cursor!=null){
            while (cursor.moveToNext()){
                String name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME));
                String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.RELATIVE_PATH));

                builder.append("名称:"+name+"---路径:"+path+"\n");
            }
            tvShow.setText(builder.toString());
        }else {
            tvShow.setText("没有图片文件");
        }
    }catch (Exception e){
        tvShow.setText("查询图片文件出错");
    }
}

(3)修改文件和删除文件

这里建议使用SAF框架读写其他app的共享存储空间的文件,原因如下:
在Android10及以上,使用Media api修改、删除其他APP的文件,需要根据异常信息进行处理,需要用户手动授权才能处理;Android9及以下,可以使用File api进行操作

使用SAF框架修改共享存储空间的文件的操作,与修改外部存储空间的操作一样,可参考上面的内容

使用Media api删除文件的操作,可作为参考:

与删除共享存储空间文件的操作基本一致,只是增加了对RecoverableSecurityException异常的处理,建议使用onActivityResult接收回调信息并执行之后的操作

//删除文件从其他app的共享存储
private void deleteImgFromOtherShare(){
    try {
        //判断条件
        String selection = MediaStore.Images.Media.RELATIVE_PATH+"=? and "+MediaStore.Images.Media.DISPLAY_NAME+"=?";
        //判断结果
        String[] selectionArgs = new String[]{Environment.DIRECTORY_PICTURES+File.separator,"test.jpg"};

        Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    null,
                    selection,
                    selectionArgs,
                    null,
                    null);
        if (cursor!=null&&cursor.moveToFirst()){
            //文件存在
            int id = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
            Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,id);
            int raw = getContentResolver().delete(uri,null,null);
            cursor.close();
            tvShow.setText("删除共享存储的文件成功");
            ivShow.setImageResource(0);
            return;
        }

        tvShow.setText("未找到共享存储的文件");
        ivShow.setImageResource(0);
    }catch (RecoverableSecurityException e){
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
                    startIntentSenderForResult(e.getUserAction().getActionIntent().getIntentSender(),3000,null,0,0,0);
            }
        }catch (IntentSender.SendIntentException e2){

        }
    }
}

在这里插入图片描述

知识点

1.SAF存储访问框架

SAF存储访问框架,即Storage Access Framework,为Android4.4引入的存储访问框架。

它主要通过文件管理器的形式,选择需要的文件,并将uri回调给用户。

具体用法如下:

Intent intent = new Intent();
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivity(intent,requestCode);

并使用onActivityResult接收回调数据即可。

2.共享存储空间对应表
文件类型Uri地址文件夹名称
图片
(image/*)
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
(content://media/external/images/media)
DIRECTORY_PICTURES
DIRECTORY_DCIM
视频
(video/*)
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
(content://media/external/video/media)
DIRECTORY_MOVIES
DIRECTORY_DCIM
音频
(audio/*)
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
(content://media/external/audio/media)
DIRECTORY_ALARMS
DIRECTORY_MUSIC
DIRECTORY_NOTIFICATIONS
DIRECTORY_PODCASTS
DIRECTORY_RINGTONES
下载
(*/*)
MediaStore.Downloads.EXTERNAL_CONTENT_URI
(content://media/external/downloads)
DIRECTORY_DOWNLOADS
其他
(*/*)
MediaStore.Files.getContentUri("external")
(content://media/external/file)
DIRECTORY_DOWNLOADS
DIRECTORY_DOCUMENTS

上表为文件类型的对应表,具体解释如下:

文件类型: 存放文件的种类,如:图片、视频、音频等,建议将对应的文件类型存放在相应的文件夹中。

uri地址: 插入数据库的uri地址,不同的文件类型使用的uri地址不一样,需要根据类型选择。

文件夹名称: 存放文件的文件夹名称,这里的文件夹位于手机存储中。

3.权限请求表

这里的Media api只测试了Android10以上的版本,Android10以下可以使用File api存储文件

操作框架
SAF框架Media apiFile api
自身内部存储读文件无法读取无法读取不需要权限
写文件不需要权限
修改文件不需要权限
删除文件不需要权限
外部存储读文件Android11无法读取
Android10可以读取,不需要权限
无法读取不需要权限
写文件不需要权限
修改文件不需要权限
删除文件不需要权限
共享存储读文件不需要权限不需要权限Android10无法读取
Android11需要存储权限
写文件不需要权限不需要权限
修改文件不需要权限不需要权限
删除文件不需要权限不需要权限
其他外部存储读文件Android11无法读取
Android10可以读取,不需要权限
无法读取未进行测试
写文件
修改文件
删除文件
共享存储读文件不需要权限需要权限未进行测试
写文件不需要权限无必要
修改文件不需要权限需要权限,需要单独处理异常信息
删除文件不需要权限需要权限,需要单独处理异常信息

问题点

1.File api受限:

在Android9及以下版本中,File api可以正常使用;Android10以上版本中,尽量避免使用File api,可以使用SAF框架和Media api操作文件

2.Media api效率:

在实际的测试中,当存在大量的文件时,使用Media api查询文件等操作会耗费大量的时间,可以使用SAF框进行文件的操作。但是使用SAF框架会对用户的使用不够友好,因此需要根据实际情况处理。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值