数据存储在我们android开发中是不可避免的,而且,我们也都知道数据存储的方式,文件存储,SharedPreference,数据库存储等。但是应该也有一部分人, 只是知道这些存储方式,或者说只知道怎么用,但是不知道具体被保存在什么地方。本篇文章将详细分析这三种存储方式。
算了不卖关子了,其实,在我看来数据存储,或者说数据的持久化,就分为两类。内部存储,外部存储。然后接下来开始我们的表演。
路径:data/data/包名
路径:data/data/包名/file
/**
* Returns the absolute path to the directory on the filesystem where files
* created with {@link #openFileOutput} are stored.
*
* The returned path may change over time if the calling app is moved to an
* adopted storage device, so only relative paths should be persisted.
*
* No additional permissions are required for the calling app to read or
* write files under the returned path.
*
* @return The path of the directory holding application files.
* @see #openFileOutput
* @see #getFileStreamPath
* @see #getDir
*/
public abstract File getFilesDir();
通过官方注释我们可以看到,这个方法返回一个绝对路径,这个绝对路径是有openFilePutput()方法创建的文件。重要的是最后一句,当在这个路径下调用程序来进行读写操作的时候,是不需要任何额外的权限的。也就是说,我们在使用内部存储的方法存储数据的时候,不需要用在manifest文件中声明权限,也不需要考虑android6.0的运行时权限的。想想还是很happy滴
根据上面的介绍我们知道了,这个方法是用来创建一个内部存储的文件的。
/**
* Open a private file associated with this Context's application package
* for writing. Creates the file if it doesn't already exist.
*
* No additional permissions are required for the calling app to read or
* write the returned file.
*
* @param name The name of the file to open; can not contain path
* separators.
* @param mode Operating mode.
* @return The resulting {@link FileOutputStream}.
* @see #MODE_APPEND
* @see #MODE_PRIVATE
* @see #openFileInput
* @see #fileList
* @see #deleteFile
* @see java.io.FileOutputStream#FileOutputStream(String)
*/
public abstract FileOutputStream openFileOutput(String name, @FileMode int mode)
throws FileNotFoundException;
再看官方的注释,打开一个和应用程序包名相关联的私有文件来写入。如果文件不存在就会创建这个文件。并且又说明了进行读写操作的时候是不用任何权限的。
这个方法需要传两个参数,第一个参数就是你想要创建文件的文件名,注意不能包含”/”斜杠符号。
第二个参数是操作模式。官方给我们了两种模式选择:
/**
* File creation mode: for use with {@link #openFileOutput}, if the file
* already exists then write data to the end of the existing file
* instead of erasing it.
* @see #openFileOutput
* 文件创建模式,在openFileOuput方法中使用,如果文件存在,那么会在已存在的文件后面接着写入数据,而不是删除已存在的数据。
*/
public static final int MODE_APPEND = 0x8000;
/**
* File creation mode: the default mode, where the created file can only
* be accessed by the calling application (or all applications sharing the
* same user ID).
* 文件创建模式,默认的模式,用这个模式创建的文件只能被当前调用的应用程序访问。(或者所有拥有相同UID的应用,
* 这个UID其实就是每个进程的UID,也就是说同一进程访问,这里涉及到多进程的知识,在此不详细展开了)
*/
public static final int MODE_PRIVATE = 0x0000;
private void createInternalPathHxy() {
try {
//用MODE_PRIVAT模式,创建一个hxy.txt文件
FileOutputStream outputStream = openFileOutput("hxy.txt", MODE_PRIVATE);
//创建一个Usr对象(实现了Serializable接口,来让User对象可以通过流写入)
User user = new User("hxy", 23);
//下面的就没么好说的了,写入方法和冲刷方法
outputStream.write(user.toString().getBytes());
outputStream.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
而且,我并没有在Mainfest文件中声明任何权限。由于内部存储空间必须有root的android机才对用户可见,所以我们用as的虚拟机运行程序,通过DeviceFileExplorer看下写入的结果。
可以看到,基本和我们预想的都一样。既然写入成功了,那我们看下读取的方法吧。
/**
* Open a private file associated with this Context's application package
* for reading.
*
* @param name The name of the file to open; can not contain path
* separators.
*
* @return The resulting {@link FileInputStream}.
*
* @see #openFileOutput
* @see #fileList
* @see #deleteFile
* @see java.io.FileInputStream#FileInputStream(String)
*/
public abstract FileInputStream openFileInput(String name)
throws FileNotFoundException;
官方注释,打开一个和程序包名相关联的私有文件来读取。这个方法只有一个参数,就是要打开的文件名,这个文件名也不能包含“/”斜杠符号。
在读取数据之前,我们修改了写入的代码,来看看是否会覆盖之前的内容而不是在后面添加
// User user = new User("hxy", 23);
User user = new User("xavier", 111);
private void getDataFromInternalPath() {
try {
//用StringBuilder来接收数据,而不是用String+=的方法。
StringBuilder sb = new StringBuilder();
//每次读取1024个byte的数据
byte[] bytes = new byte[1024];
FileInputStream inputStream = openFileInput("hxy.txt");
int len = 0;
while ((len = inputStream.read(bytes)) != -1) {
sb.append(new String(bytes, 0, len));
}
Log.e("读取到的数据", sb.toString());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
//打印结果如下,确实是覆盖了之前的内容。
08-24 07:52:49.190 15360-15360/com.example.hxytest E/读取到的数据: User{name='xavier', age=111}
下面连个方法就不详细介绍了,一个是删除内部存储文件,一个是获取内部存储文件的列表方法。(只能操作file文件夹下的文件)
/**
* Delete the given private file associated with this Context's
* application package.
*
* @param name The name of the file to delete; can not contain path
* separators.
*
* @return {@code true} if the file was successfully deleted; else
* {@code false}.
*
* @see #openFileInput
* @see #openFileOutput
* @see #fileList
* @see java.io.File#delete()
*/
public abstract boolean deleteFile(String name);
/**
* Returns an array of strings naming the private files associated with
* this Context's application package.
*
* @return Array of strings naming the private files.
*
* @see #openFileInput
* @see #openFileOutput
* @see #deleteFile
*/
public abstract String[] fileList();
路径:data/data/包名/cache
获取内部存储空间的缓存路径。他并没有类似openFileOutput和openFileInput的方法也没偶遇delete和fileList的方法。所以如果要往这个文件夹下写入文件就要用一般用到的File file = new File(); 先创建一个文件,然后再利用FileOutputStream往文件里写入数据。这个好像用的挺少的,可能是因为当系统内存不足时,会把他整个目录删掉吧。
/**
* Retrieve, creating if needed, a new directory in which the application
* can place its own custom data files. You can use the returned File
* object to create and access files in this directory. Note that files
* created through a File object will only be accessible by your own
* application; you can only set the mode of the entire directory, not
* of individual files.
*
* The returned path may change over time if the calling app is moved to an
* adopted storage device, so only relative paths should be persisted.
*
* Apps require no extra permissions to read or write to the returned path,
* since this path lives in their private storage.
*
* @param name Name of the directory to retrieve. This is a directory
* that is created as part of your application data.
* @param mode Operating mode.
*
* @return A {@link File} object for the requested directory. The directory
* will have been created if it does not already exist.
*
* @see #openFileOutput(String, int)
*/
public abstract File getDir(String name, @FileMode int mode);
这个方法其实是直接在内部存储空间创建文件夹的方法。
File dir = getDir("xavier", MODE_PRIVATE);
这个文件夹是和上面所说的file和cache是同级的。而且我们发现他会自动往我们的“xavier”参数前,加一个app_,这个需要注意一下,但是并不影响我们还是通过”xavier”来访问”app_xavier”。
往这个自定义文件夹里面写入文件的方法如下。
private void writeToFile() {
try {
File dir = getDir("xavier", MODE_PRIVATE);
File file = new File(dir, "hxy2.txt");
FileOutputStream outputStream = new FileOutputStream(file);
User user = new User("xavier", 111);
outputStream.write(user.toString().getBytes());
outputStream.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
可以看到确实吧hxy2.txt写入倒了内存中。
小结
1、内部存储空间的路径为data/data/包名
2、内部存储空间只有file文件夹下的读,写,删,操作系统给我们提供了。
3、内部存储空间的文件都是只能本程序访问,其他程序没有权限访问。
4、内部存储空间的文件 在应用被卸载的时候会被一并删除,更新的时候不会。
5、访问内部存储空间,并不需要任何的权限。
6、内部存储空间其实就是手机的内存,所以不能往这里面存入太大的文件,不然手机没有内存就无法正常使用了。
7、cache与files的差别在于,如果手机的内部存储空间不够了,会自行选择cache目录进行删除,因此,不要把重要的文件放在cache文件里面,可以放置在files里面
相信很多开发者对于这种存储方式都不陌生。但是,你知道sharedPreference是以什么样的方式保存的吗?你知道有几种获取SharedPreference对象的方法吗?你知道。。。
用的最多的应该就是,直接getSharedPrederence(name,mode)获取sp对象,然后进行存储或者读取操作。其实这个方法是调用的Context类的方法。
Context.java
/**
* Retrieve and hold the contents of the preferences file 'name', returning
* a SharedPreferences through which you can retrieve and modify its
* values. Only one instance of the SharedPreferences object is returned
* to any callers for the same name, meaning they will see each other's
* edits as soon as they are made.
* 检索和保存这个偏好文件"name"的内容,返回一个SharedPreferences的对象通过这个对象,
* 可以检索和修改他的值。每次调用这个方法,如果name一样,那么返回的SharedPreferences对象也一样。
*
* @param name Desired preferences file. If a preferences file by this name
* does not exist, it will be created when you retrieve an
* editor (SharedPreferences.edit()) and then commit changes (Editor.commit()).
* 要请求的偏好文件。如果当前name命名的偏好文件不存在,那么当通过获取一个editor对象,并且调用commit()方法
* 那么这个name命名的文件将会被创建。
* @param mode Operating mode.
*
* @return The single {@link SharedPreferences} instance that can be used
* to retrieve and modify the preference values.
*
* @see #MODE_PRIVATE
*/
public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);
官方注释,有这么几个点需要我们注意
1、SharedPreferences对于每个name来说是单例模式。
2、偏好文件,在editor.commit()方法之后才会被创建,getSharedPreferences方法是不会创建 名字为name的偏好文件的。
3、关于mode参数。我们一般都取MODE_PRIVATE,这里的MODE_PRIVATE 和上面openFileOutput方法里的MODE_PRIVATE 是一个意思。而且,官方已经废除了MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 这两个参数意思分别是允许其他应用读取和写入我们的sharedPreferences,官方也是为用户的安全着想,废除了危险的操作模式。
private void sharedPreferences() {
SharedPreferences sp1 = getSharedPreferences("hxy", MODE_PRIVATE);
SharedPreferences sp2 = getSharedPreferences("hxy", MODE_PRIVATE);
SharedPreferences sp3 = getSharedPreferences("hxy", MODE_PRIVATE);
Log.e("sp", "sp1" + sp1.toString());
Log.e("sp", "sp2" + sp2.toString());
Log.e("sp", "sp3" + sp3.toString());
}
08-29 05:52:28.494 9647-9647/com.example.hxytest E/sp: sp1android.app.SharedPreferencesImpl@dfe8a50
08-29 05:52:29.886 9647-9647/com.example.hxytest E/sp: sp2android.app.SharedPreferencesImpl@dfe8a50
08-29 05:52:31.645 9647-9647/com.example.hxytest E/sp: sp3android.app.SharedPreferencesImpl@dfe8a50
根据代码和打印的Log日志我们可以证明第一条。
下图我们可以看到,我们的内部存储空间多了一个文件夹shared_prefs,这个文件夹其实就是所有通过SharedPreferences方法存储数据的文件夹,就是说通过SharedPreferences保存的数据都在shared_prefs文件夹里面。但是该文件夹是个空的,里面并没有我们想要创建的”hxy”这个文件,所以也证明了第二条。
这些都是大家经常用的操作,就简单看下代码和结果。
private void putSharedPreferences() {
SharedPreferences sp = getSharedPreferences("hxy", MODE_PRIVATE);
SharedPreferences.Editor edit = sp.edit();
edit.putString("userName", "xavier");
edit.putInt("age", 23);
edit.apply();
// boolean commit = edit.commit();
}
private void getSp() {
SharedPreferences sp = getSharedPreferences("hxy", MODE_PRIVATE);
String userName = sp.getString("userName", "");
int age = sp.getInt("age", 0);
Log.e("sp", "userName == " + userName + " , age == " + age);
}
Log日志:
08-29 06:32:41.663 29764-29764/com.example.hxytest E/sp: userName == xavier , age == 23
Log日志表示我们成功读取到了数据,这里需要注意的是,edit.apply()和edit.commit(),代码中也可以看到commit()方法是有个boolean类型的返回值的,用来表示使用正常写入了数据。这两个方法的区别是,apply()是异步的,commit()是同步的,写入文件毕竟是个IO操作,所以及时写入的数据量很少,为了不阻塞我们的UI线程,一般还是推荐使用apply()方法来提交。
可以看到,系统是通过xml文件的方式存储sp的,而且也成功吧hxy.xml写入到了shared_prefs文件夹里。
上面的方法是通过Context类里面的Context.getSharedPreferences(name,mode)方法来获取的。其实还可以通过Activity的getPreferences(mode)方法和PreferenceManager.getDefaultSharedPreferences(context)来获取。
private void putSharedPreferencesByPreferenceManager() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor edit = sp.edit();
edit.putString("saveType", "preferenceManager");
edit.apply();
}
private void putSharedPreferencesByActivity() {
SharedPreferences sp = getPreferences(MODE_PRIVATE);
SharedPreferences.Editor edit = sp.edit();
edit.putString("saveType", "activity");
edit.apply();
}
这个结果也是显而易见的。Activity.getPreferences(mode)方法会以当前类名为文件名,来存储sp。
PreferenceManager.getDefaultSharedPreferences(context)会以当前包名+”_preferences”为文件名,来存储sp。
目前android数据库存储的方式,也是被广泛应用了,但是应该已经没有人还通过SQLiteOpenHelper 调用getWritableDatabase()或者getReadableDatabase()方法获取到SQLiteDatabase 然后再 增删改查。。。都开始使用第三口方框架 比如 greenDAO,Realm,ActiveAndroid 等。
但是,通过原生的数据库操作sqlite的方法,其数据库.db文件,默认是保存在内部存储空间的 databases 文件夹里。在此也不进行演示了。
通过以上的分析,我想大家应该对以上的数据存储方式有了更清晰的认识。而且上面的存储方式其最终文件都保存在了系统内存内部,所以,我在开头把他们都归结为内部存储方式就是这样来的。
如有错误,欢迎指正~