主要代码分析

主要代码分析

  • 由于自己对python的不熟悉导致在复现过程中运行程序有困难,所以这里对主要程序进行逐句分析。

driver5.py代码分析

#!/usr/bin/env python

import sys
import os
import subprocess
import logging
from threading import Thread

# plg是AppsPlayground的库,以下涉及到的代码在后面进行分析

# plg.utils.androidutil包含各种帮助实用程序,用于启动带有“adb”的命令
import plg.utils.androidutil as au

# plg.utils.logcat包含获取“logcat”消息的代码。
import plg.utils.logcat as lc

# plg.metadata从应用程序中提取元数据
# 元数据被定义为:描述数据及其环境的数据
from plg.metadata import getmetadata

# plg.explore.explore提供运行应用程序显示地面的主要入口点
from plg.explore import explore

# plg.settings包含整个代码中使用的各种常量。有些已经不用了。
from plg.settings import LAUNCHER_PKG


# plg.androdevice:带有视图层次结构的连接套接字和 monkey 的 android 设备
# Monkey是运行在模拟器或设备上的一个程序,用来伪随机地模拟点击和触摸等用户事件
# run_monkey():这个monkey设备可以防止进入外部包,还可以检测ANRs(Application Not Responding,应用程序无响应)
from plg.androdevice import run_monkey


MAX_EMULATOR_WAIT = 120 # in seconds


# 终止由启动模拟程序返回的进程
def finish(device):
    print('killing', device, file=sys.stderr)
    au.killemulator(device)



# *args 是一个元组tuple(元组与列表类似,不同之处在于元组的元素不能修改)
# 可以接受序列的输入参数。当函数的参数不确定时,可以使用 *args
def main(avd, app, *args):
    emu_cmd = ['emulator64-x86', '@{}'.format(avd)] #启动模拟器的命令
    
    # 如果输入在args中的前两个元素是:-system filepath
    # 这个是用来指定初始系统文件。提供文件名,以及绝对路径或相对于工作目录的路径
    # 如果不使用此选项,则默认为系统目录中的 system.img 文件
    if args and args[0] == '-system': 
        emu_cmd.extend(args[:2]) # 顾头不顾尾,取前两个元素放到emu_cmd列表后
        args = args[2:] # args中第三个元素一直到最后重新赋值给args元组
    
    
    #对于计算机上运行的第一个虚拟设备实例,默认值为 5554
    # 在特定计算机上运行的第一个虚拟设备的控制台使用控制台端口 5554 和 adb 端口 5555。
    # 后续实例使用的端口号渐增 2,例如 5556/5557、5558/5559 等。
    # 范围是 5554 到 5682,可用于 64 个并发虚拟设备。 
    port = 5554
    
    # 如果args内容不为空,就把args的第一个值赋给port,之后的给 *args
    if args:
        port, *args = args
    
    log = 'log.txt'
    
    # 如果args内容不为空,就把args的第一个值赋给log,之后的给 *args
    if args:
        log, *args = args # we will ignore other args
    
    
    #启动模拟器的命令;
    #extend()函数用于在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表)
    #-no-snapshot-save:执行快速启动,但在退出时不保存模拟器状态
    #-port:设置用于控制台和 adb 的 TCP 端口号
    #-m 512:是给客户机分配 512MB 内存
    #-enable-kvm:利用 KVM 来访问硬件提供的虚拟化服务
    emu_cmd.extend(['-no-snapshot-save', '-port', str(port), '-qemu', '-m',
        '512', '-enable-kvm']) 
    
    # the x86 emulators need help finding some libraries
    
    
    
    # environ 是一个字符串所对应环境的映像对象
    #把 android-sdk/tools/lib 添加到 LD_LIBRARY_PATH 路径中
    emu_env = os.environ.copy()
    emu_env['LD_LIBRARY_PATH'] = ('/home/ubuntu/Android/sdk/'
        'tools/lib')
    
    
    device = 'emulator-{}'.format(port)

    au.init() # 杀死当前存在的模拟器
    # file=sys.stderr:将当前默认的错误输出结果保存为 file
    print('launching', device, file=sys.stderr)
    
    # subprocess.Popen 类来处理基本的进程创建和管理
    # 开始执行命令
    subprocess.Popen(emu_cmd, env=emu_env)
    
    
    ''' Python的异常处理机制的语法结构: 
    try:
        <语句>
    except <name>:
        <语句>  #如果在try部份引发了名为'name'的异常,则执行这段代码
    else:
        <语句>  #如果没有异常发生,则执行这段代码
    '''
    try:
        # 等待设备上线
        au.waitfordevice(device, timeout=MAX_EMULATOR_WAIT)
    except subprocess.TimeoutExpired:
        # 保留一个“timeout”用于错误处理
        # 当想要自己的模拟器启动,它实际上可能并没有启动
        print('time out expired while waiting for', device, file=sys.stderr)
        raise # 该语句引发当前上下文中捕获的异常
    
    
    # 从 time 模块中引入sleep函数
    # 使用sleep函数可以让程序休眠(推迟调用线程的运行)
    from time import sleep
    #sleep(300)   # 休眠300秒
    # getadbcmd():帮助函数,返回命令 adb -s emulator-5554 install app
    # 这个命令的意思是:在emulator-5554模拟器上安装app(.apk)
    install_cmd = au.getadbcmd(['install', app], device)
    
    
    try:
        # 子进程执行install_cmd中的命令,并将其输出形成字符串返回
        # 如果子进程退出码不是0,抛出subprocess.CalledProcessError异常
        # 将stderr参数设置为subprocess.STDOUT,表示将错误通过标准输出流输出
        subprocess.check_output(install_cmd, stderr=subprocess.STDOUT)
    except subprocess.CalledProcessError as e:
        # join():用于将序列中的元素以指定的字符(这里应该是空格)连接生成一个新的字符串
        # splitlines():按行分割字符串,返回值是个列表
        info = 'install-failed ret:%d %s' % (e.returncode,
                b' '.join(e.output.splitlines()))
        print('info', file=sys.stderr)
        finish(device) # 终止由启动模拟程序返回的进程



    # config logcat配置日志
    lc_file = log
    
    # 执行命令:adb -s emulator -5554 logcat -c
    # -c:清除屏幕上的日志
    lc.clearlogcat(device)
    
    # 
    lc.logcat(lc_file, device) # file open/close is done by callee
    
    
    # 从应用程序中提取元数据
    metainfo = getmetadata(app)

    # launch monkey to prevent straying and deal with ANRs
    # 触发monkey来防止走失和避免ANRs
    ''' 使用Thread类实现多线程:
    1、创建Thread类的实例;
    2、通过Thread类的构造方法的target关键字参数执行线程函数;通过args关键字参数指定传给线程函数的参数。
    3、调用Thread对象的start方法启动线程。
    其中:
    target: 要执行的方法
    args: 要传入方法的参数
    '''
    # 这里run_monkey()执行的命令是:adb -s emulator-5554 shell monkey --port 1080 -p metainfo['name']
    # --port 端口号:为测试分配一个专用的端口号
    # -p:用此参数指定一个或多个包(Package,即App)。指定包之后,monkey将只允许系统启动指定的APP,如果不指定包,将允许系统启动设备中的所有APP.
    # 该命令的意思是:在emulator-5554模拟器上,启动指定的应用程序
    t = Thread(target=run_monkey, args=(device, metainfo['name']))
    t.daemon = True #daemon被设置为True时,如果主线程退出,那么子线程也将跟着退出
    t.start() #启动线程

    print('begin exploring')
    # explore():AppsPlayground的主要入口点
    explore(device, metainfo)
    print('finish exploring')
    finish(device) # 终止由启动模拟程序返回的进程


if __name__ == '__main__':
    # 输入的参数中不能有 help、-h、-help
    if ('help' in sys.argv or '-h' in sys.argv or '-help' in sys.argv or
            len(sys.argv) < 3):
        print('usage:', sys.argv[0],
                'avd app [-system <system.img>] [port [log]]')
        #sys.exit(0):表示正常退出
        #sys.exit(2):数值2为不正常,可抛异常事件供捕获
        sys.exit(2 if len(sys.argv) < 3 else 0)
        
    #
    logging.basicConfig(level=logging.WARNING, stream=sys.stderr)
    
    # 将输入的参数传入到main函数
    main(*sys.argv[1:])

plg.utils.androidutil中的init()函数:

# 杀死任何已存在的模拟器
def init(logfile=None):
    if not logfile:
        logfile = sys.stderr  # 将当前默认的错误输出结果保存为logfile
    print('(re)starting adb server', file=logfile)
    killserver()
    startserver()
    #sleep(10) # let adb start
    print('killing any emulators already present', file=logfile)
    for device in getdevices():
        killemulator(device)

plg.utils.androidutil中的getadbcmd()函数:

def getadbcmd(args=None, device=None):
    ''' helper function:
        args - arguments excluding adb and device'''
    preargs = [ADB]
    if device:
        #strip()方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列。
        device = device.strip() 
        if device:
            preargs += ['-s', device]
    if not args:
        return preargs
    return preargs + args

plg.utils.androidutil中的runadbcmd()函数:

def runadbcmd(args, device=None):
    # 运行由args参数提供的命令
    # 如果命令行执行成功,check_call返回返回码 0
    # 否则抛出subprocess.CalledProcessError异常
    return subprocess.check_call(getadbcmd(args, device))

plg.utils.logcat中的clearlogcat()函数:

def clearlogcat(device=None):
    return androidutil.runadbcmd(['logcat', '-c'], device)

plg.utils.logcat中的logcatlines()、_logcat()、logcat()函数:


def _enqueue_output(out, q):
    for line in iter(out.readline, b''):
        q.put(line)
    out.close()


def logcatlines(device=None, args=''):
    # cmd的值是:adb -s emulator-5554 logcat
    # 该命令的意思是:查看日志输出
    cmd = ' '.join(androidutil.getadbcmd(args, device)) + ' logcat ' +args
    
    '''
    这里使用了pty模块,pty模块定义了处理伪终端概念的操作:启动另一个进程,并能够以编程方式读写其控制终端。
    因为伪终端处理是高度依赖于平台的,所以只有在Linux下才有代码可以做到这一点。
    pty.openpty():
    Open a new pseudo-terminal pair, using os.openpty() if possible, 
    or emulation code for generic Unix systems. Return a pair of file 
    descriptors (master, slave), for the master and the slave end, respectively.
    打开一个新的伪终端对,分别为主机和从机端返回一对文件描述符(主机、从机)。
    '''
    logmaster, logslave = pty.openpty()
    
    # subprocess模块中的Popen类来创建进程,并与进程进行复杂的交互
    # 参数stdout, stderr分别表示子程序的标准输出、错误句柄。
    # 参数shell设为True,指定的命令会在shell里解释执行
    # 参数close_fds设为True,执行新进程前把除了0、1、2以外的文件描述符都先关闭
    logcatp = subprocess.Popen(cmd, shell=True,
            stdout=logslave, stderr=logslave, close_fds=True)
    
    # os.fdopen()方法:用于通过文件描述符 fd 创建一个文件对象,并返回这个文件对象
    # 这里logmaster文件描述符是一个小整数
    stdout = os.fdopen(logmaster)
    
    
    q = Queue()
    
    
    t = Thread(target=_enqueue_output, args=(stdout, q))
    t.daemon = True
    t.start()
    while logcatp.poll() is None:
        try:
            yield q.get(True, 1)
        except Empty:
            continue


def _logcat(device, fname, logcatargs):
    with open(fname, 'w') as f:
        for line in logcatlines(device, logcatargs):
            f.write(line)
            f.flush()


def logcat(fname, device=None, logcatargs=''):
    ''' run logcat and collect output in file fname.'''
    # 通过Process类创建进程,基本使用与 Thread() 类似
    proc = Process(target=_logcat, args=(device, fname, logcatargs))
    proc.start()

plg.metadata中的getmetadata()函数:

# 从应用程序中提取元数据
def getmetadata(apk):
    # 在子进程执行命令,以字符串形式返回执行结果的输出
    # 如果子进程退出码不是0,抛出subprocess.CalledProcessError异常
    # 执行的命令是:aapt d badging apk,用来查看apk版本及其相关信息
    aaptout = subprocess.check_output(['aapt', 'd', 'badging', apk])
    
    # 创建了一个存放元数据的字典
    data = {}
    data['uses-permission'] = []
    data['uses-feature'] = []
    data['uses-library'] = []
    data['launchable'] = []
    
    # splitlines():按照行('
', '
', 
')分隔,返回一个包含各行作为元素的列表
    # 这里没有参数,相当于keepends为 False,不包含换行符
    # 直接对file对象使用for循环读每行数据
    for line in aaptout.splitlines():
        line = line.decode() # 解码(将字节码转换为字符串,将比特位显示成字符)
        
        # split():通过指定分隔符 ' 来对字符串进行切片
        tokens = line.split("'")
        
        # startswith():用于检查字符串是否是以指定子字符串开头,如果是则返回 True,否则返回 False
        if line.startswith('package:'):
            data['name'] = tokens[1]
            data['versionCode'] = tokens[3]
            data['versionName'] = tokens[5]
        elif line.startswith('uses-permission'):
            data['uses-permission'].append(tokens[1])
        elif line.startswith('sdkVersion'):
            data['sdkVersion'] = tokens[1]
        elif line.startswith('targetSdkVersion'):
            data['targetSdkVersion'] = tokens[1]
        elif line.startswith('uses-feature'): # both required and not required
            data['uses-feature'].append(tokens[1])
        elif line.startswith('uses-library'): # both required and not required
            data['uses-library'].append(tokens[1])
        elif line.startswith('application:'):
            data['app-label'] = tokens[1]
            data['app-icon'] = tokens[3]
        elif line.startswith('launchable activity') or line.startswith(
                'launchable-activity'):
            data['launchable'].append(dict(name=tokens[1],
                label=tokens[3], icon=tokens[5]))
    return data

plg.androdevice中的run_monkey()函数:

def run_monkey(name=None, pkg=None):
    '''
    This monkey process prevents going to outside packages and also detects
    ANRs. In future we may do something else for these two functionalities. In
    case of ANR, kill emulator so that this worker eventually crashes.

    Parameters
    ----------
    name: str
        the device name such as 'emulator-5554'
    pkg: str
        the application package outside which plg should not go
    '''
    cmd = ['shell', 'monkey', '--port', '1080']
    if pkg:
        cmd.extend(['-p', pkg])
    runadbcmd(cmd, name)
    killemulator(name)

plg.explore中的explore()函数:

def explore(dev, appinfo):
    ''' The main entrypoint for AppsPlayground.

    Parameters
    ----------
    dev:
        can be a string like 'emulator-5554' or an `AndroidDevice` instance
    appinfo:
        the metadata for the app, such as one derived from
        plg.metadata.getmetadata()
    '''
    if type(dev) is AndroidDevice:
        androdev = dev
        dev = dev.name
    else:
        dev = str(dev)
        androdev = AndroidDevice(dev)
    DevState(dev, androdev, appinfo).explore()

参考资料

原文地址:https://www.cnblogs.com/yangdd/p/13215537.html