移动测试基础 分享一个 apk 批量安装脚本

heyniu · 发布于 2016年10月08日 · 最后由 heyniu 回复于 2016年12月07日 · 1545 次阅读

Installer

一个应用批量安装的python脚本

产生背景

展会期间需要推广app,考虑到量大,展会人数众多采用WIFI/4G网络下载也是比较慢的,需要一个分流方案。由此,想到了adb安装,这个脚本应运而来。

实现

  • 傻瓜式一键启动

  • 支持多设备同时安装

  • 支持安装失败给出相应提示,并重新安装

  • 友好提示

  • 支持重装/覆盖安装

  • 支持即插即用

实际效果

展会期间安装喻千台机器,现场来看只遇到过驱动不兼容的情况,效果显著。

使用方式

  • 下载adb
  • 安装驱动
  • 安装python
  • 拷贝apk到当前目录
  • 双击Installer.bat

截图

代码

Installer.bat

python Installer.py
pause.

Installer.py

#!/usr/bin/evn python
# -*- coding:utf-8 -*-

# FileName Installer.py
# Author: HeyNiu
# Created Time: 2016/9/27
"""
展会apk安装器
"""

import os
import threading
import re
import time


class Installer(threading.Thread):
    def __init__(self, interval, apk_path):
        """
        初始化
        :param interval: 间隔多久执行一次检查
        :param apk_path: apk路径
        """
        threading.Thread.__init__(self)
        self.interval = interval
        self.devices_init = []  # 初始化设备列表
        self.devices_now = []  # 当前设备列表
        self.devices_old = []  # 旧的设备列表
        self.adbtools = AdbTools()
        self.__check_devices()
        self.path = apk_path
        self.installing = []  # 正在安装apk的设备列表
        self.diff = []  # 差异化列表,用于判断设备是否有更新

    def run(self):
        while True:
            # print('After %s seconds to check devices.' % (self.interval,))
            time.sleep(self.interval)
            #  安装失败的设备重新安装
            self.__check_install_stats('Failure.log', self.devices_init, False)
            self.__check_install_stats('Failure.log', self.installing, True)
            #  安装成功的移除正在安装的列表
            self.__check_install_stats('Success.log', self.installing, False)

            self.diff = self.__diff_devices()
            if len(self.diff) > 0:
                self.__install()
            self.devices_init = self.devices_now

    def __check_devices(self):
        """
        检查已连接设备列表
        :return:
        """
        self.devices_init = list(self.adbtools.get_devices())

    @staticmethod
    def __check_install_stats(filename, l1, remove):
        """
        检查安装状态
        :return:
        """
        path = os.path.join(os.path.expanduser('~'), filename)
        if os.path.exists(path):
            l = open(path).readlines()
            for i in l:
                if i.strip() in l1:
                    l1.remove(i.strip())
                    if filename == 'Success.log':
                        l.remove(i)
            # 移除相关数据重新写入
            if filename == 'Success.log':
                with open(path, 'w') as f:
                    for i in l:
                        f.write(i)

        if remove and os.path.exists(path):
            os.remove(path)

    def __diff_devices(self):
        """
        设备列表求差集,即有新增的则执行安装
        :return:
        """
        self.devices_now = list(self.adbtools.get_devices())
        if self.devices_now != self.devices_old:
            print('Current devices: %s' % self.devices_now)
            self.devices_old = self.devices_now
        return list(set(self.devices_now).difference(set(self.devices_init)))

    def __install(self):
        """
        安装apk文件,以覆盖安装的形式
        :return:
        """
        for i in self.diff:
            if i not in self.installing:
                self.installing.append(i)
                AdbTools(str(i), self.path).start()


class AdbTools(threading.Thread):
    def __init__(self, device_id='', path=''):
        threading.Thread.__init__(self)
        self.command = ''
        self.device_id = device_id
        self.path = path
        self.__check_adb()
        self.__connection_devices()
        self.model = self.get_device_model()

    def run(self):
        self.install_replace(self.path)

    def __check_adb(self):
        """
        检查adb
        :return:
        """
        adb_path = os.path.join(os.getcwd(), "platform-tools", "adb.exe")
        if os.path.exists(adb_path):
            self.command = adb_path
        else:
            raise EnvironmentError("Adb not found.")

    def __connection_devices(self):
        """
        连接指定设备,单个设备可不传device_id
        :return:
        """
        if self.device_id == "":
            return
        self.device_id = "-s %s" % self.device_id

    def adb(self, args):
        """
        执行adb命令
        :param args:参数
        :return:
        """
        cmd = "%s %s %s" % (self.command, self.device_id, str(args))
        return os.popen(cmd)

    def shell(self, args):
        """
        执行adb shell命令
        :param args:参数
        :return:
        """
        cmd = "%s %s shell %s" % (self.command, self.device_id, str(args))
        return os.popen(cmd)

    def get_devices(self):
        """
        获取设备列表
        :return:
        """
        l = self.adb('devices').readlines()
        return (i.split()[0] for i in l if 'devices' not in i and len(i) > 5)

    def install_replace(self, path):
        """
        覆盖安装apk文件
        :return:
        """
        # adb install 安装错误常见列表
        errors = {'INSTALL_FAILED_ALREADY_EXISTS': '程序已经存在',
                  'INSTALL_DEVICES_NOT_FOUND': '找不到设备',
                  'INSTALL_FAILED_DEVICE_OFFLINE': '设备离线',
                  'INSTALL_FAILED_INVALID_APK': '无效的APK',
                  'INSTALL_FAILED_INVALID_URI': '无效的链接',
                  'INSTALL_FAILED_INSUFFICIENT_STORAGE': '没有足够的存储空间',
                  'INSTALL_FAILED_DUPLICATE_PACKAGE': '已存在同名程序',
                  'INSTALL_FAILED_NO_SHARED_USER': '要求的共享用户不存在',
                  'INSTALL_FAILED_UPDATE_INCOMPATIBLE': '版本不能共存',
                  'INSTALL_FAILED_SHARED_USER_INCOMPATIBLE': '需求的共享用户签名错误',
                  'INSTALL_FAILED_MISSING_SHARED_LIBRARY': '需求的共享库已丢失',
                  'INSTALL_FAILED_REPLACE_COULDNT_DELETE': '需求的共享库无效',
                  'INSTALL_FAILED_DEXOPT': 'dex优化验证失败',
                  'INSTALL_FAILED_DEVICE_NOSPACE': '手机存储空间不足导致apk拷贝失败',
                  'INSTALL_FAILED_DEVICE_COPY_FAILED': '文件拷贝失败',
                  'INSTALL_FAILED_OLDER_SDK': '系统版本过旧',
                  'INSTALL_FAILED_CONFLICTING_PROVIDER': '存在同名的内容提供者',
                  'INSTALL_FAILED_NEWER_SDK': '系统版本过新',
                  'INSTALL_FAILED_TEST_ONLY': '调用者不被允许测试的测试程序',
                  'INSTALL_FAILED_CPU_ABI_INCOMPATIBLE': '包含的本机代码不兼容',
                  'CPU_ABIINSTALL_FAILED_MISSING_FEATURE': '使用了一个无效的特性',
                  'INSTALL_FAILED_CONTAINER_ERROR': 'SD卡访问失败',
                  'INSTALL_FAILED_INVALID_INSTALL_LOCATION': '无效的安装路径',
                  'INSTALL_FAILED_MEDIA_UNAVAILABLE': 'SD卡不存在',
                  'INSTALL_FAILED_INTERNAL_ERROR': '系统问题导致安装失败',
                  'INSTALL_PARSE_FAILED_NO_CERTIFICATES': '文件未通过认证 >> 设置开启未知来源',
                  'INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES': '文件认证不一致 >> 先卸载原来的再安装',
                  'INSTALL_FAILED_INVALID_ZIP_FILE': '非法的zip文件 >> 先卸载原来的再安装',
                  'INSTALL_CANCELED_BY_USER': '需要用户确认才可进行安装',
                  'INSTALL_FAILED_VERIFICATION_FAILURE': '验证失败 >> 尝试重启手机',
                  'DEFAULT': '未知错误'
                  }
        print('%s installing...' % self.model)
        l = self.adb('install -r %s' % (path,)).read()
        if 'Success' in l:
            print('%s install Success' % self.model)
            #  安装记录 >> Devices.log
            with open(os.path.join(os.path.expanduser('~'), 'Devices.log'), 'a') as f:
                f.write(self.model)
                f.write('\n')
            with open(os.path.join(os.path.expanduser('~'), 'Success.log'), 'a') as f:
                f.write(self.device_id.replace('-s ', ''))
                f.write('\n')
        if 'Failure' in l:
            reg = re.compile('\\[(.+?)\\]')
            key = re.findall(reg, l)[0]
            try:
                print('%s install Failure >> %s' % (self.model, errors[key]))
            except KeyError:
                print('%s install Failure >> %s' % (self.model, key))
            with open(os.path.join(os.path.expanduser('~'), 'Failure.log'), 'w') as f:
                f.write(self.device_id.replace('-s ', ''))
        # 中途拔掉了设备,输出为安装成功,才能从正在安装列表中移除
        if l == '':
            with open(os.path.join(os.path.expanduser('~'), 'Success.log'), 'a') as f:
                f.write(self.device_id.replace('-s ', ''))
                f.write('\n')

    def get_device_model(self):
        """
        获取设备型号
        :return:
        """
        return self.shell('getprop ro.product.model').read().strip()


if __name__ == '__main__':
    path = list((f for root, dirs, files in os.walk(os.getcwd()) for f in files if f.endswith('.apk')))[0]
    Installer(5, path).start()

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 24 条回复
96

提供另一个比较简单的方案,adb install PATH*.apk

4024

那么长的.py脚本和adb install PATH*.apk 是否等同?如果是,当然是adb命令简单多了

110

#2楼 @ppliulijun
#3楼 @missgong0

看完程序再评价嘛 😓

9591
heyniu · #5 · 2016年10月08日 作者

#2楼 @ppliulijun

#3楼 @missgong0
你们的并不能满足需求啊,这么简单的肯定知道啊,本质也是通过adb install安装

6853

程序还可以,不过确定要在展会上用吗?

9591
heyniu · #7 · 2016年10月08日 作者

#6楼 @codeskyblue 已经用了啊,展会时间10.1-10.3

3092

上千台每台都需要手动开启usb调试吧……
而且每台设备的驱动都要等待电脑安装啊,这时间估计比安装时间还长,怎么克服

9591
heyniu · #9 · 2016年10月09日 作者

#8楼 @sailen usb调试是需要手动打开,驱动安装的是通用驱动,兼容绝大部分机器,不用安装了,插上去手机授权就能装了
还是比wifi下载快很多

3092

#9楼 @heyniu 那不错,不过我们没有这样的场景,嘿嘿

9591
heyniu · #11 · 2016年10月09日 作者

#10楼 @sailen 嘿嘿,没关系学习下挺好的,感觉这种场景试用地推,厂商装机那种,不过厂商的应该是搞成一个包 直接刷机刷进去

2562

感觉主要是做翻译呢

7575

self.adbtools = AdbTools()
这个是引入的第三方库啊?

9591
heyniu · #14 · 2016年10月11日 作者

#13楼 @jphtmt 不是啊,自己写的

5911

好厉害,直接拿走用了,跑不通再研究~😀

96

#3楼 @missgong0 请教下adb install PATH*.apkz这个是怎么用 我的提示can't find path to install

4112

不理解Installer(threading.Thread)和AdbTools(threading.Thread) 为什么同时多线程?

9591
heyniu · #19 · 2016年10月25日 作者

#18楼 @chentest 线程里面包含线程

96

一直卡在Current devices: ['82cee6']不动是怎么回事

9591
heyniu · #21 · 2016年12月06日 作者

#20楼 @yufan2014 就是安装成功了,又没有新的设备接入 就会隔几秒打印一下当前设备

96

#21楼 @heyniu 没有安装成功啊,手机上根本就没被安装上

96

#21楼 @heyniu 是不是安装的手机都要root?

9591
heyniu · #24 · 2016年12月07日 作者

#23楼 @yufan2014 不用root,你要确保apk跟脚本在同一个目录
还有你把图发出来

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册