Android儲存、上傳、下載
參考文章
1、 需求
在android開發中常用的儲存、上傳、下載,之前反反覆覆寫過很多遍,現在進行一些整理,方便後面直接搬運程式碼。尤其是在android7.0及以上版本中又加強了對儲存安全的控制,所以整理整理還是很有必要的。
2、 認識android的儲存系統
android中的儲存型別
- 共享首選項
在鍵值對中儲存私有原始資料。 - 內部儲存
在裝置記憶體中儲存私有資料。 - 外部儲存
在共享的外部儲存中儲存公共資料。 - SQLite 資料庫
在私有資料庫中儲存結構化數 - 網路連線
在網路中使用您自己的網路伺服器儲存資料。
2.1、使用共享首選項
最常用的功能:記住密碼、儲存登入資訊
SharedPreferences 可以儲存和檢索原始資料型別(布林值、浮點值、整型值、長整型和字串)的永久性鍵值(key-value)對, 此資料將永久保留(在應用解除安裝時會被清除)。
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資料庫操作和加密
建立新 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();
- 谷歌提示 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、應用下載檔案儲存到手機
有兩種方式,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.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; //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);
}
fos.close();
bis.close();
is.close();
return file;
} else {
throw new IOException("未發現有SD卡");
}
}
5、手機中的檔案上傳
以上傳圖片為例
5-1、上傳單張圖片
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);