Android缓存机制——一般存储实现
一、Android缓存机制
Android缓存分为内存缓存和文件缓存(磁盘缓存)。在早期,各大图片缓存框架流行之前,常用的内存缓存方式是软引用(SoftReference)和弱引用(WeakReference),如大部分的使用方式:HashMap<String url, SoftReference<Drawable>> imageCache;这种形式。从Android 2.3(Level 9)开始,垃圾回收器更倾向于回收SoftReference或WeakReference对象,这使得SoftReference和WeakReference变得不是那么实用有效。同时,到了Android 3.0(Level 11)之后,图片数据Bitmap被放置到了内存的堆区域,而堆区域的内存是由GC管理的,开发者也就不需要进行图片资源的释放工作,但这也使得图片数据的释放无法预知,增加了造成OOM的可能。因此,在Android3.1以后,Android推出了LruCache这个内存缓存类,LruCache中的对象是强引用的。
二、二级缓存工作机制
所谓二级缓存实际上并不复杂,当Android端需要获得数据时比如获取网络中的图片,我们首先从内存中查找(按键查找),内存中没有的再从磁盘文件或sqlite中去查找,若磁盘中也没有才通过网络获取;当获得来自网络的数据,就以key-value对的方式先缓存到内存(一级缓存),同时缓存到文件或sqlite中(二级缓存)。注意:内存缓存会造成堆内存泄露,所有一级缓存通常要严格控制缓存的大小,一般控制在系统内存的1/4。
三、离线缓存
离线缓存就是在网络畅通的情况下将从服务器收到的数据保存到本地,当网络断开之后直接读取本地文件中的数据。本质就是要控制好文件的存储、读取。
我们的应用程序一般会产生以下几种类型的数据:
file-普通的文件存储
database-数据库文件(.db文件)
sharedPreference-配置数据(.xml文件)
cache-图片缓存文件
应用内数据的所有路径:
/data/data/com.xxx.xxx/cache - 应用内缓存(注:对应方法getCacheDir())
/data/data/com.xxx.xxx/databases - 应用内数据库
/data/data/com.xxx.xxx/shared_prefs - 应用内配置文件
/data/data/com.xxx.xxx/files - 应用内文件(注:对应方法getFilesDir())
SD卡的文件(开发者自定义的)
不管是内置还是外置SD卡,获取路径的方法是一样:
获取SD卡根目录:Environment.getExternalStorageDirectory().getAbsolutePath();
外部Cache路径:/mnt/sdcard/android/data/com.xxx.xxx/cache 一般存储缓存数据(注:通过getExternalCacheDir()获取)
外部File路径:/mnt/sdcard/android/data/com.xxx.xxx/files 存储长时间存在的数据 (注:通过getExternalFilesDir(String type)获取, type为特定类型,可以是以下任何一种
Environment.DIRECTORY_MUSIC, Environment.DIRECTORY_PODCASTS, Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS, Environment.DIRECTORY_PICTURES, or Environment.DIRECTORY_MOVIES. )
四、Android常用存储:
Android平台进行数据存储的五大方式,分别如下:
-
1 使用SharedPreferences存储数据
-
2 文件存储数据
-
3 SQLite数据库存储数据
-
4 使用ContentProvider存储数据
-
5 网络存储数据
以下介绍了前两种的使用,数据库可以使用库比较方便,如:greenDao; 其他不过多介绍。主要针对缓存做介绍。
(一)、SharedPreferences详解
使用 SharedPreferences 保存key-value对的步骤一般是这样:
使用Activity类的getSharedPreferences方法获取到 SharedPreferences 对象,指定文件名和访问权限
获得SharedPreferences.Editor对象,并使用该对象的 putXxx方法保存key-value对。
通过SharedPreferences.Editor的commit方法保存(提交)key-value对。
(1)获取SharedPreferences的两种方式:
1 调用Context对象的getSharedPreferences()方法
2 调用Activity对象的getPreferences()方法
(2)两种方式的区别:
-
调用Context对象的getSharedPreferences()方法获得的SharedPreferences对象可以被同一应用程序下的其他组件共享.
-
调用Activity对象的getPreferences()方法获得的SharedPreferences对象只能在该Activity中使用.
(3)SharedPreferences的四种操作模式:
Context.MODE_PRIVATE
Context.MODE_APPEND
Context.MODE_WORLD_READABLE
Context.MODE_WORLD_WRITEABLE
-
Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容
-
Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件.
-
Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件.
-
MODE_WORLD_READABLE:表示当前文件可以被其他应用读取.
-
MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入.
(4)将数据保存至SharedPreferences:
SharedPreferences sp = getSharedPreferences("sp_demo", Context.MODE_PRIVATE);
sp.edit().putString("name", "小张").putInt("age", 11).commit();
或者下面的写法也可以
SharedPreferences preferences=getSharedPreferences("user",Context.MODE_PRIVATE);
Editor editor=preferences.edit();
String name="xixi";
String age="22";
editor.putString("name", name);
editor.putString("age", age);
editor.commit();
(5)从SharedPreferences获取数据:
SharedPreferences preferences=getSharedPreferences("user", Context.MODE_PRIVATE);
String name=preferences.getString("name", "defaultname");
String age=preferences.getString("age", "0");
(二)、文件存储(File)详解
文件存储是 Android 中最基本的一种数据存储方式,它不对存储的内容进行任何的格式化处理,所有数据都是原封不动的保存到文件当中的。它比较适合用于存储一些简单的文本数据或二进制数据。如果你想使用文件存储方式来保存一些较为复杂的文本数据,就需要定义一套自己的格式规范,方便于之后将文件重新解析出来。
(1)Context 提供的文件存储方法(openFileOutput、openFileInput)
- Context 类中提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中。
- 这个方法接收两个参数,第一个参数是文件创建时使用的名称,注意这里指定的文件名不可以包含路径,因为所有文件都是默认储存到 /data/data/<包名>/files/ 目录下的。
- 第二个参数是文件的操作模式,主要有两种模式可选,MODE_PRIVATE(覆盖原文) 和 MODE_APPEND(追加内容) 。
- openFileOutput() 方法返回的是一个 FileOutputStream 对象,得到这个对象之后就可以使用 Java 流的方式将数据写入到文件中了。
(I)将一段文本内容保存到文件中:
public void save() {
String data = "hello to save";
FileOutputStream out = null;
BufferedWriter writer = null;
try {
//设置文件名称,以及存储方式
out = openFileOutput("data", Context.MODE_PRIVATE);
//创建一个OutputStreamWriter对象,传入BufferedWriter的构造器中
writer = new BufferedWriter(new OutputStreamWriter(out));
//向文件中写入数据
writer.write(data);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意:文件默认会存储到/data/data/package/files中;
-
类似于将数据存储到文件中,Context 类中还提供了一个 openFileInput()方法,用于从文件中读取数据。
-
openFileInput() 方法只接收一个参数,即要读取的文件名,然后系统会自动到 /data/data/<包名>/files/ 目录下去加载这个文件,并返回一个 FileInputStream 对象。
(II)展示如何从文件中读取文本数据:
public String load() {
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();
try {
//设置将要打开的存储文件名称
in = openFileInput("data");
//FileInputStream -> InputStreamReader ->BufferedReader
reader = new BufferedReader(new InputStreamReader(in));
String line = new String();
//读取每一行数据,并追加到StringBuilder对象中,直到结束
while ((line = reader.readLine()) != null) {
content.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return content.toString();
}
(III)四种文件保存的模式。
Context.MODE_PRIVATE 为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下写入的内容会覆盖原文件的内容。
Context.MODE_APPEND 检查文件是否存在,存在就往文件追加内容,否则就创建新文件。
MODE_WORLD_READABLE 表示当前文件可以被其他应用读取。
MODE_WORLD_WRITEABLE 表示当前文件可以被其他应用写入。
(2)内部存储:一般IO流操作File
注意内部存储不是内存。内部存储位于系统中很特殊的一个位置,如果你想将文件存储于内部存储中,那么文件默认只能被你的应用访问到,且一个应用所创建的所有文件都在和应用包名相同的目录下。也就是说应用创建于内部存储的文件,与这个应用是关联起来的。当一个应用卸载之后,内部存储中的这些文件也被删除。从技术上来讲如果你在创建内部存储文件的时候将文件属性设置成可读,其他app能够访问自己应用的数据,前提是他知道你这个应用的包名,如果一个文件的属性是私有(private),那么即使知道包名其他应用也无法访问。 内部存储空间十分有限,因而显得可贵,另外,它也是系统本身和系统应用程序主要的数据存储所在地,一旦内部存储空间耗尽,手机也就无法使用了。所以对于内部存储空间,我们要尽量避免使用。Shared Preferences和SQLite数据库都是存储在内部存储空间上的。内部存储一般用Context来获取和操作。
getFilesDir()获取你app的内部存储空间,相当于你的应用在内部存储上的根目录。
应用安装后都会在Android 根目录生成 /data/data/packagename,当前应用读取不需要读写权限
注意:
有些开发者可能看到过应用的根目录为 /data/user/0/packagename 的情况,这里解释一下,Android 4.2 版本添加了同一设备可以登录不同用户的功能(由于专利原因仅限于平板电脑,手机不支持此功能),所以为了区分不同用户在同一应用中的设置和存储的数据,添加了该系列的路径,该路径指向 /data/data/packagename
- getFileDir() 方法得到的是该目录下 files 文件夹的 File 对象
- getChacheDir() 方法得到的是该目录下 cache 文件夹的 File 对象
- 直接调用ContextWrapper的 openFileOutput(String name,int mode) 也会在该目录下 files 文件夹下创建相应的文件,并且是私密的。可以修改为其他应用可访问的,通过 openFileOutput 方法的 mode 参数来完成
注意:
-
该目录只有 root 权限下可以查看,会随着应用卸载删除
-
应用程序详情中清除数据会将 packagename 下所有数据以及内置存储、外置 SD 卡存储空间中 /Android/data/packagename 的整个目录删除删除
-
应用程序详情中清除缓存会将 packagename/cache 目录下所有数据以及内置存储、外置 SD 卡存储空间中 /Android/data/packagename/cache 的整个目录删除
如果是要创建一个文件,如下
File file = newFile(context.getFilesDir(), filename);`
安卓还为我们提供了一个简便方法 openFileOutput()`来读写应用在内部存储空间上的文件,下面是一个向文件中写入文本的例子:(参考以上)
String filename = "myfile";
String string = "Hello world!";
FileOutputStream outputStream;
try{
outputStream = openFileOutput(filename, Context.MODE_PRIVATE);
outputStream.write(string.getBytes());
outputStream.close();
} catch(Exception e) {
e.printStackTrace();
}
内部存储的其他一些操作:
A.列出所有的已创建的文件,这个可能不容易想到,Context
居然有这样的方法。
String[] files = Context.fileList();
for(String file : files) {
Log.e(TAG, "file is "+ file);
}
B.删除文件,能创建就要能够删除,当然也会提供了删除文件的接口,它也非常简单,只需要提供文件名
if(Context.deleteFile(filename)) {
Log.e(TAG, "delete file "+ filename + " sucessfully“);
} else {
Log.e(TAG, "failed to deletefile " + filename);
}
C.创建一个目录,需要传入目录名称,它返回 一个文件对象用到操作路径
File workDir = Context.getDir(dirName, Context.MODE_PRIVATE);
Log.e(TAG, "workdir "+ workDir.getAbsolutePath();
总结一下文件相关操作,可以得出以下三个特点:
- 文件操作只需要向函数提供文件名,所以程序自己只需要维护文件名即可;
- 不用自己去创建文件对象和输入、输出流,提供文件名就可以返回File对象或输入输出流
- 对于路径操作返回的都是文件对象。
(3)外部存储:IO(读写sdcard上的文件)
手机自带 ROM 的存储路径
1、getExternalCacheDir() 方法,获取内置存储卡中 /Android/data/packagename/cache 目录的路径,4.4及之后读写不需要权限,会随着应用卸载删除
2、getExternalFilesDir() 方法,获取内置存储卡中 /Android/data/packagename/files 目录的路径,4.4之后读写不需要权限,会随着应用卸载删除,该方法参数为 "null" 时不指定子文件夹,指定时创建子文件夹保存文件。创建的文件其他应用只要有读写权限也可以读取,如果要私密的就使用内部存储。
3、Environment.getExternalStorageDirectory() 方法得到的是内置存储目录的根路径目录,读写需要权限,不会随着应用卸载删除
4、Environment.getExternalStoragePublicDirectory() 方法得到的是内置存储目录根路径下的特定类型文件的公共目录,读写需要权限,不会随着应用卸载删除
5、注意,内置存储中 /Android/data/packagenaem/ 路径的读写 Android 版本不同,需要权限不同,所以开发时应始终声明读写权限
其中读写步骤按如下进行:
1、调用Environment的getExternalStorageState()方法判断手机上是否插了sd卡,且应用程序具有读写SD卡的权限,如下代码将返回true
Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)
2、调用Environment.getExternalStorageDirectory()方法来获取外部存储器,也就是SD卡的目录,或者使用"/mnt/sdcard/"目录
3、使用IO流操作SD卡上的文件
注意点:手机应该已插入SD卡,对于模拟器而言,可通过mksdcard命令来创建虚拟存储卡
必须在AndroidManifest.xml上配置读写SD卡的权限
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
(I)文件写操作函数
private void write(String content) {
if (Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) { // 如果sdcard存在
File file = new File(Environment.getExternalStorageDirectory()
.toString()
+ File.separator
+ DIR
+ File.separator
+ FILENAME); // 定义File类对象
if (!file.getParentFile().exists()) { // 父文件夹不存在
file.getParentFile().mkdirs(); // 创建文件夹
}
PrintStream out = null; // 打印流对象用于输出
try {
out = new PrintStream(new FileOutputStream(file, true)); // 追加文件
out.println(content);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close(); // 关闭打印流
}
}
} else { // SDCard不存在,使用Toast提示用户
Toast.makeText(this, "保存失败,SD卡不存在!", Toast.LENGTH_LONG).show();
}
}
II)文件读操作函数
private String read() {
if (Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) { // 如果sdcard存在
File file = new File(Environment.getExternalStorageDirectory()
.toString()
+ File.separator
+ DIR
+ File.separator
+ FILENAME); // 定义File类对象
if (!file.getParentFile().exists()) { // 父文件夹不存在
file.getParentFile().mkdirs(); // 创建文件夹
}
Scanner scan = null; // 扫描输入
StringBuilder sb = new StringBuilder();
try {
scan = new Scanner(new FileInputStream(file)); // 实例化Scanner
while (scan.hasNext()) { // 循环读取
sb.append(scan.next() + "\n"); // 设置文本
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (scan != null) {
scan.close(); // 关闭打印流
}
}
} else { // SDCard不存在,使用Toast提示用户
Toast.makeText(this, "读取失败,SD卡不存在!", Toast.LENGTH_LONG).show();
}
return null;
}
五、总结的一些使用代码:
常用存储:
小的数据体,参数,值(存储在shared_prefs)
网络数据json(这种数据一般不大,存储在文件中file(.txt等等) )
图片、音乐、视频(建议数据较大的存储在SD卡中,图片(有框架的用框架))
(一)小的数据(SharedPrefs)
工具类
public class AppConfig {
private static AppConfig appConfig;
private SharedPreferences preferences;
/**
* App根目录.
*/
public String APP_PATH_ROOT;
private AppConfig() {
preferences = MyApplication.getInstance().getSharedPreferences("test", Context.MODE_PRIVATE);
APP_PATH_ROOT = FileUtil.getRootPath(MyApplication.getInstance()).getAbsolutePath() + File.separator +
"test";
}
public synchronized static AppConfig getInstance() {
if (appConfig == null)
appConfig = new AppConfig();
return appConfig;
}
/*
public void initialize() {
IOUtils.createFolder(APP_PATH_ROOT);
}*/
public void putInt(String key, int value) {
preferences.edit().putInt(key, value).commit();
}
public int getInt(String key, int defValue) {
return preferences.getInt(key, defValue);
}
public void putString(String key, String value) {
preferences.edit().putString(key, value).commit();
}
public String getString(String key, String defValue) {
return preferences.getString(key, defValue);
}
public void putBoolean(String key, boolean value) {
preferences.edit().putBoolean(key, value).commit();
}
public boolean getBoolean(String key, boolean defValue) {
return preferences.getBoolean(key, defValue);
}
public void putLong(String key, long value) {
preferences.edit().putLong(key, value).commit();
}
public long getLong(String key, long defValue) {
return preferences.getLong(key, defValue);
}
public void putFloat(String key, float value) {
preferences.edit().putFloat(key, value).commit();
}
public float getFloat(String key, float defValue) {
return preferences.getFloat(key, defValue);
}
}
(二)文件存储——网络数据(string-->txt)
存储数据
/**
* 存储文件:.txt
*
* @param fileName 文件名称
* @param content 存储的文件内容
*/
public static void wirteCache(String fileName, String content) {
//设置缓存
File file = new File(MyApplication.getInstance().getCacheDir(), fileName + ".txt");
if (file.exists()) {
boolean isdelete = file.delete();
if (isdelete) {
Log.e("delete", "success");
}
} else {
byte[] bytes = content.getBytes();
FileOutputStream output = null;
try {
output = new FileOutputStream(file);
output.write(bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
读取数据
/**
* 读取缓存数据
*
* @param fileName
* @return
*/
public static String readCache(String fileName) {
String result = null;
// 读取文件
File file = new File(MyApplication.getInstance().getCacheDir() + File.separator
+ fileName + ".txt");
if (file.exists()) {
FileInputStream in = null;
try {
in = new FileInputStream(file);
byte[] buffer;
buffer = new byte[in.available()];
int count = 0;
while ((count = in.read(buffer)) > 0) {
result = new String(buffer);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
} else {
//不存在缓存
return "";
}
return result;
}
(三)文件存储——图片,音乐,视频
存储
/**
* 文件缓存策略
*
* @param url 文件路径
* @param fileName 文件名称
* @param fileFormat 文件格式
*/
public static void downFile(String url, String fileName, String fileFormat) {
try {
String path = "";
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
//判断sd卡是否存在,不存在,存储在应用内部
path = Environment.getExternalStorageDirectory() + "/";
Log.e(TAG, "Storage file");
} else {
path = MyApplication.getInstance().getFilesDir() + "/";
Log.e(TAG, "APP file");
}
File fileDir = new File(path + "test");
if (!fileDir.exists()) {
fileDir.mkdir();
}
File file = new File(fileDir.getAbsolutePath() + "/" + fileName + "." + fileFormat);
if (file.exists()) {
} else {
URL myURL = new URL(url);
URLConnection conn = myURL.openConnection();
conn.connect();
InputStream is = conn.getInputStream();
int fileSize = conn.getContentLength();
if (fileSize <= 0)
throw new RuntimeException("can not know the file`s size");
if (is == null)
throw new RuntimeException("stream is null");
FileOutputStream fos = new FileOutputStream(file.getPath());
byte buf[] = new byte[1024];
while (true) {
// 循环读取
int numread = is.read(buf);
if (numread == -1) {
break;
}
fos.write(buf, 0, numread);
}
try {
is.close();
} catch (Exception ex) {
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
读取
/**
* 获取文件地址
*
* @param fileName 文件名称
* @param fileFormat 文件格式
* @return
*/
public static String getDownFilePath(String fileName, String fileFormat) {
String path = "";
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
//判断sd卡是否存在,不存在,存储在应用内部
path = Environment.getExternalStorageDirectory() + "/";
} else {
path = MyApplication.getInstance().getFilesDir() + "/";
Log.e(TAG, "cache file");
}
// 读取文件
File file = new File(path + "test" + File.separator
+ fileName + "." + fileFormat);
if (file.exists()) {
return file.getAbsolutePath();
} else {
return "";
}
}