Android Studio 学习记录-存储卡

目录 

私有存储空间与公共存储空间

在存储卡上读写文件

运行时动态申请权限


        本文介绍Android的文件存储方式-在存储卡上读写文件,包括:公有存储空间与私有存储空间有什么区别、如何利用存储卡读写文本文件、如何利用存储卡读写图片文件、如何在App运行的时候动态申请权限等。

私有存储空间与公共存储空间

        为了更规范地管理手机存储空间,Android从7. 0开始将存储卡划分为私有存储和公共存储两大部分,也就是分区存储方式,系统给每个App都分配了默认的私有存储空间。App在私有空间上读写文件无须任何授权,但是若想在公共空间读写文件,则要在AndroidManifest.xml里面添加下述的权限配置。

<--存储卡读写-->
<uses-permission android:name="android.permission, WRITE_EXTERNAL STORAGE" />
<uses-permission android:name="android.permission, READ_EXTERNAL_STORAGE" />

          但是即使App声明了完整的存储卡操作权限,系统仍然默认禁止该App访问公共空间。打开手机的系统设置界面,进入到具体应用的管理页面,会发现该应用的存储访问权限被禁止了。

        当然禁止访问只是不让访问存储卡的公共空间,App自身的私有空间依旧可以正常读写。这缘于Android把存储卡分成了两块区域,一块是所有应用均可访问的公共空间,另一块是只有应用自己才可访问的专享空间。虽然Android给每个应用都分配了单独的安装目录,但是安装目录的空间很紧张,所以Android在存储卡的“AIndroid/data”目录下给每个应用又单独建了一个文件目录,用来保存应用自己需要处理的临时文件。这个目录只有当前应用才能够读写文件,其他应用是不允许读写的。由于私有空间本身已经加了访问权限控制,因此它不受系统禁止访问的影响,应用操作自己的文件目录自然不成问题。因为私有的文件目录只有属主应用才能访问,所以一旦属主应用被卸载,那么对应的目录也会被删掉。

        既然存储卡分为公共空间和私有空间两部分,它们的空间路径获取方法自然也就有所不同。若想获取公共空间的存储路径,调用的是Environment.getExternalStoragePublicDirectory方法;若想获取应用私有空间的存储路径,调用的是getExternalFilesDir方法。下面是分别获取两个空间路径的代码例子:      

        // Android7.0之后默认禁止访问公共存储目录
        // 获取系统的公共存储路径
        String publicPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString();
        // 获取当前App的私有存储路径
        String privatePath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString();
        boolean isLegacy = true;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            // Android10的存储空间默认采取分区方式,此处判断是传统方式还是分区方式
            isLegacy = Environment.isExternalStorageLegacy();
        }
        String desc = "系统的公共存储路径位于" + publicPath +
                "\n\n当前App的私有存储路径位于" + privatePath +
                "\n\nAndroid7.0之后默认禁止访问公共存储目录" +
                "\n\n当前App的存储空间采取" + (isLegacy?"传统方式":"分区方式");
        tv_path.setText(desc);

        该例子运行之后获得的路径信息如图所示,可见应用的私有空间路径位于“存储卡根目录/Android/data/应用包名/files/Download”这个目录中。

 在存储卡上读写文件

        文本文件的读写借助于文件IO流FileOutputStream和FileInputStream.其中,FileOutputStream用于写文件,FileInputStream用于读文件,它们读写文件的代码例子如下:

 // 把字符串保存到指定路径的文本文件
    public static void saveText(String path, String txt) {
        // 根据指定的文件路径构建文件输出流对象
        try (FileOutputStream fos = new FileOutputStream(path)) {
            fos.write(txt.getBytes()); // 把字符串写入文件输出流
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 从指定路径的文本文件中读取内容字符串
    public static String openText(String path) {
        String readStr = "";
        // 根据指定的文件路径构建文件输入流对象
        try (FileInputStream fis = new FileInputStream(path)) {
            byte[] b = new byte[fis.available()];
            fis.read(b); // 从文件输入流读取字节数组
            readStr = new String(b); // 把字节数组转换为字符串
        } catch (Exception e) {
            e.printStackTrace();
        }
        return readStr; // 返回文本文件中的文本字符串
    }

        接着分别创建写文件页面和读文件页面,其中写文件页面调用saveText方法保存文本,;而读文件页面调用readText方法从指定路径的文件中读取文本内容。

        然后运行测试App,先打开文本写入页面,录入注册信息后保存为私有目录里的文本文件。再打开文本读取页面,App自动在私有目录下找到文本文件列表,并展示其中一个文件的文本内容。

         文本文件读写可以转换为对字符串的读写,而图片文件保存的是图像数据,需要专门的位图工具Bitmap处理。位图对象依据来源不同又分成3种获取方式,分别对应位图工厂BitmapFactory的下列3个方法:

  • decodeResource:从指定的资源文件中获取位图数据。例如下面代码表示从资源文件
    huawei.png获取位图对象:
    Bitmap bitmap = BitmapFactory.decodeResource (getResources(),R.drawable.huawei)
  • decodeFile:从指定路径的文件中获取位图数据。注意从Android 10开始,该方法只适用于
    私有目录下的图片,不适用公共空间下的图片。
  • decodeStream:从指定的输入流中获取位图数据。比如使用IO流打开图片文件,此时文件
    输入流对象即可作为decodeStream方法的入参,相应的图片读取代码如下:
        // 从指定路径的图片文件中读取位图数据
        public static Bitmap openImage(String path) {
            Bitmap bitmap = null; // 声明一个位图对象
            // 根据指定的文件路径构建文件输入流对象
            try (FileInputStream fis = new FileInputStream(path)) {
                bitmap = BitmapFactory.decodeStream(fis); // 从文件输入流中解码位图数据
            } catch (Exception e) {
                e.printStackTrace();
            }
            return bitmap; // 返回图片文件中的位图数据
        }

        得到位图对象之后,就能在图像视图上显示位图。图像视图ImageView提供了下列方法显示各种来源的图片:

  • setImageResource:设置图像视图的图片资源,该方法的入参为资源图片的编号,形如“R.drawable. 去掉扩展名的图片名称”。
  • setImageBitmap:设置图像视图的位图对象,该方法的入参为Bitmap类型。
  • setImageURI:设置图像视图的路径对象,该方法的入参为Uri类型。字符串格式的文件路径可通过代码“Uri.parse(file_path)”转换成路径对象。

        读取图片文件的方法很多,把位图数据写入图片文件却只有一个,即通过位图对象的compress方法将位图数据压缩到文件输出流。具体的图片写入代码如下:

//把位图数据保存到指定途径的图片文件
public static void saveImage(String path, Bitmap bitmap)    {
    //根据指定的文件路径构建文件输出流对象
    try (FileOutputStream fos = new FileOutputStream(path))    {
        //把位图数据压缩到文件输出流中
        bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
    } catch (Exception e)    {
        e.printStackTrace();
    }
}

        接下来完整演示一遍图片文件的读写操作。首先创建图片写入页面,从某个资源图片读取位图数据,再把位图数据保存为私有目录的图片文件,相关代码示例如下:

// 获取当前App的私有下载目录
String path = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + "/";
// 从指定的资源文件中获取位图对象
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.huawei);
String file_path = path + DateUtil.getNowDateTime("") + ".jpeg";
FileUtil.saveImage(file_path, bitmap); // 把位图对象保存为图片文件
tv_path.setText("图片文件的保存路径为:\n" + file_path);

        然后创建图片读取页面,从私有目录找到图片文件,并挑出一张在图像视图上显示,相关代码示例如下:

        // 获得指定目录下面的所有图片文件
        mFilelist = FileUtil.getFileList(mPath, new String[]{".jpeg"});
        if (mFilelist.size() > 0) {
            // 打开并显示选中的图片文件内容
            String file_path = mFilelist.get(0).getAbsolutePath();
            tv_content.setText("找到最新的图片文件,路径为"+file_path);
            // 显示存储卡图片文件的第一种方式:直接调用setImageURI方法
            //iv_content.setImageURI(Uri.parse(file_path)); // 设置图像视图的路径对象
            // 第二种方式:先调用decodeFile方法获得位图,再调用setImageBitmap方法
            //Bitmap bitmap = BitmapFactory.decodeFile(file_path);
            //iv_content.setImageBitmap(bitmap); // 设置图像视图的位图对象
            // 第三种方式:先调用FileUtil.openImage获得位图,再调用setImageBitmap方法
            Bitmap bitmap = FileUtil.openImage(file_path);
            iv_content.setImageBitmap(bitmap); // 设置图像视图的位图对象

        运行测试App,先打开图片写入页面,点击“把资源图片保存到存储卡”按钮,此时写入界面如图所示。

        再打开图片读取页面,App自动在私有目录下找到图片文件列表,并展示其中一张图片,此时读取界面如图所示。 

 

运行时动态申请权限 

        App若想访问存储卡的公共空间,就要在AndroidManifest.xml里面添加下述的权限配置。

<!--存储卡读写-->
<uses-permission android:name="android.permission. WRITE EXTERNAL STORAGE" />
<uses-permission android:name="android.permission. READ_EXTERNAL_STORAGE"/>

        然而即使App声明了完整的存储卡操作权限,从Android 7. 0开始,系统仍然默认禁止该App访问公共空间,必须到设置界面手动开启应用的存储卡权限才行。尽管此举是为用户隐私着想,可是用户怎么知道要手工开权限呢?就算用户知道,去设置界面找到权限开关也颇费周折。为此Android支持在Java代码中处理权限,处理过程分为3个步骤,详述如下:

        1.检查App是否开启了指定权限

        权限检查需要调用ContextCompat的checkSelfPermission方法,该方法的第一个参数为活动实例,第二个参数为待检查的权限名称,例如存储卡的写权限名为Manifest.permission1.WRITE_EXTERNALSTORAGE.注意checkSelfPermission方法的返回值,当它为PackageManager. PERMISSION_GRANTED时表示已经授权,否则就是未获授权。

        2.请求系统弹窗,以便用户选择是否开启权限

        一旦发现某个权限尚未开启,就得弹窗提示用户手工开启,这个弹窗不是开发者自己写的提
醒对话框,而是系统专门用于权限申请的对话框。调用ActivityCompat的requestPermissions方法,即可命令系统自动弹出权限申请窗口,该方法的第一个参数为活动实例,第二个参数为待申请的权限名称数组,第三个参数为本次操作的请求代码。

        3.判断用户的权限选择结果

        然而上面第二步的requestPermissions方法没有返回值,那怎么判断用户到底选了开启权限还是拒绝权限呢?其实活动页面提供了权限选择的回调方法onRequestPermissionsResult,如果当前页面请求弹出权限申请窗口,那么该页面的Java代码必须重写onRequestPermissionsResult方法,并在该方法内部处理用户的权限选择结果。具体到编码实现上,前两步的权限校验和请求弹窗可以合并到一块,先调用checkSelfPermission方法检查某个权限是否已经开启,如果没有开启再调用requestPermissions方法请求系统弹窗。合并之后的检查方法代码示例如下,此处代码支持一次检查一个权限,也支持一次检查多个权限:

    // 检查多个权限。返回true表示已完全启用权限,返回false表示未完全启用权限
    public static boolean checkPermission(Activity act, String[] permissions, int requestCode) {
        boolean result = true;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            int check = PackageManager.PERMISSION_GRANTED;
            // 通过权限数组检查是否都开启了这些权限
            for (String permission : permissions) {
                check = ContextCompat.checkSelfPermission(act, permission);
                if (check != PackageManager.PERMISSION_GRANTED) {
                    break; // 有个权限没有开启,就跳出循环
                }
            }
            if (check != PackageManager.PERMISSION_GRANTED) {
                // 未开启该权限,则请求系统弹窗,好让用户选择是否立即开启权限
                ActivityCompat.requestPermissions(act, permissions, requestCode);
                result = false;
            }
        }
        return result;
    }

        注意到上面代码有判断安卓版本号,只有系统版本大于Android 6. 0(版本代号为M),才执行后续的权限校验操作。这是因为从Android 6. 0开始引入了运行时权限机制,在Android 6. 0之前,只要App在AndroidManifest.xml中添加了权限配置,则系统会自动给App开启相关权限;但在Android6. 0之后,即便事先添加了权限配置,系统也不会自动开启权限,而要开发者在App运行时判断权限的开关情况,再据此动态申请未获授权的权限。

        回到活动页面代码,一方面增加权限校验入口,比如点击某个按钮后触发权限检查操作,其中Manifest.permission. WRITE_EXTERNAL_STORAGE表示存储卡权限,入口代码如下:

if (v.getId() == R.id.btn_Permission) {
            if (PermissionUtil.checkPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, R.id.btn_Permission % 65536)) {
                //已获授权,则直接跳转到下个页面
                startActivity(new Intent(this, Permission.class));
            }
        }

        另一方面还要重写活动的onRequestPermissionsResult方法,在方法内部校验用户的选择结果,若用户同意授权,就执行后续业务;若用户拒绝授权,只能提示用户无法开展后续业务了。重写后的方法代码如下:

@Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        // requestCode不能为负数,也不能大于2的16次方即65536
        if (requestCode == R.id.btn_Permission % 65536) {
            if (PermissionUtil.checkGrant(grantResults)) { // 用户选择了同意授权
                Intent intent = new Intent(this, Permission.class);
                intent.putExtra("is_external", true);
                startActivity(intent);
            } else {
                //ToastUtil.show(this, "需要允许存储卡权限才能写入公共空间噢");
                Toast.makeText(this, "需要允许存储卡权限才能写入公共空间噢", Toast.LENGTH_SHORT).show();
            }
        }
    }

        以上代码为了简化逻辑,将结果校验操作封装为PermissionUtil的checkGrant方法,该方法遍历授权结果数组,依次检查每个权限是否都得到授权了。详细的方法代码如下:

    // 检查权限结果数组,返回true表示都已经获得授权。返回false表示至少有一个未获得授权
    public static boolean checkGrant(int[] grantResults) {
        boolean result = true;
        if (grantResults != null) {
            for (int grant : grantResults) { // 遍历权限结果数组中的每条选择结果
                if (grant != PackageManager.PERMISSION_GRANTED) { // 未获得授权
                    result = false;
                }
            }
        } else {
            result = false;
        }
        return result;
    }

        代码都改好后,运行测试App,由于一开始App默认未开启存储卡权限,因此点击按钮btn_file_write触发了权限校验操作,弹出如图所示的存储卡权限申请窗口。

        点击弹窗上的“始终允许”按钮,表示同意赋予存储卡读写权限,然后系统自动给App开启了存储卡权限,并执行后续处理逻辑,也就是跳转到了FileWriteActivity页面,在该页面即可访问公共空间的文件了。但在Android 10系统中,即使授权通过,App仍然无法访问公共空间,这是因为Android 10默认开启沙箱模式,不允许直接使用公共空间的文件路径,此时要修改AndroidManifest.xml,给application节点添加如下的requestLegacyExternalStorage属性:

android:requestLegacyExternalStorage="true"

        从Android 11开始,为了让应用在升级时也能正常访问公共空间,还得修改AndroidManifest.xml给application节点添加如下的preserveLegacyExternalStorage属性,表示暂时关闭沙箱模式:

android:preserveLegacyExternalStorage="true"

        除了存储卡的读写权限,还有部分权限也要求在运行时动态申请,这些权限名称的取值说明见表。

权限名称的取值说明
代码中的权限名称权限说明
Manifest.permission. READ_EXTERNAL_STORAGE读存储卡                                        
Manifest.permission. WRITE_EXTERNAL_STORAGE写存储卡
Manifest.permission. READ_CONTACTS读联系人
Manifest.permission. WRITE_CONTACTS写联系人
Manifest.permission. SEND_SMS发送短信
Manifest.permission. RECEIVE_SMS接收短信
Manifest.permission. READ_SMS读短信
Manifest.permission. READ_CALL_LOG读通话记录
Manifest.permission. WRITE_CALL LOG写通话记录
Manifest.permission. CAMERA相机
Manifest.permission. RECORD_AUDIO录音
Manifest.permission. ACCESS_FINE_LOCATION精确定位

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值