1. 程式人生 > >開啟真機的View Server引入HierarchyViewer

開啟真機的View Server引入HierarchyViewer

其實相關文章網上也有不少了,不過在真機上開啟View Server的中文文章好像只有一篇,前段時間按照這篇文章的內容,並結合英文源文去hack我的Nexus S(4.1.2)也走了一點彎路。現在總結一下我的步驟(其實有相當一部分拷貝了這篇,衷心感謝原文作者)。並寫點在開啟View Server之後monkeyrunner的指令碼。

先交待一下背景,monkeyrunner作為自動化測試Android系統工具在某些情況下還是比Robotium易用一些,不過monkeryrunner判斷測試結果是否正確的方法是把實際測試中的截圖與預先截好的正確的屏跟做比對!這個辦法不夠靈活。假如返回結果會顯示在一個文字框中,我從文字框裡取出字串能直接跟預期的字串比較,這樣就省事多了。

Android SDK自帶一個工具叫做monitor,它裡面的Hierarchy Viewer可以看到app的UI結構、控制元件屬性等等。monkeyrunner有一個類By,通過By可以在程式碼中根據控制元件ID定位到該控制元件從而寫更有針對性程式碼(比如點選按鈕、比如獲取文字框中的字串)。

可是出於安全考慮,Hierarchy Viewer只能連線Android開發版手機或是模擬器。只有當裝置或模擬器上啟動一個叫做View Server的服務,Hierarchy Viewer才能與其進行socket通訊,才能看到app的“View”。而絕大多數商業手機是無法開啟View Server的,所以Hierarchy Viewer也就無法連線到普通的商業手機。而By又依賴於Hierarchy Viewer,所以如果想在普通的商業手機上通過控制元件ID去做一些操作,連線模擬器執行通過的指令碼連線真機執行是會拋錯的。

不過小米手機是個例外,通過執行如下命令可以輕易開啟它的View Server:
adb shell service call window 1 i32 4939
然後通過執行如下命令判斷是否開啟View Server:
adb shell service call window 3
若返回值是:Result: Parcel(00000000 00000001 '........') 說明View Server處於開啟狀態
若返回值是:Result: Parcel(00000000 00000000 '........') 說明View Server處於關閉狀態
如果想關閉View Server執行如下命令:
adb shell service call window 2 i32 4939

除了小米手機之外,別的手機能不能開啟View Server?經過一番調查和實踐,其實只要是root,並且裝有busybox的手機,通過修改手機/system/framework中的某個檔案,就能夠開啟View Server。

下面就是我總結的開啟View Server的步驟(提醒:如果照我的步驟導致你的手機變磚,本人概不負責):

1.準備工作

a.解鎖手機,刷入第三方Recovery。這一步不是開啟View Server必須要做的。但是萬一手機通過正常方式啟動不了了,可以通過第三方Recovery裡的restore功能恢復手機系統,當然前提是在修改系統檔案前先通過backup功能做一個備份。

b.root手機。root的作用是獲取對手機系統檔案的讀寫許可權,這樣你就可以修改那個不允許開啟View Server的系統檔案了。

c.在手機中安裝BusyBox應用。我們在給自己生成的odex檔案簽名時會用到它。

d.用第三方Recovery備份手機系統。這一步不是必須步驟。

2.開始hack (再次提醒:請確保把下面每個步驟所有文字全部仔細看完後再開始操作)

a.將手機通過USB連線PC,確保adb服務執行正常。

b.備份手機上/system/framework/中的檔案至PC。備份的時候請確保PC上儲存備份檔案的資料夾結構與手機中的/system/framework相同,比如先在D盤上建立hack\system\framework的資料夾結構,然後執行
adb pull /system/framework D:\hack\system\framework

c.進入adb shell,輸出BOOTCLASSPATH:
echo $BOOTCLASSPATH
然後將輸出的路徑先暫時存起來。我的是(每個機器的$BOOTCLASSPATH都不一定一樣):
/system/framework/core.jar:/system/framework/core-junit.jar:/system/framework/bouncycastle.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/android.policy.jar:/system/framework/services.jar:/system/framework/apache-xml.jar

d.在命令列視窗中進入D:\hack,然後執行baksmali反編譯\system\framework下的services.odex檔案:
java -jar baksmali-1.4.2.jar –x -a <api level> –c <local bootclasspath> system\framework\services.odex
引數解釋:https://code.google.com/p/smali/wiki/DeodexInstructions
想特別說明的是“-a”後跟的數字,表示你係統的API Level(與你的系統版本有關)。系統版本和API Level的對照關係如下:

這一步在我的機器(version 4.1.2)上的命令是:
java -jar baksmali-1.4.2.jar -x -a 16 -c system\framework\core.jar:system\framework\core-junit.jar:system\framework\bouncycastle.jar:system\framework\ext.jar:system\framework\framework.jar:system\framework\android.policy.jar:system\framework\services.jar:system\framework\apache-xml.jar system\framework\services.odex
此步成功的話,在D:\hack下,會有個out資料夾生成。
注意,-c後面跟的是本地備份的jar包路徑,把上一步暫存的路徑中system前面的“/”去掉,把其它的“/”換成“\”。
這裡順便解釋一下dex檔案、odex檔案和smali檔案:

  • dex檔案:dex是Dalvik VM executes的全稱,即Android Dalvik執行程式,並非Java的位元組碼而是Dalvik位元組碼,16進位制機器指令。
  • odex檔案:將dex檔案依據具體機型而優化,形成的optimized dex檔案,提高軟體執行速度,減少軟體執行時對RAM的佔用。
  • smali檔案:將dex檔案變為可讀易懂的程式碼形式,反編譯出文件的一般格式。

e.用Eclipse開啟out\com\android\server\wm\WindowManagerService.smali檔案查詢.method private isSystemSecure()Z這個函式,在這段程式碼的倒數7,8行“:goto_21”和“return v0”之間加入“const/4 v0, 0x0”一行。
.method private isSystemSecure()Z函式最後幾行變為:
if-eqz v0, :cond_22

const/4 v0, 0x1

:goto_21
const/4 v0, 0x0
return v0

:cond_22
const/4 v0, 0x0

goto :goto_21
.end method

f.現在執行smali,重新編譯:
java -jar smali-1.4.2.jar -o classes.dex out
這時候,應該在D:\hack資料夾中出現了classes.dex檔案

g.用zip工具把生成的classes.dex打成jar包
zip.exe services_hacked.jar classes.dex

h.進入adb shell,輸入su然後回車,獲得ROOT許可權

i.接著輸入mount | grep /system檢視哪個分割槽掛載了/system,例如我的是:
/dev/block/platform/s3c-sdhci.0/by-name/system /system ext4 ro,relatime,barrier=1,data=ordered 0 0

j.接著輸入以下命令重新掛載/system,並更改/system許可權(請將“/dev/block/platform/s3c-sdhci.0/by-name/system”替換成你的/system掛載分割槽):
mount -o remount /dev/block/platform/s3c-sdhci.0/by-name/system /system
這一步的作用是為了後面的p步能夠將/system/framework裡的services.odex替換掉。

k.再次輸入mount | grep /system 確認/system已經改成可寫的了(以前是“ro”,現在是“rw”)

l.將services_hacked.jar和dexopt-wrapper複製到手機的/data/local/tmp資料夾中
adb push D:\hack\services_hacked.jar /data/local/tmp
adb push D:\hack\dexopt-wrapper /data/local/tmp

m.進入adb shell,輸入su後,將dexopt-wrapper的許可權改為777
chmod 777 /data/local/tmp/dexopt-wrapper

n.cd到/data/local/tmp資料夾下,執行:
./dexopt-wrapper ./services_hacked.jar ./services_hacked.odex <c步暫存的bootclasspath,但要排除掉“:/system/framework/services.jar”>
這一步在我的機器上的命令是:
./dexopt-wrapper ./services_hacked.jar ./services_hacked.odex /system/framework/core.jar:/system/framework/core-junit.jar:/system/framework/bouncycastle.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/android.policy.jar:/system/framework/apache-xml.jar
這樣,便在/data/local/tmp資料夾中生成了services_hacked.odex這個檔案

o.給我們自己生成的services_hacked.odex簽名:
busybox dd if=/system/framework/services.odex of=/data/local/tmp/services_hacked.odex bs=1 count=20 skip=52 seek=52 conv=notrunc
引數解釋:

  • if - input file
  • of - output file
  • bs - block size (1 byte)
  • count - number of blocks
  • skip - input file offset
  • seek - output file offset
  • conv=notrunc - don’t truncate the output file.

p.將/system/framework裡的services.odex替換成我們自己製作的services_hacked.odex
dd if=/data/local/tmp/services_hacked.odex of=/system/framework/services.odex
稍過一會,手機就會自動重啟

q.成功重啟後,用以下命令開啟View Server:
adb shell service call window 1 i32 4939

r.用以下命令檢視View Server是否開啟:
adb shell service call window 3
返回的值若是Result: Parcel(00000000 00000001 '........'),那麼你就成功開啟View Server了!

3.災難恢復

如果你不幸在上一節p步手機重啟後進不了HOME,一直處在bootloop狀態,不要用拔電池的方式重啟手機。這個時候你已經可以使用adb了,在命令列窗口裡執行:
adb push D:\hack\system\framework\services.odex /system/framework/services.odex
就可以把之前備份的services.odex再拷回去,這樣手機就能進入HOME了。

如果你十分不小心重啟了手機,這時候你會發現既進不了HOME也使用不了adb,那就只能進入第三方的Recovery,用之前的備份去恢復手機系統了。

下面的是如何利用HierarchyViewer和By這兩個類去靈活完成monkeyrunner的指令碼(monkeyrunner的其它基本程式碼在這裡不贅述)。

先假設一個場景,有一個app,開啟後有一個按鈕,點選這個按鈕後,正常情況下會在下面的文字框裡返回“ok”。我們需要用程式碼實現點選這個按鈕,然後取得文字框中的返回值與預期結果“ok”做比對。

我們通過前面介紹的Hierarchy Viewer看到app裡按鈕的ID是“id/button”,文字框的ID是“id/output”。

為了通過控制元件ID操作手機,我們需要在程式碼開頭import這兩個類:
from com.android.monkeyrunner.easy import By
from com.android.chimpchat.hierarchyviewer import HierarchyViewer

然後用下面的程式碼獲得按鈕物件:
hierarchyViewer = device.getHierarchyViewer()
viewNodeButton = hierarchyViewer.findViewById("id/button")

用下面的程式碼獲得按鈕的中心座標:
pointButton = HierarchyViewer.getAbsoluteCenterOfView(viewNodeButton)

這個時候pointButton.x是按鈕的中心點橫座標,pointButton.y是按鈕的中心點縱座標,可是有了這兩個座標,我們還不能直接用device.touch(x, y, "DOWN_AND_UP")的方式去點這個按鈕,因為這個座標是以開發設計app時手機的螢幕解析度為基準的,所以我們還需要換算一下才知道在目前的測試手機上按鈕的中心座標是什麼。

先通過Hierarchy Viewer查到設計時的螢幕解析度(比方說是320和533),並在程式碼中定義:
originalResolutionWidth = 320
originalResolutionHeight = 533

再通過MonkeyDevice的API獲得目前的測試手機的螢幕解析度:
actualResolutionWidth = int(device.getProperty("display.width"))
actualResolutionHeight = int(device.getProperty("display.height"))

然後用下面程式碼得到目的手機解析度與開發設計時的解析度的比值:
xRatio = float(actualResolutionWidth) / originalResolutionWidth
yRatio = float(actualResolutionHeight) / originalResolutionHeight

有了xRatio和yRatio,我們用下面的程式碼輕而易舉就能點到正確的座標上了:
device.touch(int(pointRegister.x * xRatio), int(pointRegister.y * yRatio), "DOWN_AND_UP")

按鈕點下後,我們需要用下面程式碼獲取文字框裡的返回值:
viewNodeOutput = hierarchyViewer.findViewById("id/output")
output = viewNodeOutput.namedProperties.get("text:mText").value

這樣我們就能用output與預期的“ok”做比對了:
if output == "ok":
    print "success"
else:
    print "fail"

最後加一句關於unittest的,如果想按照python的unittest框架寫測試用例,會用到
self.assertEquals(expectedString, actualString)
這樣的語句,如果是中文作業系統,跑的時候有可能會出現LookupError: unknown encoding gbk這樣的錯誤,請參考Android 自動化測試學習筆記裡面提供的方法解決。

更新20130912:
如果要點選Menu裡的Label,會發現所有的id名都一樣。這個時候怎麼辦?也許可以用device.press('KEYCODE_DPAD_UP/DOWN/LEFT/RIGHT')的方法來導航到你需要點選的Label,不過我沒有試過。
第三方的包AndroidViewClient,可以通過Label上的Text定位到你想點選的Label。
1.把二進位制的jar下載下來並放到sdk\tools\lib下
2.在py檔案裡from com.dtmilano.android.viewclient import ViewClient
3.然後device, serialno = ViewClient.connectToDeviceOrExit(),啟動一個activity,用viewclient = ViewClient(device, serialno)和viewclient.dump()可以拿到所有的控制元件,然後通過Text就能找到需要的控制元件了。具體請參考http://blog.csdn.net/jiguanghoverli/article/details/10189401https://github.com/dtmilano/AndroidViewClient/issues/22
如果在執行過程中看到Exception: adb="adb.exe" is not executable. Did you forget to set ANDROID_HOME in the environment?這種錯誤,把adb.exe放到C:\Windows\system32\下面。
另外,引入這個第三方包還有一個好處是,在測試某些app時不用考慮解析度的問題了(目前我碰到的是如果點選某個app的menu裡的label時不需要考慮解析度,沒有調查到底是因為menu的原因,還是不同的app的開發機制原因)。

更新20130913:
在Windows中文系統下,即使按正文中連結裡的辦法解決了LookupError: unknown encoding gbk這樣的錯誤,但碰到真正的中文(如果不“解決”,就算assert的是英文,也會報上面的錯誤)還是會報錯,如AssertionError: '\xe5\x9f\x8e\xe5\xb8\x82' != u'\u57ce\u5e02',這時需要把被比較的字串encode("UTF-8")一下,具體請參考http://1.vb.blog.163.com/blog/static/104546220071113105047729/