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

作者: 吾非言 | 来源:发表于2017-10-09 16:03 被阅读358次

    作者:邹峰立,微博: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地址
    阅读原文


    微信公众号:书客创作

    相关文章

      网友评论

        本文标题:【Android】数据存储全方案之文件存储

        本文链接:https://www.haomeiwen.com/subject/tcrjextx.html