记录最近在 Android 开发时遇见的两个问题的解决办法:
- Android 应用启动页面全屏及消除白屏的问题
- Android 中存储空间的问题
1. Android 应用启动页
打开大多数应用都会进入到一个“欢迎页面”,在我们的应用中,把起名为 “SplashActivity”,类似下面页面这样。
SplashActivity.png在开发的过程中会遇见两个问题:
- 怎样做到页面的全屏?
- 打开应用的时候会有个白屏或者黑屏(依使用的不同主题而定)一闪而过(时间很短,但是肉眼可见),再进入到这个
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
网友评论