zoukankan      html  css  js  c++  java
  • 从Android手机的抢红包插件说起

    为防止盗链,本文首发于于果的博客,转载请注明出处!原文链接:https://www.cnblogs.com/yuxiuyan/p/14524302.html,

    前语

    最近,Android手机上的手机管家更新了新版本,提供了红包闹钟功能,只要有微信红包或者QQ红包,就会自动提醒。恰逢最近又在做UI自动化的工作,使用到UI Automator框架。几行代码,就可以让手机自动完成某些操作,很有意思,今天就来扒一扒这背后的原理。

    UI Automator

    首先,官方文档镇楼:https://developer.android.com/training/testing/ui-automator

    传统的手工测试,我们需要点击一些控件元素,来查看输出的结果是否符合预期。比如在登录界面,输入正确的用户名和密码,点击登录按钮后,就可以正常登录。

    如果这些操作,每一次都需要手工执行的话,是需要大量的人力成本的,比如手机QQ安卓端, 手工用例有上万条。所以就需要大力推广自动化测试。

    UI自动化作为测试金字塔的最顶层,承担了端到端的需求回归与灰度验证任务,其重要性不言而喻。

    测试金字塔

    UI Automator作为一款Google谷歌推出的,用于UI自动化测试的工具,有着优秀的API与社区文档。也是目前主流的Android自动化测试框架。它提供了一系列用于获取手机上页面控件元素和操作元素的方法,非常方便。

    注意UI Automator测试框架是基于instrumentation的API,运行在Android JunitRunner 之上,同时UI Automator Test只运行在 Android 4.3(API level 18)以上版本

    从一次抢红包说起

    想想我们平时抢红包的流程是什么样的呢?

    假如你现在正在刷剧,这时候通知栏提醒你微信有红包了,于是你点击通知栏的消息,进入了微信页面,找到了红包,再点击拆红包的按钮,小手一抖,几毛到手。

    这么一想,其实这些步骤完全是一个体力活,要是有个机器人能自动抢就好了!

    这个机器人的背后就是AccessibilityService,当然它的具体作用我们稍后再讲。

    按照我们的现有的逻辑,自动抢红包大致分为以下几个步骤:

    1. 识别获取通知栏的微信红包的通知事件
    2. 点击通知栏的消息
    3. 获取红包的消息
    4. 点击按钮拆红包

    这里面最最重要的两个步骤就是识别,操作。接下来我们侃侃这两步。

    怎么识别页面控件元素?

    首先,我们先来认识一下UI Automator viewer这个工具,位于<android-sdk>/tools/bin目录下,他可以很方便地扫描和分析 Android 设备上当前显示的界面组件,展示一棵完整的控件树,与某一个叶子节点(控件元素)的属性。

    UI Automator view工具

    从上图我们可以看到,页面的一个登录按钮元素,有自己的text属性,resource-id属性,content-desc属性等等。

    UI Automator中,存在uiDevice类,可以通过findObject方法,查看到这些控件元素。

    UiObject2 login_btn = uiDevice.findObject(By.desc("登录"));
    

    现在我们深入findObject方法,

        public UiObject2 findObject(BySelector selector) {
            // 这里返回匹配选择器的第一个节点,如果没有找到匹配的话,就返回null
            AccessibilityNodeInfo node = ByMatcher.findMatch(this, selector, getWindowRoots());
            return node != null ? new UiObject2(this, selector, node) : null;
        }
    

    可以看到,这里传入了一个选择器selector,然后在ByMatcherfindMatch方法中查询,如果找到了,就返回一个AccessibilityNodeInfo的node,如果没有找到就返回null。

    首先看ByMatcher是什么东东?这是一个实用工具类,通过它的方法,我们可以在一个树形结构中搜索到匹配selector的节点。

    findMatch方法很简单,就是一个从根节点开始搜索的树型搜索方法,不用多说。

    AccessibilityNodeInfo是什么呢?这相当于一个节点,在AccessibilityService的角度来看,这就是一个可访问到的控件节点。

    那这么来看,findMatch的第三个参数,就是传入的控件树的根节点了吗?我们深入看一下这里的getWindowRoots方法的关键代码,

     /** 这里返回活动窗口容器的root节点的列表 */
        AccessibilityNodeInfo[] getWindowRoots() {
            // 等待线程空闲后再执行
            waitForIdle();
            // 初始化一个root节点的集合
            Set<AccessibilityNodeInfo> roots = new HashSet();
    
            // 通过UiAutomation获取当前最底部的根窗口容器的root节点
            AccessibilityNodeInfo activeRoot = getUiAutomation().getRootInActiveWindow();  // 这里使用UiAutomation的方法
            if (activeRoot != null) {
                roots.add(activeRoot);
            }
    
            // 多窗口容器的搜索
            if (UiDevice.API_LEVEL_ACTUAL >= Build.VERSION_CODES.LOLLIPOP) {
                for (AccessibilityWindowInfo window : getUiAutomation().getWindows()) { // 这里使用UiAutomation的方法
                    AccessibilityNodeInfo root = window.getRoot();
                    …………
                    roots.add(root);
                }
            }
            return roots.toArray(new AccessibilityNodeInfo[roots.size()]);
        }
    

    这里要提一下, UiAutomation是Google在Android4.3的时候,发布的一个自动化框架,它提供了与系统底层交互的能力。

    再往下,我们看看UiAutomationgetWindows方法的关键代码:

        public List<AccessibilityWindowInfo> getWindows() {
          ……
            return AccessibilityInteractionClient.getInstance()
                    .getWindows(connectionId);
        }
    

    这里获取了AccessibilityInteractionClient的实例,然后返回了client的getWindows方法结果。然后再看一下这个getWindows方法的关键代码,

        public List<AccessibilityWindowInfo> getWindows(int connectionId) {
                ……
                IAccessibilityServiceConnection connection = getConnection(connectionId);
                if (connection != null) {
                    // 首先去查询缓存,如果缓存是有的,直接返回
                    List<AccessibilityWindowInfo> windows = sAccessibilityCache.getWindows();
                       ……
                        return windows;
                    }
                    ……
                     // 如果上面的缓存不存在,就调用connection.getWindows方法
                     windows = connection.getWindows();
                     ……
                    if (windows != null) {
                        // 把上面获取到的新的windows放置缓存,并返回
                        sAccessibilityCache.setWindows(windows);
                        return windows;
                    }
                } 
                 ……
        }
    

    IAccessibilityServiceConnection开始,在IDE中就开始提示Cannot resolve symbol 'IAccessibilityServiceConnection',无法再跳转追踪了。这是因为这个文件属于aidl文件,这是Android中用于跨进程通信的接口文件,其具体源码可以在GoogleSource上面看到,有兴趣的同学可以去看一下:IAccessibilityServiceConnection.aidl。 这说明,到这里,UI Automation进程开始了与AccessibilityService进程的通信。我们把当前的程序可以当做是客户端,那么Android系统服务就是服务端,从这里开始,真正深入到Android系统的核心。在下面,就是Android Native的Library库。

    这里,我们可以用时序图总结一下:

    获取控件的时序图

    怎么操作页面页面元素?

    我们现在已经知道了UI Automator是怎么识别控件的,那怎么操作控件元素呢?比如实现控件的自动点击。

    我们还是从源码开始入手。比如一个控件元素的点击动作,在UiObject2类中,关键代码如下:

        public void click() {
            mGestureController.performGesture(mGestures.click(getVisibleCenter()));
        }
    

    首先,getVisibleCenter方法可以根据控件节点信息,也就是上面提到的AccessibilityNodeInfo,获取到这个控件节点的中心坐标点。然后把这个坐标点传给mGestureclick方法,这里是为了封装点击动作,最后交给mGestureController对象的performGesture方法去实施这个点击动作。

    对于mGestureclick方法,这个mGesture是一个构造工厂,它的click方法直接生成了一个PointerGesture对象,这个对象表示的是执行手势操作时的动作。比如手势的开始坐标点,结束坐标点,持续时间,移动方向,速度等等。

    重点看一下mGestureController对象的performGesture方法,其关键代码如下:

    public void performGesture(PointerGesture ... gestures) {
              …………
             // 执行传入的手势操作动作
            MotionEvent event;   // 这个是关于运动事件
            for (……) {
                     …………
                   // 初始化运动事件,并调用UI Automation的injectInputEvent注入事件,异步执行
                    event = getMotionEvent(……);
                    getDevice().getUiAutomation().injectInputEvent(event, true);
                    …………
                }
              …………
            }
    }
    

    这里可以看到事件的注入,也是通过UI Automation来完成的。看一下injectInputEvent方法的关键代码,

        public boolean injectInputEvent(InputEvent event, boolean sync) {
            …………
            // 异步执行,这段代码之前有关于锁的操作
            return mUiAutomationConnection.injectInputEvent(event, sync);
            …………
        }
    

    我们发现也是通过一个connection来执行操作的,这个connection对象对应的IUiAutomationConnection类,也属于一个aidl文件。

    这里也放一个时序图,

    点击事件的时序图

    AccessibilityService

    AccessibilityService根据官方说明,是指开发者通过增加类似contentDescription的属性,从而在不修改代码的情况下,让残障人士能够获得使用体验的优化,大家可以打开AccessibilityService来试一下,点击区域,可以有语音或者触摸的提示,帮助残障人士使用App。

    当然,现在国内,AccessibilityService已经被玩儿坏了,越来越多的App借用AccessibilityService来实现了一些其它功能,甚至是灰色产品。

    在国内,通过AccessibilityService实现的功能包括免Root自动安装,自动抢红包,微信消息自动回复等等黑科技。

    当然也有一些恶意功能,比如软件防卸载。当用户想要卸载你的App的时候,一般会来到设置界面,找到你的App然后选择卸载,那么如果我们监控这个页面,如果发现是自己的App,就直接退出,这样不就无法卸载了吗?是的,简简单单,但是背后的恶意却让人心寒。

    使用AccessibilityService做自动化的步骤

    大家看了上面的分析,可能对自动化有了一点兴趣,其实归纳起来,步骤很简单:

    1. 分析整个操作流程,拆解成关于每个控件的识别与操作。
    2. 利用uiautomatorviewer等工具,查看对应UI控件的属性,进行唯一性识别
    3. 编写代码,查找到元素后,进行点击等操作
    4. 兼容性处理

    结语

    大家经常说“面试造火箭,工作拧螺丝”。其实大家平时工作可能都是“拧拧螺丝”,但是站在个人职业发展角度来看,是不可取的。只有不断挖深自己的技术护城河,才能提高个人的不可替代性。在“拧螺丝”的时候,我们不妨抬头看看,整个“火箭”是的构造。

    工作拧螺丝

    参考

    https://juejin.cn/post/6844903456809943053

    https://developer.android.com/reference/android/app/UiAutomation

    https://testerhome.com/topics/1887

    https://blog.csdn.net/luoyanglizi/article/details/51980630

    https://www.kancloud.cn/digest/uiautomatorpriciple/192698

    Android层次结构

  • 相关阅读:
    高级(线性)素数筛
    Dijkstra(迪杰斯特拉)算法
    简单素数筛
    【解题报告】 POJ1958 奇怪的汉诺塔(Strange Tower of Hanoi)
    4 jQuery Chatting Plugins | jQuery UI Chatbox Plugin Examples Like Facebook, Gmail
    Web User Control Collection data is not storing
    How to turn on IE9 Compatibility View programmatically in Javascript
    从Javascrip 脚本中执行.exe 文件
    HtmlEditorExtender Ajax
    GRIDVIEW模板中查找控件的方式JAVASCRIPT
  • 原文地址:https://www.cnblogs.com/yuxiuyan/p/14524302.html
Copyright © 2011-2022 走看看