Robotium Robotium 处理跨进程的问题

xuxu · 2015年02月27日 · 最后由 securitytest 回复于 2017年09月26日 · 1824 次阅读
本帖已被设为精华帖!

先贴上年前的两个帖子:
@qinggchu http://testerhome.com/topics/1826
@yangchengtest http://testerhome.com/topics/1876

Adb-For-Test
https://github.com/gb112211/Adb-For-Test
用在 robotium 中去处理跨进程的时候还有坑没填上,主要就是权限问题,这里可以看下@qinggchu的这篇博客http://blog.csdn.net/qingchunjun/article/details/43343735
robotium 中使用 uiautomator 命令获取 uidump.xml 的时候是需要 root 权限才能获取到的,所以在跨进程时,要通过解析 uidump.xml 去定位元素位置的时候,就需要使用 root 过的真机才行,关键需要使用 su 命令。

参考@qinggchu修改的例子,年后回来就花了点时间修改了下 Adb-For-Test,Adb-For-Robotium,简单测试了下,基本上可以满足需求,例子中有一个执行 robotium 脚本时录制的视频。

主要修改的地方就是获取 uidump.xml,原谅我去解析的时候先是用的 cat 命令

// 获取设备当前界面的控件信息,并解析uidump.xml文件
    private void uidump() {

        ShellUtils.suShell("uiautomator dump /data/local/tmp/uidump.xml");
        sleep(2000);
        ShellUtils.suShell("chmod 777 /data/local/tmp/uidump.xml");
        sleep(2000);

        try {
            xml = ShellUtils.StringTOInputStream(ShellUtils
                    .getShellOut(ShellUtils
                            .shell("cat /data/local/tmp/uidump.xml")));
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        dumps = new UiDumpService().getDumps(xml);
    }
//需要root权限执行命令时使用该方法
    public static void suShell(String cmd) {
        Process ps = null;
        DataOutputStream os;

        try {
            ps = Runtime.getRuntime().exec("su");
            os = new DataOutputStream(ps.getOutputStream());

            os.writeBytes(cmd + "\n");
            os.writeBytes("exit\n");
            os.flush();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public static InputStream StringTOInputStream(String in) throws Exception {
        ByteArrayInputStream is = new ByteArrayInputStream(in.getBytes("ISO-8859-1"));
        return is;
    }

Adb-For-Robotium(https://github.com/gb112211/adb-For-Robotium.git

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

我提供一个思路. adb 调用 uiautomator 是不需要 root 的. adb 支持 wifi 模式, 预先把 adb 服务设置为 wifi 模式, 并开放一个端口. 这样 android 自己就可以通过 adb 客户端连接自己的 adb 服务.

这样就可以绕过 root 了.

@xuxu 感谢侠帅哥,前面通过这个文档学了不少东西。
个人补充一下:ANDROID 4.4 以后的版本写文件有权限的问题,保存文件只能放在/SDCARD/ANDROID/DATA/APP 的目录下。下次发布的时候,文件路径拉出来给个变量吧~

xuxu #3 · 2015年02月27日 Author

@seveniruby 哈哈 还真没想过,又开阔了思路~回头试下

#3 楼 @xuxu 我之前试过在 android 上用 adb 连接 android 自己,可以执行 adb shell. 有点无限递归自己的味道.

但是这样的操作还是不稳定啊

xuxu #6 · 2015年02月27日 Author

@kasi 稳定还是比较稳定的,就是速度比较慢。

xuxu,果断膜拜

通过 uiautomator 脚本封装一个 server 端,robotium 和它通信,来实现跨进程的操作。
uiautomator 通过反射获取 AccessibilityNodeInfo 信息,递归获取控件树信息;
这样效率比 uiautomator dump 快好多倍,只需要几毫秒的时间;

xuxu #9 · 2015年03月02日 Author

@marker 求分享下!!

//获取window_dump.xml
public String getWindowInfo() {
        try {
            Object bridge = ReflectionUtils.invokeMethod(this.getUiDevice(),
                    "getAutomatorBridge", new Class[] {}, new Object[] {});

            if (bridge == null) {
                return "AutomatorBridge is Null";
            }

            Object queryController = ReflectionUtils.invokeMethod(bridge,
                    "getQueryController", new Class[] {}, new Object[] {});

            if (queryController == null) {
                return "queryController is Null.";
            }

            Object rootNode = ReflectionUtils
                    .invokeMethod(queryController, "getAccessibilityRootNode",
                            new Class[] {}, new Object[] {});

            AccessibilityNodeInfo root = (AccessibilityNodeInfo) rootNode;

            if (root != null) {
                return Utils.getWindowInfo(root, this.getUiDevice()
                        .getDisplayWidth(), this.getUiDevice()
                        .getDisplayHeight());
            } else {
                return "Root Node is Null";
            }
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(tag, "Error", e);
            return e.getMessage();
        }
    }

//根据root获取控件数信息
    public static String getWindowInfo(AccessibilityNodeInfo root, int screenW, int screenH) {
        if (root == null) {
            return "";
        }
        final long startTime = SystemClock.uptimeMillis();
        try {          
            XmlSerializer serializer = Xml.newSerializer();
            StringWriter stringWriter = new StringWriter();
            serializer.setOutput(stringWriter);
            serializer.startDocument("UTF-8", true);
            serializer.startTag("", "hierarchy");
            dumpNodeRec(root, serializer, 0, screenW, screenH);
            serializer.endTag("", "hierarchy");
            serializer.endDocument();
            return stringWriter.toString();

        } catch (IOException e) {
            Log.e("UITestin", "failed to dump window to file", e);
        }
        final long endTime = SystemClock.uptimeMillis();
        Log.w("UITestin", "Fetch time: " + (endTime - startTime) + "ms");
        return "";
    }

//参考uiautomator部分源码
    private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer,
            int index, int screenW, int screenH) throws IOException {
        serializer.startTag("", "node");
        if (!nafExcludedClass(node) && !nafCheck(node))
            serializer.attribute("", "NAF", Boolean.toString(true));
        serializer.attribute("", "index", Integer.toString(index));
        serializer.attribute("", "text", safeCharSeqToString(node.getText()));
        serializer.attribute("", "class", safeCharSeqToString(node.getClassName()));
        serializer.attribute("", "package", safeCharSeqToString(node.getPackageName()));
        serializer.attribute("", "content-desc", safeCharSeqToString(node.getContentDescription()));
        serializer.attribute("", "checkable", Boolean.toString(node.isCheckable()));
        serializer.attribute("", "checked", Boolean.toString(node.isChecked()));
        serializer.attribute("", "clickable", Boolean.toString(node.isClickable()));
        serializer.attribute("", "enabled", Boolean.toString(node.isEnabled()));
        serializer.attribute("", "focusable", Boolean.toString(node.isFocusable()));
        serializer.attribute("", "focused", Boolean.toString(node.isFocused()));
        serializer.attribute("", "scrollable", Boolean.toString(node.isScrollable()));
        serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable()));
        serializer.attribute("", "password", Boolean.toString(node.isPassword()));
        serializer.attribute("", "selected", Boolean.toString(node.isSelected()));
        serializer.attribute("", "bounds",
                AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node, screenW, screenH).toShortString());
        int count = node.getChildCount();
        for (int i = 0; i < count; i++) {
            AccessibilityNodeInfo child = node.getChild(i);
            if (child != null) {
                if (child.isVisibleToUser()) {
                    dumpNodeRec(child, serializer, i, screenW, screenH);
                    child.recycle();
                } else {
//                    Log.i(tag, String.format("Skipping invisible child: %s", child.toString()));
                }
            } else {
//                Log.i(tag, String.format("Null child %d/%d, parent: %s",
//                        i, count, node.toString()));
            }
        }
        serializer.endTag("", "node");
    }

//反射相关方法
    public static Object invokeMethod(Object object, String methodName,
            Class<?>[] parameterTypes, Object[] parameters) {

        // 根据 对象、方法名和对应的方法参数 通过反射 调用上面的方法获取 Method 对象

        Method method = getDeclaredMethod(object, methodName, parameterTypes);

        // 如果Method为空,则肯定没有结果,直接返回null即可。
        if (method == null){
            return null;
        }

        // 抑制Java对方法进行检查,主要是针对私有方法而言
        method.setAccessible(true);

        try {
            if (null != method) {
                // 调用object 的 method 所代表的方法,其方法的参数是 parameters
                return method.invoke(object, parameters);
            }

        } catch (IllegalArgumentException e) {

            e.printStackTrace();

        } catch (IllegalAccessException e) {

            e.printStackTrace();

        } catch (InvocationTargetException e) {

            e.printStackTrace();

        }

        return null;

    }

    public static Method getDeclaredMethod(Object object, String methodName,
            Class<?>... parameterTypes) {

        Method method = null;

        for (Class<?> clazz = object.getClass(); clazz != Object.class; clazz = clazz
                .getSuperclass()) {

            try {

                method = clazz.getDeclaredMethod(methodName, parameterTypes);
                return method;

            } catch (Exception e) {

                // 这里甚么都不要做!并且这里的异常必须这样写,不能抛出去。

                // 如果这里的异常打印或者往外抛,则就不会执行clazz =
                // clazz.getSuperclass(),最后就不会进入到父类中了

            }
        }

        return null;

    }
xuxu #6 · 2015年03月02日 Author

@marker 谢谢!回头试下看看!~

感谢 分享
目前我就是遇到真机测试的时候遇到这个问题

已经解决 感谢楼主

#8 楼 @marker 这个复杂么

#14 楼 @young
不复杂,上面已经提供获取 window dump 的源码;
server 端还需要自己封装,可以参考 AppiumBootstrap 源码

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册