Android存儲、上傳、下載

參考文章

1、徹底理解android中的內部存儲與外部存儲

2、谷歌官方文檔參考


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_READABLEMODE_WORLD_WRITEABLE可讀、可寫模式)已被棄用。從 Android N (7.0)開始,使用這些常量將會導致引發 SecurityException。這意味着,面向 Android N 和更高版本的應用無法按名稱共享私有文件,嘗試共享“file://”URI 將會導致引發 FileUriExposedException。 如果您的應用需要與其他應用共享私有文件,則可以將 FileProviderFLAG_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中的資源文件到手機

存儲assetsraw目錄下的資源到手機

這裏寫圖片描述

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);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章