欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android QA专用,Python实现不一样的多渠道打包工具详情

程序员文章站 2023-03-31 10:36:56
相对于美团打包方案,我为什么要写这个工具? 除了gradle的多渠道打包,目前最流行的应该是美团使用python直接添加渠道文件的打包方式了,速度真是杠杠的!但是,这里有一个问题:需要一个已签名无...

相对于美团打包方案,我为什么要写这个工具?

除了gradle的多渠道打包,目前最流行的应该是美团使用python直接添加渠道文件的打包方式了,速度真是杠杠的!但是,这里有一个问题:需要一个已签名无渠道号的apk,那么问题来了,这个apk哪里来的?懂行的朋友该说了,gradle随便打个release包不完事了嘛。是的,我也是这么想的。但是领导说qa打包的时候需要改一部分配置文件代码(例如app版本、后台环境、版本bulabulabula),这样会带来潜在的问题。能不能直接通过命令行的形式打包,即qa不需要改动任何代码,只需要设置不同的参数就好了。小脑瓜一转,有了。gradle可以。吭哧吭哧半天,做好了。然后qa说:敲命令行多麻烦,还可能出错。能不能直接写个界面,我点点按钮就可以了。so,就有了这个python写的工具。我们暂时叫它qa打包工具。

写在前面

为什么要有这个qa打包工具,这个工具存在的唯一目的就是根据qa的选择,执行命令行打包,实际还是gradle打包。如果和您预期不同或者您已经一眼看穿这小把戏。左拐出门,走好了您馁。如果您对gradle配置参数打包或者对python如何实现感兴趣,请继续~

效果图

Android QA专用,Python实现不一样的多渠道打包工具详情

first blood 多渠道配置

第一滴血总让人满怀期待,完后只剩空虚寂寞冷。千篇一律的东西。这里以百度和豌豆荚为例,直接贴代码。

android {
    productflavors {
        wandoujia {}
        baidu {}
        productflavors.all { flavor ->
            flavor.manifestplaceholders = [umeng_channel_value: name]
        }

    }
    // 重命名生成的apk文件
    applicationvariants.all { variant ->
        variant.outputs.each { output ->
            def outputfile = output.outputfile
            if (outputfile != null && outputfile.name.endswith('.apk') && variant.productflavors.size() > 0) {
                file outputdirectory = new file(outputfile.parent);
                def filename = "(" + variant.productflavors[0].name + ")test_${defaultconfig.versionname}_${releasetime()}.apk"
                output.outputfile = new file(outputdirectory, filename)
            }
        }
    }
}

def releasetime() {
    return new date().format("yyyymmddhhmmss", timezone.gettimezone("utc"))
}

gradle设置参数后的打包命令

打所有渠道包

gradlew assemblerelease -pserver_type=1 -pis_debug=false -pminifyenabled=false

打指定渠道包(以百度为例)

gradlew assemblebaidurelease -pserver_type=1 -pis_debug=false -pminifyenabled=false

gradlew asseblexxxx是默认的打包方式,-p后面是我们自己配置的各种参数。

gradle参数配置

首先想一个问题,gradle命令中的参数怎么就莫名其妙变成了我们程序可访问的参数。一个是gradle一个是android,真正实现了两不沾。也很简单,用文件连接。

gradle在build时,会在app/build/generated/source/buildconfig/渠道/release/包名/下生成buildconfig.java文件,就是这个文件实现了两者的通信。

gradle设置及获取参数(划重点)

代码是最好的语言表述者。

android {
    defaultconfig {
        applicationid "com.yikousamo.test"
        versioncode 1
        versionname "1.0.0"
        minsdkversion 14
        targetsdkversion 21
        // 服务器类型
        buildconfigfield 'int', 'server_type', '1'
        // 是否开启调试,默认开启
        buildconfigfield 'boolean', 'is_debug', 'true'
        // 是否开启混淆,默认不混淆
        buildconfigfield 'boolean', 'minifyenabled', 'false'
    }

   buildtypes {
        if (project.hasproperty('server_type')
                && project.hasproperty('is_debug')
                && project.hasproperty('minifyenabled')){

            release {
                buildconfigfield 'int', 'server_type', server_type
                buildconfigfield 'boolean', 'is_debug', is_debug
                buildconfigfield 'boolean', 'minifyenabled', minifyenabled
                minifyenabled boolean.parseboolean(minifyenabled)
                zipalignenabled false
                shrinkresources false
                signingconfig signingconfigs.gliv
                proguardfiles getdefaultproguardfile('proguard-android.txt'), 'proguard-rules.pro'
            }
        }
        debug {
            minifyenabled false
            zipalignenabled false
            shrinkresources false
            // 包名增加后缀,不同apk可以在相同设备上安装
            applicationidsuffix ".debug"
            signingconfig signingconfigs.gliv
            proguardfiles getdefaultproguardfile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

这里配置了三个可以参数,分别是server_type,is_debug,minifyenabled,defaultconfig里是对应的默认值。可以依据自己实际需求增加更多的参数。道理一样,不再赘述。

判断有没有指定参数的方法为project.hasproperty('xxx'),下面就是设置到对应的参数中。这里有个小技巧,debug设置applicationidsuffix相当于改包名,这样在同一部手机上可以同时存在debug和release包。

设置完参数之后,在build时,在buildconfig中会生成对应的代码。

package com.yikousamo.test;

public final class buildconfig {
  public static final boolean debug = false;
  public static final string application_id = "com.yikousamo.test";
  public static final string build_type = "release";
  public static final string flavor = "baidu";
  public static final int version_code = 1;
  public static final string version_name = "1.0.0";
  // fields from build type: release
  public static final int db_version = 1;
  public static final boolean is_debug = true;
  public static final boolean minifyenabled = false;
  public static final int server_type = 1;
  // fields from default config.
}

注意下这里的包名是app的包名。这也就意味着我们可以在代码中引用这个类。例如,在baseapplication中设置引用public static boolean isdebug = buildconfig.is_debug。其余依据业务需要,同理。到这里就已经完全完成了多渠道打包各种参数的配置。接下来是python实现命令行打包。

python打包工具实现思路

前文说过,python(3.5.0版本)在这里唯一的作用是用界面替代qa输入命令行。

python执行命令行的方式有三种:

os.system("cmd") subprocess.popen commands.getstatusoutput

作为python新手,三种方式的优劣我就不妄加评价了。凑合着用,反正在我眼里都是垃圾。这里采用第二种产生子进程的方式执行cmd命令。界面实现采用的tkinter。代码很简单,也没有多少行。直接放大了。

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

# 一口仨馍
import subprocess
from tkinter import *
from tkinter import messagebox
from tkinter.ttk import combobox
from tkinter.filedialog import askdirectory
import os

root = tk()
root.title("android渠道包")
root.geometry('500x340')  # 是x 不是*
rootpath = stringvar()  # 项目根目录
frm = frame(root)
#第一个字母直接大写,省去uppercase
channels = ['baidu', 'wandoujia']
un_special_channel = '所有渠道包'


def get_frame():
    frame = frame(frm, pady=3)
    frame.pack()
    return frame


def get_entry(frm):
    entry = entry(frm, width=12)
    entry.pack()
    return entry


def get_combobox(frm, value):
    combobox = combobox(frm, width=9)
    combobox["state"] = "readonly"  # 只读
    combobox['values'] = value  # 设置下拉列表的值
    combobox.current(0)  # 设置下拉列表默认显示的值,0为 numberchosen['values'] 的下标值
    combobox.pack()
    return combobox


def get_label(frm, text):
    label(frm, text=text, font=(17), width=14,anchor ='w', justify='left').pack(side=left)


def select_path():
    path_ = askdirectory()
    rootpath.set(path_)


# 选择根目录
frm_choose_root_dir = get_frame()
rootpathentry = entry(frm_choose_root_dir, textvariable=rootpath, width=18)
rootpathentry.pack(side=left)
button(frm_choose_root_dir, text="项目根目录", width=12, command=select_path).pack()
frm_choose_root_dir.pack(side=top)

# servertype
frm_server_type = get_frame()
get_label(frm_server_type, 'servertype:')
servertypecombox = get_combobox(frm_server_type, (0, 1, 2, 3))

# versioncode
frm_version_code = get_frame()
get_label(frm_version_code, 'versioncode:')
versioncodeentry = get_entry(frm_version_code)

# versionname
frm_version_name = get_frame()
get_label(frm_version_name, 'versionname:')
versionnameentry = get_entry(frm_version_name)

# isdebug
frm_is_debug = get_frame()
get_label(frm_is_debug, 'isdebug:')
isdebugcombobox = get_combobox(frm_is_debug, (true, false))

# dbversion
frm_db_version = get_frame()
get_label(frm_db_version, 'dbversion:')
dbversionentry = get_entry(frm_db_version)

# 混淆
frm_minifyenabled = get_frame()
get_label(frm_minifyenabled, '混淆:')
minifyenabledcombobox = get_combobox(frm_minifyenabled, (true, false))

# 指定渠道
frm_special_release = get_frame()
get_label(frm_special_release, '渠道:')
channels.insert(0, un_special_channel)
specifyreleasecombobox = get_combobox(frm_special_release, tuple(channels))


def click_confirm():
    if(rootpathentry.get().strip() == "" or
       versioncodeentry.get().strip() == "" or
       versionnameentry.get().strip() == "" or
       dbversionentry.get().strip() == ""):
        messagebox.askokcancel('提示', '干哈~不填完咋么打包~')
        return
    do_gradle()


def do_gradle():
    # 切换到项目根目录
    os.chdir(rootpathentry.get())
    # 获取当前工作目录
    print(os.getcwd())
    if specifyreleasecombobox.get() == un_special_channel:
        do_all_release()
    else:
        do_specify_release(specifyreleasecombobox.get())


# 打指定渠道包
def do_specify_release(channel):
    cmd = 'gradlew assemble'+channel+'release' \
          ' -pserver_type=' + servertypecombox.get() + \
          ' -pversion_code=' + versioncodeentry.get() + \
          ' -pversion_name=' + versionnameentry.get() + \
          ' -pdb_version=' + dbversionentry.get() + \
          ' -pis_debug=' + isdebugcombobox.get().lower() + \
          ' -pminifyenabled=' + minifyenabledcombobox.get().lower()
    subprocess.popen(cmd, shell=true, stdout=subprocess.pipe)


# 打所有的渠道包
def do_all_release():
    cmd = 'gradlew assemblerelease' \
          ' -pserver_type=' + servertypecombox.get() + \
          ' -pversion_code=' + versioncodeentry.get() + \
          ' -pversion_name=' + versionnameentry.get() + \
          ' -pdb_version=' + dbversionentry.get() + \
          ' -pis_debug=' + isdebugcombobox.get().lower() + \
          ' -pminifyenabled=' + minifyenabledcombobox.get().lower()
    subprocess.popen(cmd, shell=true, stdout=subprocess.pipe)

button(root, text='确定', width=12, command=lambda: click_confirm()).pack(side=bottom)
frm.pack()
root.mainloop()

多渠道验证

假设现在已经打了一个豌豆荚的包。那么怎么验证是否真的改变了anroidmanifest.xml中umeng_channel对应的值呢?也许你需要apktool。

apktool可以反编译得到程序的源代码、图片、xml配置、语言资源等文件。这里我们只关心anroidmanifest.xml。

反编译步骤:

下载apktool 将需要反编译的apk文件放到该目录下,打开命令行界面 ,定位到apktool文件夹 输入命令:java -jar apktool_2.2.1.jar decode test.apk 之后发现在文件夹下多了个test文件夹,查看anroidmanifest.xml即可