android菜鸟学习笔记17----Android数据存储(一)文件读写
假如有如下需求,要求能够记录用户输入的用户名和密码,下次登录时,能直接获取之前保存的用户名密码,并在相应的EditText中显示。
要保存用户输入的数据,最先想到的应该就是文件读写了。
通过对android应用打包安装过程的观察,发现每个应用安装之后,都会在/data/data/下新建一个与应用包名相同的目录,该应用的所有文件都存放在该目录下。
例如:
main_layout.xml代码:
1 <?xml version="1.0" encoding="utf-8"?> 2 3 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 4 5 android:layout_width="match_parent" 6 7 android:layout_height="match_parent" 8 9 android:layout_margin="10dp" 10 11 android:orientation="vertical" > 12 13 <TextView 14 15 android:id="@+id/tv_info" 16 17 android:layout_width="match_parent" 18 19 android:layout_height="wrap_content" 20 21 android:textSize="25sp" 22 23 android:textColor="#00ff00" 24 25 android:text="@string/tv_info_text"/> 26 27 <EditText 28 29 android:id="@+id/et_username" 30 31 android:layout_width="match_parent" 32 33 android:layout_height="wrap_content" 34 35 android:hint="@string/et_name_text"/> 36 37 <EditText 38 39 android:id="@+id/et_password" 40 41 android:layout_width="match_parent" 42 43 android:layout_height="wrap_content" 44 45 android:hint="@string/et_password_text" 46 47 android:inputType="textPassword"/> 48 49 <CheckBox 50 51 android:id="@+id/cb_reme" 52 53 android:layout_width="wrap_content" 54 55 android:layout_height="wrap_content" 56 57 android:text="@string/cb_reme_text" 58 59 /> 60 61 <Button 62 63 android:id="@+id/btn_login" 64 65 android:layout_width="wrap_content" 66 67 android:layout_height="wrap_content" 68 69 android:text="@string/btn_login_text"/> 70 71 </LinearLayout>
FileUtils.java代码:
1 package cn.csc.utils; 2 3 import java.io.FileOutputStream; 4 5 public class FileUtils { 6 7 public static boolean writeFile(String data){ 8 9 String path = "/data/data/cn.csc.file/userinfo.txt"; 10 11 try { 12 13 FileOutputStream fos = new FileOutputStream(path); 14 15 fos.write(data.getBytes()); 16 17 fos.close(); 18 19 return true; 20 21 } catch (Exception e) { 22 23 // TODO Auto-generated catch block 24 25 e.printStackTrace(); 26 27 return false; 28 29 } 30 31 } 32 33 }
MainActivity.java:
1 public class MainActivity extends Activity { 2 3 private EditText et_username; 4 5 private EditText et_password; 6 7 private CheckBox cb_reme; 8 9 private Button btn_login; 10 11 @Override 12 13 protected void onCreate(Bundle savedInstanceState) { 14 15 super.onCreate(savedInstanceState); 16 17 setContentView(R.layout.main_layout); 18 19 et_username = (EditText) findViewById(R.id.et_username); 20 21 et_password = (EditText) findViewById(R.id.et_password); 22 23 cb_reme = (CheckBox) findViewById(R.id.cb_reme); 24 25 btn_login = (Button) findViewById(R.id.btn_login); 26 27 btn_login.setOnClickListener(new OnClickListener() { 28 29 @Override 30 31 public void onClick(View v) { 32 33 // TODO Auto-generated method stub 34 35 String username = et_username.getText().toString(); 36 37 String password = et_password.getText().toString(); 38 39 boolean checked = cb_reme.isChecked(); 40 41 if(TextUtils.isEmpty(username)||TextUtils.isEmpty(password)){ 42 43 Toast.makeText(MainActivity.this, "用户名密码不能为空",Toast.LENGTH_SHORT).show(); 44 45 return; 46 47 } 48 49 if(checked){ 50 51 if(username.equals("dqrcsc")&&password.equals("abcdef")){ 52 53 if(FileUtils.writeFile(username+"#"+password)){ 54 55 Toast.makeText(MainActivity.this, "保存成功",Toast.LENGTH_SHORT).show(); 56 57 } 58 59 else{ 60 61 Toast.makeText(MainActivity.this, "保存失败",Toast.LENGTH_SHORT).show(); 62 63 } 64 65 Toast.makeText(MainActivity.this, "登录成功",Toast.LENGTH_SHORT).show(); 66 67 }else{ 68 69 Toast.makeText(MainActivity.this, "登录失败",Toast.LENGTH_SHORT).show(); 70 71 } 72 73 74 75 } 76 77 } 78 79 }); 80 81 } 82 83 }
运行结果:
注意到该文件的权限,默认仅能被当前用户读写。
保存用户名,密码成功,还需要实现下次打开时,自动读取保存的用户名密码,对应的方法可以在onCreate()中调用,已实现打开时自动加载。
在FileUtils中添加读取文件的方法:
1 public static String readFile(){ 2 3 String path = "/data/data/cn.csc.file/userinfo.txt"; 4 5 try { 6 7 BufferedReader br = new BufferedReader(new FileReader(path)); 8 9 String line = br.readLine(); 10 11 br.close(); 12 13 return line; 14 15 } catch (Exception e) { 16 17 // TODO Auto-generated catch block 18 19 e.printStackTrace(); 20 21 } 22 23 return null; 24 25 }
修改MainActivity.java中onCreate(),添加如下代码:
1 String info = FileUtils.readFile(); 2 3 if(TextUtils.isEmpty(info)){ 4 5 return; 6 7 } 8 9 if(!info.contains("#")){ 10 11 return; 12 13 }else{ 14 15 String[] strs = info.split("#"); 16 17 et_password.setText(strs[1]); 18 19 et_username.setText(strs[0]); 20 21 cb_reme.setChecked(true); 22 23 }
上述直接操作写死的文件路径,是存在问题的,如果我修改了当前应用的包名,然后去操作写死的文件,就会出现权限被拒绝的错误:
如Manifest.xml中package修改为cn.csc.file1,注意要修改Activity节点,name使用全限定名:cn.csc.file.MainActivity。
运行程序,出现如下错误:
java.io.FileNotFoundException: /data/data/cn.csc.file/userinfo.txt (Permission denied)
文件仍然存在,但是相对与当前应用,已属于别的用户所有,而该文件只能被所属用户进行读写操作。
当然,如果把userinfo.txt的权限修改下,还是可以读写的。
adb shell挂载linux文件系统,然后进入/data/data/file目录
然后chmod 666 userinfo.txt
然后,就可以直接让cn.csc.file1应用直接读写了。
其实,Context有可以获取当前包文件存储路径的API:
getFilesDir();//返回/data/data/包名/files目录
getCacheDir();//返回/data/data/包名/cache目录
由于是在FileUtils类中读写文件,要使用Context的API,则需要把Context作为参数传递:
在FileUtils中添加对原有读写文件方法的重载:
1 public static boolean writeFile(Context context, String data){ 2 3 File filesDir = context.getFilesDir(); 4 5 try { 6 7 FileOutputStream fos = new FileOutputStream(new File(filesDir, "userInfo.txt")); 8 9 fos.write(data.getBytes()); 10 11 fos.close(); 12 13 return true; 14 15 } catch (Exception e) { 16 17 // TODO Auto-generated catch block 18 19 e.printStackTrace(); 20 21 return false; 22 23 } 24 25 } 26 27 public static String readFile(Context context){ 28 29 File filesDir = context.getFilesDir(); 30 31 try { 32 33 BufferedReader br = new BufferedReader(new FileReader(new File(filesDir, "userInfo.txt"))); 34 35 String line = br.readLine(); 36 37 br.close(); 38 39 return line; 40 41 } catch (Exception e) { 42 43 // TODO Auto-generated catch block 44 45 e.printStackTrace(); 46 47 } 48 49 return null; 50 51 }
修改MainActivity.java,调用新添加的读写文件方法,把当前Activity实例作为Context参数传递:
FileUtils.writeFile(MainActivity.this,username+"#"+password)
String info = FileUtils.readFile(this);
重新运行程序:
cn.csc.file1目录下多出了一个空的目录files
然后,填写信息,点登录,保存数据
files目录下,便多出了userInfo.txt的文件
文件的默认权限总是-rw- --- ---,即只有该文件所有者可以读写,其他用户均不具有对该文件的读写权限,如何设置文件权限呢?
Context提供了两个方法:
abstract FileInputStream openFileInput(String name)
返回指向/data/data/包名/files/name文件的FileInputStream实例
abstract FileOutputStream openFileOutput(String name, int mode)
返回指向/data/data/包名/files/name文件的FileOutputStream实例,文件不存在时,创建该文件,并根据mode参数,设置文件的权限。
mode的取值可以为:
Context.MODE_PRIVATE:(实际值:0x00000000)只有所有者具有读写权限,文件已存在直接覆盖
Context.MODE_APPEND:(实际值:0x00008000)文件已存在,则追加
Context.MODE_WORLD_READABLE:(实际值:0x00000001)其他所有用户具有读权限
Context.MODE_WORLD_WRITEABLE:(实际值:0x00000002)其他所有用户具有写权限
可以将这些取值位或操作,然后传递给openFileOutput()方法,设置同时具有多个属性。
修改FileUtils中的两个文件读写方法:
1 public static boolean writeFile(Context context, String data){ 2 3 try { 4 5 FileOutputStream fos = context.openFileOutput("userInfo.txt", Context.MODE_PRIVATE); 6 7 fos.write(data.getBytes()); 8 9 fos.close(); 10 11 return true; 12 13 } catch (Exception e) { 14 15 // TODO Auto-generated catch block 16 17 e.printStackTrace(); 18 19 return false; 20 21 } 22 23 } 24 25 public static String readFile(Context context){ 26 27 try { 28 29 BufferedReader br = new BufferedReader(new InputStreamReader(context.openFileInput("userInfo.txt"))); 30 31 String line = br.readLine(); 32 33 br.close(); 34 35 return line; 36 37 } catch (Exception e) { 38 39 // TODO Auto-generated catch block 40 41 e.printStackTrace(); 42 43 } 44 45 return null; 46 47 }
修改mode字段,查看对应文件权限:
MODE_PRIVATE:
MODE_APPEND:
此时,点击登陆按钮,文件大小变成了26,存放了两条用户名及密码信息,可见,是在已存在文件中,追加内容。并且,文件权限变味了同组用户也可读写。
MODE_WORLD_READABLE:
所有用户均可读
MODE_WORLD_WRITEABLE:
所有用户均可写
MODE_WORLD_READABLE| MODE_WORLD_WRITEABLE:
所有用户均可读可写
MODE_APPEND| MODE_WORLD_READABLE| MODE_WORLD_WRITEABLE:
所有用户均可读可写,并且是追加模式,当文件存在时,向已存在文件中追加内容,而非覆盖操作。
注意:由于MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE这两种模式过于危险,易产生安全问题,在Android4.2中已然废弃。
20150709补充:
还有一个模式:Context.MODE_MULTI_PROCESS
一般用于多个进程对同一个文件的读写,同组用户对其都有读写权限,与MODE_APPEND不同的是每次都会覆盖已存在的文件,而不追加。
读写缓存中的文件:
利用Context的getCacheDir();//返回/data/data/包名/cache目录,获取缓存的存放路径。
在FileUtils中添加两个方法:
1 public static boolean writeCache(Context context, String data){ 2 3 File filesDir = context.getCacheDir(); 4 5 try { 6 7 FileOutputStream fos = new FileOutputStream(new File(filesDir, "userInfo.txt")); 8 9 fos.write(data.getBytes()); 10 11 fos.close(); 12 13 return true; 14 15 } catch (Exception e) { 16 17 // TODO Auto-generated catch block 18 19 e.printStackTrace(); 20 21 return false; 22 23 } 24 25 } 26 27 public static String readCache(Context context){ 28 29 File filesDir = context.getCacheDir(); 30 31 try { 32 33 BufferedReader br = new BufferedReader(new FileReader(new File(filesDir, "userInfo.txt"))); 34 35 String line = br.readLine(); 36 37 br.close(); 38 39 return line; 40 41 } catch (Exception e) { 42 43 // TODO Auto-generated catch block 44 45 e.printStackTrace(); 46 47 } 48 49 return null; 50 51 }
在MainActivity调用这两个读写Cache的方法:
运行结果:
注意:文件和缓存的区别:
Clear cache会直接清空cache目录,并且不会弹出警告信息
Clear data则会先弹出警告窗口,确认之后,才会清空files目录。
一般优化工具,都会清空Cache目录,而不会直接清空files目录,所以常用的文件要保存在files目录中,而临时文件则可以考虑放在cache目录中。
读写SD卡中的文件:
Environment类提供了几个很实用的方法:
static File getDataDirectory() /data/data/包名/files
static File getDownloadCacheDirectory() 下载缓存路径
static File getExternalStorageDirectory() SD卡路径
static String getExternalStorageState() SD卡状态
static File getRootDirectory() 根目录
测试上述方法输出:
1 Log.i("Environment","getDataDirectory:"+Environment.getDataDirectory().getPath()); 2 3 Log.i("Environment","getDownloadCacheDirectory:"+Environment.getDownloadCacheDirectory().getPath()); 4 5 Log.i("Environment","getExternalStorageDirectory:"+Environment.getExternalStorageDirectory().getPath()); 6 7 Log.i("Environment","getRootDirectory:"+Environment.getRootDirectory().getPath()); 8 9 Log.i("Environment","getExternalStorageState:"+Environment.getExternalStorageState());
修改FileUtils添加两个读写SD卡的方法:
1 public static boolean writeSD(String data){ 2 3 try { 4 5 String state = Environment.getExternalStorageState(); 6 7 if(state.equals(Environment.MEDIA_MOUNTED)){ 8 9 File ext = Environment.getExternalStorageDirectory(); 10 11 FileOutputStream fos = new FileOutputStream(new File(ext,"userInfo.txt")); 12 13 fos.write(data.getBytes()); 14 15 fos.close(); 16 17 return true; 18 19 } 20 21 return false; 22 23 24 25 } catch (Exception e) { 26 27 // TODO Auto-generated catch block 28 29 e.printStackTrace(); 30 31 return false; 32 33 } 34 35 } 36 37 public static String readSD(){ 38 39 try { 40 41 String state = Environment.getExternalStorageState(); 42 43 if(state.equals(Environment.MEDIA_MOUNTED)){ 44 45 File ext = Environment.getExternalStorageDirectory(); 46 47 BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(ext.getPath()+"/userInfo.txt"))); 48 49 String line = br.readLine(); 50 51 br.close(); 52 53 return line; 54 55 } 56 57 } catch (Exception e) { 58 59 // TODO Auto-generated catch block 60 61 e.printStackTrace(); 62 63 } 64 65 return null; 66 67 }
此外,写SD需要配置权限: <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
运行结果:
注意:在读写SD卡之前,需要先检查SD卡的状态,已挂载才能进行读写操作。此外,读写SD卡需要在Manifest.xml中配置相关的读写权限。在4.0之前读取SD卡是不需要配置读SD卡的权限的,4.0之后,添加了SD卡读取保护的设置,如果用户设置了SD卡读取保护,则读取SD卡中的文件也需要配置权限的。所以,最好同时在Manifest.xml中配置上读写SD卡的权限:
1 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 2 3 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>