ATX Android WebView 研究笔记

codeskyblue · August 29, 2018 · Last by z1073336883 replied at October 12, 2018 · 6730 hits
本帖已被设为精华帖!

Android WebView 研究笔记

记录一下省的忘记了,遍看源码,边猜测,边实验

chromedriver 用了哪些命令和资源

目前 Android WebView 支持主要还是靠 ChromeDriver 来实现的。

Chromedriver 貌似一定要 ADB 才行。其中用到了命令有 ps, grep, adb forward, am start 等等

来源: https://github.com/bayandin/chromedriver/blob/907b958e09dbfdafb13e9257b181009d0ab43b4a/chrome/adb_impl.cc

既然还用到了adb forward .. localabstract, 先打开一个带 webview 的 app(这里用了微信)
查看下有哪些服务监听了 localabstract

$ cat /proc/net/unix | grep @
0000000000000000: 00000002 00000000 00010000 0001 01 32803 @bthcitraffic
0000000000000000: 00000002 00000000 00010000 0001 01 3789231 @chrome_devtools_remote
0000000000000000: 00000002 00000000 00010000 0001 01 32963 @/data/misc/bluedroid/.a2dp_ctrl
0000000000000000: 00000002 00000000 00010000 0001 01    76 @time_genoff
0000000000000000: 00000002 00000000 00010000 0001 01    88 @android:debuggerd
0000000000000000: 00000002 00000000 00010000 0001 01 4640516 @webview_devtools_remote_13680
0000000000000000: 00000002 00000000 00010000 0001 01 17596 @jdwp-control
0000000000000000: 00000003 00000000 00000000 0001 03 4453235 @jdwp-control
0000000000000000: 00000003 00000000 00000000 0001 03 4250761 @jdwp-control

其中@符号代码监听的本地 Unix socket
这里看到有两个 @chrome_devtools_remote@webview_devtools_remote_13680 比较吸引人。
chrome_devtools_remote应该对应的是浏览器。
webview_devtools_remote_13680对应的应该是应用内的 WebView,根据源码感觉这个 13680 应该是个 pid

odin:/ $ ps | grep 13680
USER      PID   PPID  VSIZE  RSS   WCHAN              PC  NAME
u0_a280   13680 591   2522104 168072 SyS_epoll_ 0000000000 S com.tencent.mm:tools

com.tencent.mm这个应该就是微信了。

先把这个 forward 到本地

$ adb forward tcp:5000 localabstract:webview_devtools_remote_13680

看 chromedriver 的代码,里面自带一些 http 接口,具体看下面的代码
https://github.com/bayandin/chromedriver/blob/33218feb63bc972c7175390ee2302fe5a2f25056/chrome/devtools_http_client.cc#L92

有个比较简单的接口获取浏览器的版本号,以此来确定该使用的 chromedriver 版本。

$ curl localhost:5000/json/version
{
   "Android-Package": "com.tencent.mm",
   "Browser": "Chrome/57.0.2987.132",
   "Protocol-Version": "1.2",
   "User-Agent": "Mozilla/5.0 (Linux; Android 7.1.1; OD103 Build/NMF26F; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 MQQBrowser/6.2 TBS/044205 Mobile Safari/537.36",
   "V8-Version": "5.7.492.72",
   "WebKit-Version": "537.36 (@user1128ad40ab2e3d761c06d1)"
}

从这个链接里面可以看到没有 Chrome Version(57) 对应的应该使用的 chromedriver 版本号 (2.29) https://chromedriver.storage.googleapis.com/2.41/notes.txt

去这个地址下载 https://npm.taobao.org/mirrors/chromedriver

继续更新

猜测:除了微信有个调试的按钮,其他的 H5 的应用应该也是可以的吧。
先试了试 Appium 自带的 demo apk
https://github.com/appium/java-client/blob/master/src/test/java/io/appium/java_client/ApiDemos-debug.apk
安装到手机上之后,进入Views -> WebView,然后看到这么一个简单的界面

图层结构显示也有 Hierarchy

Chrome 浏览器打开 chrome://inspect

更多相关的内容参考谷歌的 Chrome-Devtools 文档 https://developers.google.com/web/tools/chrome-devtools/remote-debugging/?hl=zh-cn

在浏览器里面并没有发现当前这个应用的 webview。
重新读了下谷歌文档 - 远程调试 WebView

需要在应用中开启 WebViewDebugable 模式才行,也就是说默认不开启喽。

使用 VXP 强制开始 WebView

那有没有强制开启的方法呢? 很快谷歌出来一篇文章
强制开启 android webview debug 模式使用 Chrome inspect

看来手机需要 Root 才行,还用到了高端的 Xposed(不对呀,我明明记得可以不用 root 的呀),继续谷歌了下发现还有个 VirtualXposed 可以不用 root
相关的 VirtualXposed(简称 VXP)简介 https://github.com/android-hacker/VirtualXposed
安装很简单,跟普通的 App 一样,下载 apk 到手机,安装就 ok 了。
接下来用 VXP 自带的安装器,安装 ApiDemos-debug.apk
然后再安装个插件 WebViewDebugHook 同样是个 Apk 下载 (官方地址:https://github.com/feix760/WebViewDebugHook),用 VXP 安装上就好了。

关于那个插件的原理可以在这里看到 https://www.jianshu.com/p/d6699cd4505e

安装上之后,先打开

进入到模块中,勾选

返回,然后向下滑动,进入到这个界面,点击箭头位置

拖到最下面点击 重启 是插件更改生效。

PS: 使用 VXP 安装的应用pm list packages是看不到的,只有在运行的时候用ps才可以查看到

回到应用界面,点击刚才安装的 APK API Demos, 重新进入到 WebView,然后打开chrome://inspect
这个时候终于看到了,期望的界面。

应用使用的 Chrome 版本是 62.0.3202.84
Appium 的代码里面有个对应的表可以直接查改用哪一个 chromedriver https://github.com/appium/appium-chromedriver/blob/master/lib/chromedriver.js#L20

PS D:\Temp> .\chromedriver.exe -h
Usage: D:\Temp\chromedriver.exe [OPTIONS]

Options
  --port=PORT                     port to listen on
  --adb-port=PORT                 adb server port
  --log-path=FILE                 write server log to file instead of stderr, increases log level to INFO
  --log-level=LEVEL               set log level: ALL, DEBUG, INFO, WARNING, SEVERE, OFF
  --verbose                       log verbosely (equivalent to --log-level=ALL)
  --silent                        log nothing (equivalent to --log-level=OFF)
  --version                       print the version number and exit
  --url-base                      base URL path prefix for commands, e.g. wd/url
  --whitelisted-ips               comma-separated whitelist of remote IPv4 addresses which are allowed to connect to Chr
omeDriver

chromedriver 默认监听 9515 端口。

先来段简单的代码试试。获取 WebView 中显示的内容

# coding: utf-8
from selenium import webdriver

options = {'androidPackage': 'com.appium.android.apis', 'androidUseRunningApp': True}
d = webdriver.Remote("http://localhost:9515", {"chromeOptions": options})
# /html/body/a
el = d.find_element_by_xpath("/html/body/a")
print(el.text)

运行,之后就报错了,提示

WebDriverException: Message: unknown error: com.appium.android.apis is not installed on device 3578298f
  (Driver info: chromedriver=2.41.578737 (49da6702b16031c40d63e5618de03a32ff6c197e),platform=Windows NT 10.0.16299 x86_6
4)

估计是通过pm path <package)name>判断的有没有安装,也罢,再安装一遍好了。然后给 options 加了androidProcess参数

options = {
    'androidPackage': 'io.appium.android.apis',
    'androidUseRunningApp': True,
    'androidProcess': 'io.appium.android.apis'
}

重新再运行代码终于出来了

Hello World! - 1

哎,真是麻烦。。 -~~

续:分析 chromedriver 到底在做什么?

chromedriver 有个参数--verbose可以输出更详细的信息。

./chromedriver --verbose

然后写段 Python 代码请求一下看看

from selenium import webdriver
from contextlib import contextmanager

@contextmanager
def driver(package_name):
    capabilities = {
        "androidDeviceSerial": "bf755cab",
        "androidPackage": package_name,
        "androidUseRunningApp": True,
    }
    dr = webdriver.Remote("http://localhost:9515", {
        "chromeOptions": capabilities
    })
    try:
        yield dr
    finally:
        dr.quit()

def main():
    package_name = "io.appium.android.apis"
    package_name = "com.xueqiu.android"

    with driver(package_name) as dr:
        print(dr.current_url)
        #dr.save_screenshot("s.png")

if __name__ == "__main__":
    main()

运行这段 Python 代码之后,你会看到 chromedriver 有一堆的输出。下面我把主要的代码贴出来

[17.612][DEBUG]: Sending adb command: host:devices
[17.612][DEBUG]: Sending adb command: host:transport:bf755cab|shell:pm path com.xueqiu.android
[17.930][DEBUG]: Received adb response: package:/data/app/com.xueqiu.android-1/base.apk
[17.930][DEBUG]: Sending adb command: host:transport:bf755cab|shell:ps
--- 这里还缺少获取 cat /proc/net/unix的步骤,log中看不到
[17.988][DEBUG]: Sending adb command: host-serial:bf755cab:forward:tcp:12604;localabstract:webview_devtools_remote_8495

--- 验证当前的Browser版本号是否在支持的范围内
[17.989][DEBUG]: DevTools request: http://localhost:12604/json/version
[17.992][DEBUG]: DevTools response: {
   "Android-Package": "com.xueqiu.android",
   "Browser": "Chrome/62.0.3202.84",
   "Protocol-Version": "1.2",
   "User-Agent": "Mozilla/5.0 (Linux; Android 6.0.1; SM901 Build/MXB48T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36",
   "V8-Version": "6.2.414.37",
   "WebKit-Version": "537.36 (@957d898f0f6e46cd9661d91d2bae899f34c1c4b6)",
   "webSocketDebuggerUrl": "ws://localhost:12604/devtools/browser"
}
--- 获取所有的tab页
[17.992][DEBUG]: DevTools request: http://localhost:12604/json
[17.995][DEBUG]: DevTools response: [ {
   "description": "{\"attached\":true,\"empty\":false,\"height\":1698,\"screenX\":0,\"screenY\":222,\"visible\":true,\"width\":1080}",
   "devtoolsFrontendUrl": "http://chrome-devtools-frontend.appspot.com/serve_rev/@957d898f0f6e46cd9661d91d2bae899f34c1c4b6/inspector.html?ws=localhost:12604/devtools/page/82563818-74f8-4ed3-b6af-f294509fa0b6",
   "faviconUrl": "https://assets.imedao.com/broker/static/images/favicon.8d2e0522.png",
   "id": "82563818-74f8-4ed3-b6af-f294509fa0b6",
   "title": "平安证券 极速开户",
   "type": "page",
   "url": "https://broker.xueqiu.com/open/pazq-l2-v1?snb_from=tab",
   "webSocketDebuggerUrl": "ws://localhost:12604/devtools/page/82563818-74f8-4ed3-b6af-f294509fa0b6"
}]

--- 
[18.000][INFO]: resolved localhost to ["::1","127.0.0.1"]  # 这个感觉多此一举
[18.004][DEBUG]: DEVTOOLS COMMAND Log.enable (id=1)
[18.004][DEBUG]: DEVTOOLS COMMAND DOM.getDocument (id=2)
[18.004][DEBUG]: DEVTOOLS COMMAND Runtime.enable (id=3)
[18.005][DEBUG]: DEVTOOLS COMMAND Page.enable (id=4) 
[18.005][DEBUG]: DEVTOOLS COMMAND Page.enable (id=5)
[18.011][DEBUG]: DEVTOOLS EVENT Log.entryAdded {
   "entry": {
      "level": "warning",
      "lineNumber": 0,
      "source": "rendering",
      "text": "The key \"viewport-fit\" is not recognized and ignored.",
      "timestamp": 1568087651303.1,
      "url": "https://broker.xueqiu.com/open/pazq-l2-v1?snb_from=tab"
   }
}
[18.012][DEBUG]: DEVTOOLS RESPONSE DOM.getDocument (id=2) {
   "root": {
      "backendNodeId": 61,
      "baseURL": "https://broker.xueqiu.com/open/pazq-l2-v1?snb_from=tab",
      "childNodeCount": 2,
... 太多了,省略不写了
[18.012][DEBUG]: DEVTOOLS EVENT Runtime.executionContextCreated {
   "context": {
      "auxData": {
         "frameId": "8495.4",
         "isDefault": true
      },
      "id": 19,
      "name": "",
      "origin": "https://broker.xueqiu.com"
   }
}
[18.015][DEBUG]: DEVTOOLS EVENT Runtime.consoleAPICalled {
   "args": [ {
      "type": "string",
      "value": "{\"url\":\"/account/bind/show.json\",\"type\":\"GET\",\"data\":{},\"success\":\"cb1\",\"error\":\"cb2\",\"name\":\"request\"}"
   } ],
   "executionContextId": 19,
   "stackTrace": {
      "callFrames": [ {
         "columnNumber": 2213,
         "functionName": "render",
         "lineNumber": 11,
         "scriptId": "470",
         "url": "https://assets.imedao.com/broker/static/js/open/pazq.96a2b2c8.js"
      }
... 后面还很长,不写了
[18.031][DEBUG]: DEVTOOLS COMMAND Runtime.evaluate (id=8) {
   "expression": "(function() { // Copyright (c) 2012 The Chromium Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style license that can be\n// found in the LICENSE file.\n\n/**\n * Enum f...",
   "returnByValue": true
}
[18.039][DEBUG]: DEVTOOLS RESPONSE Runtime.evaluate (id=8) {
   "result": {
      "type": "object",
      "value": {
         "status": 0,
         "value": 1
      }
   }
}
--- 不知道哪里调用的
[18.039][INFO]: RESPONSE InitSession {
   "acceptSslCerts": true,
   "applicationCacheEnabled": false,
   "browserConnectionEnabled": false,
   "browserName": "chrome",
   "chrome": {
      "chromedriverVersion": "2.29.461585 (0be2cd95f834e9ee7c46bcc7cf405b483f5ae83b)"
   },
   "cssSelectorsEnabled": true,
   "databaseEnabled": false,
   "handlesAlerts": true,
   "hasTouchScreen": true,
   "javascriptEnabled": true,
   "locationContextEnabled": true,
   "mobileEmulationEnabled": false,
   "nativeEvents": true,
   "pageLoadStrategy": "normal",
   "platform": "ANDROID",
   "rotatable": false,
   "takesHeapSnapshot": true,
   "takesScreenshot": true,
   "unexpectedAlertBehaviour": "",
   "~~~": "..."
}

-- GetURL的逻辑  (很长,看的我头疼)
[18.054][DEBUG]: DEVTOOLS COMMAND Runtime.evaluate (id=11) {
   "expression": "var isLoaded = document.readyState == 'complete' ||    document.readyState == 'interactive';if (isLoaded) {  var frame = document.createElement('iframe');  frame.name = 'chromedriver dummy frame'; ..."
}
[18.108][DEBUG]: DEVTOOLS EVENT DOM.childNodeCountUpdated {
   "childNodeCount": 4,
   "nodeId": 10
}
[18.108][DEBUG]: DEVTOOLS EVENT Page.frameAttached {
   "frameId": "8495.18",
   "parentFrameId": "8495.4",
   "stack": {
      "callFrames": [ {
         "columnNumber": 240,
         "functionName": "",
         "lineNumber": 0,
         "scriptId": "478",
         "url": ""
      } ]
   }
}
[18.108][DEBUG]: DEVTOOLS EVENT Page.frameStartedLoading {
   "frameId": "8495.18"
}
[18.108][DEBUG]: DEVTOOLS EVENT Page.frameNavigated {
   "frame": {
      "id": "8495.18",
      "loaderId": "8495.18",
      "mimeType": "text/html",
      "name": "chromedriver dummy frame",
      "parentId": "8495.4",
      "securityOrigin": "://",
      "url": "about:blank"
   }
}
[18.108][DEBUG]: DEVTOOLS EVENT Runtime.executionContextCreated {
   "context": {
      "auxData": {
         "frameId": "8495.18",
         "isDefault": true
      },
      "id": 26,
      "name": "",
      "origin": "https://broker.xueqiu.com"
   }
}
[18.108][DEBUG]: DEVTOOLS EVENT Page.frameStoppedLoading {
   "frameId": "8495.18"
}
[18.108][DEBUG]: DEVTOOLS RESPONSE Runtime.evaluate (id=11) {
   "result": {
      "description": "176",
      "type": "number",
      "value": 176
   }
}
[18.108][INFO]: Done waiting for pending navigations. Status: ok
[18.108][DEBUG]: DEVTOOLS COMMAND Runtime.evaluate (id=12) {
   "expression": "(function() { // Copyright (c) 2012 The Chromium Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style license that can be\n// found in the LICENSE file.\n\n/**\n * Enum f...",
   "returnByValue": true
}
[18.133][DEBUG]: DEVTOOLS EVENT DOM.childNodeCountUpdated {
   "childNodeCount": 3,
   "nodeId": 10
}
[18.133][DEBUG]: DEVTOOLS EVENT Runtime.executionContextDestroyed {
   "executionContextId": 26
}
[18.133][DEBUG]: DEVTOOLS EVENT Page.frameDetached {
   "frameId": "8495.18"
}
[18.136][DEBUG]: DEVTOOLS RESPONSE Runtime.evaluate (id=12) {
   "result": {
      "type": "object",
      "value": {
         "status": 0,
         "value": "https://broker.xueqiu.com/open/pazq-l2-v1?snb_from=tab"
      }
   }
}
[18.136][INFO]: Waiting for pending navigations...
[18.136][DEBUG]: DEVTOOLS COMMAND Runtime.evaluate (id=13) {
   "expression": "1"
}
[18.141][DEBUG]: DEVTOOLS RESPONSE Runtime.evaluate (id=13) {
   "result": {
      "description": "1",
      "type": "number",
      "value": 1
   }
}
[18.141][INFO]: Done waiting for pending navigations. Status: ok
[18.141][INFO]: RESPONSE GetUrl "https://broker.xueqiu.com/open/pazq-l2-v1?snb_from=tab"

参考

附言 1  ·  June 11, 2019

今天试了下微信小程序,发现不能用 应用内打开 debugx5.qq.com 开启 webview 调试了。不过发现使用 Xposed 强制依然有效。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 15 条回复 时间 点赞

你快肥来~ 定位下微信切 webview 切不过去的问题,是 chromedriver 的 bug 吗,atx 可以代理到 ps 的命令吗,如果可以能不能处理一下

是啊,我也是这个问题,大神快回来写

同版本的 chomedriver 已经饥渴难耐

@seveniruby 大佬帮看看 ,你 webview 最熟悉了

思寒_seveniruby 将本帖设为了精华贴 30 Aug 19:45

技术贴 收藏先 慢慢看 多谢楼主

@BensonMax 我在 6.0 和 7.0 可以打印出 page_source 了,这应该算是切过去了吧;8.0 上的是 chromedriver ps 命令的 bug,之前听思寒@seveniruby 讲课说过,目前还没能力弄个 adb proxy。。@codeskyblue作者来搞一下

driver = ChromeDriver(d).driver(process='com.tencent.mm:toolsmp')  #我的进程是这个,另外最好从搜一搜进小程序,因为我直接进小程序,发现chrome://inspect没有webview
time.sleep(3)
print(driver.page_source)

这个思路很好,很棒

Jacc 回复

这有何难

codeskyblue 回复

👏 期待新版本😀

有办法 像 atx-agent,在手机上起个 http,去连接 chrome_devtools_remote 这个 unix domain socket,然后发送请求完成 web 测试么?这样就不用 adb 了

蓝眼墨 回复

但是 chromedriver 需要 adb 呀,或者说不用 chromedriver,直接基于 chrome_devtools 开搞

codeskyblue 回复

chromedriver 就是用 adb forward tcp:9222 localabstract:chrome_devtools_remote 映射这个 socket 然后发送 http 请求的,直接和 chrome_devtools_remote 交互就不用过 chromedriver

蓝眼墨 回复

直接和 chrome_devtools_remote 交互就不用过 chromedriver 这个你实现了么

17Floor has deleted

没遇到过哎

19Floor has deleted

Hello World! - 1 出来了,可是小程序还是出不来,为甚么

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 13 Dec 14:44
codeskyblue 专栏文章:2018年 终总结 中提及了此贴 18 Feb 10:26
linpengcheng 基于 ATX-Server 的 UI 自动化测试框架 中提及了此贴 13 May 17:40
lan-tianyu [Topic was deleted] 中提及了此贴 21 Jun 15:46
codeskyblue webview 研究踩到的坑 中提及了此贴 10 Sep 11:42
codeskyblue webview 研究踩到的坑 中提及了此贴 11 Sep 10:47
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up