×

Android 开发碎碎念1

96
lijiankun24
2017.09.19 21:48* 字数 1851

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

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

1. Android 应用启动页

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

SplashActivity.png

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

  1. 怎样做到页面的全屏?
  2. 打开应用的时候会有个白屏或者黑屏(依使用的不同主题而定)一闪而过(时间很短,但是肉眼可见),再进入到这个 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 去除白屏/黑屏

  1. 通过下面属性,将 系统级窗口 的背景设置为 bg_splash.png 图片,如果不设置则 系统级窗口 是白色/黑色,所以才会有应用打开时一闪而过的白屏/黑屏。
<item name="android:windowBackground">@drawable/bg_splash</item>
  1. 设置 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/ 文件及其中的内容。

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

  1. 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();
  1. data/data/包名/cache:存放应用的缓存信息,包括 WebView 的缓存数据
  context.getCacheDir();
  1. data/data/包名/databases:存放应用的数据库文件
  context.getDataDir()
  context.getDatabasePath(String name)
  context.deleteDatabase(String name)
  1. data/data/包名/shared_prefs:存放应用内的 SharedPreferences 数据
  context.getSharedPreferences(name,mode)//返回的是 SharedPreferences 对象
  context.deleteSharedPreferences(name)
  1. /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中的内部存储与外部存储 -- 我家就在狗熊岭

Android 存储路径浅析 -- 墨眉无锋

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

Android 使用反射调用StorageManager中 Hide方法getVolumeList、getVolumeState -- adayabetter

Android 相关
Web note ad 1