开启真机的View Server引入HierarchyViewer/By写monkeyrunner自动化测试脚本

其实相关文章网上也有很多了,不过在真机上开启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.jarsmali-1.4.2.jarzip.exedexopt-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/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/

相关文章
相关标签/搜索