記錄最近在 Android 開發時遇見的兩個問題的解決辦法:
- Android 應用啟動頁面全屏及消除白屏的問題
- Android 中存儲空間的問題
1. Android 應用啟動頁
打開大多數應用都會進入到一個“歡迎頁面”,在我們的應用中,把起名為 “SplashActivity”,類似下面頁面這樣。
在開發的過程中會遇見兩個問題:
- 怎樣做到頁面的全屏?
- 打開應用的時候會有個白屏或者黑屏(依使用的不同主題而定)一閃而過(時間很短,但是肉眼可見),再進入到這個
SplashActivity
中,怎么消除白屏或黑屏?
1.1 全屏顯示
在 style.xml
中聲明一個 啟動頁主題
,并且在 AndroidManifest.xml
中將 SplashActivity
的主題將 啟動頁主題
設置為 SplashActivity
的如下所示:
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<!-- 啟動頁主題 -->
<style name="LaunchTheme" parent="AppTheme">
<item name="android:windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@drawable/bg_splash</item>
<item name="android:windowIsTranslucent">true</item>
</style>
1.1.1 隱藏狀態欄和標題欄
下面三個屬性設置可以隱藏 Activity 的狀態欄和標題欄:
<item name="android:windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
1.1.2 去除白屏/黑屏
- 通過下面屬性,將
系統級窗口
的背景設置為bg_splash.png
圖片,如果不設置則系統級窗口
是白色/黑色,所以才會有應用打開時一閃而過的白屏/黑屏。
<item name="android:windowBackground">@drawable/bg_splash</item>
- 設置
SplashActivity
的整體背景為bg_splash.png
圖片。這個設置的是應用級窗口
的背景。
<android:background="@drawable/bg_splash"
.../>
通過上面兩個設置,系統級窗口和應用級窗口的背景都是
bg_splash.png
圖片,應用在打開時就不會出現白屏/黑屏
的情況了。
1.1.3 虛擬按鍵遮擋背景的問題
在沒有虛擬導航欄按鍵的手機上,上面的設置的背景即可完美的顯示;但是在有虛擬導航欄按鍵的手機上,如果只是按照上面的代碼設置背景,會出現虛擬導航欄遮擋 系統級窗口
背景圖的問題。在 啟動頁主題
中添加如下設置,即可解決這個問題:
<item name="android:windowIsTranslucent">true</item>
2. Android 中的存儲空間
Android 中的存儲分為:內部存儲和外部存儲,下面分別介紹。
2.1 內部存儲
內部存儲是在 /data/
目錄下,該目錄下的文件在下面兩種情況可以查看:
- 在
root
的手機上(手機獲取root
權限,可以使用市場上一些常用的 Root 應用) - 使用模擬器調試應用時,可以使用
Android Device Monitor
中提供的File Explorer
工具查看。
除上面兩種情況外,在沒有 root 的手機上,普通用戶沒有辦法查看該目錄下的文件。
該目錄下有多個子目錄,對于開發者比較重要的子目錄有兩個:
2.1.1 /data/app/
在該文件目錄下存放著安裝在此手機上的應用的 APK 文件,當調試應用的時候,在控制臺輸出的內容中出現 uploading ……
的一項,這就是將我們的 APK 文件上傳到此目錄下,之后才開始安裝應用。
2.1.2 /data/data/
在該目錄下,系統都會為已安裝在手機上的應用自動創建一個與之對應的目錄,該目錄以應用的包名命名,如: /data/data/com.lijiankun24.androidpractice/
的目錄,用于存儲 com.lijiankun24.androidpractice
應用的私有數據。
這個目錄用于 App 中的 WebView 緩存頁面信息,SharedPreferences 和 SQLiteDatabase 持久化應用相關數據等。
當用戶卸載此應用時,系統會自動刪除 /data/data/com.lijiankun24.androidpractice/
文件及其中的內容。
在該目錄下對存儲內容又進行了分類,如下所示:
-
data/data/包名/files
:應用的普通數據,對于data/data/包名/files
目錄下的文件有如下操作的 API 供調用:
context.getFilesDir();
context.openFileInput(String name);
context.openFileOutput(String name, int mode);
context.deleteFile(String name);
context.fileList();
-
data/data/包名/cache
:存放應用的緩存信息,包括WebView
的緩存數據
context.getCacheDir();
-
data/data/包名/databases
:存放應用的數據庫文件
context.getDataDir()
context.getDatabasePath(String name)
context.deleteDatabase(String name)
-
data/data/包名/shared_prefs
:存放應用內的SharedPreferences
數據
context.getSharedPreferences(name,mode)//返回的是 SharedPreferences 對象
context.deleteSharedPreferences(name)
/data
Environment.getDataDirectory();
2.2 外部存儲
Android 設備都支持外部存儲,該存儲可能是可移除的存儲介質(例如 SD 卡)或內部(不可移除)存儲。
保存到外部存儲中的文件是全局可讀寫的
通過 USB 線將手機連接到計算機上時,在計算機上啟用 USB 大容量存儲可以傳輸文件。
2.2.1 外部存儲狀態和路徑
在對外部存儲操作的時候,首先需要獲取對外部存儲的讀寫權限,在 AndroidManifest.xml
要申明權限,如下所示:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Environment.getExternalStorageState(); // 獲取外部存儲的狀態,得到的具體值請查看源碼注釋
Environment.getExternalStorageDirectory(); // 獲取外部存儲的文件,返回的路徑是:/storage/emulated/0
2.2.2 獲取外部存儲公眾目錄
Android 系統在外部存儲中提供了十個文件用于存儲對應的文件,存儲在這些文件中的文件,不會隨著應用卸載而被刪除。
這些文件的獲取方式如下所示:
Environment.getExternalStoragePublicDirectory(type);
- DIRECTORY_MUSIC:/storage/emulated/0/Music
- DIRECTORY_PODCASTS:/storage/emulated/0/Podcasts
- DIRECTORY_RINGTONES:/storage/emulated/0/Ringtones
- DIRECTORY_ALARMS:/storage/emulated/0/Alarms
- DIRECTORY_NOTIFICATIONS:/storage/emulated/0/Notifications
- DIRECTORY_PICTURES:/storage/emulated/0/Pictures
- DIRECTORY_MOVIES:/storage/emulated/0/Movies
- DIRECTORY_DOWNLOADS:/storage/emulated/0/Downloads
- DIRECTORY_DCIM:/storage/emulated/0/Dcim
- DIRECTORY_DOCUMENTS:/storage/emulated/0/Documents
2.2.3 獲取外部存儲私有目錄
在外部存儲中存在私有目錄,其位置在 SD 卡的 /Android/data 目錄下,會生成對應包名的文件夾用于存儲該應用的外部存儲的私有文件。
在這些目錄下的文件,會隨著應用卸載而被刪除。
如下所示:
context.getExternalCacheDir(); // /storage/emulated/0/Android/data/應用包名/cache
context.getExternalFilesDir(type); // /storage/emulated/0/Android/data/應用包名/files
context.getObbDir(); // /storage/emulated/0/Android/obb/應用包名
2.2.4 通過反射獲取外部存儲
Environment.getExternalStorageDirectory()
有時候并不會給出我們想要的存儲路徑,比如:有的手機支持擴展多個 sdcard,如果想獲取多個存儲設備的信息,這個 API 就不能滿足了。
但是系統自帶的文件管理器是怎么獲取得存儲設備信息的呢?在 Android SDK 中有個 StorageManager
類,其中有個方法是 getVolumeList()
,源碼如下:
/**
* Returns list of all mountable volumes.
* @hide
*/
public StorageVolume[] getVolumeList() {
if (mMountService == null) return new StorageVolume[0];
try {
Parcelable[] list = mMountService.getVolumeList();
if (list == null) return new StorageVolume[0];
int length = list.length;
StorageVolume[] result = new StorageVolume[length];
for (int i = 0; i < length; i++) {
result[i] = (StorageVolume)list[i];
}
return result;
} catch (RemoteException e) {
Log.e(TAG, "Failed to get volume list", e);
return null;
}
}
getVolumeList()
方法是隱藏的,不能在應用代碼中直接調用,所以只能通過反射來調用這個方法。
通過反射,得到 StorageManager
類和 StorageVolume
類,就可以得到手機的所有存儲設備信息,封裝代碼放在了 GitHub 上 CustomStorageManager,如下所示:
// CustomStorageManager.java
public class CustomStorageManager {
private static CustomStorageManager INSTANCE = null;
private Context mContext = null;
private CustomStorageManager() {
}
public static CustomStorageManager getInstance() {
if (INSTANCE == null) {
synchronized (CustomStorageManager.class) {
if (INSTANCE == null) {
INSTANCE = new CustomStorageManager();
}
}
}
return INSTANCE;
}
public void init(Context context) {
mContext = context.getApplicationContext();
}
public List<MyStorageVolume> getStorage() {
List<MyStorageVolume> volumeList = new ArrayList<>(3);
StorageManager storageManager = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE);
try {
Class<?>[] paramClasses = {};
Method method = StorageManager.class.getMethod("getVolumeList", paramClasses);
Object[] params = {};
Object[] invokes = (Object[]) method.invoke(storageManager, params);
if (invokes != null) {
for (Object object : invokes) {
volumeList.add(new MyStorageVolume(object));
}
}
} catch (Exception e) {
e.printStackTrace();
}
return volumeList;
}
/**
* 獲取Volume掛載狀態, 例如Environment.MEDIA_MOUNTED
*
* @param context 上下文
* @param path 目錄路徑
* @return 掛載狀態
*/
public static String getVolumeState(Context context, String path) {
//mountPoint是掛載點名Storage'paths[1]:/mnt/extSdCard不是/mnt/extSdCard/
//不同手機外接存儲卡名字不一樣。/mnt/sdcard
StorageManager mStorageManager = (StorageManager) context
.getSystemService(STORAGE_SERVICE);
String status = null;
try {
Method mMethodGetPathsState = mStorageManager.getClass().
getMethod("getVolumeState", String.class);
status = (String) mMethodGetPathsState.invoke(mStorageManager, path);
} catch (Exception e) {
e.printStackTrace();
}
return status;
}
/**
* 獲取目錄可用空間大小
*
* @param path 獲取目錄
* @return 存儲目錄可用空間大小
*/
public static long getAvailableSize(String path) {
try {
StatFs sf = new StatFs(path);
long blockSize = sf.getBlockSize();
long availableCount = sf.getAvailableBlocks();
return availableCount * blockSize;
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
/**
* 獲取目錄總存儲空間
*
* @param path 存儲目錄
* @return 總存儲空間大小
*/
public static long getTotalSize(String path) {
try {
StatFs sf = new StatFs(path);
long blockSize = sf.getBlockSize();
long totalCount = sf.getBlockCount();
return totalCount * blockSize;
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
public static String getSizeStr(long fileLength) {
String strSize;
try {
if (fileLength >= 1024 * 1024 * 1024) {
strSize = (float) Math.round(10 * fileLength / (1024 * 1024 * 1024)) / 10 + " GB";
} else if (fileLength >= 1024 * 1024) {
strSize = (float) Math.round(10 * fileLength / (1024 * 1024 * 1.0)) / 10 + " MB";
} else if (fileLength >= 1024) {
strSize = (float) Math.round(10 * fileLength / (1024)) / 10 + " KB";
} else if (fileLength >= 0) {
strSize = fileLength + " B";
} else {
strSize = "0 B";
}
} catch (Exception e) {
e.printStackTrace();
strSize = "0 B";
}
return strSize;
}
}
// MyStorageVolume.java
public class MyStorageVolume {
private int mStorageId;
private String mPath;
private boolean mPrimary;
private boolean mRemovable;
private boolean mEmulated;
private long mMtpReserveSpace;
private boolean mAllowMassStorage;
private long mMaxFileSize;
private String mState;
public MyStorageVolume(Object reflectItem) {
try {
Method fmStorageId = reflectItem.getClass().getDeclaredMethod("getStorageId");
fmStorageId.setAccessible(true);
mStorageId = (Integer) fmStorageId.invoke(reflectItem);
} catch (Exception e) {
e.printStackTrace();
}
try {
Method fmPath = reflectItem.getClass().getDeclaredMethod("getPath");
fmPath.setAccessible(true);
mPath = (String) fmPath.invoke(reflectItem);
} catch (Exception e) {
e.printStackTrace();
}
try {
Method fmPrimary = reflectItem.getClass().getDeclaredMethod("isPrimary");
fmPrimary.setAccessible(true);
mPrimary = (Boolean) fmPrimary.invoke(reflectItem);
} catch (Exception e) {
e.printStackTrace();
}
try {
Method fisRemovable = reflectItem.getClass().getDeclaredMethod("isRemovable");
fisRemovable.setAccessible(true);
mRemovable = (Boolean) fisRemovable.invoke(reflectItem);
} catch (Exception e) {
e.printStackTrace();
}
try {
Method fisEmulated = reflectItem.getClass().getDeclaredMethod("isEmulated");
fisEmulated.setAccessible(true);
mEmulated = (Boolean) fisEmulated.invoke(reflectItem);
} catch (Exception e) {
e.printStackTrace();
}
try {
Method fmMtpReserveSpace = reflectItem.getClass().getDeclaredMethod("getMtpReserveSpace");
fmMtpReserveSpace.setAccessible(true);
mMtpReserveSpace = (Long) fmMtpReserveSpace.invoke(reflectItem);
} catch (Exception e) {
e.printStackTrace();
}
try {
Method fAllowMassStorage = reflectItem.getClass().getDeclaredMethod("allowMassStorage");
fAllowMassStorage.setAccessible(true);
mAllowMassStorage = (Boolean) fAllowMassStorage.invoke(reflectItem);
} catch (Exception e) {
e.printStackTrace();
}
try {
Method fMaxFileSize = reflectItem.getClass().getDeclaredMethod("getMaxFileSize");
fMaxFileSize.setAccessible(true);
mMaxFileSize = (Long) fMaxFileSize.invoke(reflectItem);
} catch (Exception e) {
e.printStackTrace();
}
try {
Method fState = reflectItem.getClass().getDeclaredMethod("getState");
fState.setAccessible(true);
mState = (String) fState.invoke(reflectItem);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 獲取 Volume 掛載狀態, 例如 Environment.MEDIA_MOUNTED
*
* @param context 上下文
* @return 獲取 Volume 掛載狀態
*/
public String getVolumeState(Context context) {
return CustomStorageManager.getVolumeState(context, mPath);
}
/**
* 獲取當前存儲設備是否是處于掛起狀態
*
* @param context 上下文
* @return true 表示處于掛起,即可用;false 表示處于非掛起,即不可用
*/
public boolean isMounted(Context context) {
return getVolumeState(context).equals(Environment.MEDIA_MOUNTED);
}
/**
* 獲取存儲設備的唯一標識
*
* @return 存儲設備的唯一表示 Id
*/
public String getUniqueFlag() {
return "" + mStorageId;
}
/**
* 獲取目錄可用空間大小
*
* @return 獲取當前空間可用大小
*/
public long getAvailableSize() {
return CustomStorageManager.getAvailableSize(mPath);
}
/**
* 獲取目錄總存儲空間
*
* @return 獲取空間總可用大小
*/
public long getTotalSize() {
return CustomStorageManager.getTotalSize(mPath);
}
@Override
public String toString() {
return "MyStorageVolume{" +
"\nmStorageId=" + mStorageId +
"\n, mPath='" + mPath + '\'' +
"\n, mPrimary=" + mPrimary +
"\n, mRemovable=" + mRemovable +
"\n, mEmulated=" + mEmulated +
"\n, mMtpReserveSpace=" + mMtpReserveSpace +
"\n, mAllowMassStorage=" + mAllowMassStorage +
"\n, mMaxFileSize=" + mMaxFileSize +
"\n, mState='" + mState + '\'' +
"\n, getTotalSize='" + CustomStorageManager.getSizeStr(getTotalSize()) + '\'' +
"\n, getAvailableSize='" + CustomStorageManager.getSizeStr(getAvailableSize()) + '\'' +
'}' + "\n";
}
}
2.2.5 注意
由于外部存儲出現不可用的狀態,比如:當用戶移除提供外部存儲的 SD 卡時,所以在訪問它之前,需要確認外部存儲是否處于可用的狀體,如果返回的狀態是:MEDIA_MOUNTED
,那么就可以操作外部存儲。如下:
/* 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;
}
參考資料:
android AppCompat, splash啟動白屏(黑屏)全屏,去掉狀態欄,以及splash與虛擬按鍵遮擋 -- robert_cysy
獲取Android設備上的所有存儲設備 -- wangsf1112
Android 使用反射調用StorageManager中 Hide方法getVolumeList、getVolumeState -- adayabetter