其實相關文章網上也有很多了,不過在真機上開啓View Server的中文文章好像只有一篇,前段時間按照這篇文章的內容,並結合英文源文去hack個人Nexus S(4.1.2)也走了一點彎路。如今總結一下個人步驟(其實有至關一部分拷貝了這篇,衷心感謝原文做者)。並寫點在開啓View Server以後monkeyrunner的腳本。html
先交待一下背景,monkeyrunner做爲自動化測試Android系統工具在某些狀況下仍是比Robotium易用一些,不過monkeryrunner判斷測試結果是否正確的方法是把實際測試中的截屏與預先截好的正確的屏跟作比對!這個辦法不夠靈活。假如返回結果會顯示在一個文本框中,我從文本框裏取出字符串能直接跟預期的字符串比較,這樣就省事多了。java
Android SDK自帶一個工具叫作monitor,它裏面的Hierarchy Viewer能夠看到app的UI結構、控件屬性等等。monkeyrunner有一個類By,經過By能夠在代碼中根據控件ID定位到該控件從而寫更有針對性代碼(好比點擊按鈕、好比獲取文本框中的字符串)。python
但是出於安全考慮,Hierarchy Viewer只能鏈接Android開發版手機或是模擬器。只有當設備或模擬器上啓動一個叫作View Server的服務,Hierarchy Viewer才能與其進行socket通訊,才能看到app的「View」。而絕大多數商業手機是沒法開啓View Server的,因此Hierarchy Viewer也就沒法鏈接到普通的商業手機。而By又依賴於Hierarchy Viewer,因此若是想在普通的商業手機上經過控件ID去作一些操做,鏈接模擬器運行經過的腳本鏈接真機運行是會拋錯的。android
不太小米手機是個例外,經過執行以下命令能夠輕易開啓它的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 4939git
除了小米手機以外,別的手機能不能開啓View Server?通過一番調查和實踐,其實只要是root,而且裝有busybox的手機,經過修改手機/system/framework中的某個文件,就可以開啓View Server。github
下面就是我總結的開啓View Server的步驟(提醒:若是照個人步驟致使你的手機變磚,本人概不負責):shell
1.準備工做apache
a.解鎖手機,刷入第三方Recovery。這一步不是開啓View Server必需要作的。可是萬一手機經過正常方式啓動不了了,能夠經過第三方Recovery裏的restore功能恢復手機系統,固然前提是在修改系統文件前先經過backup功能作一個備份。api
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文件:
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
參數解釋:
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/