Android 开发碎碎念1

记录最近在 Android 开发时遇见的两个问题的解决办法:

  1. Android 应用启动页面全屏及消除白屏的问题
  2. Android 中存储空间的问题

1. Android 应用启动页

打开大多数应用都会进入到一个“欢迎页面”,在我们的应用中,把起名为 “SplashActivity”,类似下面页面这样。



在开发的过程中会遇见两个问题:

  1. 怎样做到页面的全屏?
  2. 打开应用的时候会有个白屏或者黑屏(依使用的不同主题而定)一闪而过(时间很短,但是肉眼可见),再进入到这个 SplashActivity 中,怎么消除白屏或黑屏?

1.1 全屏显示

style.xml 中声明一个 启动页主题,并且在 AndroidManifest.xml 中将 SplashActivity 的主题将 启动页主题 设置为 SplashActivity 的如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 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 的状态栏和标题栏:

1
2
3
<item name="android:windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>

1.1.2 去除白屏/黑屏

  1. 通过下面属性,将 系统级窗口 的背景设置为 bg_splash.png 图片,如果不设置则 系统级窗口 是白色/黑色,所以才会有应用打开时一闪而过的白屏/黑屏。

    1
    <item name="android:windowBackground">@drawable/bg_splash</item>
  2. 设置 SplashActivity 的整体背景为 bg_splash.png 图片。这个设置的是 应用级窗口 的背景。

    1
    2
    <android:background="@drawable/bg_splash"
    .../>

通过上面两个设置,系统级窗口和应用级窗口的背景都是 bg_splash.png 图片,应用在打开时就不会出现 白屏/黑屏 的情况了。

1.1.3 虚拟按键遮挡背景的问题

在没有虚拟导航栏按键的手机上,上面的设置的背景即可完美的显示;但是在有虚拟导航栏按键的手机上,如果只是按照上面的代码设置背景,会出现虚拟导航栏遮挡 系统级窗口 背景图的问题。在 启动页主题 中添加如下设置,即可解决这个问题:

1
<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/ 文件及其中的内容。

在该目录下对存储内容又进行了分类,如下所示:

  1. data/data/包名/files:应用的普通数据,对于 data/data/包名/files 目录下的文件有如下操作的 API 供调用:

    1
    2
    3
    4
    5
    context.getFilesDir();
    context.openFileInput(String name);
    context.openFileOutput(String name, int mode);
    context.deleteFile(String name);
    context.fileList();
  2. data/data/包名/cache:存放应用的缓存信息,包括 WebView 的缓存数据

    1
    context.getCacheDir();
  3. data/data/包名/databases:存放应用的数据库文件

    1
    2
    3
    context.getDataDir()
    context.getDatabasePath(String name)
    context.deleteDatabase(String name)
  4. data/data/包名/shared_prefs:存放应用内的 SharedPreferences 数据

    1
    2
    context.getSharedPreferences(name,mode)//返回的是 SharedPreferences 对象
    context.deleteSharedPreferences(name)
  5. /data

    1
    Environment.getDataDirectory();

2.2 外部存储

Android 设备都支持外部存储,该存储可能是可移除的存储介质(例如 SD 卡)或内部(不可移除)存储。

保存到外部存储中的文件是全局可读写的

通过 USB 线将手机连接到计算机上时,在计算机上启用 USB 大容量存储可以传输文件。

2.2.1 外部存储状态和路径

在对外部存储操作的时候,首先需要获取对外部存储的读写权限,在 AndroidManifest.xml 要申明权限,如下所示:

1
2
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

1
2
Environment.getExternalStorageState(); // 获取外部存储的状态,得到的具体值请查看源码注释
Environment.getExternalStorageDirectory(); // 获取外部存储的文件,返回的路径是:/storage/emulated/0

2.2.2 获取外部存储公众目录

Android 系统在外部存储中提供了十个文件用于存储对应的文件,存储在这些文件中的文件,不会随着应用卸载而被删除。

这些文件的获取方式如下所示:

1
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 目录下,会生成对应包名的文件夹用于存储该应用的外部存储的私有文件。

在这些目录下的文件,会随着应用卸载而被删除。

如下所示:

1
2
3
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(),源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 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,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
// 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,那么就可以操作外部存储。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 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中的内部存储与外部存储我家就在狗熊岭

Android 存储路径浅析墨眉无锋

获取Android设备上的所有存储设备wangsf1112

Android 使用反射调用StorageManager中 Hide方法getVolumeList、getVolumeStateadayabetter