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"

参考


↙↙↙阅读原文可查看相关链接,并与作者交流