【Android】数据存储全方案之文件存储

作者:邹峰立,微博:zrunker,邮箱:zrunker@yahoo.com,微信公众号:书客创作,个人平台:www.ibooker.cc

本文选自书客创作平台第6篇文章。阅读原文

书客创作

Android中可以在设备本身的存储设备或者外接设备中创建用户存储的保存数据的文件。这些文件在默认状态下是不能在不能的程序间共享,但是可以通过Content Provider进行数据共享。

文件存储不对存储的内容进行任何的格式化处理,所有数据都是原封不动地保存到文件当中,因而它比较适合用于存储一些简单的文本数据或二进制数据。

文件存储有两种方式,一种是存储到手机内存中(memory),一种是存储到sd卡中。该如何实现这两种方式呢?

首先要理解什么是文件的操作模式?

  1. MODE_PRIVATE:当指定同样文件名时会覆盖原文件中的内容。
  2. MODE_APPEND:当该文件已存在时就往文件中追加内容,不会创建新文件。
  3. 还有另外两种(android4.2被废弃),MODE_ WORLD_ READABLE和MODE_WORLD _WRITEABLE,这两种模式表示允许其他的应用程序对我们程序中的文件进行读写操作。

通过案例说明该如何进行文件存储:本案例中有一个EditText当点击‘保存到内存’按钮将会把EditText输入内容保存到内存文件testmemory.json,当点击‘读取内存’按钮将会把testmemory.json中的数据读取出来,显示到TextView上。当点击‘保存到SD卡’按钮将会把EditText输入内容保存到内存文件testsd.json,当点击‘读取SD卡’按钮将会把testsd.json中的数据读取出来,显示到TextView上。

布局:

<?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"
    android:orientation="vertical">

    <EditText
        android:id="@+id/edittext"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="textMultiLine"
        android:padding="15dp" />

    <Button
        android:id="@+id/btn_memory"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="@string/save_memory" />

    <Button
        android:id="@+id/btn_sd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="@string/save_sd" />

    <Button
        android:id="@+id/btn_read_memory"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="@string/read_memory" />

    <Button
        android:id="@+id/btn_read_sd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="@string/read_sd" />

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/read_text"
        android:padding="10dp" />
</LinearLayout>

效果图:


效果图

一、存储到手机内存中

理解:既然是想将数据保存到文件当中,那么对于文件的读写,必然要借助于流。如果要将文件存储到内存中,可以借助于Content类提供的openFileOutput和openFileInput两个方法来操作文件的写入写出。

/**
 * @param name 文件名
 * @param mode 文件的操作模式
 */
FileOutputStream openFileOutput(String name, int mode);

注:openFileOutput方法是用来获取文件输出流,用于写入内容。在该方法中,参数name是指文件名不可以包含路径,因为所有的文件都是默认存储到/data/data/<package name>/files/目录下。

/**
 * @param name 文件名
 */
FileInputStream openFileInput(String name);

注:openFileInput方法是用来获取文件输入流,用来读取内容。同样,在该方法中,参数name是指文件名不可以包含路径。

实现:

定义数据保存方法:writeMemoryData(Object obj)

/**
  * 保存数据到内存
  *
  * @param obj 待保存数据
  * @return true/false(成功/失败)
  */
private boolean writeMemoryData(Object obj) {
    boolean bool = false;
    FileOutputStream fos = null;
    try {
       // 构建Properties
       Properties properties = new Properties();
       // Properties添加数据
       properties.put(mKey, obj);
       fos = this.openFileOutput("testmemory.json", Context.MODE_PRIVATE);
       // 将数据写入文件(流)
       properties.store(fos, "测试文件");
       bool = true;
    } catch (FileNotFoundException e) {
       e.printStackTrace();
    } catch (IOException e) {
       e.printStackTrace();
    } finally {
       if (fos != null)
          try {
              fos.close();
          } catch (IOException e) {
              e.printStackTrace();
          }
    }
    return bool;
}

注:上面代码中提到Properties,可以理解为属性设置文件集,它继承Hashtable<Object, Object>,而Hashtable继承Map,所以可以把Properties当中Map来使用,通过保存键值对相关信息。

定义数据读取方法:readMemoryData(String key)

/**
 * 读取内存数据
 *
 * @param key 数据对应键值
 * @return 待读取的数据
 */
private Object readMemoryData(String key) {
   Object obj = null;
   FileInputStream fis = null;
   try {
       // 构建Properties
       Properties properties = new Properties();
       fis = this.openFileInput("testmemory.json");
       // 加载文件
       properties.load(fis);
       obj = properties.get(key);
   } catch (FileNotFoundException e) {
       e.printStackTrace();
   } catch (IOException e) {
       e.printStackTrace();
   } finally {
       if (fis != null)
          try {
             fis.close();
          } catch (IOException e) {
             e.printStackTrace();
          }
   }
   return obj;
}

注:参数key是用来取Properties中保存的数据。

到这里就要开始写逻辑实现:

/**
 * 文件存储
 * Created by 邹峰立 on 2017/9/19 0019.
 */
public class FileActivity extends AppCompatActivity implements View.OnClickListener {
    private EditText editText;
    private TextView textView;
    private final String mKey = "mKey";

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_file);

        initView();
    }

    // 初始化控件
    private void initView() {
        editText = findViewById(R.id.edittext);
        textView = findViewById(R.id.text);
        Button saveMemoryBtn = findViewById(R.id.btn_memory);
        saveMemoryBtn.setOnClickListener(this);
        Button readMemoryBtn = findViewById(R.id.btn_read_memory);
        readMemoryBtn.setOnClickListener(this);
    }

    // 按钮点击事件监听
    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_memory:// 保存到内存
                String text = editText.getText().toString().trim();
                if (!TextUtils.isEmpty(text)) {
                    boolean bool = writeMemoryData(text);
                    if (bool) {
                        Toast.makeText(this, "写入成功", Toast.LENGTH_SHORT).show();
                        editText.setText("");
                    } else
                        Toast.makeText(this, "写入失败", Toast.LENGTH_SHORT).show();
                }
                break;
            case R.id.btn_read_memory:// 读取内存
                String str = readMemoryData(mKey).toString();
                textView.setText(str);
                break;
        }
    }
}

因为上面已经提到writeMemoryData和readMemoryData,所以这里省略了writeMemoryData和readMemoryData,只是用来简单说明具体实现逻辑。

二、存储到sd卡中

内存存储一般只用于存储小数据,当数据量较大的时候,可以将大数据保存到SD卡相关文件当中。

Environment类简介:

Environment可以说是操作SD卡一个非常重要的类。

  1. Environment两个重要常量:
  • Environment.MEDIA_MOUNTED:外部存储器可读可写。
  • Environment.MEDIA_ MOUNTED_ READ_ONLY:外部存储器只读。
  1. Environment常用方法:
  • getExternalStorageDirectory():获取SDCard的目录,/mnt/sdcard。
  • getExternalStorageState():获取外部存储器的当前状态。

在本案例当中,需要借助于Environment判断SD卡状态(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)),以及获取SD卡目录(Environment.getExternalStorageDirectory())。

定义数据写入SD卡文件方法:writeSdData(Object obj)

/**
 * 写入SD卡文件
 *
 * @param obj 待写入对象
 */
private boolean writeSdData(Object obj) {
    boolean bool = false;
    if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {// 判断外部存储是否可读可写
       RandomAccessFile raf = null;
       try {
          // 获取SD卡路径
          File sdDir = Environment.getExternalStorageDirectory();
          // 获取SD卡目录 /mnt/sdcard。
          String sdPath = sdDir.getAbsolutePath();
          // 创建文件
          File file = new File(sdPath, "testsd.json");
          if (!file.exists()) {
             boolean bool1 = file.createNewFile();
             if (!bool1)
                return false;
             }


//            FileOutputStream fos = null;
//            try {
//                fos = new FileOutputStream(file);
//                fos.write(obj.toString().getBytes());
//            } catch (FileNotFoundException e) {
//                e.printStackTrace();
//            } catch (IOException e) {
//                e.printStackTrace();
//            } finally {
//                try {
//                    if (fos != null)
//                        fos.close();
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            }


             // 指定文件创建RandomAccessFile对象
             raf = new RandomAccessFile(file, "rw");
             // 将文件记录指针移动最后
             raf.seek(file.length());
             // 写入内容
             raf.write(obj.toString().getBytes());
             bool = true;
       } catch (FileNotFoundException e) {
             e.printStackTrace();
       } catch (IOException e) {
             e.printStackTrace();
       } finally {
           try {
               if (raf != null)
                  raf.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
   }
   return bool;
}

注:这里有写了两种方式对文件进行写入,一种是通过FileOutputStream直接写入,另外一种通过RandomAccessFile对象对文件写入。这里主要区别在于RandomAccessFile对象可以指定文件写入位置,操作更加方便。而FileOutputStream虽然提供了一个FileOutputStream(File file, boolean append)的构造方法,当append为true的时候,会在文件尾部进行写入,当append为false的时候会覆盖之前的文件,但是没法制定写入具体位置。

定义读取SD卡文件内容方法:readSdData()

/**
 * 读取SD卡文件内容
 */
private String readSdData() {
    StringBuilder sb = new StringBuilder();
    if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {// 判断外部存储是否可读可写
       // 获取SD卡路径
       File sdDir = Environment.getExternalStorageDirectory();
       // 获取SD卡目录 /mnt/sdcard。
       String sdPath = sdDir.getAbsolutePath();
       // 创建文件
       File file = new File(sdPath, "testsd.json");

       InputStream is = null;
       try {
          is = new FileInputStream(file);
          int len;
          byte[] buffer = new byte[1024];
          while ((len = is.read(buffer)) != -1) {
             sb.append(new String(buffer, 0, len));
          }
       } catch (FileNotFoundException e) {
             e.printStackTrace();
       } catch (IOException e) {
             e.printStackTrace();
       } finally {
             try {
                if (is != null)
                   is.close();
             } catch (IOException e) {
                   e.printStackTrace();
             }
       }
   }
   return sb.toString();
}

到这里就可以开始写具体逻辑实现:

/**
 * 文件存储
 * Created by 邹峰立 on 2017/9/19 0019.
 */
public class FileActivity extends AppCompatActivity implements View.OnClickListener {
    private EditText editText;
    private TextView textView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_file);

        initView();
    }

    // 初始化控件
    private void initView() {
        editText = findViewById(R.id.edittext);
        textView = findViewById(R.id.text);
        Button sdBtn = findViewById(R.id.btn_sd);
        sdBtn.setOnClickListener(this);
        Button readSdBtn = findViewById(R.id.btn_read_sd);
        readSdBtn.setOnClickListener(this);
    }

    // 按钮点击事件监听
    @Override
    public void onClick(View view) {
        switch (view.getId()) {
             case R.id.btn_sd:// 保存到SD卡
                String text = editText.getText().toString().trim();
                if (!TextUtils.isEmpty(text)) {
                    boolean bool = writeSdData(text);
                    if (bool) {
                        Toast.makeText(this, "写入成功", Toast.LENGTH_SHORT).show();
                        editText.setText("");
                    } else
                        Toast.makeText(this, "写入失败", Toast.LENGTH_SHORT).show();
                }
                break;
             case R.id.btn_read_sd:// 读取SD卡
                String str = readSdData();
                textView.setText(str);
                break;
        }
    }
}

因为上面已经提到writeSdData和readSdData,所以这里省略了writeSdData和readSdData,只是用来简单说明具体实现逻辑。

可能遇到问题:

问题1:没有安装SD卡。当手机没有安装SD卡情况下,是没法进行数据保存。

问题2、当程序运行的时候,会发现无论如何操作都无法保存数据,这是为什么呢?这是因为SD卡文件的读取和写入需要权限,所以需要在AndroidManifest.xml文件中添加如下权限:

<!-- 往sdcard中读取数据的权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 在sdcard中写入文件的权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 在sdcard中创建/删除文件的权限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />

问题3、当再次运行程序的时候,会发现在Android6.0+版本中,依然无法保存数据,这又是为什么呢?这是由于Android从6.0开始对使用权限做了大的改动,其中将【对外部存储设备的读写权限】放到了运行时申请的列表里,App的开发者必须要主动的申请访问设备的权限,这样才能使用外部存储设备。当然系统软件除外。那么又该如何动态申请权限呢?

这里要用到两个方法:

/**
 * @param context 上下文对象
 * @param permission 待检测权限
 */
int checkSelfPermission(@NonNull Context context, @NonNull String permission)

该方法是用来检测权限permission,如果检测结果等于PackageManager.PERMISSION_GRANTED说明当前应用程序可使用该权限。

/**
 * @param activity 活动页面
 * @param permissions 权限组-请求权限集合
 * @param requestCode 请求码-一般用在请求结果回调方法用来进行请求权限组判断
 */
ActivityCompat.requestPermissions(final @NonNull Activity activity,
            final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode)

所以对于上面SD卡操作逻辑可以修改为:

/**
 * 文件存储
 * Created by 邹峰立 on 2017/9/19 0019.
 */
public class FileActivity extends AppCompatActivity implements View.OnClickListener {
    private final int PERMISSION_OPER_EXTERNAL_STORAGE = 55;
    private String[] permissions = {
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
    };
    private int sdOperType = 0;
    private EditText editText;
    private TextView textView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_file);

        initView();
    }

    // 初始化控件
    private void initView() {
        editText = findViewById(R.id.edittext);
        textView = findViewById(R.id.text);
        Button sdBtn = findViewById(R.id.btn_sd);
        sdBtn.setOnClickListener(this);
        Button readSdBtn = findViewById(R.id.btn_read_sd);
        readSdBtn.setOnClickListener(this);
    }

    // 按钮点击事件监听
    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_sd:// 保存到SD卡
                applyPermission();
                sdOperType = 1;
                break;
            case R.id.btn_read_sd:// 读取SD卡
                applyPermission();
                sdOperType = 2;
                break;
        }
    }

    // 判断是否可以操作SD
    private boolean isOperSd() {
        return hasPermission(permissions);
    }

    // Android6.0 动态申请文件读写权限
    private void applyPermission() {
        if (!hasPermission(permissions)) {
            requestPermission(PERMISSION_OPER_EXTERNAL_STORAGE, permissions);
        }
    }

    // 权限请求回调
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case PERMISSION_OPER_EXTERNAL_STORAGE:// SD卡读写权限成功
                switch (sdOperType) {
                    case 1:// 保存数据到SD卡
                        if (isOperSd()) {
                            String text1 = editText.getText().toString().trim();
                            if (!TextUtils.isEmpty(text1)) {
                                boolean bool = writeSdData(text1);
                                if (bool) {
                                    Toast.makeText(this, "写入成功", Toast.LENGTH_SHORT).show();
                                    editText.setText("");
                                } else
                                    Toast.makeText(this, "写入失败", Toast.LENGTH_SHORT).show();
                            }
                        } else {
                            Toast.makeText(this, "你没有操作SD卡权限", Toast.LENGTH_SHORT).show();
                        }
                        break;
                    case 2:// 读取SD卡数据
                        if (isOperSd()) {
                            String str1 = readSdData();
                            textView.setText(str1);
                        } else {
                            Toast.makeText(this, "你没有操作SD卡权限", Toast.LENGTH_SHORT).show();
                        }
                        break;
                }
                break;
        }
    }

    /**
     * 权限检查方法
     */
    public boolean hasPermission(String... permissions) {
        for (String permission : permissions) {
            if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
                return false;
            }
        }
        return true;
    }

    /**
     * 权限请求方法
     */
    public void requestPermission(int code, String... permissions) {
        ActivityCompat.requestPermissions(this, permissions, code);
    }
}

问题4:通过上面的方法完善之后,大部分的机型都可以使用,而一些特殊机型如HUAWEI Mate8(Android 6.0)依旧没法进行数据保存,这又是为什么呢?首先可以说明的是这是一些非常特殊的情况,对于这些特殊情况,可能存在的问题已经不是应用层可以解决的,如果非要让HUAWEI Mate8支持,可以通过以下方法让HUAWEI Mate8恢复SD卡和U盘的读取权限。

  1. 首先要保证设备插入一张SD卡。
  2. 进入【设置->高级设置->内存和存储】然后改变【默认存储位置】为“SD卡”,之后系统会提示要重启手机。
  3. 重启完成后,按照2的方法再次将【默认存储位置】改回“内部存储”,再次重启手机。

GitHub地址
阅读原文


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