Android平台监听系统截屏方案预研及相关知识点
最近有個(gè)針對(duì)系統(tǒng)截屏的需求,所以預(yù)研了Android平臺(tái)上捕獲系統(tǒng)截屏的方案。
最直接的方式就是監(jiān)聽(tīng)手機(jī)的系統(tǒng)截屏組合鍵(電源鍵+音量下鍵),但是這種方式實(shí)現(xiàn)難度大,且有的機(jī)型使用特殊手勢(shì)進(jìn)行截屏,兼容性問(wèn)題難以解決。
所以網(wǎng)上流行的方案是監(jiān)聽(tīng)系統(tǒng)截屏目錄下文件創(chuàng)建事件或者多媒體數(shù)據(jù)庫(kù)圖片資源變更通知。我對(duì)兩種方式都做了測(cè)試,多多少少都存在一些問(wèn)題,現(xiàn)整理如下:
通過(guò)FileObserver監(jiān)聽(tīng)系統(tǒng)截屏目錄下的文件創(chuàng)建
FileObserver可以對(duì)一個(gè)文件或者目錄進(jìn)行監(jiān)聽(tīng),它是基于linux的inotify實(shí)現(xiàn),可以監(jiān)聽(tīng)文件創(chuàng)建、訪問(wèn)、修改等操作。
雖然文檔上說(shuō)FileObserver可以實(shí)現(xiàn)遞歸監(jiān)聽(tīng),即被監(jiān)聽(tīng)文件夾下所有文件和級(jí)聯(lián)子目錄的改變都會(huì)觸發(fā)監(jiān)聽(tīng)器。但是,真正實(shí)驗(yàn)下來(lái)發(fā)現(xiàn),不是這么回事!被監(jiān)聽(tīng)目錄的子目錄的本身改動(dòng)以及子目錄下的文件改動(dòng)都不會(huì)觸發(fā)監(jiān)聽(tīng)器。因此,要想實(shí)現(xiàn)遞歸監(jiān)聽(tīng),必須自己遞歸實(shí)現(xiàn)對(duì)每個(gè)子目錄的監(jiān)聽(tīng)。
FileObserver可以監(jiān)聽(tīng)多種類型的事件:
| ACCESS | 被監(jiān)聽(tīng)文件被訪問(wèn) |
| MODIFY | 被監(jiān)聽(tīng)文件被修改 |
| ATTRIB | 被監(jiān)聽(tīng)文件或目錄的權(quán)限、Owner等屬性被改變 |
| CLOSE_WRITE | 被監(jiān)聽(tīng)的可寫文件或者目錄(已經(jīng)被打開(kāi))被關(guān)閉 |
| CLOSE_NOWRITE | 被監(jiān)聽(tīng)的只讀文件或者目錄(已經(jīng)被打開(kāi))被關(guān)閉 |
| OPEN | 被監(jiān)聽(tīng)文件或者目錄被打開(kāi) |
| MOVED_FROM | 文件或者子目錄從當(dāng)前被監(jiān)聽(tīng)目錄下被移走 |
| MOVED_TO | 文件或者子目錄從其他目錄被移動(dòng)到當(dāng)前被監(jiān)聽(tīng)目錄下 |
| CREATE | 在當(dāng)前被監(jiān)聽(tīng)目錄下,創(chuàng)建文件或者子目錄 |
| DELETE | 在當(dāng)前被監(jiān)聽(tīng)目錄下刪除一個(gè)文件 |
| DELETE_SELF | 被監(jiān)聽(tīng)的文件或者目錄本身被刪除,此時(shí)監(jiān)聽(tīng)將被停止 |
| MOVE_SELF | 被監(jiān)聽(tīng)的文件或者目錄本身被移動(dòng) |
| ALL_EVENTS | 上面多有事件的并集 |
FileObserver是抽象類,我們需要實(shí)現(xiàn)onEvent方法處理具體業(yè)務(wù)邏輯。此外,創(chuàng)建FileObserver對(duì)象時(shí),需要指定被監(jiān)聽(tīng)文件或者目錄,以及需要監(jiān)聽(tīng)的事件類型。
經(jīng)過(guò)實(shí)際測(cè)試,發(fā)現(xiàn)使用FileObserver進(jìn)行文件(夾)監(jiān)控,有幾點(diǎn)需要注意:
OK,FileObserver的基本情況介紹完了,下面我們看下使用FileObserver監(jiān)聽(tīng)系統(tǒng)截圖的方案和可行性:因?yàn)槲覀円O(jiān)聽(tīng)系統(tǒng)截圖,因此理論上只需要監(jiān)聽(tīng)系統(tǒng)截圖目錄的CREATE事件即可。基本代碼如下所示:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | //三星Note3下的系統(tǒng)截圖目錄 String path = "/storage/emulated/0/Pictures/Screenshots"; //小米4下的系統(tǒng)截圖目錄 //path = "/storage/emulated/0/DCIM/Screenshots"; //指定監(jiān)聽(tīng)路徑path和事件類型CREATE FileObserver fileObserver = new FileObserver(path,FileObserver.CREATE) { public void onEvent(int event, String path) { //這里最好啟動(dòng)一個(gè)線程去加載系統(tǒng)截屏的圖片,否則會(huì)導(dǎo)致線程被阻塞,無(wú)法監(jiān)聽(tīng)到后續(xù)事件。 //此外,這里的path僅是圖片文件名,不是完整路徑 //收到CREATE事件后,立即去加載圖片是獲取不到的,需要延遲幾百毫秒才可以加載到,估計(jì)是圖片正在落地。 } }; //開(kāi)始監(jiān)聽(tīng) fileObserver.startWatching(); //結(jié)束監(jiān)聽(tīng) fileObserver.stopWatching(); |
但是實(shí)際測(cè)試下來(lái)發(fā)現(xiàn),在三星Note3上可以準(zhǔn)確的監(jiān)聽(tīng)系統(tǒng)截圖,并可以獲取到系統(tǒng)截圖圖片。但是在小米4上,根本監(jiān)聽(tīng)不到CREATE事件(實(shí)際上,截屏圖片已經(jīng)在系統(tǒng)截屏目錄了)。
在小米4上僅能監(jiān)聽(tīng)到ACCESS(被觸發(fā)多次)和OPEN事件。但是OPEN事件在三星Note3上會(huì)觸發(fā)多次,而且Android手機(jī)千奇百怪,要想找到一個(gè)系統(tǒng)截屏?xí)r,所有手機(jī)都會(huì)觸發(fā)一次的FileObserver事件,會(huì)很難,而且存在很大的兼容性問(wèn)題。
因此,通過(guò)FileObserver監(jiān)聽(tīng)系統(tǒng)截圖存在兩個(gè)比較大的問(wèn)題:
所以目前來(lái)看,通過(guò)FileObserver監(jiān)聽(tīng)系統(tǒng)截圖不靠譜。
通過(guò)ContentObserver監(jiān)聽(tīng)多媒體數(shù)據(jù)庫(kù)(圖片)的資源變化
我們知道:通過(guò)系統(tǒng)截屏生成一張圖片時(shí),這張圖片不僅會(huì)存儲(chǔ)在系統(tǒng)截屏目錄中,還會(huì)通過(guò)MediaProvider類在多媒體數(shù)據(jù)庫(kù)中插入一條記錄,方便系統(tǒng)圖庫(kù)進(jìn)行查詢。而且MediaProvider會(huì)將唯一標(biāo)識(shí)這張圖片的URI通知到感興趣的ContentObserver。(關(guān)于多媒體數(shù)據(jù)庫(kù)下面會(huì)進(jìn)行詳細(xì)介紹)
因此,我們的方案就是通過(guò)ContentObserver監(jiān)聽(tīng)多媒體數(shù)據(jù)庫(kù)圖片資源的變化。基本代碼如下所示:
| 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 | //查詢的表字段 static final String[] PROJECTION = new String[]{ MediaStore.Images.Media.DATA,MediaStore.Images.Media.DATE_ADDED}; //根據(jù)時(shí)間降序排序 static final String SORT_ORDER = MediaStore.Images.Media.DATE_ADDED + " DESC"; //mHandler表示主線程的Handler,這樣回調(diào)函數(shù)onChange就會(huì)在主線程被調(diào)用 ContentObserver contentObserver = new ContentObserver(mHandler) { public void onChange(boolean selfChange) { super.onChange(selfChange); //從API16開(kāi)始,才有兩個(gè)參數(shù)的onChange方法,所以這里要主動(dòng)調(diào)用下面的onChange方法。 onChange(selfChange, null); } public void onChange(boolean selfChange, Uri uri) { //若調(diào)用父類方法就死循環(huán)了 //super.onChange(selfChange,uri); if (uri == null) { //API16以下版本 Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, PROJECTION, null, null,SORT_ORDER); if (cursor != null && cursor.moveToFirst()) { //完整路徑 String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)); //添加圖片的時(shí)間,單位秒 long dateAdded = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED)); long currentTime = System.currentTimeMillis() / 1000; //加個(gè)過(guò)濾條件必須是3S內(nèi)的圖片,且路徑中包含截圖字樣“screenshot” if (Math.abs(currentTime - dateAdded) <= 3l && path.toLowerCase().contains("screenshot")) { //這就是系統(tǒng)截屏的圖片了,這里測(cè)試發(fā)現(xiàn)需要等待幾百M(fèi)S,才能加載到圖片。因此具體實(shí)現(xiàn)時(shí),最好在獨(dú)立線程,每隔100MS嘗試加載一次,做好超時(shí)處理。 Bitmap b1 = BitmapFactory.decodeFile(path); } } } else { //API16及以上版本 if (uri.toString().matches(EXTERNAL_CONTENT_URI_MATCHER + "/\\d+")) { Cursor cursor = contentResolver.query(uri, PROJECTION, null, null, null); if (cursor != null && cursor.moveToFirst()){ //完整路徑 String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)); //添加圖片的時(shí)間,單位秒 long dateAdded = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED)); long currentTime = System.currentTimeMillis() / 1000; if (Math.abs(currentTime - dateAdded) <= 3l && path.toLowerCase().contains("screenshot")) { //這就是系統(tǒng)截屏的圖片了 Bitmap b2 = MediaStore.Images.Media.getBitmap(contentResolver, uri); } } } } } } //通過(guò)ContentResolver注冊(cè)ContentObserver,監(jiān)聽(tīng)"content://media/external/video/media" getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver); //不需要監(jiān)聽(tīng)的時(shí)候,一定要把原來(lái)的ContentObserver注銷掉。 getContentResolver().unregisterContentObserver(contentObserver); |
上述代碼中,我們?cè)贏PI16以上和以下采取了兩種不同的方案:
上述方案,經(jīng)過(guò)測(cè)試,發(fā)現(xiàn)存在一些問(wèn)題:
簡(jiǎn)單來(lái)說(shuō),就是沒(méi)辦法完全確定觸發(fā)onChange回調(diào)的事件一定是系統(tǒng)截屏行為。因此,在onChange回調(diào)方法中,判斷此次回調(diào)是不是系統(tǒng)截屏觸發(fā)的,是個(gè)難點(diǎn)。但是這個(gè)問(wèn)題解決不好,就會(huì)造成一定的誤差。比如:我通過(guò)相機(jī)拍攝了一張圖片,就會(huì)觸發(fā)上面的onChange回調(diào)。所以上面的代碼加了兩個(gè)過(guò)濾條件:必須是3S內(nèi)的圖片,且圖片路徑中包含截圖字樣“screenshot”。但是這樣也不能確保百分之百?zèng)]有誤差。
綜上所述,不管是通過(guò)FileObserver還是ContentObserver,都不能完全準(zhǔn)確地監(jiān)控系統(tǒng)截屏操作。(相比于IOS直接提供了API級(jí)別的支持,Android還是很蛋疼啊…)
多媒體數(shù)據(jù)庫(kù)
Android中的多媒體數(shù)據(jù)記錄(圖片、音頻、視頻等)是存儲(chǔ)在DB中的,即多媒體數(shù)據(jù)庫(kù)。這個(gè)數(shù)據(jù)庫(kù)文件存儲(chǔ)在/data/data/com.android.providers.media/databases目錄中。如下圖所示:
其中internal.db是內(nèi)部存儲(chǔ)數(shù)據(jù)庫(kù)文件,external.db是存儲(chǔ)卡數(shù)據(jù)庫(kù)文件。多媒體數(shù)據(jù)操作主要就是圍繞這兩個(gè)數(shù)據(jù)庫(kù)來(lái)進(jìn)行的,這兩個(gè)數(shù)據(jù)庫(kù)的結(jié)構(gòu)是完全一樣的。如下所示:
上面是存儲(chǔ)不同多媒體數(shù)據(jù)的表,其中video表主要存儲(chǔ)視頻數(shù)據(jù);videothumbnails表主要存儲(chǔ)視頻縮略圖數(shù)據(jù);audio_xx表主要存儲(chǔ)音頻數(shù)據(jù),音頻數(shù)據(jù)比較復(fù)雜,又需要album相關(guān)表存儲(chǔ)專輯信息,artist相關(guān)表存儲(chǔ)歌手信息;images表主要存儲(chǔ)圖片數(shù)據(jù)。thumbnails表主要存儲(chǔ)圖片縮略圖數(shù)據(jù)。
這里我們主要看下images表結(jié)構(gòu),如下所示:
可見(jiàn),images表是基于files表的視圖。其中,_data字段表示圖片的完整路徑,data_added字段表示添加圖片的時(shí)間,width和height字段分別表示圖片的寬度和高度,_display_name字段則表示圖片名稱。
下面看兩個(gè)具體案例,我們分別通過(guò)系統(tǒng)截屏手勢(shì)和相機(jī)獲取一張圖片,然后看下這兩種圖片在images表中的存儲(chǔ)。
首先是截屏獲得的圖片,其表記錄如下所示:
然后是相機(jī)拍攝出的圖片,其表記錄如下所示:
從上述兩張圖片的表數(shù)據(jù)可知:
- 圖片id確實(shí)是遞增的。
- 系統(tǒng)截圖和相機(jī)拍攝的圖片存儲(chǔ)在不同的目錄。
- 系統(tǒng)截圖圖片是png格式,相機(jī)拍攝圖片是jpeg格式。
- bucket_display_name字段指出了圖片的來(lái)源途徑,它是根據(jù)_data字段生成的。
- 系統(tǒng)截屏圖片的寬高就是屏幕的寬高,而相機(jī)拍攝圖片的寬高則和具體手機(jī)有關(guān),但一般都大于屏幕寬高。
- 向其他字段的含義也很明確,此處不再贅述。
上面我們是通過(guò)sql語(yǔ)句直接查詢圖片數(shù)據(jù),其實(shí)Android系統(tǒng)給我們封裝了MediaStore類,它提供了多媒體數(shù)據(jù)存儲(chǔ)與獲取相關(guān)API,其基本結(jié)構(gòu)如下所示(詳細(xì)結(jié)構(gòu)可參見(jiàn)源碼):
其中Images.ImageColumns類主要封裝了images表的字段。Images.Media類主要提供了查詢和插入圖片數(shù)據(jù)的API(這類API很簡(jiǎn)單,都是通過(guò)ContentResolver和uri,呼起對(duì)應(yīng)的MediaProvider完成真正的DB操作),以及可以通過(guò)getBitmap方法獲取圖片的Bitmap對(duì)象,而Images.Thumbnails類則提供了操作縮略圖的相關(guān)API。同樣的,其他的內(nèi)部類(Audio、Video)分別對(duì)應(yīng)音頻表和視頻表。
Images.Media.getBitmap方法很便利,其實(shí)現(xiàn)也很簡(jiǎn)單,首先通過(guò)uri獲取輸入流(詳情參見(jiàn)源碼),然后通過(guò)BitmapFactory類解碼獲取Bitmap。如下所示:
| 1 2 3 4 5 6 | public static final Bitmap getBitmap(ContentResolver cr, Uri url)throws FileNotFoundException, IOException { InputStream input = cr.openInputStream(url); Bitmap bitmap = BitmapFactory.decodeStream(input); input.close(); return bitmap; } |
從MediaStore類的源碼可知,它提供的API都是通過(guò)ContentResolver和Uri呼起對(duì)應(yīng)MediaProvider來(lái)實(shí)現(xiàn)的,MediaProvider才是真正實(shí)現(xiàn)多媒體數(shù)據(jù)庫(kù)操作的場(chǎng)所。關(guān)于MediaProvider,又是單獨(dú)話題了,感興趣的可以去看源碼。
MediaStore類為每一種資源分配了單獨(dú)的Uri地址,例如:視頻資源的基礎(chǔ)地址是MediaStore.Vedio.MediaEXTERNAL_CONTENT_URI,即content://media/external/video/media,圖片資源的基礎(chǔ)地址是MediaStore.Images.Media.EXTERNAL_CONTENT_URI,即content://media/external/images/media。
這些基礎(chǔ)地址都是數(shù)據(jù)集合類型,對(duì)應(yīng)的個(gè)體數(shù)據(jù)類型則是在基礎(chǔ)地址后面加上圖片ID。例如:上面我們通過(guò)系統(tǒng)截屏獲得的圖片資源ID是233494,那么唯一標(biāo)識(shí)這張圖片的uri就是content://media/external/images/media/233494,通過(guò)這個(gè)uri,就可以獲取這張圖片的所有信息了(上面getBitmap方法的第二個(gè)參數(shù)就是這種個(gè)體數(shù)據(jù)類型uri)。實(shí)際操作中,要使用哪種類型的URI,則要根據(jù)具體情況而定。
因此,獲取系統(tǒng)截屏圖片的Bitmap對(duì)象有兩種方式:
ContentProvider的數(shù)據(jù)更新通知機(jī)制
上面介紹的第二種方案,依賴的就是ContentProvider的數(shù)據(jù)更新通知機(jī)制。因?yàn)镃ontentProvider是以URI形式來(lái)組織資源的,所以當(dāng)數(shù)據(jù)變更時(shí),也是以URI形式通知感興趣的ContentObserver。
整個(gè)數(shù)據(jù)更新機(jī)制的示意圖如下所示:
其中,ContentService服務(wù)就是管理所有ContentObserver監(jiān)聽(tīng)器的場(chǎng)所,它運(yùn)行在System進(jìn)程,以多叉樹(shù)的形式組織所有監(jiān)聽(tīng)器。而MediaProvider則負(fù)責(zé)操作多媒體數(shù)據(jù)庫(kù),并以URI的形式發(fā)出數(shù)據(jù)變更通知到ContentService服務(wù),ContentService負(fù)責(zé)從樹(shù)形數(shù)據(jù)結(jié)構(gòu)中找出對(duì)該URI感興趣的ContentObserver,然后跨進(jìn)程回調(diào)ContentObserver.onChange方法。
所以這里的關(guān)鍵點(diǎn)就是ContentService服務(wù)中多叉樹(shù)數(shù)據(jù)結(jié)構(gòu)的建立和查詢。其中多叉樹(shù)的節(jié)點(diǎn)是ObserverNode,如下所示:
| 1 2 3 4 5 6 7 8 9 10 11 12 | class ObserverNode{ String mName;//節(jié)點(diǎn)名稱 ArrayList<ObserverNode> mChildren = new ArrayList<ObserverNode>();//孩子節(jié)點(diǎn) ArrayList<ObserverEntry> mObservers = new ArrayList<ObserverEntry>();//該節(jié)點(diǎn)上的監(jiān)聽(tīng)器 } class ObserverEntry{ //跨進(jìn)程回調(diào)的接口 IContentObserver observer; //該參數(shù)就是注冊(cè)監(jiān)聽(tīng)器時(shí)的第二個(gè)參數(shù),若為false,則表示若變化的URI是正在監(jiān)聽(tīng)的URI的父節(jié)點(diǎn)或者相同節(jié)點(diǎn)時(shí),就會(huì)觸發(fā)回調(diào)。若為true,則在上述時(shí)機(jī)之上,若變化的URI是正在監(jiān)聽(tīng)的URI的子節(jié)點(diǎn)時(shí),也會(huì)觸發(fā)回調(diào)。 boolean notifyForDescendants; } |
上面我們監(jiān)聽(tīng)系統(tǒng)截屏事件時(shí),監(jiān)聽(tīng)的URI是content://media/external/images/media,且notifyForDescendents參數(shù)為true。因此,注冊(cè)之后,ContentService服務(wù)的多叉樹(shù)數(shù)據(jù)結(jié)構(gòu)如下所示:
而當(dāng)系統(tǒng)截屏圖片插入到多媒體數(shù)據(jù)庫(kù)時(shí),MediaProvider會(huì)發(fā)出content://media/external/images/media/xxx形式的通知,該通知到達(dá)ContentService服務(wù)后,就會(huì)在上面的多叉樹(shù)數(shù)據(jù)結(jié)構(gòu)中進(jìn)行檢索,以找到對(duì)此URI感興趣的監(jiān)聽(tīng)器。
其中當(dāng)查找到media節(jié)點(diǎn)時(shí),就會(huì)把media節(jié)點(diǎn)中的notifyForDescendants屬性為true(即正在通知的URI是content://media/external/images/media的子節(jié)點(diǎn))的ObserverEntry對(duì)象收集起來(lái)。最后,通過(guò)ObserverEntry對(duì)象的observer接口屬性回調(diào)到應(yīng)用程序進(jìn)程的ContentObserver.onChange方法,這樣整個(gè)流程就完整了。
這里在應(yīng)用程序進(jìn)程注冊(cè)URI時(shí),需要特別注意,ContentService服務(wù)在組織多叉樹(shù)數(shù)據(jù)結(jié)構(gòu)時(shí),遇到/、#、?這三個(gè)特殊符號(hào),就會(huì)停止構(gòu)造子節(jié)點(diǎn),因此content://media/external/images/media/#、content://media/external/images/media//#和content://media/external/images/media/#/?等URI形成的多叉樹(shù)結(jié)構(gòu)都是相同的,即上面的樹(shù)形結(jié)構(gòu)。(一開(kāi)始我在注冊(cè)URI時(shí),以為#號(hào)的作用和ContentProvider中#號(hào)一樣,代表所有的整型ID,坑了我很久)。
參考文檔
總結(jié)
以上是生活随笔為你收集整理的Android平台监听系统截屏方案预研及相关知识点的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 中断处理的那些事儿
- 下一篇: Android实战】DroidPlugi