最近UC瀏覽器中文版出了一個快速搜索的功能, 在使用其餘app的時候, 若是複製了一些內容, 屏幕頂部會彈一個窗口, 提示一些操做, 點擊後跳轉到UC, 顯示這個懸浮窗不須要申請android.permission.SYSTEM_ALERT_WINDOW權限. php
以下圖, 截圖是在使用Chrome時截的, 可是屏幕頂部卻有UC的view浮在屏幕上. 我使用的是小米, 我並無給UC授懸浮窗權限, 因此我看到這個懸浮窗時是很震驚的. java
作過懸浮窗功能的人都知道, 要想顯示懸浮窗, 要有一個服務運行在後臺, 經過getSystemService(Context.WINDOW_SERVICE)拿到WindowManager, 而後向其中addView, addView第二個參數是一個WindowManager.LayoutParams, WindowManager.LayoutParams中有一個成員type, 有各類值, 通常設置成TYPE_PHONE就能夠懸浮在不少view的上方了, 可是調用這個方法須要申請android.permission.SYSTEM_ALERT_WINDOW權限, 在不少機型上, 這個權限的名字叫懸浮窗, 好比小米手機上默認是禁用這個權限的, 有些惡意app會用這個權限彈廣告, 並且很難追查是哪一個應用彈的. 若是這個權限被禁用, 那麼結果就是懸浮窗沒法展現, 好比有道詞典的複製查詞功能, 在小米手機上常常沒用, 實際上是用戶沒有受權, 並且應用也沒有引導用戶給它打開受權. android
如今UC能突破這個限制, 我很好奇它是怎麼作到的. 瀏覽器
Android開發有點蛋疼的地方就是太容易被反編譯, 但有時這也成爲咱們研究別人app的一種手段. app
使用apktool能夠很輕鬆的反編譯UC. 學習
逆向別人的app, 比較關鍵的地方是怎麼找代碼, 由於代碼基本上都是混淆的, 直接看確定是看不懂的, 只能去找, 突破口通常在字符資源上, 好比咱們看到上圖中的快速搜索是UC的字符, 那麼咱們到res/values/strings.xml去找快速搜索, 就能夠找到下面的內容 測試
<string name="dark_search_banner_search">快速搜索</string>
這裏咱們拿到了快速搜索對應的名字dark_search_banner_search, Android在編譯時會給每一個資源分配一個id, 咱們grep一下這個字符資源的名字就能知道id是多少, 通常在R.java, res/values/public.xml中有定義, 我直接到public.xml中找到了它的id this
<public type="string" name="dark_search_banner_search" id="0x7f070049" />
有了字符資源的id 0x7f070049, 咱們再在代碼裏面grep一下這個id, 就能知道哪幾個文件使用了這個字符資源. spa
之因此這麼肯定是在代碼裏, 是由於UC在咱們複製的內容不一樣時, 懸浮窗標題會不同, 必定是在代碼裏控制的, 結果以下 線程
./com/uc/browser/b/f.smali
結果可能和你們不同, 可是必定會找到一個被混淆的smali文件
這一部應該是最噁心的. smali代碼和java代碼的關係, 就像彙編代碼和C++代碼, 可是smali比彙編代碼要容易理解的多, 否則也不會有那麼多公司故意將代碼寫在C++層了.
雖然代碼都被混淆了, 並且以咱們不熟悉的方式出現, 但咱們能夠根據一些蛛絲馬跡來判斷代碼的執行, 好比Framework的類和API是不能被混淆的, 這也是咱們能看懂smali的緣由之一, 咱們能夠結合這些麪包屑來還原整個app代碼, 固然這須要咱們對smali很熟悉, 若是不熟悉smali, 至少要對Android的API熟悉. 由於有時實在看不懂, 咱們要靠猜來還原一段代碼的邏輯.
首先在代碼裏面找到0x7f070049, 發現了以下代碼
(省略) const v3, 0x7f070049 invoke-virtual {v1, v3}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String; move-result-object v1 iput-object v1, v0, Lcom/uc/browser/b/a;->dpC:Ljava/lang/String; :cond_9 (省略) invoke-virtual {v0, v1}, Lcom/uc/browser/b/a;->o(Landroid/graphics/drawable/Drawable;)V :try_end_2 .catch Ljava/lang/Exception; {:try_start_2 .. :try_end_2} :catch_0 goto/16 :goto_0 (省略)
這是0x7f070049出現以後的一部分代碼, 一路看下來, 其實都是在取值賦值, 就拿0x7f070049來講:
#使v3寄存器的值爲0x7f070049 const v3, 0x7f070049 #v1是Resources實例, 調用它的getString方法, 方法的參數是v3中的值 invoke-virtual {v1, v3}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String; #將結果存入v1寄存器 move-result-object v1
其實就是咱們經常使用的getResources().getString
其實若是一直這麼看下去, 會發現毫無頭緒, 剩下的代碼一直在幹差很少的事情, 因此我只截取了這部分, 注意最後一行
goto/16 :goto_0
也就是說, 有可能代碼轉到goto_0那兒去了, 那麼看看goto_0那裏又寫了些什麼
:goto_0 (省略) const-string v1, "window" invoke-virtual {v0, v1}, Landroid/content/Context;->getSystemService(Ljava/lang/String;)Ljava/lang/Object; move-result-object v0 check-cast v0, Landroid/view/WindowManager; invoke-interface {v0}, Landroid/view/WindowManager;->getDefaultDisplay()Landroid/view/Display; move-result-object v0 invoke-virtual {v0}, Landroid/view/Display;->getWidth()I move-result v0 iget-object v1, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams; iput v0, v1, Landroid/view/WindowManager$LayoutParams;->width:I iget-object v0, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams; invoke-virtual {v10}, Lcom/uc/browser/b/a;->getContext()Landroid/content/Context; move-result-object v1 invoke-virtual {v1}, Landroid/content/Context;->getResources()Landroid/content/res/Resources; move-result-object v1 const v2, 0x7f0d0022 invoke-virtual {v1, v2}, Landroid/content/res/Resources;->getDimension(I)F move-result v1 float-to-int v1, v1 iput v1, v0, Landroid/view/WindowManager$LayoutParams;->height:I iget-object v0, v10, Lcom/uc/browser/b/a;->mWindowManager:Landroid/view/WindowManager; iget-object v1, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams; invoke-interface {v0, v10, v1}, Landroid/view/WindowManager;->addView(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V
其實看到const-string v1, "window", 咱們就應該有所警戒了, 這多是關鍵代碼了. 爲何這麼說? 由於懸浮窗的實現裏面, 須要獲取WindowManager, 從而須要調用Context.getSystemService(Context.WINDOW_SERVICE), 而官方文檔寫了Context.WINDOW_SERVICE就是常量window. 然後咱們看到代碼中構造了WindowManager.LayoutParams, 最終在addView時傳入.
看到這裏, 我也以爲很奇怪, 我在懸浮窗原理中寫的是我知道的實現懸浮窗的方法, UC的實現好像跟我調用的是相同的API, 也沒看到反射之類可能展現奇技淫巧的代碼, 爲何UC就能夠不須要權限直接顯示懸浮窗呢?
我認爲addView的第二個參數WindowManager.LayoutParams多是關鍵, 因此我須要知道UC是如何構造這個WindowManager.LayoutParams的.
因爲是系統的類, 沒法混淆, 直接搜索LayoutParams就找到了下面的代碼
iget-object v1, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
這句話就是把v10的值賦給v1, v10是com/uc/browser/b/a的成員dpx, 那麼打開com/uc/browser/b/a.smali看看dpx究竟是怎麼構造的.
(省略) .field dpx:Landroid/view/WindowManager$LayoutParams; (省略) .line 68 new-instance v0, Landroid/view/WindowManager$LayoutParams; invoke-direct {v0}, Landroid/view/WindowManager$LayoutParams;-><init>()V iput-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams; .line 69 if-eqz p2, :cond_0 .line 70 iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams; const/16 v1, 0x7d5 iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I .line 74 :goto_0 iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams; const/4 v1, 0x1 iput v1, v0, Landroid/view/WindowManager$LayoutParams;->format:I (省略)
這裏的代碼就很簡單的, 我最早看的是下面這段
const/16 v1, 0x7d5 iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I
這兩句代碼就是把WindowManager.LayoutParams.type字段設成0x7d5, 官網上寫了0x000007d5是WindowManager.LayoutParams.TYPE_TOAST的值.
實際測試了一下, 將type設置成TYPE_TOAST果真有奇效, 不須要android.permission.SYSTEM_ALERT_WINDOW權限就能顯示一個懸浮窗.
以前我一直覺得調用了系統WindowManager.addView須要android.permission.SYSTEM_ALERT_WINDOW權限, 但實際上調用這個方法是不須要權限的, 在Android源碼中有這麼一段
public int checkAddPermission(WindowManager.LayoutParams attrs) { int type = attrs.type; if (type < WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW || type > WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) { return WindowManagerImpl.ADD_OKAY; } String permission = null; switch (type) { case TYPE_TOAST: // XXX right now the app process has complete control over // this... should introduce a token to let the system // monitor/control what they are doing. break; case TYPE_INPUT_METHOD: case TYPE_WALLPAPER: // The window manager will check these. break; case TYPE_PHONE: case TYPE_PRIORITY_PHONE: case TYPE_SYSTEM_ALERT: case TYPE_SYSTEM_ERROR: case TYPE_SYSTEM_OVERLAY: permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW; break; default: permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW; } if (permission != null) { if (mContext.checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { return WindowManagerImpl.ADD_PERMISSION_DENIED; } } return WindowManagerImpl.ADD_OKAY; }
能夠猜到這個方法是往系統的WindowManager裏addView的時候作權限檢查用的, 那個type就是咱們在構造WindowManager.LayoutParams時賦值的type, 能夠看到, 除了TYPE_TOAST, 其餘都是要權限的, 並且很是喜感的是, 代碼中的註釋還說他們如今對這種type毫無限制, 應該引入標記來限制開發者.
看到有評論說這樣的是不支持點擊的. 我以前寫的一個app有懸浮窗播放功能, 支持拖動窗口和點擊暫停, 關閉窗口等等, 實測功能正常.
可是在2.3上不能接收點擊事件.
評論區的浮海大蝦同窗有更多補充以下:
TYPE_TOAST一直均可以顯示, 可是用TYPE_TOAST顯示出來的在2.3上沒法接收點擊事件, 所以仍是沒法隨意使用.
下面是我以前研究後臺線程顯示對話框的時候記得筆記, 你們能夠看看咱們項目中有需求須要在後臺任務中顯示Dialog, 項目最初的作法是用Activity模擬Dialog, 一個Activity已經承載了近20種Dialog, 代碼混亂至極. 後來我發現Dialog能夠經過改變Window Type實現不依賴Activity顯示, 而後就很興奮的要在使用這種方式來做爲新的實現方式.
最初WindowType是WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, 但是這是懸浮窗了, MIUI會默認禁止(真他媽操蛋,也沒有任何提示)最終放棄. 後來試着換成了WindowManager.LayoutParams.TYPE_TOAST, 起初效果很好,MIUI也不由止了, 哪裏都能顯示, 這下開心了. 但是後來又發如今2.3上不能接收點擊事件, 也就是說Dialog上的按鈕不能點擊, 這他媽就很操蛋了, 又放棄了. 又試了試其餘的Type都不能知足需求, 結果以下:TYPE_SEARCH_BAR: 未知
TYPE_ACCESSIBILITY_OVERLAY: 拒絕使用
TYPE_APPLICATION: 只能配合Activity在當前APP使用TYPE_APPLICATION_ATTACHED_DIALOG: 只能配合Activity在當前APP使用
TYPE_APPLICATION_MEDIA: 沒法使用(什麼也不顯示)
TYPE_APPLICATION_PANEL: 只能配合Activity在當前APP使用(PopupWindow默認就是這個Type)
TYPE_APPLICATION_STARTING: 沒法使用(什麼也不顯示)
TYPE_APPLICATION_SUB_PANEL: 只能配合Activity在當前APP使用TYPE_BASE_APPLICATION: 沒法使用(什麼也不顯示)
TYPE_CHANGED: 只能配合Activity在當前APP使用
TYPE_INPUT_METHOD: 沒法使用(直接崩潰)
TYPE_INPUT_METHOD_DIALOG: 沒法使用(直接崩潰)
TYPE_KEYGUARD_DIALOG: 拒絕使用
TYPE_PHONE: 屬於懸浮窗(而且給一個Activity的話按下HOME鍵會出現看不到桌面上的圖標異常狀況)
TYPE_TOAST: 不屬於懸浮窗, 但有懸浮窗的功能, 缺點是在Android2.3上沒法接收點擊事件
TYPE_SYSTEM_ALERT: 屬於懸浮窗, 可是會被禁止
關於UC如何處理2.3的問題, 我並無仔細看, 由於我確實是沒有在2.3上測過使用TYPE_TOAST的狀況, 但願有機器的同窗能幫忙測一下UC這個功能在2.3上的具體表現. 另外我的的解決方案是在2.3上使用級別更高的type, 我記得剛開始用小米的時候, 是沒有懸浮窗這個權限的管理的, 加上2.3的手機如今不少都沒有維護了, 直接申請android.permission.SYSTEM_ALERT_WINDOW也無妨.
但仍是但願能有同窗告知一下UC在2.3上是如何表現這個功能的.
如今咱們都知道了如何在不申請權限的狀況下顯示懸浮窗, 我相信以中國Android開發者的腦洞, 必定會有不少有趣或噁心的功能被開發出來, 一方面我本身以爲這個東西頗有用, 能夠實現一些很神奇的功能, 另外一方面又擔憂這個API被濫用, 最終不得不限制權限.
還有就是, 逆向分析僅用於學習, 不要幹違法的事情.
本人技術有限, 若是文中有錯誤的歡迎指正, 以避免誤導他人