公眾號原標題:測試:“系統相簿裡怎麼看不到我剛儲存的圖片,是我操作不對嗎?”
一、序
Hi,大家好,我是承香墨影!
App 內,建立一個檔案並儲存檔案到本地的需求,是很常見的 I/O 操作。而如果這個檔案變成了一張圖片,那你涉及到的就不僅僅是一個 I/O 操作了,還需要考慮如何更新 MediaStore,這樣才可以在系統相簿中,看到它。
這裡說的 MediaStore,本質上是 Android 維護的一個檔案系統的資料庫,它記錄了當前磁碟上所有的檔案索引,我們可以通過它,快速的查詢當前系統的檔案。
MediaStore 重新整理的時機是不一定的,也就是說,儲存的一張圖片檔案,MediaStore 並不會立即重新整理檔案系統,將此檔案索引記錄下來。而系統本身是存在一些自動重新整理 MediaStore 的時機,例如:重啟手機。表現就是,當你儲存了一張圖片到本地資料夾中之後,通過檔案管理器類的 App,可以在目錄下找到這漲照片,但是在系統相簿中,是無法立即看到它的,同時你想用諸如 微信、QQ 去分享這張圖片的時候,也是找不到的。所以在我們儲存圖片檔案之後,去觸發系統重新整理 MediaStore 就尤為重要了。
本文就來講講,如何在儲存圖片之後,重新整理系統 MediaStore 那些事。
重新整理系統 Media 通常有如下幾種方式:
- 通過操作 MediaStore 類。
- 傳送廣播更新 MediaStore。
- 通過操作 MediaScannerConnection 類。
這三種方式,各有優缺點,我們慢慢分析。
二、操作 MediaStore
這裡說的操作 MediaStore,實際是操作它的一個內部類 MediaStore.Images.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 的原始碼:
http://androidxref.com/6.0.0_r1/xref/packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerReceiver.java
本質上,還是 /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 的原始碼,有興趣可以去這裡檢視:
http://androidxref.com/6.0.0_r1/xref/packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerService.java
這是一個系統服務,我到這裡就不繼續跟下去了,回過頭來繼續看原始碼。
不過看到 connect()
方法的時候,那對應的,一定有 disconnect()
方法存在了,前面 bindService()
了一個系統服務,我們一定要有一個時機去呼叫 unbindService()
,否則就會造成洩露。
MediaScannerConnection 確實提供了 disconnect()
方法,但是我們通過 scanFile()
方法拿不到這個物件。這裡處理的非常的巧妙,不需要我們手動去觸發 disconnect()
,它是自維護的。
繼續看 scanFile()
裡被我們忽略的 ClientProxy
類,邏輯都在這裡面。
在 scanNextPath()
中,會去判斷傳遞進去的檔案路徑是否都掃描過,如果已經沒有更多需要掃描的路徑了,就自己去呼叫 disconnect()
方法,回收資源。
到這裡,也就解答了我們剛才的疑問,MediaScannerConnection 已經幫我們考慮了很多事情,我們只需要呼叫它的標準 API 就好了。
五、查缺補漏
5.1 掃描其他型別的媒體檔案
在 Android 下,不僅僅只有圖片,對於其他媒體檔案,使用本文介紹的方法,也是適用的。
5.2 避免某個目錄被 MediaStore 掃描
看完到這裡應該會知道,哪怕我們什麼都不做,在手機下次重啟的時候,系統依然會去全盤掃描檔案系統,更新 MediaStore。
但是有時候,我們有一些目錄下的媒體檔案,並不想讓 MediaStore 掃描到,例如在 SDCard 上快取的圖片、圖示等,這些我們都不想出現在系統相簿內。
解決辦法其實在官方文件中已經寫了。
https://developer.android.com/guide/topics/data/data-storage.html
這裡簡單說一下,當不需要被 MediaStore 掃描的目錄下,建立一個名為 .nomedia
的空檔案,它將阻止媒體掃描程式讀取這個目錄下的媒體檔案。也就無法通過 MediaStore 分享給其他程式。
當然,一些重要的檔案,依然建議放在自己的私有目錄下。
六、小結
關於在 MediaStore 重新整理圖片,本文基本上就算是講清楚了。我推薦的方法,是使用 MediaScannerConnection 來實現。
你看了本文,還有什麼更多的問題可以在留言區討論,如果覺得好,可以這篇文章,分享給你需要的朋友們。
今天在公眾號後臺回覆成長『成長』,將會得到我整理的一些學習資料,也能回覆『加群』,一起學習進步。
推薦閱讀: