Android 開發碎碎念1

記錄最近在 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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容