You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

已编写权限代码但Android应用不请求存储运行时权限问题

解决Android存储权限请求不弹出及图片保存失败问题

听起来你碰到了个挺头疼的偶发权限问题——卸载重装应用后,有时候存储权限请求弹不出来,直接导致保存图片时抛出java.io.FileNotFoundException: /storage/emulated/0/Abc/Abc 1601370822977.png (Permission denied)错误。我来帮你拆解下核心原因,再给出针对性的修复方案:

一、问题根源分析

你的基础权限逻辑框架没问题,但漏掉了两个关键场景:

  1. 用户勾选「不再询问」后拒绝权限:这种情况下requestPermissions不会弹出系统权限对话框,直接触发拒绝回调,而你的代码没做任何处理,导致后续保存操作直接报错。
  2. Android 10+(API 29)的Scoped Storage变更:从Android 10开始,系统限制了直接访问外部存储根目录的行为,即便你申请了WRITE_EXTERNAL_STORAGE权限,也可能无法在/storage/emulated/0/Abc这类目录下创建文件。

二、具体修复方案

1. 处理「不再询问」的权限永久拒绝场景

修改saveImage方法,在请求权限前判断是否需要向用户解释权限用途,以及是否被永久拒绝:

private void saveImage(){
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
        if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED){
            finallySave();
        } else {
            // 检查是否需要向用户解释权限必要性
            if(shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)){
                new AlertDialog.Builder(this)
                        .setTitle("需要存储权限")
                        .setMessage("保存图片需要访问您的存储,请授予权限")
                        .setPositiveButton("确定", (dialog, which) -> {
                            requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},STORAGE_WRITE_REQUEST_CODE);
                        })
                        .setNegativeButton("取消", null)
                        .show();
            } else {
                // 要么是首次请求,要么是用户勾选了「不再询问」
                if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED){
                    // 永久拒绝,引导用户去设置页开启权限
                    new AlertDialog.Builder(this)
                            .setTitle("权限被拒绝")
                            .setMessage("存储权限已被永久拒绝,请前往设置开启")
                            .setPositiveButton("去设置", (dialog, which) -> {
                                Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                                Uri uri = Uri.fromParts("package", getPackageName(), null);
                                intent.setData(uri);
                                startActivity(intent);
                            })
                            .setNegativeButton("取消", null)
                            .show();
                } else {
                    requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},STORAGE_WRITE_REQUEST_CODE);
                }
            }
        }
    } else {
        finallySave();
    }
}

2. 适配Android 10+的存储机制

方案A:改用应用专属外部存储目录(推荐,无需权限)

从Android 4.4开始,应用可以访问外部存储的专属目录(/storage/emulated/0/Android/data/[你的包名]/files/),这个目录不需要申请存储权限,且卸载应用时会自动删除:

private File getImageDir() {
    // 获取应用专属外部存储的图片目录
    File directory = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "Abc");
    if (!directory.exists()) {
        if (!directory.mkdirs()) {
            Toast.makeText(this, "Could not create directory", Toast.LENGTH_SHORT).show();
            return null;
        }
    }
    return directory;
}

方案B:使用MediaStore保存图片(适用于需要其他应用访问的图片)

如果你的图片需要被相册或其他应用识别,可通过MediaStore插入图片,Android 10+无需申请WRITE_EXTERNAL_STORAGE:

private void finallySave() {
    try {
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.DISPLAY_NAME, "Abc " + System.currentTimeMillis() + ".png");
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/Abc");
        } else {
            values.put(MediaStore.Images.Media.DATA, new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Abc").getAbsolutePath());
        }

        ContentResolver resolver = getContentResolver();
        Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
        if (uri != null) {
            OutputStream outputStream = resolver.openOutputStream(uri);
            bitmaps[0].compress(Bitmap.CompressFormat.PNG, 100, outputStream);
            outputStream.flush();
            outputStream.close();
            Toast.makeText(this, "图片保存成功", Toast.LENGTH_SHORT).show();
        }
    } catch (Exception e) {
        e.printStackTrace();
        Toast.makeText(this, "图片保存失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
    }
}

3. 完善错误处理逻辑

你的finallySave方法把异常吞掉了,这会导致无法定位具体问题,建议添加日志和用户提示:

private void finallySave(){
    try {
        if(imagesDir == null){
            Toast.makeText(this, "存储目录初始化失败", Toast.LENGTH_SHORT).show();
            return;
        }
        File outFile = new File(imagesDir, "Abc " + System.currentTimeMillis() + ".png");
        FileOutputStream outputStream = new FileOutputStream(outFile);
        bitmaps[0].compress(Bitmap.CompressFormat.PNG, 100, outputStream);
        outputStream.flush();
        outputStream.close();
        scanFile(outFile, "image/png");
        Toast.makeText(this, "图片保存成功", Toast.LENGTH_SHORT).show();
    } catch (Exception e) {
        e.printStackTrace();
        Toast.makeText(this, "图片保存失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
    }
}

三、额外优化建议

  • 卸载重装后首次启动时,尽量在进入主页面之前就请求存储权限,避免用户触发保存操作时才弹出,提升体验。
  • 针对Android 13+,存储权限拆分为READ_MEDIA_IMAGES和WRITE_EXTERNAL_STORAGE,若仅需保存图片,申请WRITE_EXTERNAL_STORAGE即可,但需注意Scoped Storage的限制。

内容的提问来源于stack exchange,提问作者Shreemaan Abhishek

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 10:27:49