前几天稍微改装了一下 UIAutoatorview 生成脚本,刚发完帖子发现论坛已经有人发出来了,闭门造车不是个好习惯哈,我做这个东西的初衷呢?就是是这段时间学习 APPIUM 觉得写脚本调式费劲,就是想能确定元素是否真的可以定位到,可以执行,最终的一切都是为了支持 Appium 的执行,目前程序完成的比较粗糙,健壮性也不太好,主要是为了给不是特别深入的朋友提供些思路和参考


我这里面的检查执行呢?底层调用时通过 Appium 的 Bootstrap 实现,这样才能保持和 Appium 环境一致,如果用坐标点确定的话,生成的脚本放到 Appium 里面也不一定是有效的,执行环境变了


具体实现,大家看过 Bootstrap 的会知道,Bootstrap 其实是发布了一个 Socket 服务端,并且是长连接,所以当 Bootstrap 运行起来我们可以通过 Socket 去连接发送请求

/**
     * @Descrition:执行初始化连接
    * */
public void init() {
        try {
            String host = "127.0.0.1";
            int port = 4724;
            InetSocketAddress remote = new InetSocketAddress(host, port);
            sc = SocketChannel.open();
            sc.connect(remote);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
/**
     * @Descrition:发送请求
    * */
public void sendMsg(){
        boolean run = true;
      while (run) {


                String sendMsg = QueueMsg.getSendMsg();          //这里简单弄了个队列信息
                if (null!=sendMsg&&!sendMsg.equals("")) {
                    printMsg(sendMsg, sendMsg);
                    InputStream inputStream = new ByteArrayInputStream(sendMsg.getBytes());
                    BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
                    String str = br.readLine();
                    printMsg(sendMsg, "读入一行数据,开始发送...");
                    w_bBuf = ByteBuffer.wrap(str.getBytes("UTF-8"));
                    //将缓冲区中数据写入通道
                    sc.write(w_bBuf);
                    printMsg(sendMsg, "数据发送成功...");
                    w_bBuf.clear();
                    printMsg(sendMsg, "接收服务器端响应消息...");
                    try {
                        Thread.currentThread();
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    r_bBuf.clear();
                    //将字节序列从此通道中读入给定的缓冲区r_bBuf
                    sc.read(r_bBuf);
                    r_bBuf.flip();
                    String resultMsg = Charset.forName("UTF-8").decode(r_bBuf).toString();
                        QueueMsg.setResultMsg(resultMsg);
                }
      }
}

我们看 UIAutomatorView 这个工具去 Dump 控件文件会发现它是执行的 UIAutoamtor,Boostrap 也是执行的 UIAutoamtor,那么我们启动 Boostrap 之后,线程是阻塞的,所以在用 UIAutomatorView 去 Dump 是无法 Dump 的,所以我们需要对 Boostrap 进行改造一下,通过 Boostrap 去 Dump 控件文件

首先在 Boostrap 中的 AndroidCommandExecutor.java 类要加入一行命令

map.put("dump", new Dump());

然后写一个 Dump 类

public class Dump extends CommandHandler {
    private static final File dumpFolder = new File(Environment.getDataDirectory(), "local/tmp");
    private static final String dumpFileName = "myuidump.xml";
    @Override
    public AndroidCommandResult execute(AndroidCommand command) {
        File file = new File(dumpFolder, dumpFileName);
        try {
            QueryController queryController = UiAutomatorBridge.getInstance().getQueryController();
            AccessibilityNodeInfo accessibilityRootNode = queryController.getAccessibilityRootNode();
            String msg = "success";
            try {
                AccessibilityNodeInfoDumper.dumpWindowToFile(accessibilityRootNode, file);
            } catch (Exception e) {
                msg = e.toString();
            }
            return getSuccessResult(msg);
        } catch (final Exception e) {
            Logger.debug("Dump Exception: " + e);
            return getErrorResult(e.getMessage());
        }
    }
}

完成之后重新打 Bootstrap 放到设备里

UIAutomator 里面的 UiAutomatorHelper 类的 getUiHierarchyFile 方法要改动一下,通过 Bootstrap 去 Dump 控件文件

monitor.subTask("Taking UI XML snapshot...");
//原来的Dunp方法

 //try {
//            device.executeShellCommand(
//                    command,
//                    new CollectingOutputReceiver(commandCompleteLatch),
//                    XML_CAPTURE_TIMEOUT_SEC * 1000);
//            commandCompleteLatch.await(XML_CAPTURE_TIMEOUT_SEC, TimeUnit.SECONDS);
//
//            monitor.subTask("Pull UI XML snapshot from device...");
//            device.getSyncService().pullFile(UIDUMP_DEVICE_PATH,
//                    dst.getAbsolutePath(), SyncService.getNullProgressMonitor());
//        } catch (Exception e) {
//            throw new RuntimeException(e);
//        }


//修改后的Dunp方法
      try {
            QueueMsg.setSendMsg("{\"cmd\":\"action\",\"action\":\"dump\",\"params\":{}} ");
            monitor.subTask("Pull UI XML snapshot from device...");
            String value = QueueMsg.getResultMsg();
            BootstrapCmdDomain bootstrapCmdDomain = JSON.parseObject(value, BootstrapCmdDomain.class);
            int status = bootstrapCmdDomain.getStatus();
            if (0 == status) {
                device.getSyncService().pullFile(MyUIDUMP_DEVICE_PATH,
                        dst.getAbsolutePath(), SyncService.getNullProgressMonitor());
            } else {
                throw new Exception(bootstrapCmdDomain.getValue());
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

{\"cmd\":\"action\",\"action\":\"dump\",\"params\":{}} 这写个命令可以在 Appium 的日志里获取,大家看我的命令多打了一个空格,因为 Bootstrap 读取消息的时候少读了一个字节,会报出转 JSON 异常,所以这里面一定要多加一个空格。

剩下就是封装一些点击事件,输入文本脚本生成的事情了


完成后的结果,我们可以手动把 APP 启动起来,然后打开 UIAtuomator,弄一个输入的事件


选择一个元素,鼠标右键


确定后设备里面的输入被执行了,右侧的文本框打印出了脚本,这里的打印信息,只有事件被正确执行才能生成脚本,生成的脚本规则是自己定义的,所以这里按照自己的需求写自己的算法



点击事件,我们点击下【搜索按钮】

点击【搜索按钮】后的结果


UI 界面什么的都比较粗糙,其实就把它做一个辅助工具,为 Appium 服务,所有功能 OK 就可以了,当然大家可以自己加一些拖拽、JS 执行、长按等事件的操作


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