編輯:關於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。兩個端界面大概長這樣。
最初我的選擇不是TKinter,這個Python自帶的原生包功能非常原始,而且最關鍵的是用的人太少,沒人寫中文文檔,我如果要使用它,就必須去看英文的原文。但是其他幾個GUI端也都有相應的缺點,PyQt環境搭建麻煩,而且中文文檔大多是Qt4的,WxPython文檔少,功能也不見得比Tkinter強大多少。最終還是硬著頭皮吧Tkinter的官方文檔看完了,因為沒有現成的代碼,所以我就把所有代碼丟到了一起,全部放在gui.py中,整個代碼完全沒有結構而言,不過再怎樣,功能也算是實現了。
Tkinter有幾個坑爹的點我簡單的列一下。
Entry就是一個Text輸入框,單行的那種。一旦設置為只讀屬性,就無法顯示文字,所以整個Tkinter如果要做成一個不允許輸入又要展示狀態的信息媒介,That’s Impossible。
Tkinter的標簽在初始化生成之後,就再也無法改變了,無法做動態的更新。
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()
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端的處理就相對比較順暢了,我把收集內存信息和CPU信息都放在Tkinter了,因此在Web端只負責展示就行了。當然,最後我又突發奇想把收集流量信息放在Web端了,主要是流量信息不完全用走勢圖可以完全展示,因此就把這個功能放在Web端了。
最初web端我是設計用來查看走勢圖的,因此有這麼幾個頁面:主頁、內存信息、CPU信息、流量信息,本來還有一個gxfinfo,但是不同Android手機展示出來的矩陣不同,統一處理的方案我還沒想出來,暫時就擱淺了,等後續想出來後我再補充,在導航欄上我把打開GUI端的按鈕集成了進來,也就是說以後我要使用就只要打開WEB端就行了,一站式解決方案。
首頁原來我是放置一些說明的,後來發現在首頁也可以加一些功能,比如直接查看包名、查看Activity,查看手機上有多少app等簡單實用的功能。這些功能自己從shell中去獲取也不難,但是集成進來之後顯得更簡單易用了。
獲取的命令很簡單,在*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
依照上面的方式給代碼就行了。效果就是這樣:
這兩部分的內容類似,都是作為一個展示的頁面來處理,就需要後端給數據,數據收集在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,有興趣的朋友可以自行翻閱。
最近自家的系統要做一個升級服務,裡面有三個功能,第一個是系統升級,也就是下載OTA包推送到recovery裡升級的,而第二個是MCU升級,這就涉及到我們自家系統的一些情況
Android Studio系列-HelloWorld前言Hello 各位,小巫這裡要記錄一些關於如何使用Android Studio開發Android app,這一篇是
??上一篇文章,我們主要分析了Activity的正常情況下生命周期及其方法,本篇主要涉及內容為Activity的異常情況下的生命周期。Activity異常生命周期??異常
今天我們來簡單說一下Android NDK的使用方法。眾所周知,so文件在Android的開發過程中起到了很重要的作用,無論與底層設備打交道還是在Android安全領域。