参考文章
1、 需求
在android开发中常用的存储、上传、下载,之前反反复复写过很多遍,现在进行一些整理,方便后面直接搬运代码。尤其是在android7.0及以上版本中又加强了对存储安全的控制,所以整理整理还是很有必要的。
2、 认识android的存储系统
android中的存储类型
- 共享首选项
在键值对中存储私有原始数据。 - 内部存储
在设备内存中存储私有数据。 - 外部存储
在共享的外部存储中存储公共数据。 - SQLite 数据库
在私有数据库中存储结构化数 - 网络连接
在网络中使用您自己的网络服务器存储数据。
###2.1、使用共享首选项
最常用的功能:记住密码、保存登录信息
SharedPreferences 可以保存和检索原始数据类型(布尔值、浮点值、整型值、长整型和字符串)的永久性键值(key-value)对, 此数据将永久保留(在应用卸载时会被清除)。
AS 中 DDMS 打开位置:/data/data//shared_prefs
2.1.1、获取SharedPreferences 对象
getSharedPreferences() - 如果您需要多个按名称(使用第一个参数指定)识别的首选项文件,请使用此方法。
getPreferences() - 如果您只需要一个用于 Activity 的首选项文件,请使用此方法。 由于这将是用于 Activity 的唯一首选项文件,因此无需提供名称。
2.1.2、写入值
调用 edit() 以获取 SharedPreferences.Editor;
使用 putBoolean() 和 putString() 等方法添加值;
使用 commit() 提交新值。
2.1.3、取值
使用 getBoolean() 和 getString() 等 SharedPreferences 方法。
2.1.4、示例代码
public class Calc extends Activity {
public static final String PREFS_NAME = "MyPrefsFile";
@Override
protected void onCreate(Bundle state){
super.onCreate(state);
. . .
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
boolean silent = settings.getBoolean("silentMode", false);
setSilent(silent);
}
@Override
protected void onStop(){
super.onStop();
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("silentMode", mSilentMode);
editor.commit();
}
}
...
SharedPreferences sp = getSharedPreferences("user", MODE_PRIVATE);
public String getNickName() {
return sp.getString("nickname", "");
}
public void setNickName(String nickname) {
sp.edit().putString("nickname", nickname).commit();
}
...
2.1.5、用户首选项
严格来说,共享首选项并非用于保存“用户首选项”,例如用户所选择的铃声。 如果您有兴趣为您的应用创建用户首选项,请参阅 PreferenceActivity,其中为您提供了一个 Activity 框架,用于创建将会自动永久保留(通过共享首选项)的用户首选项。
###2.2、使用内部存储InternalStorage
可以直接在设备的内部存储中保存文件。默认情况下,保存到内部存储的文件是应用的私有文件,其他应用(和用户)不能访问这些文件。 当用户卸载应用时,这些文件也会被移除。
2.2.1、创建私有文件并写入到内部存储
- 1、使用文件名称和操作模式调用 openFileOutput()。 这将返回一个 FileOutputStream;
- 2、使用 write() 写入到文件;
- 3、使用 close() 关闭流式传输。
String FILENAME = "hello_file";
String string = "hello world!";
FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(string.getBytes());
fos.close();
操作模式有MODE_PRIVATE 将会创建文件(或替换具有相同名称的文件),并将其设为应用的私有文件。 其他可用模式包括:MODE_APPEND(追加)、MODE_WORLD_READABLE(可读) 和 MODE_WORLD_WRITEABLE(可写)。
自 API 级别 17 以来,常量 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE (可读、可写模式)已被弃用。从 Android N (7.0)开始,使用这些常量将会导致引发 SecurityException。这意味着,面向 Android N 和更高版本的应用无法按名称共享私有文件,尝试共享“file://”URI 将会导致引发 FileUriExposedException。 如果您的应用需要与其他应用共享私有文件,则可以将 FileProvider 与 FLAG_GRANT_READ_URI_PERMISSION 配合使用。另请参阅共享文件,**我的这篇文章中也讲到了这个。**主要有这几步:
- 1、定义一个FileProvider
- 2、指定共享文件的目录
- 3、使用FileProvider
2.2.2、从内部存储读取文件
- 1、调用 openFileInput() 并向其传递要读取的文件名称。 这将返回一个 FileInputStream。
- 2、使用 read() 读取文件字节。
- 3、然后使用 close() 关闭流式传输。
String FILENAME = "hello_file";
FileInputStream fis = openFileInput(FILENAME);
fis.read(new byte[1024]);
fis.close();
2.2.3、保存缓存文件
如果您想要缓存一些数据,而不是永久存储这些数据,应该使用 getCacheDir() 来打开一个 File,它表示您的应用应该将临时缓存文件保存到的内部目录。
当设备的内部存储空间不足时,Android 可能会删除这些缓存文件以回收空间。 但您不应该依赖系统来为您清理这些文件, 而应该始终自行维护缓存文件,使其占用的空间保持在合理的限制范围内(例如 1 MB)。 当用户卸载您的应用时,这些文件也会被移
2.2.4、内部存储的其他实用方法
- getFilesDir()
获取 存储内部文件 的文件系统目录 的绝对路径。 - getDir()
在您的内部存储空间内创建(或打开现有的)目录。 - deleteFile()
删除保存在内部存储的文件。 - fileList()
返回您的应用当前保存的一系列文件。
2.3、使用外部存储ExternalStorage
外部存储(ExternalStorage)指的是可移除的存储介质(例如 SD 卡)或内部(不可移除)存储。
这个“内部(不可移除)存储”可以理解为:分配某个内部存储器分区用作外部存储。
保存到外部存储的文件是全局可读取文件,而且,在计算机上启用 USB 大容量存储以传输文件后,可由用户修改这些文件。
说到这里可能你有点疑惑,为啥有两个内部存储?其实,
内存,我们在英文中称作memory,内部存储,我们称为InternalStorage,外部存储我们称为ExternalStorage,这在英文中本不会产生歧义,但是当我们翻译为中文之后,前两个都简称为内存,于是,混了。
请移步参考文档1、彻底理解android中的内部存储与外部存储
2.3.1、 使用作用域目录访问
官方文档上提到了一个使用作用域目录访问,这个是因为在您的应用清单文件中请求 READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE 权限后将允许应用访问外部存储上的所有公共目录,这可能导致访问的内容超出应用需要的内容,如果应用只需要访问外部存储中的特定目录,就可以使用作用域目录访问了。
**2.3.2、获取外部存储的访问权限 **
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
从 Android 4.4 开始,如果您仅仅读取或写入应用的私有文件(/data/data/包名,下的文件),则不需要这些权限。
**2.3.3、检查存储是否可用 **
有以下方法
/* Checks if external storage is available for read and write */
public boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}
/* Checks if external storage is available to at least read */
public boolean isExternalStorageReadable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return true;
}
return false;
}
2.3.4、与其他应用共享文件
与其他应用共享文件,可以将文件存储到公共文件夹下,例如 Music/、Pictures/ 和 Ringtones/ 等。
获取方式:
类型有DIRECTORY_MUSIC、DIRECTORY_PICTURES、 DIRECTORY_RINGTONES
或其他类型
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
你也可以在这些公共目录下创建自己的子目录,例如:
public File getAlbumStorageDir(String albumName) {
// Get the directory for the user's public pictures directory.
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
2.3.5、在媒体扫描程序中隐藏您的文件
如果你想把自己的文件放到外部存储中,又不想让 MediaStore扫描到的话,你可以在目录中添加一个.nomedia
的空文件(No Media,不让MediaStore扫描)。 但是如果你的文件是应用的私有文件,那你还是应该将其保存在应用的私有目录中。
2.3.6、保存应用的私有文件
如果你的文件不想让其他应用使用,你可以将文件存到应用的私有文件目录下。
通过调用 getExternalFilesDir()
来获取应用在外部存储上的私有存储目录。在内部存储上的私有存储目录是通过getFilesDir()
来获取的。
这个方法需要传一个类型File getExternalFilesDir (String type)
,这些类型有DIRECTORY_MUSIC、DIRECTORY_MOVIES等等,
具体的可以查看这里
同样的和内部存储是提到的一样,从 Android 4.4 开始,读取或写入应用私有目录中的文件不再需要 READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE 权限。
2.3.7、缓存文件夹的使用
有时候应用会临时产生一些数据需要存储,这时我们就可以利用缓存文件夹了,
外部存储目录下的缓存目录,可以通过 getExternalCacheDir()
方法获取到。
在使用缓存的时候,谷歌的建议是:为节省文件空间并保持应用性能,您应该在应用的整个生命周期内仔细管理您的缓存文件并移除其中不再需要的文件,这一点非常重要。
要自己管理缓存,而不是等到系统自动回收。
###2.4、使用SQLite 数据库
虽然我一般都是使用郭霖的LitePal进行相关的操作的,但是还是需要了解原生的相关操作方法。另外还有一些相关的框架可以了解,比如GreenDao、OrmLite、Realm
LitePal的使用可以去郭霖的blog看,上面有一个系列,讲解的很详细。我的这篇文章中有提到-LitePal结合SQLCipher实现DB数据库操作和加密
Android数据库高手秘籍(二)——创建表和LitePal的基本用法中对传统建表和LitePal的使用做了详细介绍。
创建新 SQLite 数据库的推荐方法是创建 SQLiteOpenHelper 的子类并覆盖 onCreate() 方法,在此方法中,您可以执行 SQLite 命令以创建数据库中的表。
引用Android数据库高手秘籍(二)——创建表和LitePal的基本用法中的部分内容:
比如说我们想新建一张news表,其中有title,content,publishdate,commentcount这几列,分别代表着新闻标题、新闻内容、发布时间和评论数,那么代码就可以这样写:
public class MySQLiteHelper extends SQLiteOpenHelper {
public static final String CREATE_NEWS = "create table news ("
+ "id integer primary key autoincrement, "
+ "title text, "
+ "content text, "
+ "publishdate integer,"
+ "commentcount integer)";
public MySQLiteHelper(Context context, String name, CursorFactory factory,
int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_NEWS);
}
...
}
可以看到,我们把建表语句定义成了一个常量,然后在onCreate()方法中去执行了这条建表语句,news表也就创建成功了。这条建表语句虽然简单,但是里面还是包含了一些小的细节,我来解释一下。首先,根据数据库的范式要求,任何一张表都应该是有主键的,所以这里我们添加了一个自增长的id列,并把它设为主键。然后title列和content列都是字符串类型的,commentcount列是整型的,这都很好理解,但是publishdate列该怎么设计呢?由于SQLite中并不支持存储日期这种数据类型,因此我们需要将日期先转换成UTC时间(自1970年1月1号零点)的毫秒数,然后再存储到数据库中,因此publishdate列也应该是整型的。
现在,我们只需要获取到SQLiteDatabase的实例,数据库表就会自动创建了,如下所示:
SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 1);
SQLiteDatabase db = dbHelper.getWritableDatabase();
关于LitePal的学习请移步郭霖的Android数据库高手秘籍专栏
- 谷歌提示 Android 没有实施标准 SQLite 概念之外的任何限制。我们推荐包含一个可用作唯一 ID 的自动增量值关键字段,以便快速查找记录。 私有数据不要求这样做,但如果您实现了一个内容提供程序,则必须包含使用 BaseColumns._ID 常量的唯一 ID。
3、 存储apk中的资源文件到手机
存储assets
和raw
目录下的资源到手机
public class Utils {
public final String mmpk_name = "GisTest.mmpk"; //文件名字
public final String File_name = "GisTest.tpk"; //文件名字
public final String Package_name = "com.cnbs.gisdemo"; //项目包路径
public final String Save_Path = "/data"
+ Environment.getDataDirectory().getAbsolutePath()+"/"
+ Package_name
+"/arcgis";
public void saveRawToSD(Context context) {
try {
String filename = Save_Path + "/" + File_name;
File dir = new File(Save_Path);
if (!dir.exists()) {
dir.mkdir();
}
if (!(new File(filename)).exists()) {
InputStream is = context.getResources().openRawResource(R.raw.gistest);
FileOutputStream fos = new FileOutputStream(filename);
byte[] buffer = new byte[1024];
int count = 0;
while ((count = is.read(buffer)) > 0) {
fos.write(buffer, 0, count);
}
fos.close();
is.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void saveAssetsToSD(Context context) {
try {
String filename = Save_Path + "/" + File_name;
File dir = new File(Save_Path);
if (!dir.exists()) {
dir.mkdir();
}
if (!(new File(filename)).exists()) {
InputStream is = context.getResources().getAssets().open("GisTest.tpk");
FileOutputStream fos = new FileOutputStream(filename);
byte[] buffer = new byte[1024];
int count = 0;
while ((count = is.read(buffer)) > 0) {
fos.write(buffer, 0, count);
}
fos.close();
is.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
4、应用下载文件存储到手机
可以参考我的这篇blog Android应用更新详解,兼容7.0
有两种方式,1、用HttpURLConnection下载,代码参考如下,2、用DownloadManager来下载,参考链接的文章。
/**
* 从服务器中下载APK
*/
@SuppressWarnings("unused")
public static void downLoadApk(final Context mContext, final String downURL, final String appName) {
final ProgressDialog pd; // 进度条对话框
pd = new ProgressDialog(mContext);
pd.setCancelable(false);// 必须一直下载完,不可取消
pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setMessage("正在下载安装包,请稍后");
pd.setTitle("版本升级");
pd.setProgressNumberFormat("%1d MB /%2d MB");
pd.show();
new Thread() {
@Override
public void run() {
try {
File file = downloadFile(downURL, appName, pd);
sleep(3000);
installApk(mContext, file);
// 结束掉进度条对话框
pd.dismiss();
} catch (Exception e) {
pd.dismiss();
}
}
}.start();
}
/**
* 从服务器下载最新更新文件
*
* @param path 下载路径
* @param pd 进度条
* @return
* @throws Exception
*/
private static File downloadFile(String path, String appName, ProgressDialog pd) throws Exception {
// 如果相等的话表示当前的sdcard挂载在手机上并且是可用的
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
// 获取到文件的大小
int fileSize = conn.getContentLength() / 1024/ 1024; //KB
pd.setMax(fileSize);
InputStream is = conn.getInputStream();
String fileName = SD_FOLDER + appName + ".apk";
File file = new File(fileName);
try {
// 目录不存在创建目录
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
} catch (Exception e) {
// TODO: handle exception
}
FileOutputStream fos = new FileOutputStream(file);
BufferedInputStream bis = new BufferedInputStream(is);
byte[] buffer = new byte[1024];
int len;
int total = 0;
while ((len = bis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
total += len;
// 获取当前下载量
pd.setProgress(total/1024/ 1024);
}
fos.close();
bis.close();
is.close();
return file;
} else {
throw new IOException("未发现有SD卡");
}
}
如果需要使用Retrofit 网络框架进行下载,可以参考博文
Retrofit 2.0 超能实践(四),完成大文件断点下载
5、手机中的文件上传
以上传图片为例
5-1、上传单张图片
也可以参考Retrofit 2.0 超能实践(三),轻松实现多文件/图片上传/Json字符串/表单
private String tmpPic = "";
private void uploadPic(File file) {
Map<String,String> map = new HashMap<>();
map.put("type","userHeadImg");
map.put("userId",MyApplication.getInstance().getUser().getUserId()+"");
Map<String,RequestBody> obj = new HashMap<>();
RequestBody fbody = RequestBody.create(MediaType.parse("image/*"), file);
obj.put("Imgs\";filename=\"icon.jpg",fbody);
Subscriber subscriber = new Subscriber<HttpResult.BaseResponse<String>>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
Toast.makeText(SetInfoActivity.this,"请稍后再试", Toast.LENGTH_SHORT).show();
}
@Override
public void onNext(HttpResult.BaseResponse<String> response) {
if (response.code==1) {
// Toast.makeText(SetInfoActivity.this,"上传成功", Toast.LENGTH_SHORT).show();
tmpPic = response.obj;
Uri uri = Uri.parse(HttpMethods.BASE_URL+tmpPic);
simpleDraweeView.setImageURI(uri);
userBean.setHeadImg(tmpPic);
} else {
Toast.makeText(SetInfoActivity.this,"上传失败", Toast.LENGTH_SHORT).show();
}
}
};
HttpMethods.getInstance().uploadPic(subscriber, map, obj);
}
接口设置
//上传图片(单张)
public void uploadPic(Subscriber<HttpResult.BaseResponse<String>> subscriber, Map<String, String> options,Map<String, RequestBody> obj) {
Observable observable = networkServicePic.uploadPic(options, obj);
toSubscribe(observable, subscriber);
}
//上传图片(单张)
@Multipart
@POST("userInfoAct/uploadHeadImg.html")
Observable<HttpResult.BaseResponse<String>> uploadPic(@QueryMap Map<String, String> options, @PartMap Map<String, RequestBody> obj);
5-2、上传多张图片
利用List上传多张图片
private void uploadImg(List<AdjunctList> list, int taskType, int taskId, int taskPointId, final int localFlawId) {
List<File> listFile = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
AdjunctList adjunct = list.get(i);
String fileUri = adjunct.getFile_uri();
File file = new File(fileUri);
listFile.add(file);
}
List<MultipartBody.Part> parts = UploadFileUtils.filesToMultipartBodyParts(listFile);
Map<String, String> options = new HashMap<>();
options.put("userId", userId + "");
options.put("token", tokenTIme);
options.put("taskId", taskId + "");
options.put("taskPointId", taskPointId + "");
options.put("taskType", taskType + "");
HttpMethods.getInstance().uploadAdjunct(new Subscriber<HttpResult.TaskUploadResponse>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(final HttpResult.TaskUploadResponse response) {
String code = response.code;
if ("0".equals(code)) {
ContentValues values = new ContentValues();
values.put("is_upload", MConstant.UP_Success + "");
DataSupport.updateAll(AdjunctList.class, values,
"user_id =? and local_flaw_id =?", userId + "", localFlawId + "");
} else if ("2".equals(code)) { //强制下线
new CenterHintToast(mActivity, mActivity.getResources().getString(R.string.logout_hint));
MyUtils.forceExit(mActivity);
} else {
//上传失败,的记录
ContentValues values = new ContentValues();
values.put("is_upload", MConstant.UP_Filed + "");
DataSupport.updateAll(AdjunctList.class, values,
"user_id =? and local_flaw_id =?", userId + "", localFlawId + "");
}
}
}, options, parts);
}
接口设置
//上传任务附件
public void uploadAdjunct(Subscriber<HttpResult.TaskUploadResponse> subscriber, Map<String, String> options, List<MultipartBody.Part> parts) {
Observable observable = networkService.uploadAdjunct(options,parts);
toSubscribe(observable,subscriber);
}
//上传任务附件
@Multipart
@POST("api/business/uploadAct/uploadImgs")
Observable<HttpResult.TaskUploadResponse> uploadAdjunct(@QueryMap Map<String, String> options, @Part() List<MultipartBody.Part> parts);