【Android】公共存储空间的数据写入&读取(相册读取所有图片)

发布于:2025-09-11 ⋅ 阅读:(28) ⋅ 点赞:(0)

在这里插入图片描述
三三要成为安卓糕手

一:外部存储的权限管理

  • 在此之前(Android 5.1 及以下),权限仅需在AndroidManifest.xml中静态声明,

  • API 23(Android 6.0)谷歌正式引入动态权限管理机制的版本

  • API 28(Android 8.0) 及以下:对存储权限要求宽松,应用可以自由访问外部存储中的文件。

  • API 29 (Android 10):引入了分区存储机制,应用只能访问自己的存储区域,或者使用 MediaStore API 来访问公共媒体文件。

  • API 30 (Android 11):进一步加强了对外部存储的控制,引入了 MANAGE_EXTERNAL_STORAGE,但建议开发者使用分区存储和 MediaStore。

  • API 33 (Android 13):权限管理更加精细化,引入了 READ_MEDIA_IMAGES、READ_MEDIA_VIDEO 和 READ_MEDIA_AUDIO 等特定权限。

二:MediaStore涉及权限声明

1:读写权限

译为:媒体库

之前的学习都是访问手机内部的存储空间,并不需要权限;在高版本安卓当中要使用MediaStore访问媒体文件,这就涉及一些权限的管理

Android33(13)以下的版本需要添加两个权限

	//读取外部存储权限
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
	//写入外部存储权限
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

如果是版本13以上,支持单独精细化的对媒体权限做申请

2:动态权限申请

        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE},
                100);

在清单Manifest中声明的权限是静态声明,主要告诉App需要哪些权限,但仅是 “告知”,并不能直接让 App 获得对应权限的使用能力;

在6.0后,对于“危险权限”,必须通过代码进行动态申请,也就是调用 ActivityCompat.requestPermissions() 这类方法,让用户在 App 运行时明确授予这些权限。

``

  • ActivityCompat.requestPermissions()

    • AndroidX中的方法,用于兼容较低版本的 Android 系统(如 Android 2.3 - Android 5.1 等),虽然这些设备原本是没有动态权限管理机制的,但是通过这个方法也能模拟实现动态权限申请的逻辑
  • Activity.requestPermissions()

    • 这是Activity类本身的方法,从 Android 6.0(API 23)开始引入,专门用于在运行时动态申请危险权限。
    • 如果应用只针对 Android 6.0 及以上的设备开发,那么可以直接使用这个方法。
    • 如果要兼容更低版本的系统,直接使用它会导致在低版本设备上出现方法找不到等错误 。

总结:前者兼容性更高,后者适合高版本

三:公共存储空间的数据写入

方法说明:使用OkHttp下载网络中的图片,保存在公共存储空间下;

代码中OkHttp部分不再进行说明

    private void downloadFile() {
        String imgPath = "http://titok.fzqq.fun/uploads/20240826/b4ee80207d67072b2227034c496f7fce.jpeg";

        OkHttpClient okHttpClient = new OkHttpClient();
        Request request = new Request.Builder()
                .url(imgPath)
                .build();

        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(@NonNull Call call, @NonNull IOException e) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(PublicFileActivity.this,"下载失败",Toast.LENGTH_SHORT).show();
                    }
                });
            }

            @Override
            public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {


                ContentValues values = new ContentValues();
                //名字
                values.put(MediaStore.Images.Media.DISPLAY_NAME,"my_picture.jpeg");
                //类型
                values.put(MediaStore.Images.Media.MIME_TYPE,"image/jpeg");
                //路径
                values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);

                Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

                if(uri != null){
                    try(OutputStream outputStream = getContentResolver().openOutputStream(uri);
                        InputStream inputStream = response.body().byteStream()){

                        byte[] bytes = new byte[1024];
                        int readByte;
                        //响应中读到程序里是用字节读,在写到内存中也是字节
                        while((readByte = inputStream.read(bytes)) != -1){
                            outputStream.write(bytes,0,bytes.length);
                        }
                        outputStream.flush();
                    }
                }

                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(PublicFileActivity.this, "保存成功", Toast.LENGTH_SHORT).show();
                    }
                });

            }
        });
    }

1:公共存储空间

“emulated” 最常见的含义是 “模拟的”

使用模拟机,相机拍下来的图片也是放到了Pictures文件夹下

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2:getContentResolver().insert()

插入应用共享的数据(如媒体库、联系人、短信等)数据,插入位置主要是公共存储区域

参数一: Uri url

  • 指向要插入数据的目标地址(内容 URI),明确要操作的数据集合

  • MediaStore 用于访问媒体数据(图片、音频、视频等)的类,Images.Media.EXTERNAL_CONTENT_URI 明确了查询的是外部存储中的图片集合。

  • 示例:

    • 向系统图片库插入图片:MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    • 向联系人添加记录:ContactsContract.Contacts.CONTENT_URI

参数二: ContentValues values

  • 存储要插入的字段和对应值的键值对集合(类似数据库的 Map)。
  • 需根据目标 ContentProvider 支持的字段设置,例如插入图片时需指定文件名、MIME 类型、路径等。
ContentValues values = new ContentValues();
                //名字
                values.put(MediaStore.Images.Media.DISPLAY_NAME,"my_picture.jpeg");
                //类型
                values.put(MediaStore.Images.Media.MIME_TYPE,"image/jpeg");
                //路径
                values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);

3:getContentResolver().openOutputStream(uri)

通过内容解析器(ContentResolver)打开一个指向目标 URI 的输出流,主要用于向指定 URI 对应的资源写入数据

四:公共存储空间的数据读取

使用MediaStore读取指定的图片,就读取咱们刚刚保存保存在公共存储区域的图片;

这段代码虽然短,但是非常儿豁,((20250830170544-0amnn9o “后面对Cursor的处理之前用过一次”)),不必多说

    private void showPicture() {
        /**
         * 简记五个参数:
         * 查询内容uri,外部存储中图片集合
         * 查询图片哪些信息,id,name
         * 筛选条件
         * 排序方式
         */
        Cursor query = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME},
                MediaStore.Images.Media.DISPLAY_NAME + "= ?", new String[]{"my_picture.jpeg"},
                MediaStore.Images.Media.DATE_TAKEN + " ASC");
        if (query != null && query.moveToFirst()){
            int idIndex = query.getColumnIndexOrThrow(MediaStore.Images.Media._ID);
            long id = query.getLong(idIndex);
            Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
            imageView.setImageURI(uri);
        }
    }

1:getContentResolver().query()

通过内容解析器(ContentResolver)向指定的内容 URI 发送查询请求,获取符合条件的数据集合

参数二:projection

new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME}

  • 作用:指定要从查询结果中返回的列(字段)。

  • 解释:数组中的每个字符串对应媒体数据库中的一个列名。

    • MediaStore.Images.Media._ID 是图片在媒体库中的唯一标识 ID;
    • MediaStore.Images.Media.DISPLAY_NAME 是图片的显示名称(文件名)。通过指定这些列,查询结果只会包含这些列的数据,避免获取不必要的信息,提高效率。

获取某一个列名所在的索引,之前学习数据库的时候有学习过类似的东西,脑子里要有那张移动数据库的图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

参数三:selection

MediaStore.Images.Media.DISPLAY_NAME + " = ?"

  • 作用:指定查询的筛选条件(类似 SQL 中的 WHERE 子句)
  • 解释:? 是占位符,防止 SQL 注入等安全问题

参数四:selectionArgs

new String[]{"my_picture.jpeg"}

  • 作用:为 selection 中的占位符 ? 提供具体的值。
  • 解释:数组中的值会按顺序替换 selection 中的 ?。这里表示要查询显示名称为 my_picture.jpeg 的图片。

参数五:sortOrder

null

  • 作用:指定查询结果的排序方式(类似 SQL 中的 ORDER BY 子句)。
  • 解释:如果为 null,表示默认的排序方式,按照数据的添加的时间顺序来排列,新的在前旧的在后(对应DESC降序),升序为ASC。

如果要指定根据某一项数据进行排序,比如按名称

  • 按图片显示名称升序排列:"MediaStore.Images.Media.DISPLAY_NAME ASC"
  • 按图片显示名称降序排列:"MediaStore.Images.Media.DISPLAY_NAME DESC"

2:query.getLong()

getLong(int columnIndex)Cursor 的方法,根据 “列索引” 从当前行中获取对应列的 Long 类型值(因为 _ID 在媒体库中是长整型,避免 ID 值过大导致溢出)。

  • idIndex:第一行获取到的 “图片 ID 列” 的索引。
  • id当前图片在媒体库中的唯一 ID(如 12345),这个 ID 是系统分配的,不会重复,是定位单张图片的核心标识。

3:ContentUris.withAppendedId()

ContentUris 类:Android 提供的工具类,用于处理 Content URI(内容 URI),提供了一些静态方法来操作 URI。

withAppendedId () 方法:这是 ContentUris 类的静态方法,给基础 URI 追加一个 ID,生成一个指向具体资源的 URI

  • 第一个参数是基础 URI(这里是MediaStore.Images.Media.EXTERNAL_CONTENT_URI
  • 第二个参数是资源的 ID(这里是id变量)

3:效果

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一:需求即相关代码

打开相册显示所有的照片;照片 = 相册app本身带有的图片 + 其它app(相机)带有的图片;这里就涉及到需要获取相机的READ_MEDIA_IMAGES权限

public class AlbumActivity extends AppCompatActivity {


    private RecyclerView recycleView;

    ArrayList<Uri> pictures = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_album);
        
        //如果安卓版本在11以上,对权限的申请更为细分
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.READ_MEDIA_IMAGES},100);
        }else{
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
                            Manifest.permission.WRITE_EXTERNAL_STORAGE},100);
        }

        
        recycleView = findViewById(R.id.recycler_view);
        recycleView.setLayoutManager(new GridLayoutManager(this, 3));

        Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME},
                null, null, null);

        if (cursor != null && cursor.moveToFirst()) {
            do {
                int idIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID);
                long id = cursor.getLong(idIndex);
                //通过图像的id获取图像的uri
                Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
                //添加到列表
                pictures.add(uri);
            } while (cursor.moveToNext());

            //把获取到的数据显示到RecyclerView
            ImageAdapter adapter = new ImageAdapter(pictures);
            recycleView.setAdapter(adapter);
        }
    }
}

1:细分权限声明

当前使用的模拟机版本是33,在安卓11也就是api30之上,对权限做了更加细分操作,就需要单独对图像的权限进行申请

左图没有权限申请的弹窗直接进入相册界面;右图代码中进行了api的判定并进行了相应权限的声明,获取到了权限,就可以查看相册中所有的照片了

当然,现在的大部分出厂手机,最低的安卓版本都是12了,车载和pos机,可能还是用的8.0,10.0

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2:RecyclerView布局

<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView 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:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".file.AlbumActivity">

</androidx.recyclerview.widget.RecyclerView>

3:do while循环读取照片的弊端

这是没有进行分段的处理,容易出现一些问题比如卡顿,这里涉及的数据量不大,所以不做过多的展开

4:固定用法

Build.VERSION.SDK.INT 指的是当前安卓api版本

Build.VERSION.CODES.R指的是api30这个版本

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

二:适配器

public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {

    private final ArrayList<Uri> mPictures;

    public ImageAdapter(ArrayList<Uri> mPictures){
        this.mPictures = mPictures;
    }

    //item加载到parent的视图中,创建这样类型的ViewHolder
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_image, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        //显示图片
        Uri uri = mPictures.get(position);
        holder.imageView.setImageURI(uri);
    }

    @Override
    public int getItemCount() {
        return mPictures == null ? 0 : mPictures.size();
    }

    class ViewHolder extends RecyclerView.ViewHolder{

        private final ImageView imageView;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            imageView = itemView.findViewById(R.id.image_view);
        }
    }
}

1: mPictures 的命名逻辑

变量命名前缀 m 是一种广泛使用的编码规范(源于早期 Android 源码风格),其中 m 代表 “member”(成员变量),用于区分成员变量和局部变量。

  • m:表示这是一个类的成员变量
  • Pictures:描述变量的含义,这里表示它是一个存储图片 Uri 的集合

2:适配器关联的布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/image_view"
        android:layout_width="110dp"
        android:layout_height="110dp"
        android:scaleType="centerCrop"/>

</LinearLayout>

三:页面跳转中this的使用范围

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这段代码中,this 处于 View.OnClickListener 的匿名内部类里面。此时,this 指代的是 View.OnClickListener 这个匿名内部类的实例,而不是外部的 Activity 实例。


网站公告

今日签到

点亮在社区的每一天
去签到