Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> AndroidTestTool開發筆記

AndroidTestTool開發筆記

編輯:關於Android編程

前言

這段時間在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。兩個端界面大概長這樣。

客戶端

web端

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('當前打開的包名為: '+data['package_name']+"");
                $("#activity_name").html('當前打開的avtivity為: '+data['avtivity_name']+"")
            }
        )
    });

獲取所有第三方應用的命令是這個:


adb shell pm list package -3

依照上面的方式給代碼就行了。效果就是這樣:

web端

內存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 = $("");
                for (var i = 0; i < arr.length; i++) {
                    select = select.append("")
                }
                $("#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(
                        "測試結果:" +
                        "
" + "測試一共耗時:" + data['time'] + "秒" + "
" + "總計流量消耗: " + data['total'] + "byte" + "
" + "上行流量: " + data['up'] + "byte" + "
" + "下行流量: " + data['down'] ) } ); $("#selection").html(''); $.ajax({ url: '/datashow/getdirlist/flowinfo', success: function (data) { var arr = data['data']; var select = $(""); for (var i = 0; i < arr.length; i++) { select = select.append("") } $("#selection").append(select) } }); } });

最終的效果圖就是這樣:

流量監控

結語

整個開發周期前後大概花了兩周時間,這些知識熟的朋友應該可以更快,在使用前端知識的時候,我基本上都是靠w3cschool來解決,大概的概念我懂,但是具體的編碼還是需要去copy。在整個項目寫完後,我感覺我對這塊的了解也更加深入了,重復造輪子的好處就是可以深入理解這些東西的來源,比如adb的使用,Android的一些知識。真正在工作當中使用的話,當然還是直接使用一些大公司的產品比較好,他們的東西比較成熟,精准度從一定程度上來說也比我們自己寫的質量會高一些。

最後的最後,代碼放在Github,有興趣的朋友可以自行翻閱。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved