今天在做SD卡的代碼優(yōu)化的工作。之前公司的應(yīng)用是在MainActivity中申請(qǐng)讀寫SD卡權(quán)限,如果用戶選擇了拒絕,那么直接彈窗提示用戶必須賦予SD卡讀寫權(quán)限,否則將直接退出應(yīng)用。雖然微信等app都是這樣的邏輯,但是還是覺(jué)得很不友好。在如今這個(gè)Android手機(jī)的大環(huán)境中,SD讀寫權(quán)限沒(méi)有那么十分嚴(yán)重。
因此,我們將對(duì)這里的邏輯進(jìn)行改造。
1. Android中的內(nèi)部存儲(chǔ)與外部存儲(chǔ)
Android SD卡主要有兩種存儲(chǔ)方式 Internal 、 External Storage
Internal內(nèi)部存儲(chǔ),應(yīng)用私有目錄
這個(gè)目錄的特點(diǎn)是:
- 內(nèi)部存儲(chǔ)不需要申請(qǐng)任何權(quán)限
- 這個(gè)目錄始終可用,這個(gè)文件夾用于 App 中的 WebView 緩存頁(yè)面信息,SharedPreferences 和 SQLiteDatabase 持久化應(yīng)用相關(guān)數(shù)據(jù)等。
- 當(dāng)用戶卸載 App 時(shí),系統(tǒng)自動(dòng)刪除 data/data 目錄下對(duì)應(yīng)包名的文件夾及其內(nèi)容。
對(duì)于沒(méi)有root的手機(jī)是沒(méi)辦法看到data/data目錄的,但是我們可以通過(guò)Androidstudio提供的Device File Explorer來(lái)查看。

External Storage外部存儲(chǔ)
外部存儲(chǔ)又分為 外部私有存儲(chǔ) 、外部公有存儲(chǔ)
Private files 外部存儲(chǔ)空間中的應(yīng)用私有目錄
考慮內(nèi)部存儲(chǔ)空間容量有限,普通用戶不能直接直觀地查看目錄文件等其他原因,Android 在外部存儲(chǔ)空間中也提供有特殊目錄供應(yīng)用存放私有文件,文件路徑為:
/storage/emulated/0/Android/data/app package name
它的特點(diǎn)是:
默認(rèn)情況下,系統(tǒng)并不會(huì)自動(dòng)創(chuàng)建外部存儲(chǔ)空間的應(yīng)用私有目錄。
宿主 App 可以直接讀寫內(nèi)部存儲(chǔ)空間中的應(yīng)用私有目錄;而在 4.4 版本開(kāi)始,宿主 App 才可以直接讀寫外部存儲(chǔ)空間中的應(yīng)用私有目錄,使開(kāi)發(fā)人員無(wú)需在 Manifest 文件中或者動(dòng)態(tài)申請(qǐng)外部存儲(chǔ)空間的文件讀寫權(quán)限
當(dāng)用戶卸載 App 時(shí),系統(tǒng)也會(huì)自動(dòng)刪除外部存儲(chǔ)空間下的對(duì)應(yīng) App 私有目錄文件夾及其內(nèi)容。
自 Android 7.0 開(kāi)始,系統(tǒng)對(duì)應(yīng)用私有目錄的訪問(wèn)權(quán)限進(jìn)一步限制。其他 App 無(wú)法通過(guò) file:// 這種形式的 Uri 直接讀寫該目錄下的文件內(nèi)容,而是通過(guò) FileProvider 訪問(wèn)。
Public files 外部存儲(chǔ)空間中的公共目錄
這里說(shuō)的就是我們平時(shí)所看到的存儲(chǔ)目錄了,用戶可以隨意在里面進(jìn)行創(chuàng)建刪除等操作。這里面保存的大多是一些與應(yīng)用無(wú)關(guān)的數(shù)據(jù),當(dāng)應(yīng)用被卸載,用戶仍然希望保留于設(shè)備當(dāng)中的信息。常見(jiàn)如,拍照類應(yīng)用的圖片文件,用戶是使用瀏覽器手動(dòng)下載的文件等。
在這里讀寫目錄屬于Dangerous Permissions危險(xiǎn)權(quán)限了,如果工程的targetSdkVersion >=23,就要考慮權(quán)限問(wèn)題了 。動(dòng)態(tài)申請(qǐng)權(quán)限在這里就不講了。
說(shuō)完了Android中內(nèi)部存儲(chǔ)和外部存儲(chǔ)的區(qū)別,講一下我是如何改造的。
2. 應(yīng)用改造
這里我們提示應(yīng)用升級(jí)的案例來(lái)說(shuō)明是如何改造的。
在應(yīng)用進(jìn)入的閃屏頁(yè)初始化中,首先判斷是否擁有SD卡,是否獲取了讀寫SD卡權(quán)限:
if (!SdCardUtils.isSdCardExist(AppStart.this)) {
// 設(shè)置應(yīng)用中保存的根路徑
AppConstants.PARENT_FOLD_PATH = getFilesDir().getAbsolutePath();
}else {
// 設(shè)置應(yīng)用中保存的根路徑
AppConstants.PARENT_FOLD_PATH = Environment
.getExternalStorageDirectory() + File.separator + Constants.APP_NAME
+ File.separator;
}
/**
* 判斷當(dāng)前設(shè)備上SD卡外部存儲(chǔ)是否可用,這里只考慮6.0以上版本
*/
public static boolean isSdCardExist(Context context){
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
boolean isExist = false;
isExist = Environment.getExternalStorageState().equals(
android.os.Environment.MEDIA_MOUNTED);
return isExist;
}
如果我們關(guān)閉了SD卡讀寫權(quán)限,下載的更新包就會(huì)下載到內(nèi)部存儲(chǔ)空間
/**
* 構(gòu)造更新的軟件的安裝包的保存路徑名
*/
public static final String buildUpdateAPKPath() {
if (!SdCardUtils.isSdCardExist(AppContext.getInstance()) && fileDir != null && fileDir.exists()) {
return fileDir.toString() + "/";
}
String filePath = FileUtils.buildFilePath(new String[] { SdCardUtils.getSdCardPath(), APP_NAME });
File dir = new File(filePath);
if (!dir.exists()) {
dir.mkdirs();
}
return filePath;
}
應(yīng)用下載完畢,我們查看一下應(yīng)用目錄,發(fā)現(xiàn)更新包已經(jīng)被下載下來(lái)了。

然后會(huì)調(diào)用打開(kāi)apk文件的intent方法,核心方法如下
private static Intent getApkFileIntent(String updateFilePath) {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(android.content.Intent.ACTION_VIEW);
Uri uri = Uri.fromFile(new File(updateFilePath));
intent.setDataAndType(uri, "application/vnd.android.package-archive");
return intent;
}
執(zhí)行剛才的方法卻出現(xiàn)了解析安裝包失敗的錯(cuò)誤。

但是通過(guò)拷貝這個(gè)apk文件到外部存儲(chǔ)目錄,然后手動(dòng)點(diǎn)擊打開(kāi)是沒(méi)有任何問(wèn)題的。那之前無(wú)法安裝是因?yàn)槭裁茨??讓我們?cè)倏匆幌孪螺d的目錄:

了解Linux目錄權(quán)限的可以看出這里,我們對(duì)這個(gè)文件只有讀寫權(quán)限,沒(méi)有執(zhí)行權(quán)限
Linux的文件權(quán)限有以下設(shè)定:
- Linux下文件的權(quán)限類型一般包括讀,寫,執(zhí)行。對(duì)應(yīng)字母為 r、w、x。
- Linux下權(quán)限的屬組有 擁有者 、群組 、其它組 三種。每個(gè)文件都可以針對(duì)這三個(gè)屬組(粒度),設(shè)置不同的rwx(讀寫執(zhí)行)權(quán)限。
- 通常情況下,一個(gè)文件只能歸屬于一個(gè)用戶和組, 如果其它的用戶想有這個(gè)文件的權(quán)限,則可以將該用戶加入具備權(quán)限的群組,一個(gè)用戶可以同時(shí)歸屬于多個(gè)組。
知道了問(wèn)題所在,我們就辦法解決了。在打開(kāi)apk之前,下載成功之后我們需要修改這個(gè)文件的權(quán)限:
String[] command = {"chmod", "777", updateAPK.getFilePath() };
ProcessBuilder builder = new ProcessBuilder(command);
try {
builder.start();
} catch (IOException e) {
e.printStackTrace();
}
重新運(yùn)行打包apk,然后下載更新,更新結(jié)束后我們發(fā)現(xiàn)更新的apk文件的權(quán)限已經(jīng)修改了。

這個(gè)時(shí)候也可以安裝成功了。