開啟真機的View Server引入HierarchyViewer/By寫monkeyrunner自動化測試腳本
其實相關文章網上也有不少了,不過在真機上開啟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備份手機系統。這一步不是必須步驟。
e.在D盤下創建hack文件夾,下載baksmali-1.4.2.jar、smali-1.4.2.jar、zip.exe和dexopt-wrapper這些后面要用到的工具并保存在D:\hack下面。
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/10189401、https://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/

浙公網安備 33010602011771號