AndroidTestTool开发笔记

前言

这段时间在Testerhome上看了一些有关性能测试的帖子,看别人的东西,始终是别人的,只有自己写一遍才能体会其中的细节,虽然说不要重复造轮子,但是这种基础的东西,造一次轮子能够学会很多东西,最近看的东西也比较多,拿来实战一下也未尝不可。

整个工程下来难度其实不大,主要是一些基本知识,只不过涉及的面比较广,需要的要素如下:



开发相关

操作系统: Mac OS X EI capitan

Python: 2.7

Django:1.8.2

前端:Html、Css、Js、Bootstrap、jQuery、Ajax、Echarts

Android:ADB相关知识、Monkey相关知识


程序的架构

当然,用架构来形容有点夸张了,大概的模型如下图:

整个程序的模型并不复杂,都是通过ADB SHELL来操作Android设备、获取设备信息。在最初设计的时候是没有Tkinter的,因为我对这个GUI端并不熟,信号槽的信号传递和事件的绑定并不了解,而Web端相对了解一些。但是执行的时候发现有一些问题。在《Python开发测试工具(一)—Monkey》里面有详细描述这个问题。于是临时去学习Tkinter的知识,临时硬拼拼了一个GUI端出来。

前端

前端分为GUI和Web两个端,GUi端使用Tkinter,Web端使用Bootstrap和jQuery。两个端界面大概长这样。

Tkinter

最初我的选择不是TKinter,这个Python自带的原生包功能非常原始,而且最关键的是用的人太少,没人写中文文档,我如果要使用它,就必须去看英文的原文。但是其他几个GUI端也都有相应的缺点,PyQt环境搭建麻烦,而且中文文档大多是Qt4的,WxPython文档少,功能也不见得比Tkinter强大多少。最终还是硬着头皮吧Tkinter的官方文档看完了,因为没有现成的代码,所以我就把所有代码丢到了一起,全部放在gui.py中,整个代码完全没有结构而言,不过再怎样,功能也算是实现了。

Tkinter有几个坑爹的点我简单的列一下。

Entry只读时无法显示文字

Entry就是一个Text输入框,单行的那种。一旦设置为只读属性,就无法显示文字,所以整个Tkinter如果要做成一个不允许输入又要展示状态的信息媒介,That’s Impossible。

Label不能动态的改变

Tkinter的标签在初始化生成之后,就再也无法改变了,无法做动态的更新。

Placeholder效果实现起来很坑爹

Html中placeholder的效果很友好(就是文本框为空时显示一个提示语,输入内容后提示语消失的属性),但是在Tkinter中如果要实现它,就必须自己动手写两个事件,一是初始化的时候默认赋值,二是点击Entry获得焦点后做一个删除处理。

按钮回调函数的时候无法传参

这才是最坑爹的一点。。。基本上函数都要传参的诶~~~当然,通过万能的Google我还是找到了解决办法,就是使用lambda语句来处理。代码如下:


get_cpu_info = Button(master, text="开始生成cpu信息",
                              command=lambda: self.get_cpuinfo(self.cm.get_text(cpu_monitor)))

在这么多的坑中,我竟然还是坚持用完了Tkinter,当然我看的是英文的文档,也有可能有这个方法我没有看到,毕竟看英文的文档还是很操蛋的一件事。

多进程的处理

Tkinter是一个单线程的处理机制,我想在执行Monkey的同时又执行获取内存、获取CPU,那就必须挂上多进程来处理(多线程理论上也是可以的,但是实践中我发现经常会发生线程阻塞的情况)。当然,程序比较简单,不需要专门去做进程池来处理,只要每个功能开一个进程去处理就好了。代码如下:


t = multiprocessing.Process(target=lambda: self.ad.get_cpuinfo(package_name, ‘cpuinfo‘))
t.start()

GUI端的一些核心代码

  • 执行Monkey
def run_monkey(self):
        t = multiprocessing.Process(target=lambda: self.mk.merge_command(self.cm.get_text(log_path),
                                                                         *self.cm.collect(*ENTRYLIST)))
        t.start()

def merge_command(self, path, *args):
        """
        组合命令,Monkey使用
        :param path:日志地址
        :param args:Monkey命令中的其他参数
        :return:None
        """
        member = ‘ ‘.join(args)
        command = ‘adb shell monkey {} > {}‘.format(member, path)
        self.run(command)

def collect(self, *args):
        """
        收集参数中的元素,转换为列表返回
        :param args:传入的参数
        :return:list
        """
        str_list = []
        for x in args:
            str_list.append(self.get_text(x))
        return str_list
  • 获取内存信息

def run_meminfo(self, package_name):
        self.cf.read(‘monkey.conf‘)
        self.cf.set(‘monkey_check‘, ‘mark‘, ‘True‘)
        self.cf.write(open(‘monkey.conf‘, ‘w‘))
        status = self.cf.get(‘monkey_check‘, ‘mark‘)
        with open(self.ad.get_dir(‘meminfo‘), ‘w‘) as f:
            while status == ‘True‘:
                f.write(self.ad.get_meminfo(package_name))
                f.write(‘\n‘)
                time.sleep(0.5)
                self.cf.read(‘monkey.conf‘)
                if self.cf.get(‘monkey_check‘, ‘mark‘) == ‘False‘:
                    break

def get_meminfo(self, package_name):
        """
        获取内存信息
        :return:str, 内存信息
        """
        newlist = []
        f = os.popen(‘adb shell dumpsys meminfo ‘ + package_name)
        for x in f.readlines():
            newlist.append(x.strip())
        try:
            mem_total = newlist[8].split(‘   ‘)[7]
            mem_used = newlist[8].split(‘   ‘)[8]
            mem_free = newlist[8].split(‘   ‘)[9]
        except Exception:
            mem_total = ‘‘
            mem_used = ‘‘
            mem_free = ‘‘
        meminfo = ‘{},{},{}‘.format(mem_total, mem_used, mem_free)

        return meminfo

  • 获取CPU信息

def get_cpuinfo(self, package_name, url):
        """
        往cpuinfo文件夹中新写一个记录cpu信息的文件
        :param package_name:测试包名
        :param url:cpu文件的路径
        :return:None
        """
        self.cf.read(‘monkey.conf‘)
        self.cf.set(‘cpu_check‘, ‘mark‘, ‘True‘)
        self.cf.write(open(‘monkey.conf‘, ‘w‘))
        with open(self.get_dir(url), ‘w‘) as f:
            while True:
                a = os.popen(‘adb shell dumpsys cpuinfo | grep ‘ + package_name)
                cpuinfo_list = a.readlines()[0].split(‘ ‘)
                if len(cpuinfo_list) == 13:
                    cpu = [cpuinfo_list[2], cpuinfo_list[4], cpuinfo_list[7]]
                    cpuinfo = ‘,‘.join(cpu)
                    f.write(cpuinfo)
                    f.write(‘\n‘)
                    time.sleep(0.5)
                    self.cf.read(‘monkey.conf‘)
                    if self.cf.get(‘cpu_check‘, ‘mark‘) == "False":
                        break

当时写代码的时候比较随性,没有做统一的规划,写完后又不太想重新改,就这样放着吧。

Web端

Web端的处理就相对比较顺畅了,我把收集内存信息和CPU信息都放在Tkinter了,因此在Web端只负责展示就行了。当然,最后我又突发奇想把收集流量信息放在Web端了,主要是流量信息不完全用走势图可以完全展示,因此就把这个功能放在Web端了。

web端的几个页面

最初web端我是设计用来查看走势图的,因此有这么几个页面:主页、内存信息、CPU信息、流量信息,本来还有一个gxfinfo,但是不同Android手机展示出来的矩阵不同,统一处理的方案我还没想出来,暂时就搁浅了,等后续想出来后我再补充,在导航栏上我把打开GUI端的按钮集成了进来,也就是说以后我要使用就只要打开WEB端就行了,一站式解决方案。

首页

首页原来我是放置一些说明的,后来发现在首页也可以加一些功能,比如直接查看包名、查看Activity,查看手机上有多少app等简单实用的功能。这些功能自己从shell中去获取也不难,但是集成进来之后显得更简单易用了。

获取当前包名和Activity

获取的命令很简单,在*nux下的命令就是


adb shell dumpsys window windows | grep mFocusedApp

windows需要调整一下这个命令。获取之后做一些截取就可以拿到包名和Activity,把信息返回给前端即可,代码如下:


def get_cur_pknm(self):
        try:
            f = os.popen(‘adb shell dumpsys window windows | grep mFocusedApp‘)
            for x in f.readlines():
                pknm = x.strip().split(‘ ‘)[4]
            pk_info = pknm.split(‘/‘)
            pk_data = {
                ‘errmsg‘: ‘查询成功‘,
                ‘package_name‘: pk_info[0],
                ‘avtivity_name‘: pk_info[1]
            }
        except Exception as e:
            pk_data = {
                ‘errmsg‘: ‘请确认设备正确连接或者是否有打开APP?‘
            }
        return pk_data

前端接受代码:


//获取当前包名和Activity
    $("#get_cur_packagename").click(function () {
        $.getJSON(
            ‘/datashow/get_cur_packagename/‘,
            function (data) {
                $("#pkinfo").fadeIn(‘slow‘);
                $("#package_name").html(‘当前打开的包名为: <mark>‘+data[‘package_name‘]+"</mark>");
                $("#activity_name").html(‘当前打开的avtivity为: <mark>‘+data[‘avtivity_name‘]+"</mark>")
            }
        )
    });

获取所有第三方应用的命令是这个:


adb shell pm list package -3

依照上面的方式给代码就行了。效果就是这样:

内存cpu监控

这两部分的内容类似,都是作为一个展示的页面来处理,就需要后端给数据,数据收集在GUi端做处理,那么前端就需要发Ajax请求到后端获取数据,获取数据后调用Echarts进行绘图。前端的代码如下:


//内存信息获取
    $(‘#mem_query‘).click(function () {
        var filename = $("#selquery").val();
        var myChart = echarts.init(document.getElementById(‘main‘), ‘dark‘);
        $.get(‘/datashow/getmemdata/‘ + filename).done(function (data) {
            myChart.hideLoading();
            myChart.setOption({
                title: {
                    text: ‘内存监控信息‘
                },
                tooltip: {
                    trigger: ‘axis‘
                },
                legend: {
                    data: [‘内存总体使用量‘, ‘内存剩余可用量‘]
                },
                toolbox: {
                    feature: {
                        saveAsImage: {}
                    }
                },
                grid: {
                    left: ‘3%‘,
                    right: ‘4%‘,
                    bottom: ‘3%‘,
                    containLabel: true
                },
                xAxis: [
                    {
                        type: ‘category‘,
                        boundaryGap: false,
                        data: []
                    }
                ],
                yAxis: [
                    {
                        type: ‘value‘
                    }
                ],
                series: [
                    {
                        name: ‘内存剩余可用量‘,
                        type: ‘line‘,
                        areaStyle: {normal: {}},
                        data: data[‘user_data‘]
                    },
                    {
                        name: ‘内存总体使用量‘,
                        type: ‘line‘,
                        label: {
                            normal: {
                                show: true,
                                position: ‘top‘
                            }
                        },
                        opacity: ‘0.1‘,
                        areaStyle: {normal: {opacity: ‘0.1‘}},
                        data: data[‘total_data‘]
                    }
                ]
            });
        });
    });

后端数据我们收集的时候是存在一个一个的TXT文本中,因此需要做这么几件事。

1. 在初始化页面的时候,获取所有的txt文件名,在前端生成一个下拉框给用户选择。

2. 选择对应的文件查看后,发送数据给前端绘图。

因此我是这样设计的,在前端页面初始化的时候,发Ajax请求获取文件名来生成下拉框,代码如下:


$.ajax({
            url: ‘/datashow/getdirlist/meminfo‘,
            success: function (data) {
                var arr = data[‘data‘];
                var select = $("<select id=‘selquery‘ class=‘form-control‘></select>");
                for (var i = 0; i < arr.length; i++) {
                    select = select.append("<option value=‘" + arr[i] + "‘>" + arr[i] + "</option>")
                }
                $("#selection").append(select)
            }
        });

后端获取所有文件名后返回给前端,这里我会把第一个文件剔除出返回的列表,第一个文件名是我初始化项目结构的时候给的文件,并没有实际的数据,之后按倒叙返回给前端。代码如下:


def getDirList(request, cate):
    rst = []
    url = ‘{}/device_info/{}‘.format(os.getcwd(), cate)
    old_rst = os.listdir(url)
    old_rst.pop(0)
    for x in old_rst:
        rst.append(x.split(‘.‘)[0])
    rst_data = {
        ‘status‘: 200,
        ‘data‘: rst[::-1]
    }
    return JsonResponse(rst_data)

CPU信息也是同理获取,代码就不多贴了。

流量监控

流量监控的命令是由两部分组成,一个要取到应用的PID,然后通过这个PID去取相应的记录文件,两部分的代码如下:


def get_pid(self, package_name):
        """
        获取pid
        :param package_name:包名
        :return: str, pid
        """
        pid = []
        f = os.popen(‘adb shell ps | grep ‘ + package_name)
        for x in f.readlines():
            pid_list = x.split(‘ ‘)
            for y in pid_list:
                if y.strip() == package_name:
                    for z in x.split(‘ ‘):
                        if z:
                            pid.append(z)
        rst = pid[1]
        return rst

def write_flow(self, package_name, url):
        self.cf.read(‘monkey.conf‘)
        self.cf.set(‘flow_mark‘, ‘mark‘, ‘True‘)
        self.cf.write(open(‘monkey.conf‘, ‘w‘))
        with open(self.get_dir(url), ‘w‘) as fn:
            while True:
                rst_list = []
                f = os.popen(‘adb shell cat /proc/{}/net/dev | grep wlan0‘.format(self.get_pid(package_name)))
                for x in f.readlines():
                    for y in x.split(‘ ‘):
                        if y:
                            rst_list.append(y)
                up = rst_list[9]
                down = rst_list[1]
                flowInfo = ‘{},{}‘.format(down, up)
                fn.write(flowInfo)
                fn.write(‘\n‘)
                print flowInfo
                time.sleep(0.5)
                self.cf.read(‘monkey.conf‘)
                if self.cf.get(‘flow_mark‘, ‘mark‘) == "False":
                    break

流量的功能需要记录执行的时间和上行下行流量,因此在开始记录流量的时候,我会在配置文件写入一个时间戳,和已发生的上下行流量,停止的时候再记录一次信息,两个信息对减的结果返回给前端,前端就能展示时间和消耗的流量了。代码如下:


def get_flow(self, package_name, mark):
        """
        获取流量信息
        :param package_name:包名
        :param mark:获取流量的标记
        :return:
        """
        if mark == "start":
            rst_list = []
            f = os.popen(‘adb shell cat /proc/{}/net/dev | grep wlan0‘.format(self.get_pid(package_name)))
            for x in f.readlines():
                for y in x.split(‘ ‘):
                    if y:
                        rst_list.append(y)
            rst = int(rst_list[1]) + int(rst_list[9])
            conf_data = {
                ‘total‘: str(rst),
                ‘flowup‘: str(rst_list[9]),
                ‘flowdown‘: str(rst_list[1]),
                ‘timestart‘: str(time.time())
            }
            self.cf.read(‘monkey.conf‘)
            self.cf.set(‘flow_mark‘, ‘flow_total‘, conf_data[‘total‘])
            self.cf.set(‘flow_mark‘, ‘flow_up‘, conf_data[‘flowup‘])
            self.cf.set(‘flow_mark‘, ‘flow_down‘, conf_data[‘flowdown‘])
            self.cf.set(‘flow_mark‘, ‘time_start‘, conf_data[‘timestart‘])
            self.cf.write(open(‘monkey.conf‘, ‘w‘))
        else:
            rst_list = []
            f = os.popen(‘adb shell cat /proc/{}/net/dev | grep wlan0‘.format(self.get_pid(package_name)))
            for x in f.readlines():
                for y in x.split(‘ ‘):
                    if y:
                        rst_list.append(y)
            rst = int(rst_list[1]) + int(rst_list[9])
            end_data = {
                ‘total‘: str(rst),
                ‘flowup‘: str(rst_list[9]),
                ‘flowdown‘: str(rst_list[1]),
                ‘timend‘: str(time.time())
            }
            self.cf.read(‘monkey.conf‘)
            oldTotal = self.cf.get(‘flow_mark‘, ‘flow_total‘)
            oldUp = self.cf.get(‘flow_mark‘, ‘flow_up‘)
            oldDown = self.cf.get(‘flow_mark‘, ‘flow_down‘)
            oldTime = self.cf.get(‘flow_mark‘, ‘time_start‘)
            rst_data = {
                ‘total‘: str(int(end_data[‘total‘]) - int(oldTotal)),
                ‘up‘: str(int(end_data[‘flowup‘]) - int(oldUp)),
                ‘down‘: str(int(end_data[‘flowdown‘]) - int(oldDown)),
                ‘time‘: str(float(end_data[‘timend‘]) - float(oldTime))
            }
            return rst_data

因为处理的东西比较多,所以我也贴一下前端的代码。


$("#getflow").click(function () {
        var val = $("#getflow").text();
        var myDate = new Date();
        var package_name = $("#package").val();
        if (val == ‘点击开始测试‘) {
            $("#getflow").removeClass().addClass(‘btn btn-danger‘);
            $.get(
                ‘/datashow/testflow/‘,
                {mark: ‘start‘, package: package_name}
            );
            $("#getflow").text(‘点击停止测试‘);
            $("#start").text(‘开始测试时间为: ‘ + myDate.toLocaleTimeString());
            $("#end").text(‘‘);
            $("#result").html(‘‘)
        }
        else {
            $("#getflow").removeClass().addClass(‘btn btn-default‘);
            $("#end").text(‘结束测试时间为: ‘ + myDate.toLocaleTimeString());
            $("#getflow").text(‘点击开始测试‘);
            $.get(
                ‘/datashow/testflow/‘,
                {mark: ‘end‘, package: package_name},
                function (data) {
                    $("#result").html(
                        "测试结果:" +
                        "<hr>" +
                        "测试一共耗时:" + data[‘time‘] + "秒" +
                        "<hr>" +
                        "总计流量消耗: " + data[‘total‘] + "byte" +
                        "<hr>" +
                        "上行流量: " + data[‘up‘] + "byte" +
                        "<hr>" +
                        "下行流量: " + data[‘down‘]
                    )
                }
            );
            $("#selection").html(‘‘);
            $.ajax({
                url: ‘/datashow/getdirlist/flowinfo‘,
                success: function (data) {
                    var arr = data[‘data‘];
                    var select = $("<select id=‘selquery‘ class=‘form-control‘></select>");
                    for (var i = 0; i < arr.length; i++) {
                        select = select.append("<option value=‘" + arr[i] + "‘>" + arr[i] + "</option>")
                    }
                    $("#selection").append(select)
                }
            });

        }
    });

最终的效果图就是这样:

结语

整个开发周期前后大概花了两周时间,这些知识熟的朋友应该可以更快,在使用前端知识的时候,我基本上都是靠w3cschool来解决,大概的概念我懂,但是具体的编码还是需要去copy。在整个项目写完后,我感觉我对这块的了解也更加深入了,重复造轮子的好处就是可以深入理解这些东西的来源,比如adb的使用,Android的一些知识。真正在工作当中使用的话,当然还是直接使用一些大公司的产品比较好,他们的东西比较成熟,精准度从一定程度上来说也比我们自己写的质量会高一些。

最后的最后,代码放在Github,有兴趣的朋友可以自行翻阅。

时间: 2024-10-22 13:19:13

AndroidTestTool开发笔记的相关文章

张高兴的 Windows 10 IoT 开发笔记:RTC 时钟模块 DS3231

原文:张高兴的 Windows 10 IoT 开发笔记:RTC 时钟模块 DS3231 GitHub:https://github.com/ZhangGaoxing/windows-iot-demo/tree/master/DS3231 注意:不包含闹钟设置

Android开发笔记(一百零三)地图与定位SDK

集成地图SDK 国内常用的地图SDK就是百度和高德了,二者的用法大同小异,可按照官网上的开发指南一步步来.下面是我在集成地图SDK时遇到的问题说明: 1.点击基本地图功能选项,不能打开地图,弹出"key验证出错!请在AndroidManifest.xml文件中检查key设置的"的红色字提示.查看日志提示"galaxy lib host missing meta-data,make sure you know the right way to integrate galaxy&

微信订阅号开发笔记(二)

微信开发的流程其实很简单 o(∩_∩)o 哈哈!在微信网站的编辑操作 额,就不说了.虽然有人问过.下面是我的微信开发过程,简单记录下. 成为开发者 材料:1.自己的服务器资源,百度的BAE,新浪的SAE都不错. 2.懂那么点编程语言. 3.注册微信公众号. 上面的都有了之后,就可以自己动手开发了.哇咔咔,好兴奋.有木有. 在登录进去之后,怎么成为开发者?不知道,自己看去. 开始coding吧. 1.验证 if (! empty ( $_GET ['echostr'] ) && ! empt

微信订阅号开发笔记(三)

1.接收语音识别结果 if($msgType=="voice"){ //收到语音消息 //MediaId 语音消息媒体id,可以调用多媒体文件下载接口拉取数据. //Format 语音格式,如amr,speex等 $format = $postObj->Format; $mediaId = $postObj->MediaId; //开通语音识别功能,用户每次发送语音给公众号时,微信会在推送的语音消息XML数据包中,增加一个Recongnition字段. //注:由于客户端缓

微信订阅号开发笔记(四)

1.创建菜单 //创建菜单 public function createMenu(){ $url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token="; $url.=$this->getacctoken(); //目前自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单.一级菜单最多4个汉字, //二级菜单最多7个汉字,多出来的部分将会以"..."代替.请注意,创建自定义菜单

Swift开发笔记

Swift开发笔记(一) 刚开始接触XCode时,整个操作逻辑与Android Studio.Visual Studio等是完全不同的,因此本文围绕IOS中控件的设置.事件的注册来简单的了解IOS开发 1.新建一个Xcode项目,项目目录大致如图: 2.在Main.storyboard添加控件 首先要显示右侧的工具栏(Utilities),之后选择显示Object Library,就可以找到常用的一些控件了,然后添加几个控件到界面中 3.将控件和ViewController进行关联 此时要将Ed

张高兴的 Xamarin.Forms 开发笔记:为 Android 与 iOS 引入 UWP 风格的汉堡菜单 ( MasterDetailPage )

所谓 UWP 样式的汉堡菜单,我曾在"张高兴的 UWP 开发笔记:汉堡菜单进阶"里说过,也就是使用 Segoe MDL2 Assets 字体作为左侧 Icon,并且左侧使用填充颜色的矩形用来表示 ListView 的选中.如下图 但怎样通过 Xamarin.Forms ,将这一样式的汉堡菜单带入到 Android 与 iOS 中呢? 一.大纲-细节模式简介 讲代码前首先来说说这种导航模式,官方称"大纲-细节模式"(MasterDetail).左侧的汉堡菜单称为&qu

web前端开发笔记(2)

web前端开发笔记(1) 一.HTML标签书写有哪些规范? 页面编码. 文档声明. 关键字与描述. 行内元素不能包含块级元素. a标签不能嵌套a标签. 标签名和属性必须用小写字母书写,属性必须加引号,标签必须闭合,单标签页必须闭合. 页面中不要用 进行缩进,如需缩进,用css控制. html标签使用必须语义化. 要为img标签填写alt和title属性. 二.HTML静态页面出现中文乱码如何解决? 引入<meta charset="UTF-8"> 三.通常情况下块属性标签和

[APP] Android 开发笔记 003

接上节 [APP] Android 开发笔记 002 5. 使用ant release 打包 1)制作 密钥文件 release.keystore (*.keystore) keytool -genkey -v -keystore "release.keystore" -alias "release" -keyalg "RSA" -validity "10000" 这里需要注意的是: -keystore "relea