Android-存储基础

前言

存储适配系列文章:

Android-存储基础
Android-10、11-存储完全适配(上)
Android-10、11-存储完全适配(下)
Android-FileProvider-轻松掌握

在持久化数据的时候,一般都是选择存入到文件里,本篇将着重分析Android 存储相关的知识,也是为Android 10.0 11存储适配打基础。
通过本篇文章,你将了解到:

1、存储划分
2、内部存储
3、外部存储
4、易混淆点说明

1、存储划分

Android 4.4 之前

在Android 4.4 之前,由于硬件发展受限,手机自身的存储空间有限,需要通过外置SD卡来扩展存储空间。


image.png

如上图,手机自身的存储空间,称之为机身存储,在Android 4.4 之前作为内部存储使用。当然内部存储空间一般是不够用的,所以需要通过插入外置SD卡来扩充存储空间,这当做外部存储。

Android 4.4之后

在Android 4.4 之后(含),手机机身存储扩大了:


image.png

如上图,机身存储划分为两部分:

1、内部存储
2、外部存储

当然,依然可以插入SD卡来扩充存储空间,这部分的存储空间称为扩展的外部存储空间。只是现在机身存储都比较大,很少插入SD卡了。
接下来将以Android 4.4 之后的存储划分来分析具体的存储方案。

2、内部存储

存放位置

回想一下平时使用的持久化方案:

1、SharedPreferences---->适用于存储小文件
2、数据库---->存储结构比较复杂的大文件

以上这些文件都是默认放在内部存储里。
"/" 表示根目录,内部存储里给每个应用按照其包名各自划分了目录,假设App的包名为:com.fish.myapplication
那么该文件在内部存储里的目录为:
/data/user/0/com.fish.myapplication/

第一个"/"表示根目录,其后每个"/"表示目录分割符。
"0" 表示是第一个用户,后续添加了多用户则生成相应的用户目录:


image.png

如上图,新增了两个用户,生成的目录分别是:"11"、"12"。目前来说,很少开启多用户的。
一般来说,adb shell里是没有权限查看/data目录的。若要查看内部存储,通常是通过Android Studio侧边栏Device File Explorer选择对应的目标设备查看。


image.png

同样的,如果包名为:com.fish.myapplication,则对应的内部存储目录为:
/data/data/com.fish.myapplication/

/data/user/0/com.fish.myapplication/ 会将值转换到/data/data/com.fish.myapplication/ 路径下。
每个App的内部存储空间仅允许自己访问(除非有更高的权限,如root),程序卸载后,该目录也会被删除。

存储内容

除了SharedPreferences、数据库文件,内部存储还存放了哪些文件呢?
为方便起见,只查看/data/data/目录下的。


image.png

刚开始有只有两个空目录。
当进行写入SharedPreferences,创建数据库、写入文件等操作后新增了几个目录:


image.png

大致介绍一下以上目录作用:

1、cache-->存放缓存文件
2、code_cache-->存放运行时代码优化等产生的缓存
3、databases-->存放数据库文件
4、files-->存放一般文件
5、shared_prefs-->存放SharedPreferences 文件
6、lib-->存放App依赖的so库 是软链接,指向/data/app/ 某个子目录下

访问方式

既然知道了各类文件存储的目录,那么如何读写这些文件呢?
我们知道在Java 的世界里,操作文件有两种方式:

字符流和字节流

以字节流为为例,一个简单的读取写入文件Demo:

    //写入文件
    private void writeFile(String filePath) {
        if (TextUtils.isEmpty(filePath))
            return;

        try {
            File file = new File(filePath);
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream);
            String writeContent = "hello world\n";
            bos.write(writeContent.getBytes());
            bos.flush();
            bos.close();

        } catch (Exception e) {

        }
    }

    //从文件读取
    private void readFile(String filePath) {
        if (TextUtils.isEmpty(filePath))
            return;

        try {
            File file = new File(filePath);
            FileInputStream fileInputStream = new FileInputStream(file);
            BufferedInputStream bis = new BufferedInputStream(fileInputStream);
            byte[] readContent = new byte[1024];
            int readLen = 0;
            while (readLen != -1) {
                readLen = bis.read(readContent, 0, readContent.length);
                if (readLen > 0) {
                    String content = new String(readContent);
                    Log.d("test", "read content:" + content.substring(0, readLen));
                }
            }
            fileInputStream.close();
        } catch (Exception e) {

        }
    }

可以看出,通过FileInputStream/FileOutputStream构造函数传入File对象即可实现文件读写,而File对象的构造依赖于文件的存放路径,因此重点在于如何获取文件的路径。
分别说明各个目录下文件的读写:
1、读写files目录下文件

#Context.java
public abstract File getFilesDir();

使用方式:

    private String getFilePath(Context context) {
        //获取files根目录
        File fileDir = context.getFilesDir();
        //获取文件
        File myFile = new File(fileDir, "myFile");
        return myFile.getAbsolutePath();
    }

context.getFilesDir()的结果是返回files目录:

/data/user/0/com.fish.myapplication/files/

拿到对应文件的File对象后,构造相应的输入输出流即可实现对该文件的读写。可以看出,过程虽然简单但是有点枯燥,因此Google将这些步骤封装好了,直接返回对应文件的FileOutputStream/FileInputStream:

#Context.java
    public abstract FileInputStream openFileInput(String name)
        throws FileNotFoundException;

    public abstract FileOutputStream openFileOutput(String name, @FileMode int mode)
        throws FileNotFoundException;

其中name 表示文件名,mode表示访问权限。

2、读写cache目录下文件
与读取files目录相似:

#Context.java
public abstract File getCacheDir();

context.getCacheDir()的结果是返回cache目录:

/data/user/0/com.fish.myapplication/cache/

3、读写shared_prefs目录下文件
SharedPreferences 提供了简易的快速持久化数据的方案。

    private void testSP(String fileName, String key, String value) {
        if (TextUtils.isEmpty(fileName) || TextUtils.isEmpty(key) || TextUtils.isEmpty(value))
            return;

        //构造SP文件
        SharedPreferences sp = getSharedPreferences(fileName, MODE_PRIVATE);

        //写入SP
        sp.edit().putString(key, value).commit();

        //读取SP
        String myValue = sp.getString(key, "");
    }

其内部也是使用了输入输出流,以写入SP文件为例:

#SharedPreferencesImpl.java
    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        ...
        //构造输出流
        FileOutputStream str = createFileOutputStream(mFile);
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        FileUtils.sync(str);
        str.close();
        ...
    }

4、读写数据库目录下文件
创建数据库:

MyDatabaseHelper myDatabaseHelper = new MyDatabaseHelper(v.getContext(), "myDB", null, 10);

myDB是数据库文件名。打开数据库的相应表,即可读写数据。
获取数据库文件路径:

#Context.java
Context.public abstract File getDatabasePath(String name);

获取结果如下:

/data/user/0/com.fish.myapplication/databases/myDB

5、读写code_cache目录下文件

#Context.java API>=21
public abstract File getCodeCacheDir();

获取结果如下:

/data/user/0/com.fish.myapplication/code_cache/

以上是分别列举了各个子目录/文件的获取方式,如果想获取:/data/user/0/com.fish.myapplication/,可通过:

#Context.java
public abstract File getDataDir();

该方法需要API>=24。

3、外部存储

外部存储分为两部分:自带外部存储和扩展外部存储(外置SD卡)

A、自带外部存储存储

存放位置

存储的根目录是:"/"。
根目录下几个需要关注的目录:

/data/
/sdcard/
/storage/

其中/data/目录前面已经分析过。

/sdcard/是软链接,指向/storage/self/primary
而/storage/下有几个目录:


image.png

/storage/self/primary/是软链接,指向/storage/emulated/0/

也就是说/sdcard/、/storage/self/primary/ 真正指向的是/storage/emulated/0/

存储内容

image.png

如上图所示,/sdcard/目录下的子目录看起来都比较眼熟。
这些子目录分为分为三部分:

第一部分:共享存储空间

也就是所有App共享的部分,比如相册、音乐、铃声、文档等。
共享存储空间按文件类型又分为两部分:
1、媒体文件

  • DCIM/ 和 Pictures/-->存储图片
  • DCIM/、Movies/ 和 Pictures-->存储视频
  • Alarms/、Audiobooks/、Music/、Notifications/、Podcasts/ 和 Ringtones/-->存储音频文件
  • Download/-->下载的文件

2、文档和其它文件

Documents-->存储如.pdf类型等文件

第二部分:App外部私有目录

  • Android/data/--->存储各个App的外部私有目录
    与内部存储类似,命名方式是:Android/data/xx------>xx指应用的包名。
    如:/sdcard/Android/data/com.fish.myapplication

第三部分:其它目录

比如各个App在/sdcard/目录下创建的目录,如支付宝创建的目录:alipy/,微博创建的目录:com.sina.weibo/,qq创建的目录:com.tencent.mobileqq/等。

访问方式

与访问内部存储文件类似,外部存储也可以通过构造输入输出流访问文件。

读写共享存储空间

视频、图片等可能分散存储在各个不同的目录里,如果想要获取所有的图片地址,那么得需要遍历不同的目录寻找,效率显而易见的低。Android 将视频、图片等信息存储在数据库里,每当某个App想要访问这些共享的媒体文件时只需要查找数据库对应的表,读取符合条件的行,找出每个媒体的文件路径等信息。
App查询共享存储空间的媒体方式是:通过ContentProvider访问。

访问媒体文件
以查询图片为例:

    private void getImagePath(Context context) {
        ContentResolver contentResolver = context.getContentResolver();
        Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);
        while(cursor.moveToNext()) {
            String imagePath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));
        }
    }

查询到图片的地址,当然就可以展示图片了。

访问文档和其它文件
Storage Access Framework 简称SAF:存储访问框架
以查看.pdf文件为例:

    private void startSAF() {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("application/pdf");
        startActivityForResult(intent, 100);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == 100) {
            Uri uri = data.getData();
        }
    }

SAF实际上就是调用系统提供的选择器,选中后在onActivityResult(xx)里接收结果,拿到Uri后当然就可以读写对应的文件了。

读写App外部私有目录

刚开始并没有自己App的包名。


image.png

调用如下方法后:

    private void testAppDir(Context context) {
        //4个基本方法
        File fileDir = context.getExternalFilesDir(null);
        //API>=19
        File[] fileList = context.getExternalFilesDirs(null);

        File cacheDir = context.getExternalCacheDir();
        //API>=19
        File[] cacheList = context.getExternalCacheDirs();

        //指定目录,自动生成对应的子目录
        File fileDir2 = context.getExternalFilesDir(Environment.DIRECTORY_DCIM);
    }

再查看目录树:


image.png

可以看出再/sdcard/Android/data/目录下生成了com.fish.myapplication/目录,该目录下有两个子目录分别是:files/、cache/。当然也可以选择创建其它目录。
2、App卸载的时候,两者都会被清除。

读写其它目录

只要拿到根目录,就可以遍历寻找其它子目录/文件。

    private void testOtherDir(Context context) {
        File rootDir = Environment.getExternalStorageDirectory();
    }

返回的rootDir路径:/storage/emulated/0/。

B、扩展外部存储(外置SD卡)

存储位置

当给设备插入SD卡后,查看其目录:
/sdcard/ 依然指向/storage/self/primary,继续来看/storage/:


image.png

可以看出,多了sdcard1,软链接指向了/storage/77E4-07E7/。

存储内容

取决于SD卡上装了什么东西。

访问方式

还记得上面获取外部存储-App私有目录方式吗?

File[] fileList = context.getExternalFilesDirs(null);

返回File对象数组,当有多个外部存储时候,存储在数组里。


image.png

返回的数组有两个元素,一个是自带外部存储存储,另一个是刚插入的SD卡。
拿到路径后,当然就可以访问相应的文件了。

4、易混淆点说明

以上分别阐述了内部存储、自带外部存储、扩展外部存储等,这几者关系如下:


image.png

其中比较容易混淆的是:
内部存储与外部存储里的App私有目录,两者命名风格很像。

不同点:

/data/data/com.fish.myapplication/ 位于内部存储,一般用于存储容量较小的,私密性较强的文件。而/sdcard/Android/data/com.fish.myapplication/ 位于外部存储,作为App私有目录,一般用于存储容量较大的文件,即使删除了也不影响App正常功能。

相同点:

1、属于App专属,App自身访问两者无需任何权限。
2、App卸载后,两者皆被删除。
3、两者目录下增加的文件最终会被统计到"设置->存储和缓存"里。

另外,常见的在设置里的"存储与缓存"项:


image.png

当点击"Clear cache" 时:

内部存储/data/data/com.fish.myapplication/cache/、 /data/data/com.fish.myapplication/code_cache/目录会被清空
外部存储/sdcard/Android/data/com.fish.myapplication/cache/ 会被清空

当点击"Clear storage" 时:

内部存储/data/data/com.fish.myapplication/下除了lib/,其余子目录皆被删除
外部存储/sdcard/Android/data/com.fish.myapplication/被清空
\color{Red}{注:该功能慎用,因为会删除用户数据库,SP文件等,相当于重置了App}

接下来将分析Android 10.0 11 存储适配。
本文基于Android 10.0。

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容