博客

开启真机的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.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/

Topic: 技术

企业云存储和企业网盘

今天看到, 印象笔记面对中国, 推出了学校版. 我思维比较发散, 立刻就想到了曾经的 ChinaRen 幼儿园. 现在看来, 这个可能是搜狐最接地气的互动产品之一, 可惜夭折.

潜意识里还是比较艳羡 Evernote 的产品形态, 相比较于网盘, 它似乎离用户的业务更近一些, 更容易找到卖点. 网盘现在面对客户, 真是满腹衷言, 却不知从何说起

想了又想, 还是归结到产品不成熟. 不是说产品不好, 或者功能不全; 而是我们自己的定位还很摇摆, 我们设想的, 和客户目前心理上能接受的, 差别有点大.

以后会多思考多写关于企业网盘的 blog

Topic: 商业

关于消费者邮箱使用的想法

从注册的角度来讲, 使用 QQ/Weibo 等第三方认证服务是符合逻辑的.

但是从客服的角度来讲, 让用户留下一个邮件地址, 或者留下一个电话号码也是符合逻辑的.

邮箱理应成为一个很好的客服平台(当然, 某种程度上客服也是最好的营销)

我们 SendCloud 在强调触发邮之余, 也应该宣传Email对于客服的意义. 触发邮不是这件事的目的, 共同为用户提供好的服务才是我们的目的.

以前可能因为社会习惯, 因为企业的意识, 因为缺乏好用的工具, 等等...我们对邮箱的客服作用发掘得不够. 更多的把邮箱当成了一个营销平台. 希望透过我们的努力, 可以改变这一点, 或者至少可以说我们为改变这一点提供基础服务.

目前 SendCloud 的用户里面, 应该有不少是把 Email 作为服务的一部分或者延伸来使用. 好的案例我们必须进行收集和推广.

换个角度, 为什么中国的在线 CRM 没有做起来? 再说说更近的例子, 微信的服务号最终会是个什么模式?

我觉得在从帐号服务, 到 EDM 之间, 应该还是有一个 CRM 的产品空间. 跨越 CRM 去做邮件营销, 尤其是意识上的跨越, 一定会出问题. 我们做触发邮也不会真正成功

西藏第九天

本来是波澜不惊的一天因为北京的雷雨天气毁了。返程在成都中转时被迫滞留。我打趣老妈说:您这趟旅游赚了,我做了这么多次飞机,这不过是第二回安排住宿,您头一趟坐飞机旅游,就有这样的体验。

这天就是安排了上午布达拉宫,下午回北京,结果。。。

Topic: 生活

西藏第八天

一大早取到了布宫的购票证。每人限登记4张身份证,还必须包括排队者自己的身份证。所以最后持购票证去买票还很麻烦,因为我爸的身份证登记在另外一拨委托人的购票证上。。。

然后出发去西藏博物馆,强烈推荐。如果旅游前没有做过西藏地理历史宗教文化风俗功课的,最好一到拉萨就来这里补课。

中午吃了五味轩火锅,是我在拉萨见到的第一家接受刷卡的餐馆,就凭这个也值得推荐。

下午去了色拉寺——总不能来西藏九天,三大寺一个也不去吧。可惜下午寺院大部分参观点都关闭,非常遗憾,计划下次来拉萨就花三天时间一个个参观过去。

幸运的是赶上了色拉寺的辩经时间。据说这里全盛时期有8000名僧人,而哲蚌寺有12000名僧人。。。我想这可能算是最大规模的神学院了吧。亲身走一遭,才体会三大寺之“大”

Topic: 生活

西藏第七天

按照司机的吩咐,我们六点半就在酒店门口等候,但小面包直到7点才赶到。因为它也是在各个等候点接满一车人才出发。小小一辆面包总共11个乘客,除去我们剩下六人里就是三拨人。

最后有乘客还提出要去吃早饭,反正磨磨蹭蹭8点才算正式上路。经过羊八井时俺还看到了启孜峰,然后是念青唐古拉峰的观景处,大家下来一通拍照。

经过那根拉山口,很欣慰邱可心没有表现出任何不适。远眺纳木错,又是拍照。

到了纳木错湖边,我是有些小小的失望。和四年前相比,游客太多了!而且湖边招待游客的藏式大帐篷变成了一排排低矮的简易房。整个景区感觉变得俗气。

回拉萨的路上,碰到两次查身份证。不知道是常年如此,还是雪顿节的原因

找了个代排队的,把身份证交给她彻夜排队去。我和老爸一起喝了约5两青稞酒 庆祝高原之行成功

Topic: 生活

西藏第六天

这一天就是从日喀则沿318返回拉萨。能值得说的只有一个雅鲁藏布江两岸风光了。

很明显在日喀则和拉萨之间有一段江两侧就是高峡,高峡两头是雅江冲刷出的平原谷地。大概能理解为什么分前藏后藏了。

中午1点左右回到拉萨。父母下午休息,我继续外出找去纳木错的旅游情况。当发现从纳木错返回拉萨时,以及排队买布宫门票都需要身份证时,再次调整行程——决定第二天就直接去纳木错,而不是按原计划在拉萨休息一天。

进入8月,马上就是雪顿节,宾馆价格狂涨了50%,纳木错价格也涨了20%,极度不爽

晚饭在东措旁的雪莲花餐厅吃的,尼泊尔风格?环境和口味都很好,推荐

Topic: 生活

西藏第五天

如果这一天从 00:00开始记录,那就是风声、雨声、冰雹声、下雪声、水流声、翻身声、邱可心呼唤头疼的呻吟。。。

雪线还在海拔很高的地方,所以落雪早晨看实在算不得什么,就薄薄一点儿。看着又是一个多云天气,我们没做逗留,8点就回程了。

5点半回到日喀则,参观扎什伦布寺。这是今天唯一的景点。晚饭在扎什伦布寺旁步行街上的松赞餐馆,也是很不错,除了一个手抓羊肉很失败。

本次西藏之行,就过了一大半了

Topic: 生活

西藏第四天

这一天纯粹就是赶路。过了定日后离开318转向珠峰,100公里路开了4个小时,人已经不能坐在后备箱,四个人挤后排,浑身都要颠散

有个观景台,可以看到喜马拉雅山脉的一系列雪山,立刻被震撼了,这是有生以来肉眼看到的最好的风景。可惜珠穆郎玛藏在云后。

靠近珠峰的时候,有那么几十秒,峰顶露出尖来,可惜没来得及留在相机里。希望明天还有机会。

夜宿大本营。感觉星星比小时候在钻头厂看到的还多,上次在岗什卡大本营也是远远不如。今天路途虽然辛苦,但很值得。

邱可心表现很棒。安检的时候很配合,高反很难受但还是没给大人找麻烦,晚饭几乎没吃,睡前又吐,10点半睡着。

Topic: 生活

西藏第三天

8点见到了司机,没有等到预想中的边防证和身份证,不爽

司机解释说九点半才办公,下午等我们到日喀则的时候身份证和边防证一定已经在那边等着了

因为我们5个人,在后备厢加了个垫子,暂时达达坐那里
沿机场高速转S307省道直奔羊湖

过山口时海拔较高,达达可能也是因为座位不好,有点高反。一路不爽直到日喀则把胃吐干净了才缓解

羊湖很漂亮
卡若拉冰川没有进景区交门票钱,附近停下来拍了拍

中午饭后到达江孜,参观白居寺,相比大昭寺拥挤的游客非常超值。最好要一个普通话解说。泥塑和壁画超赞

离开江孜就是日喀则,因为限速停下来休息了两趟。晚饭在吾尔朵藏餐吃的,严重推荐。

正是在吾尔朵的门口邱可心大吐特吐。然后精神回来,高高兴兴吃了晚饭,回房睡觉。

卖了6瓶矿泉水,3听红牛,一罐20元的氧气,以备明后天之用。

Topic: dada 生活
订阅 RSS - 博客 | BT的花