Android:检测内存泄漏的自动化测试Python脚本 - sheldon_blogs

时间:2024-03-10 18:08:11

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