1. 程式人生 > >三種方法,重新整理 Android 的 MediaStore!讓你儲存的圖片立即出現在相簿裡!

三種方法,重新整理 Android 的 MediaStore!讓你儲存的圖片立即出現在相簿裡!

公眾號原標題:測試:“系統相簿裡怎麼看不到我剛儲存的圖片,是我操作不對嗎?”

一、序

Hi,大家好,我是承香墨影!

App 內,建立一個檔案並儲存檔案到本地的需求,是很常見的 I/O 操作。而如果這個檔案變成了一張圖片,那你涉及到的就不僅僅是一個 I/O 操作了,還需要考慮如何更新 MediaStore,這樣才可以在系統相簿中,看到它。

這裡說的 MediaStore,本質上是 Android 維護的一個檔案系統的資料庫,它記錄了當前磁碟上所有的檔案索引,我們可以通過它,快速的查詢當前系統的檔案。

MediaStore 重新整理的時機是不一定的,也就是說,儲存的一張圖片檔案,MediaStore 並不會立即重新整理檔案系統,將此檔案索引記錄下來。而系統本身是存在一些自動重新整理 MediaStore 的時機,例如:重啟手機。表現就是,當你儲存了一張圖片到本地資料夾中之後,通過檔案管理器類的 App,可以在目錄下找到這漲照片,但是在系統相簿中,是無法立即看到它的,同時你想用諸如 微信、QQ 去分享這張圖片的時候,也是找不到的。所以在我們儲存圖片檔案之後,去觸發系統重新整理 MediaStore 就尤為重要了。

本文就來講講,如何在儲存圖片之後,刷新系統 MediaStore 那些事。

刷新系統 Media 通常有如下幾種方式:

  • 通過操作 MediaStore 類。
  • 傳送廣播更新 MediaStore。
  • 通過操作 MediaScannerConnection 類。

這三種方式,各有優缺點,我們慢慢分析。

二、操作 MediaStore

這裡說的操作 MediaStore,實際是操作它的一個內部類 MediaStore.Image.Media,它提供了幾個 inserImage () 方法,供我們向 MediaStore 中插入圖片資料,併產生一個縮圖。

這個方法傳遞進去的是一個 Bitmap 物件,其餘的 title

description 分別是圖片檔案的名稱和一段描述。

舉個 Kotlin 的例子:

MediaStore.Images.Media.insertImage(
        contentResolver,
        mShareBitmap!!,
        "image_file",
        "file")

使用 inserImage() 方法,不需要我們指定路徑,會自動將圖片儲存至 Picture 目錄下。它也不支援我們指定路徑。如果我們對圖片儲存的路徑沒有要求,並且儲存的是一個 Bitmap 物件,此方法是非常的方便的。

細心的朋友可能已經發現了 inserImage()

還有一個其他的過載方法,支援我們傳遞進去一個圖片檔案路徑,不過我並不推薦使用這個方法,因為它會將原本的圖片,再 Copy 一份,到 Picture 目錄下,也就是說你最終在磁碟上會得到兩張相同的圖片。

這一點,看原始碼是最清晰的。它首先使用 BitmapFactory.decodeFile() 方法,得到一個 Bitmap,然後再去呼叫儲存 Bitmap 物件的 inserImage() 方法,所以我們最終在磁碟上會有兩張一模一樣的圖片。

三、傳送廣播

3.1 那些廣播可以更新 MediaStore

說到廣播,在 Android 4.4 之前,是可以通過 ACTION_MEDIA_MOUNTED 廣播,來通知系統重新整理 MediaStore 的,不過假如你現在還在依賴這條廣播,你會得到一個錯誤資訊。

E/AndroidRuntime(23718): java.lang.SecurityException: Permission Denial: not allowed to send broadcast android.intent.action.MEDIA_MOUNTED from pid=23718, uid=10097

在 Android 4.4 之後,這個廣播只能由系統進行廣播,App 只能對該廣播進行監聽,在當前的系統分佈環境下,這條路已經走不通了。

這樣設計也很好理解,畢竟掃描全盤是非常的耗資源,所以系統肯定要把全盤掃描的許可權拿在自己手裡不開放出來,避免被第三方 App 濫用。

不過 Android 依然給我們提供了替代方案,那就是用 MediaScannerConnection 或者傳送 ACTION_MEDIA_SCANNER_SCAN_FILE 廣播。

接下來就來說說 ACTION_MEDIA_SCANNER_SCAN_FILE 這個廣播。

3.2 使用廣播重新整理

通過廣播重新整理 MediaStore 的方式非常的簡單,只需要指定檔案路徑和 Action 就好了。

val saveAs = "Your_Created_Image_File_Path"
val contentUri = Uri.fromFile(File(saveAs))
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,contentUri)
sendBroadcast(mediaScanIntent)

正常情況下,它是沒有問題的,不過假如你發現它不生效,就需要檢查一下你檔案的路徑是否傳遞正確。

通過檢視 MediaScannerReceiver 的原始碼,可以發現 onReceive() 方法中,針對 ACTION_MEDIA_SCANNER_SCAN_FILE 還有一個限制條件,那就是傳遞進去的檔案絕對路徑,必須是以 Environment.getExternalStorageDirectory() 方法的返回值開頭。

有興趣可以仔細閱讀原始碼,這裡是 Android 6.0 的原始碼:

本質上,還是 /mnt/sdcard/ 路徑就認,而 /sdcard/ 就無法使用,所以只要我們不硬編碼檔案路徑,這個問題基本上也就不存在。

這裡也提醒我們,一定不要在程式碼裡,硬編碼檔案路徑,算是一個編碼規範了。

3.3 刪除檔案後重新整理MediaStore

本文一直都在說新增新檔案的時候,如何重新整理 MediaStore 的問題。但是其實還涉及到另外一個問題,我們刪除了一個已經被收錄在 MediaStore 中的檔案,怎麼辦?在本文裡也順便講一下。

既然放在這一小節講,首先想到的是,直接再發一個廣播出去,重新整理這個路徑,但是查閱最終執行掃描前的 MediaScanner 的 scanSingleFile() 方法,你就會知道這樣的方式是行不通的。

在這裡可以看到,當你傳遞進去的檔案路徑,指向的檔案不存在的時候,會直接 return 出去了,就執行不到重新整理的邏輯裡。

所幸的是,我在 DownloadManager 類中,找到了重新整理刪除檔案的解決辦法,依然是通過 ContentResolver 來解決。

這裡通過 ContentResolver 來向 MediaStore 中發起一個刪除檔案的操作,只需要傳遞進去一個檔案的絕對路徑即可。

四、操作 MediaScannerConnection 類

4.1 使用 MediaScannerConnection

重新整理 MediaStore 還有一個最通用也是我推薦的一個方法,那就是使用 MediaScannerConnection 進行操作。

不同於 MediaStore.Image.Media 和廣播的方式,使用 MediaScannerConnection 不僅可以儲存檔案,還可以指定檔案路徑,最好的就是,它還支援重新整理完成的回撥。

如果我們對時序有要求,並且需要制定檔案儲存路徑的話,最好的方式就是直接使用 MediaScannerConnection 類進行操作,並且這也應該是相容最好的方式。

這裡我們主要是利用 MediaScannerConnection 類的 scanFile() 方法進行觸發掃描。

通過 scanFile() 方法,我們只需要制定一個待重新整理的檔案路徑和對應的 MimeType 即可,它支援傳遞多個路徑,也可就是支援批量掃描。

注意這裡的 MimeType 是一定要填寫的,並且不能寫萬用字元 */*null,否則會導致重新整理失敗,通常我們儲存的是一個圖片的話,只需要傳遞 image/jpeg 即可。

最後一個引數, onScanCompletedListener 中可以監聽我們掃描的結果,需要注意的是,假如這裡掃描的是多個檔案路徑,它也會被回撥多次。所以如果有什麼在重新整理之後的後續操作,就需要特殊處理一下(原因後面是說)。

MediaScannerConnection.scanFile(this
        , arrayOf(picFile.absolutePath)
        , arrayOf("image/jpeg"), { path, uri ->

    Log.i("cxmyDev", "onScanCompleted : " + path)

})

scanFile() 方法的使用還是很簡單的,沒什麼需要額外交代的了。

4.2 MediaScannerConnection 原理

依然是從原始碼中找答案,我們先來看看 scanFile() 方法的實現。

scanFile() 裡,建立了一個 MediaScannerConnection 並呼叫了 connect() 方法。接下來我們繼續看 connect() 方法。

connect() 方法中,可以看到,它實際上是 bindServer()MediaScannerService 這個系統服務,所有的操作都在 MediaScannerService 中。

MediaScannerService 的原始碼,有興趣可以去這裡檢視:

這是一個系統服務,我到這裡就不繼續跟下去了,回過頭來繼續看原始碼。

不過看到 connect() 方法的時候,那對應的,一定有 disconnect() 方法存在了,前面 bindService() 了一個系統服務,我們一定要有一個時機去呼叫 unbindService(),否則就會造成洩露。

MediaScannerConnection 確實提供了 disconnect() 方法,但是我們通過 scanFile() 方法拿不到這個物件。這裡處理的非常的巧妙,不需要我們手動去觸發 disconnect(),它是自維護的。

繼續看 scanFile() 裡被我們忽略的 ClientProxy 類,邏輯都在這裡面。

scanNextPath() 中,會去判斷傳遞進去的檔案路徑是否都掃描過,如果已經沒有更多需要掃描的路徑了,就自己去呼叫 disconnect() 方法,回收資源。

到這裡,也就解答了我們剛才的疑問,MediaScannerConnection 已經幫我們考慮了很多事情,我們只需要呼叫它的標準 API 就好了。

五、查缺補漏

5.1 掃描其他型別的媒體檔案

在 Android 下,不僅僅只有圖片,對於其他媒體檔案,使用本文介紹的方法,也是適用的。

5.2 避免某個目錄被 MediaStore 掃描

看完到這裡應該會知道,哪怕我們什麼都不做,在手機下次重啟的時候,系統依然會去全盤掃描檔案系統,更新 MediaStore。

但是有時候,我們有一些目錄下的媒體檔案,並不想讓 MediaStore 掃描到,例如在 SDCard 上快取的圖片、圖示等,這些我們都不想出現在系統相簿內。

解決辦法其實在官方文件中已經寫了。

這裡簡單說一下,當不需要被 MediaStore 掃描的目錄下,建立一個名為 .nomedia 的空檔案,它將阻止媒體掃描程式讀取這個目錄下的媒體檔案。也就無法通過 MediaStore 分享給其他程式。

當然,一些重要的檔案,依然建議放在自己的私有目錄下。

六、小結

關於在 MediaStore 重新整理圖片,本文基本上就算是講清楚了。我推薦的方法,是使用 MediaScannerConnection 來實現。

你看了本文,還有什麼更多的問題可以在留言區討論,如果覺得好,可以這篇文章,分享給你需要的朋友們。

今天在公眾號後臺回覆成長『成長』,將會得到我整理的一些學習資料,也能回覆『加群』,一起學習進步。

推薦閱讀: