15.文件存储
1、数据持久化
数据持久化就是指将内存中的瞬时数据保存到存储设备中,保证手机在关机的情况下数据仍然不会丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的。
Android系统提供了三种方式简单的数据持久化功能,即文件存储、SharedPreferences存储和SQLite数据库存储。
2、文件存储
文件存储是Android中最基本的一种数据存储方式,它与Java中的文件存储类似,都是通过I/O流的形式把数据直接存储到文件中。
不同的是,Android中的文件存储分为 内部存储 和 外部存储 。
1.内部存储
内部存储是指应用程序中的数据以文件方式存储到应用程序内部中。当创建的应用程序被卸载时,其内部存储文件也随之被删除。
(1)将数据存储到文件中
Context类中提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中。
这个方法接收两个参数,第一个参数是文件名,文件是默认存储到/data/data/<package name>/files/目录下的。
第二个参数是文件的操作模式,主要有两种模式,MODE_PRIVATE和MODE_APPEND。
其中MODE_PRIVATE是默认的操作模式,表示当指定同样文件名的时候,所写入的内容将会覆盖原文件中的内容,而MODE_APPEND则表示如果该文件已存在就往文件里面追加内容,不存在就创建新文件。
下面我们就编写一个完整的例子,来学习一下如何在Android项目中使用文件存储的技术。
首先创建一个FileStorageTest项目,并修改activity_main.xml中的代码,如下所示:
<?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/edtData" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入要保存的数据" /> <Button android:id="@+id/btnWriteToApp" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="写入到内部文件" /> </LinearLayout>
在布局中添加一个EditText和一个Button,点击Button时,将EditText中的内容存储到文件当中。
修改MainActivity中的代码,如下所示:
import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; import java.io.FileOutputStream; import java.io.IOException; public class MainActivity extends AppCompatActivity implements View.OnClickListener { private EditText edtData; private Button btnWriteToApp; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); edtData = (EditText) findViewById(R.id.edtData); btnWriteToApp = (Button) findViewById(R.id.btnWriteToApp); btnWriteToApp.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btnWriteToApp: writeToApp(edtData.getText().toString()); break; } } public void writeToApp(String data) { FileOutputStream out = null; try { out = openFileOutput("myFile", MODE_PRIVATE); out.write(data.getBytes()); out.flush(); // 清空缓冲区的数据流 out.close(); // 关闭输出流 } catch (IOException e) { e.printStackTrace(); } } }
我们通过使用MainActivity去实现监听器接口的方式为Button添加监听事件,在onCreate()方法中获取了EditText和Button的实例,为Button控件添加监听器,在onClick()方法中我们获取了EditText中输入的内容,并调用writeToApp()方法把输入的内容存储到内部文件中,文件命名为myFile。
writeToApp()方法接收一个String类型的参数,用于传入要保存的字符串数据,在该方法内,通过openFileOutput()方法获得一个FileOutputStream对象(文件输出流),然后使用它的write()方法将文本内容写入到文件中,但是write()方法需要一个字节数组参数,我们通过字符串的getBytes()方法可以将字符串转化为一个字节数组。
最后,一定要关闭文件输出流。现在重新运行一下程序,并在EditText中输入一些内容,点击按钮保存,如图所示。
如何才能证实数据确实已经保存成功了呢?我们可以借助Device File Explorer来查看一下。
在这里进入到/data/data/com.sdbi.filepersistencetest/files/目录下,可以看到生成了一个myFile文件,将文件保存到桌面或着直接双击打开,查看里面的内容如图所示。
如果在保存文件或者双击打开时出现如下错误:
There were errors downloading files and/or directories: secure_mkdirs failed: Operation not permitted
我们可以到D:\Android\sdk\platform-tools目录下,使用命令行执行如下命令:
adb root
再去保存或者打开就可以了。
这样就证实了,在EditText中输入的内容确实已经成功保存到文件中了。
不过只是成功将数据保存下来还不够,我们还要学习一下,如何从文件中读取数据。
(2)从文件中读取数据
类似于将数据存储到文件中,Context类中还提供了一个openFileInput()方法,用于从文件中读取数据。
这个方法只接收一个参数,即要读取的文件名,然后系统会自动到/data/data/<package name>/files/目录下去读取这个文件,并返回一个FileInputStream对象(文件输入流),得到了这个对象之后再通过Java流的方式就可以将数据读取出来了。
下面,我们在项目中完成数据的读取代码。
修改activity_main.xml中的代码,如下所示:
<?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/edtData" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入要保存的数据" /> <Button android:id="@+id/btnWriteToApp" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="写入到内部文件" /> <Button android:id="@+id/btnReadFromApp" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="从内部文件读取" /> <TextView android:id="@+id/tvDisplay" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout>
在布局文件中增加两个控件:一个Button和一个TextView,分别用于读取文件和显示读取的数据的。
修改MainActivity中的代码,如下所示:
import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class MainActivity extends AppCompatActivity implements View.OnClickListener { private EditText edtData; private Button btnWriteToApp, btnReadFromApp; private TextView tvDisplay; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); edtData = (EditText) findViewById(R.id.edtData); btnWriteToApp = (Button) findViewById(R.id.btnWriteToApp); btnWriteToApp.setOnClickListener(this); btnReadFromApp = (Button) findViewById(R.id.btnReadFromApp); btnReadFromApp.setOnClickListener(this); tvDisplay = (TextView) findViewById(R.id.tvDisplay); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btnWriteToApp: writeToApp(edtData.getText().toString()); break; case R.id.btnReadFromApp: String strData = readFromApp(); if (!TextUtils.isEmpty(strData)) { // 判空处理 tvDisplay.setText(strData); } break; } } public void writeToApp(String data) { FileOutputStream out = null; try { out = openFileOutput("myFile", MODE_PRIVATE); out.write(data.getBytes()); out.flush(); // 清空缓冲区的数据流 out.close(); // 关闭输出流 } catch (IOException e) { e.printStackTrace(); } } public String readFromApp() { FileInputStream in = null; StringBuffer buffer = new StringBuffer(""); try { in = openFileInput("myFile"); byte[] temp = new byte[1024]; int len = 0; while ((len = in.read(temp)) > 0) { buffer.append(new String(temp, 0, len)); } in.close(); // 关闭输入流 } catch (IOException e) { e.printStackTrace(); } return buffer.toString(); } }
在这段代码中我们自己定义了一个方法:readFromApp(),专门用于从内部文件中读取数据,在该方法中,首先通过openFileInput()方法获取到了一个FileInputStream对象in(文件输入流),然后创建一个长度为1024的字节数组类型缓冲区temp,循环将文件的内容读取到缓冲区temp中,再将字节数组缓冲区中的数据转换成字符串添加到StringBuffer中,最后将StringBuffer中的数据转换为字符串返回给该方法。
注意:在循环读取输入流对象后一定要关闭文件输入流。然后,只需在onCreate()方法中调用readFromApp()方法来读取文件中存储的文本内容,如果读到的内容不为空,就调用TextView的setText()方法将内容填充到TextView里。
上述代码在对字符串进行非空判断的时候使用了TextUtils.isEmpty()方法,这是一个非常好用的静态方法,它可以一次性进行两种空值的判断。
当传入的字符串等于null或者等于空字符串的时候,这个方法都会返回true,从而使得我们不需要单独去判断这两种空值,再使用逻辑运算符连接起来了。
重新运行一下程序,点击“从内部文件读取”按钮,可以将原先保存过的数据读取出来,如图所示。
2.外部存储
外部存储就是指将文件存储到一些外部设备上,例如SD卡或者设备内嵌的存储卡,属于永久性的存储方式。
通常位于storage/sdcard文件夹,也有可能是mnt/sdcard文件夹,这个不同厂商生产的手机路径可能会不一样。
外部存储的文件可以被其他应用程序共享使用,当外部存储设备连接到计算机时,这些文件可以被浏览、修改和删除,因此这种方式是不安全的。
由于外部存储设备可能被移除、丢失或者处于其他状态,因此在使用外部设备之前必须使用Environment类的静态方法getExternalStorageState()来确认外部设备是否可用,当外部设备可用并且具有读写权限时,那么就可以通过FileOutputStream、FileInputStream对象来读写外部设备中的文件。
(1)将数据存储到外部设备的文件中
在前面程序的基础上增加一个writeToSdcard()方法用于将EditText输入的文本信息保存到外部存储设备上。代码如下所示:
public void writeToSdcard(String inputText) { String state = Environment.getExternalStorageState(); if (state.equals(Environment.MEDIA_MOUNTED)) { // String“mounted”:安装好的 File dir = Environment.getExternalStorageDirectory(); File file = new File(dir, "myData"); // 在dir目录下构建一个新文件myData Log.d("MainActivity", "path = " + dir.getPath()); //或 dir.toString() Log.d("MainActivity", "file_name = " + file.getPath()); //或 file.toString() FileOutputStream out = null; try { out = new FileOutputStream(file); out.write(inputText.getBytes()); out.flush(); out.close(); } catch (IOException e) { e.printStackTrace(); } } }
在上述代码中,我们使用Environment类的getExternalStorageState()静态方法来判断SD卡是否存在可用。
使用getExternalStorageDirectory()静态方法来获取SD卡根目录的路径,这样可以避免由于手机厂商不同而导致SD路径不同的问题。
另外,对于File对象(路径和文件)我们可以调用其getPath()或者toString()方法来查看路径内容。
(2)从外部设备的文件中读取数据
增加一个readFromSdcard()方法读取外部设备上的文件。
public String readFromSdcard() { String state = Environment.getExternalStorageState(); StringBuffer buffer = new StringBuffer(""); if (state.equals(Environment.MEDIA_MOUNTED)) { File dir = Environment.getExternalStorageDirectory(); File file = new File(dir, "myData"); FileInputStream in = null; try { in = new FileInputStream(file); byte[] temp = new byte[1024]; int len = 0; while ((len = in.read(temp)) > 0) { Log.d("MainActivity", "len = " + len); buffer.append(new String(temp, 0, len)); } in.close(); } catch (IOException e) { e.printStackTrace(); } } return buffer.toString(); }
在外部文件中写入数据和读取数据的方法完成后,我们在布局文件activity_main.xml中增加两个按钮,分别用于“写入到外部文件”和“从外部文件读取”,并且在MainActivity.java中给它们添加监听器。
修改activity_main.xml文件代码如下:
<?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/edtData" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入要保存的数据" /> <Button android:id="@+id/btnWriteToApp" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="写入到内部文件" /> <Button android:id="@+id/btnReadFromApp" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="从内部文件读取" /> <Button android:id="@+id/btnWriteToSdcard" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="写入到外部文件" /> <Button android:id="@+id/btnReadFromSdcard" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="从外部文件读取" /> <TextView android:id="@+id/tvDisplay" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout>
修改MainActivity.java文件代码如下:
import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.os.Environment; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class MainActivity extends AppCompatActivity implements View.OnClickListener { private EditText edtData; private Button btnWriteToApp, btnReadFromApp, btnWriteToSdcard, btnReadFromSdcard; private TextView tvDisplay; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); edtData = (EditText) findViewById(R.id.edtData); btnWriteToApp = (Button) findViewById(R.id.btnWriteToApp); btnWriteToApp.setOnClickListener(this); btnReadFromApp = (Button) findViewById(R.id.btnReadFromApp); btnReadFromApp.setOnClickListener(this); tvDisplay = (TextView) findViewById(R.id.tvDisplay); btnWriteToSdcard = (Button) findViewById(R.id.btnWriteToSdcard); btnWriteToSdcard.setOnClickListener(this); btnReadFromSdcard = (Button) findViewById(R.id.btnReadFromSdcard); btnReadFromSdcard.setOnClickListener(this); tvDisplay = (TextView) findViewById(R.id.tvDisplay); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btnWriteToApp: writeToApp(edtData.getText().toString()); break; case R.id.btnReadFromApp: String strData = readFromApp(); if (!TextUtils.isEmpty(strData)) { // 判空处理 tvDisplay.setText(strData); } break; case R.id.btnWriteToSdcard: writeToSdcard(edtData.getText().toString()); break; case R.id.btnReadFromSdcard: strData = readFromSdcard(); if (!TextUtils.isEmpty(strData)) { tvDisplay.setText(strData); } break; } } public void writeToApp(String data) { FileOutputStream out = null; try { out = openFileOutput("myFile", MODE_PRIVATE); out.write(data.getBytes()); out.flush(); // 清空缓冲区的数据流 out.close(); // 关闭输出流 } catch (IOException e) { e.printStackTrace(); } } public String readFromApp() { FileInputStream in = null; StringBuffer buffer = new StringBuffer(""); try { in = openFileInput("myFile"); byte[] temp = new byte[1024]; int len = 0; while ((len = in.read(temp)) > 0) { buffer.append(new String(temp, 0, len)); } in.close(); // 关闭输入流 } catch (IOException e) { e.printStackTrace(); } return buffer.toString(); } public void writeToSdcard(String inputText) { String state = Environment.getExternalStorageState(); if (state.equals(Environment.MEDIA_MOUNTED)) { // String“mounted”:安装好的 File dir = Environment.getExternalStorageDirectory(); File file = new File(dir, "myData"); // 在dir目录下构建一个新文件myData Log.d("MainActivity", "path = " + dir.getPath()); //或 dir.toString() Log.d("MainActivity", "file_name = " + file.getPath()); //或 file.toString() FileOutputStream out = null; try { out = new FileOutputStream(file); out.write(inputText.getBytes()); out.flush(); out.close(); } catch (IOException e) { e.printStackTrace(); } } } public String readFromSdcard() { String state = Environment.getExternalStorageState(); StringBuffer buffer = new StringBuffer(""); if (state.equals(Environment.MEDIA_MOUNTED)) { File dir = Environment.getExternalStorageDirectory(); File file = new File(dir, "myData"); FileInputStream in = null; try { in = new FileInputStream(file); byte[] temp = new byte[1024]; int len = 0; while ((len = in.read(temp)) > 0) { Log.d("MainActivity", "len = " + len); buffer.append(new String(temp, 0, len)); } in.close(); } catch (IOException e) { e.printStackTrace(); } } return buffer.toString(); } }
需要注意的是:
① Android系统为了保证应用程序的安全性做了相应的规定,由于操作SD卡中的数据属于系统中比较关键的信息,因此需要在清单文件的<manifest>节点中添加SD卡的读写权限,示例代码如下:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <!--添加SD卡的读写权限--> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.FileStorageTest" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="android.app.lib_name" android:value="" /> </activity> </application> </manifest>
② 在Android6.0(API 23)之后,APP需要动态获取权限,这也是为了用户数据更加安全。
import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import android.Manifest; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Environment; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.Array; import java.util.Arrays; public class MainActivity extends AppCompatActivity implements View.OnClickListener { private EditText edtData; private Button btnWriteToApp, btnReadFromApp, btnWriteToSdcard, btnReadFromSdcard; private TextView tvDisplay; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); edtData = (EditText) findViewById(R.id.edtData); btnWriteToApp = (Button) findViewById(R.id.btnWriteToApp); btnWriteToApp.setOnClickListener(this); btnReadFromApp = (Button) findViewById(R.id.btnReadFromApp); btnReadFromApp.setOnClickListener(this); tvDisplay = (TextView) findViewById(R.id.tvDisplay); btnWriteToSdcard = (Button) findViewById(R.id.btnWriteToSdcard); btnWriteToSdcard.setOnClickListener(this); btnReadFromSdcard = (Button) findViewById(R.id.btnReadFromSdcard); btnReadFromSdcard.setOnClickListener(this); tvDisplay = (TextView) findViewById(R.id.tvDisplay); int permission1 = ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); int permission2 = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); // 常量PackageManager.PERMISSION_GRANTED=0,表示已授权 if (permission1 != PackageManager.PERMISSION_GRANTED || permission2 != PackageManager.PERMISSION_GRANTED) { // 动态请求权限 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 1); // 第3个参数是一个表示请求的Code,要求>=0,作为回调方法onRequestPermissionsResult()的第1个参数 } } // 授权成功的回调方法,用不着可以不重写 // 第1个参数是一个表示请求的Code,ActivityCompat.requestPermissions()方法的第3个参数 // 第2个参数是请求的权限数组 // 第3个参数是授权结果,0表示已授权 @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { // 第3个参数是授权结果,0表示已授权 super.onRequestPermissionsResult(requestCode, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, grantResults); Log.d("MainActivity", "onRequestPermissionsResult: requestCode = " + requestCode + ", permissions = " + Arrays.toString(permissions) + ", grantResults = " + Arrays.toString(grantResults)); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btnWriteToApp: writeToApp(edtData.getText().toString()); break; case R.id.btnReadFromApp: String strData = readFromApp(); if (!TextUtils.isEmpty(strData)) { // 判空处理 tvDisplay.setText(strData); } break; case R.id.btnWriteToSdcard: writeToSdcard(edtData.getText().toString()); break; case R.id.btnReadFromSdcard: strData = readFromSdcard(); if (!TextUtils.isEmpty(strData)) { tvDisplay.setText(strData); } break; } } public void writeToApp(String data) { FileOutputStream out = null; try { out = openFileOutput("myFile", MODE_PRIVATE); out.write(data.getBytes()); out.flush(); // 清空缓冲区的数据流 out.close(); // 关闭输出流 } catch (IOException e) { e.printStackTrace(); } } public String readFromApp() { FileInputStream in = null; StringBuffer buffer = new StringBuffer(""); try { in = openFileInput("myFile"); byte[] temp = new byte[1024]; int len = 0; while ((len = in.read(temp)) > 0) { buffer.append(new String(temp, 0, len)); } in.close(); // 关闭输入流 } catch (IOException e) { e.printStackTrace(); } return buffer.toString(); } public void writeToSdcard(String inputText) { String state = Environment.getExternalStorageState(); if (state.equals(Environment.MEDIA_MOUNTED)) { // String“mounted”:安装好的 File dir = Environment.getExternalStorageDirectory(); File file = new File(dir, "myData"); // 在dir目录下构建一个新文件myData Log.d("MainActivity", "path = " + dir.getPath()); //或 dir.toString() Log.d("MainActivity", "file_name = " + file.getPath()); //或 file.toString() FileOutputStream out = null; try { out = new FileOutputStream(file); out.write(inputText.getBytes()); out.flush(); out.close(); } catch (IOException e) { e.printStackTrace(); } } } public String readFromSdcard() { String state = Environment.getExternalStorageState(); StringBuffer buffer = new StringBuffer(""); if (state.equals(Environment.MEDIA_MOUNTED)) { File dir = Environment.getExternalStorageDirectory(); File file = new File(dir, "myData"); FileInputStream in = null; try { in = new FileInputStream(file); byte[] temp = new byte[1024]; int len = 0; while ((len = in.read(temp)) > 0) { Log.d("MainActivity", "len = " + len); buffer.append(new String(temp, 0, len)); } in.close(); } catch (IOException e) { e.printStackTrace(); } } return buffer.toString(); } }
运行程序,提示请求权限,如图所示。
保存数据,我们可以看到在storage/sdcard文件夹生成一个myData文件。
输出日志如下。