Android:檢測內存泄漏的自動化測試Python腳本


  安卓開發中經常需要對app的性能進行優化,其中就包括解決內存泄漏問題,在app不大的情況下,可借助Android Studio的Android Monitor,簡單操作app,觀察內存情況,就可以找出內存泄漏點,或者引入開源項目LeakCanary,也可以很快發現內存泄漏點。當然也可以借助monkey測試,省去了我們操作app的步驟,還可以進入到一些非常規操作,然而monkey測試一般效率較低,耗時較長,這個時候我們就不能傻看着內存曲線了,那剩下的工作就是定時記錄內存信息及定時導出hprof內存堆棧文件了。基於此目的,編寫了一個簡單腳本,用於自動跑monkey測試及記錄內存信息,腳本的邏輯很簡單:插入monkey命令 -> 循環記錄內存信息 -> 導出堆棧文件 -> kill monkey進程。

以下兩個變量根據需求修改:

(1)packageName = "com.android.systemui"

(2)OUTPUT_DIR = os.path.join('d:\\', '\\tools\\tmp\\')    # 目錄"D:\tools\tmp"

 運行環境:Python 3.8.2

#! /usr/bin/python3
# -*- coding: utf-8 -*-

import os, sys, time, logging

# 下列時間單位均為秒
# 執行時間
exec_time = 15 * 60 * 60  # 10 hours, 可改成60s供測試該腳本
# 記錄內存間隔時間,exec_time/exec_interval + 1 即為記錄內存次數
exec_interval = 10  # 10 s
# 導出hprof文件間隔
dump_interval = 60 * 60  # 1 hour, 可改成30s供測試該腳本

time_passed = 0
# 打印提示間隔次數,以查看當前進度
print_interval = 1
packageName = "com.android.systemui"
bulid_type = ""

# 所有產生文件的輸出目錄,必須指定且存在
# OUTPUT_DIR = os.path.join(os.path.expanduser('~'), "test") # 目錄"~/test"
OUTPUT_DIR = os.path.join('d:\\', '\\tools\\tmp\\')  # 目錄"D:\tools\tmp"

logger = logging.getLogger('memoryleak')
FILE_LOG = True
LOG_LEVEL = logging.DEBUG


def init_logger():
    logger.setLevel(LOG_LEVEL)
    # create formatter
    # [%(filename)s:%(lineno)d] 代碼位置,暫不配
    log_format = logging.Formatter("%(asctime)s %(name)s %(levelname)-8s: %(message)s")

    def add_ch():
        # create console handler and set level to debug
        ch = logging.StreamHandler()
        ch.setLevel(LOG_LEVEL)
        ch.setFormatter(log_format)
        # add handler to logger
        logger.addHandler(ch)

    def add_fh():
        logfile = os.path.join(OUTPUT_DIR, "memoryleak_" + time.strftime('%Y%m%d%H%M%S') + ".log")
        fh = logging.FileHandler(logfile)
        fh.setLevel(LOG_LEVEL)
        fh.setFormatter(log_format)
        logger.warning('log will be outputed to console and file:[%s]' % logfile)
        logger.addHandler(fh)

    add_ch()
    if not (OUTPUT_DIR and os.path.isdir(OUTPUT_DIR) and os.access(OUTPUT_DIR, os.W_OK)):
        logger.error('OUTPUT_DIR: ' + OUTPUT_DIR + ' not exist or not writable, please check it up, exiting...')
        sys.exit(-1)
    if FILE_LOG:
        add_fh()


def start_monkey():
    # adb shell monkey -p com.gionee.filemanager --throttle 800 -v -v 300
    command = "adb shell monkey -p " + packageName
    command += " --ignore-crashes"
    command += " --ignore-timeouts"
    command += " --ignore-security-exceptions"
    command += " --ignore-native-crashes"
    command += " --monitor-native-crashes"
    command += " --throttle 800"
    command += " -v -v 1000000"
    command += " > " + os.path.join(OUTPUT_DIR, "monkeytest.log")
    logger.info("插入monkey命令:" + command)
    os.popen(command)


def record_memory():
    global time_passed
    if "eng" in bulid_type:
        memfile = os.path.join(OUTPUT_DIR, 'procrank.txt')
        # 第一次執行命令
        command = 'adb shell \"procrank | grep ' + packageName + '\|cmdline' + ' > ' + memfile
        # 后續執行命令
        commandOther = 'adb shell \"procrank | grep ' + packageName + ' >> ' + memfile + "\""
    else:
        memfile = os.path.join(OUTPUT_DIR, "meminfo.txt")
        command = 'adb shell \"dumpsys meminfo ' + packageName + \
                             ' | grep "Dalvik Heap" -A 14 -B 4 | grep -ie Private -ie Tota\"' + ' > ' + memfile
                             
        commandOther = 'adb shell \"dumpsys meminfo ' + packageName + ' | grep TOTAL -m 1\"' + ' >> ' + memfile

    exec_count = exec_time // exec_interval + 1
    logger.info("開始記錄內存信息,待記錄次數:" + str(exec_count))
    for i in range(exec_count):
        os.popen(command)  # 運行命令
        # 執行初始命令后切換為后續命令
        if i == 0:
            command = commandOther

        if i % print_interval == 0:
            logger.info("當前記錄內存次數: " + str(i))

        if (time_passed) % dump_interval == 0:
            logger.info("當前dump hprof次數: " + str(time_passed // dump_interval))
            dumpheap(str(time_passed // dump_interval))

        time_passed += exec_interval
        time.sleep(exec_interval)  # 休息n秒,再進入下一個循環,也就是每隔n秒打印一次procrank的信息

    logger.info("記錄內存信息結束")  # 運行完畢的標志


def dumpheap(name):
    command = "adb shell \"am dumpheap " + packageName + " /data/local/tmp/hprofs/\""
    command += "count" + name + ".hprof"
    os.popen(command)


def stop_monkey():
    # adb shell kill -9 `adb shell ps | grep com.android.commands.monkey | awk '{print $2}'`
    pid = os.popen("adb shell \"ps | grep monkey | awk '{print $2}'\"").read()
    pid = pid.replace("\n", "")
    logger.info("monkey pid is: " + pid + ", kill it")
    os.system("adb shell kill " + pid)


def copyheap():
    logger.info("開始導出hprof文件...")
    os.system("adb pull /data/local/tmp/hprofs/ " + OUTPUT_DIR)
    os.system("adb shell rm -r /data/local/tmp/hprofs")
    logger.info("導出hprof文件結束")


# Ensure in eng release or seleted app has flag android:debuggable="true"
def check_env():
    global bulid_type
    bulid_type_prop = os.popen("adb shell \"getprop | grep ro.build.type\"").read()
    logger.info("當前rom版本信息 :" + bulid_type_prop)
    if "eng" in bulid_type_prop:
        bulid_type = "eng"
        logger.info("當前rom版本: eng")
    elif "userdebug" in bulid_type_prop:
        bulid_type = "userdebug"
        logger.info("當前rom版本: userdebug")
    else:
        bulid_type = "user"
        logger.info("當前rom版本: user")
        package_flags = os.popen("adb shell \"dumpsys package " + packageName + " | grep pkgFlags=\"").read()
        if "DEBUGGABLE" not in package_flags:
            logger.info("當前為user版本且應用沒有設置android:debuggable=\"true\", 無法導出內存信息, 請確認環境。")
            sys.exit(-1)

    # 清空及建立hprof文件存放目錄
    if 'hprofs' in os.popen('adb shell ls /data/local/tmp').read():
        logger.info('在設備中清除上次運行產生的臨時目錄"/data/local/tmp/hprofs"...')
        os.system("adb shell rm -r /data/local/tmp/hprofs")
    logger.info('在設備中新建臨時目錄"/data/local/tmp/hprofs"...')
    os.system("adb shell mkdir -p /data/local/tmp/hprofs")


def main():
    init_logger()
    check_env()
    start_monkey()
    # 循環進行,程序主體
    record_memory()
    stop_monkey()
    copyheap()


if __name__ == '__main__':
    main()

 

附:Monkey命令使用

adb shell monkey -v -v -v -s 123123 --throttle 300 --pct-touch 40 --pct-motion 60 --pct-appswitch 0 --pct-syskeys 0 --pct-majornav 0 --pct-nav 0 --pct-trackball 0 --ignore-crashes --ignore-timeouts --ignore-native-crashes -p com.xxx.xxx 100000 > d:\monkey.txt

 

  • -p 用於約束限制,用此參數指定一個包,指定包后Monkey將被允許啟動指定應用;如果不指定包, Monkey將被允許隨機啟動設備中的應用(主Activity有android.intent.category.LAUNCHER 或android.intent.category.MONKEY類別 )。比如 adb shell monkey -p xxx.xxx.xxx 1 ; xxx.xxx.xxx 表示應用包名,1 表示monkey模擬用戶隨機事件參數,最低1,這樣就能把應用啟動起來
  • -c 指定Activity的category類別,如果不指定,默認是CATEGORY_LAUNCHER 或者 Intent.CATEGORY_MONKEY;不太常用的一個參數
  • -v 用於指定反饋信息級別,也就是日志的詳細程度,分Level1、Level2、Level3;-v 默認值,僅提供啟動提示,操作結果等少量信息 ,也就是Level1,比如adb shell monkey -p xxx.xxx.xxx -v 1 ;-v -v 提供比較詳細信息,比如啟動的每個activity信息 ,也就是Level2,比如adb shell monkey -p xxx.xxx.xxx -v -v 1 ;-v -v -v 提供最詳細的信息 ,比如adb shell monkey -p xxx.xxx.xxx -v -v -v 1
  • -s 偽隨機數生成器的種子值,如果我們兩次monkey測試事件使用相同的種子值,會產生相同的事件序列;如果不指定種子值,系統會產生一個隨機值。種子值對我們復現bug很重要。使用如下adb shell monkey -p xxx.xxx.xxx -s 11111 10;這也是偽隨機事件的原因,因為這些事件可以通過種子值進行復現
  • --ignore-crashes 忽略異常崩潰,如果不指定,那么在monkey測試的時候,應用發生崩潰時就會停止運行;如果加上了這個參數,monkey就會運行到指定事件數才停止。比如adb shell monkey -p xxx.xxx.xxx -v -v -v --ignore-crashes 10
  • --ignore-timeouts 忽略ANR,情況與4類似,當發送ANR時候,讓monkey繼續運行。比如adb shell monkey -p xxx.xxx.xxx -v -v -v --ignore-timeouts 10
  • --ignore-native-crashes 忽略native層代碼的崩潰,情況與4類似,比如adb shell monkey -p xxx.xxx.xxx -v -v -v --ignore-native-crashes 10
  • --ignore-security-exceptions 忽略一些許可錯誤,比如證書許可,網絡許可,adb shell monkey -p xxx.xxx.xxx -v -v -v --ignore-security-exceptions 10
  • --monitor-native-crashes 是否監視並報告native層發送的崩潰代碼,adb shell monkey -p xxx.xxx.xxx -v -v -v --monitor-native-crashes 10
  • --kill-procress-after-error 用於在發送錯誤后殺死進程
  • --hprof 設置后,在Monkey事件序列之前和之后立即生產分析報告,保存於data/mic目錄,不過將會生成大量幾兆文件,謹慎使用
  • --throttle 設置每個事件結束后延遲多少時間再繼續下一個事件,降低cpu壓力;如果不設置,事件與事件之間將不會延遲,事件將會盡快生成;一般設置300ms,因為人最快300ms左右一個動作,比如 adb shell monkey -p xxx.xxx.xxx -v -v -v --throttle 300 10
  • --pct-touch 設置觸摸事件的百分比,即手指對屏幕進行點擊抬起(down-up)的動作;不做設置情況下系統將隨機分配各種事件的百分比。比如adb shell monkey -p xxx.xxxx.xxx --pct-touch 50 -v -v 100 ,這就表示100次事件里有50%事件是觸摸事件
  • --pct-motion 設置移動事件百分比,這種事件類型是由屏幕上某處的一個down事件-一系列偽隨機的移動事件-一個up事件,即點擊屏幕,然后直線運動,最后抬起這種運動。
  • --pct-trackball 設置軌跡球事件百分比,這種事件類型是一個或者多個隨機移動,包含點擊事件,這里可以是曲線運動,不過現在手機很多不支持,這個參數不常用
  • --pct-syskeys 設置系統物理按鍵事件百分比,比如home鍵,音量鍵,返回鍵,撥打電話鍵,掛電話鍵等
  • --pct-nav 設置基本的導航按鍵事件百分比,比如輸入設備上的上下左右四個方向鍵
  • --pct-appswitch 設置monkey使用startActivity進行activity跳轉事件的百分比,保證界面的覆蓋情況
  • --ptc-anyevent 設置其它事件百分比
  • --ptc-majornav 設置主導航事件的百分比
  • 保存dos窗口打印的monkey信息,在monkey命令后面補上輸出地址,如adb shell monkey -p xxx.xxxx.xxx -v -v 100 > D:\monkey.txt;這樣monkey測試結束后,所有打印的信息都會輸出到這個文件里
  • 通過adb bugreport 命令可以獲取整個android系統在運行過程中所有app的內存使用情況,cpu使用情況,activity運行信息等,包括出現異常等信息。使用方法 adb bugreport > bugreport.txt ;這樣在當前目錄就會產生一個txt文件和一個壓縮包,具體信息可在壓縮包查看,txt文件只會記錄壓縮包的生成過程信息
  • -f 加載monkey腳本文件進行測試,比如 adb shell monkey -f sdcard/monkey.txt -v -v 500

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM