Android 持续集成实践(一):打包流程优化

打包流程

具体实现

Gradle配置文件归类

分类需要同步和不需要同步的配置文件,解决多分支模型下切换分支更改版本号等不需要同步的操作,gralde仍提示同步的问题,让真正需要同步的变更才提示。

  • 新建version.gradle文件,添加变更时不需要同步的参数;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    ext {
    // 打生产包命令:./gradlew assembleReleaseChannels
    // 打patch包命令:./gradlew buildTinkerPatchRelease
    // 当前编译状态,生产环境必须配置为false
    isDebug=true
    // 标识是否为补丁模式
    isPatchModel=false
    // 是否上传符号表
    uploadMappingEnable=false
    // 补丁模式,补丁文件对应的基准apk文件夹
    patchBaseApkDir=""
    version_name="1.1.0.100"
    // 规则,年+月+日+当日发版次数
    version_code=17041501
    }
  • 将version.gradle添加到项目根目录build.gradle顶部;

    1
    apply from: 'version.gradle'
  • 将变更时必须同步的参数统一添加到gradle.properties;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    # =====================================以下为第三方账号配置=====================================
    // debug
    DEBUG_BUGLY_APPID="xxx"
    DEBUG_TALKINGDATA_APPID="xxx"
    DEBUG_MEIQIA_APPID="xxx"
    // release
    RELEASE_BUGLY_APPID="xxx"
    RELEASE_TALKINGDATA_APPID="xxx"
    RELEASE_MEIQIA_APPID="xxx"
    # =====================================以下为多dex配置=====================================
    MULTIDEX_KEEP_PROGUARD_FILE=multiDexKeep.pro
    MULTIDEX_ENABLED=true
    # =====================================以下为签名配置=====================================
    SIGNING_STORE_FILE=xxx.keystore.jks
    SIGNING_STORE_PASSWORD=xxx
    SIGNING_KEY_ALIAS=xxx
    SIGNING_KEY_PASSWORD=xxx
    # =====================================以下为应用配置=====================================
    APPLICATION_ID=xxx
    # Sdk and tools
    MIN_SDK_VERSION=16
    TARGET_SDK_VERSION=22
    COMPILE_SDK_VERSION=23
    BUILD_TOOLS_VERSION=25.0.0
    MULTIDEX_VERSION=1.0.0
    # =====================================以下为依赖配置=====================================
    # App dependencies
    # https://developer.android.com/topic/libraries/support-library/features.html
    supportLibraryVersion=25.0.1
    guavaVersion=18.0
    junitVersion=4.12
    mockitoVersion=1.10.19
    powerMockito=1.6.2
    hamcrestVersion=1.3
    runnerVersion=0.4.1
    rulesVersion=0.4.1

检查multidex配置

Application启动时调用的类必须包含在主dex中,为此我们需要配置哪些类在主dex中。随着版本迭代或代码重构,可能导致配置的类包名发生改变或有新的类在Application中调用,为防止错误或遗漏,可在编译期引入自动检查机制,避免出错。

插件实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
public class CheckMultiDexKeepPlugin implements Plugin<Project> {
void apply(Project project) {
System.out.println("===================================================================");
System.out.println("开始执行检查multiDexKeep.pro合法性插件,project.name:" + project.name);
System.out.println("===================================================================");
final File multiDexKeepFile = new File(project.name, "multiDexKeep.pro");
if (!multiDexKeepFile.exists()){
throw new RuntimeException("请正确配置主dex keep文件");
}
// 生成appContext源文件路径
final String appContextClassPath = "com.zaozuo.android.app.AppContext.java";
final File appContextFile = javaClassPath2FilePath(project, appContextClassPath);
System.out.println("AppContext File Path====================>" + appContextFile.getAbsolutePath());
if (!appContextFile.exists()){
throw new RuntimeException("请正确配置AppContext Class路径");
}
// 读取appContext源文件中所有import非系统的行
System.out.println("read appContextLineList start====================>");
final String firstLine = "-keep class " + appContextClassPath.replace(".java", "");
System.out.println(firstLine);
List<String> appContextLineList = readImportNoSysLines(appContextFile);
appContextLineList.add(firstLine);
System.out.println("read appContextLineList end====================>");
// 读取multiDexKeep文件中所有的行
System.out.println("read multiDexKeepLineList start====================>");
List<String> multiDexKeepLineList = readLines(multiDexKeepFile);
System.out.println("read multiDexKeepLineList end====================>")
final boolean multiDexKeepConfigSuccess = multiDexKeepLineList.containsAll(appContextLineList);
if (multiDexKeepConfigSuccess){
System.out.println("multiDexKeep配置正确,Good Lock!====================>")
}else{
throw new RuntimeException("multiDexKeep配置不正确,请仔细检查后重试");
}
}
/**
* 生成java源文件对应的文件路径
* @param project
* @param javaClassPath
* @return
*/
private File javaClassPath2FilePath(Project project, String javaClassPath){
javaClassPath = javaClassPath.replace(".java", "");
final String[] tempArr = javaClassPath.split("\\.");
final StringBuilder builder = new StringBuilder();
for (String temp : tempArr){
builder.append("/").append(temp);
}
builder.append(".java")
final String appContextPath = builder.toString();
builder.setLength(0);
final File file = new File(project.name + "/src/main/java/", appContextPath);
return file;
}
private final static List<String> sysClassNameList = new ArrayList<>();
static {
sysClassNameList.add("import android.");
sysClassNameList.add("import com.android.");
sysClassNameList.add("import java.");
sysClassNameList.add("import dalvik.");
}
/**
* 读取appContext源文件中所有import非系统的行
* @param file
* @return
*/
private List<String> readImportNoSysLines(File file){
List<String> lineList = new ArrayList<>();
FileReader reader = new FileReader(file);
BufferedReader br = new BufferedReader(reader);
String str = null;
while((str = br.readLine()) != null) {
if (str != null){
if (str.startsWith(" ")){
str = str.trim();
}
if (str.startsWith("import")){
boolean isSysClass = false;
for (String sysClassName : sysClassNameList){
if (str.startsWith(sysClassName)){
isSysClass = true;
break;
}
}
if (!isSysClass){
str = str.replace("import ", "-keep class ");
str = str.replace(";", "");
lineList.add(str);
System.out.println(str);
}
}else if (str.startsWith("public class")){
break;
}
}
}
br.close();
reader.close();
return lineList;
}
private List<String> readLines(File file){
List<String> lineList = new ArrayList<>();
FileReader reader = new FileReader(file);
BufferedReader br = new BufferedReader(reader);
String str = null;
while((str = br.readLine()) != null) {
if (str != null){
lineList.add(str);
}
System.out.println(str);
}
br.close();
reader.close();
return lineList;
}
}

插件使用

1
2
// 在application类型模块下build.gradle中添加以下配置;
apply plugin: com.zaozuo.plugins.CheckMultiDexKeepPlugin

多渠道打包

使用walle作为多渠道打包方案,比AndroidStudio提供的productFlavors方案性能更高,使用更方便,同时提供命令行工具方便临时增加渠道包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
this.printlnLog("walle多渠道打包插件")
apply plugin: 'walle'
walle {
// 指定渠道包的输出路径
apkOutputFolder = new File("${project.bakPath}", "${project.tinkerIdValue}");
if (!apkOutputFolder.exists()){
apkOutputFolder.mkdirs()
}
println("channeApkOutputFolder==========>" + apkOutputFolder.getAbsolutePath())
// 定制渠道包的APK的文件名称
apkFileNameFormat = '${appName}-${channel}-${buildType}-v${versionName}-${versionCode}.apk';
// 渠道配置文件
channelFile = new File("${project.getProjectDir()}/channel")
println("channelFile==========>" + channelFile.getAbsolutePath())
}

调整打包输出目录

项目中使用了bugly热修复和walle多渠道打包,打包输出的文件散落在各处,每次都需要手动归类备份;同时打正式包和Patch包需要备份的文件也不一样,比较繁琐容易出错。

插件实现

  • 对外提供的配置类AdjustAssembleOutputConfig

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class AdjustAssembleOutputConfig {
    String outputBakPath = null
    String outputBakDirName = null
    boolean isPatchModelValue
    @Override
    public String toString() {
    return "AdjustAssembleOutputConfig{" +
    "outputBakPath='" + outputBakPath + '\'' +
    ", outputBakDirName='" + outputBakDirName + '\'' +
    ", isPatchModelValue=" + isPatchModelValue +
    '}';
    }
    }
  • 插件实现类AdjustAssembleOutputPlugin,分别针对打正式包和Patch包不同备份需求单独处理;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    public class AdjustAssembleOutputPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
    println("===================================================================");
    println("开始执行AdjustAssembleOutputPlugin,project.name:" + project.name);
    println("===================================================================");
    // 创建需要外部传入的配置文件
    project.extensions.create("adjustAssembleOutputConfig", AdjustAssembleOutputConfig)
    // 获取最后编译完成的variant
    def targetVariant = null
    project.android.applicationVariants.all { variant ->
    def taskName = variant.name
    project.tasks.all {
    if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {
    it.doLast {
    targetVariant = variant
    println("${it.name} doLast=========================>")
    }
    }
    }
    }
    // build完成监听
    project.gradle.addListener(new BuildListener(){
    @Override
    void buildStarted(Gradle gradle) {
    }
    @Override
    void settingsEvaluated(Settings settings) {
    }
    @Override
    void projectsLoaded(Gradle gradle) {
    }
    @Override
    void projectsEvaluated(Gradle gradle) {
    println("projectsEvaluated==========>")
    }
    @Override
    void buildFinished(BuildResult result) {
    def buildSuccess = result.getFailure() == null
    println("buildFinished==========>" + buildSuccess)
    if (!buildSuccess.toBoolean()){
    return
    }
    AdjustAssembleOutputConfig config = project['adjustAssembleOutputConfig']
    def outputBakPath = config.outputBakPath
    def outputBakDirName = config.outputBakDirName
    def isPatchModelValue = config.isPatchModelValue
    println("isPatchModelValue======================="+isPatchModelValue)
    println("outputBakPath======================="+outputBakPath)
    println("outputBakDirName======================="+outputBakDirName)
    if (isPatchModelValue){
    // 打补丁包模式
    adjustOutputForPatch(project, outputBakDirName, outputBakPath)
    deletePatchNewOfficialFile(project, outputBakDirName, outputBakPath)
    }else{
    adjustOutputForOfficial(project, outputBakDirName, outputBakPath, targetVariant.baseName)
    }
    }
    })
    }
    /**
    * 删除打patch包时,生成的新的正式包文件
    * @param project
    * @param outputBakDirName
    * @param outputBakPath
    * @return
    */
    def deletePatchNewOfficialFile(Project project, outputBakDirName, outputBakPath){
    def newOfficialDir = findBuglyOutputFile(project, outputBakDirName, outputBakPath)
    if (newOfficialDir.exists()){
    println("delete newOfficialDir==========>" + newOfficialDir)
    newOfficialDir.deleteDir()
    }else{
    throw new RuntimeException("打patch包时,生成的新的正式包文件夹不存在:" + newOfficialDir)
    }
    }
    /**
    * 为patch包整理文件输出目录
    * @param project
    * @param outputBakDirName
    * @param outputBakPath
    * @return
    */
    def adjustOutputForPatch(Project project, outputBakDirName, outputBakPath){
    def patchFile = project.file("${project.rootDir}/${project.name}/build/outputs/patch/release/patch_signed_7zip.apk")
    if (patchFile.exists()){
    // 目标文件目录
    def toFileDir = new File("${outputBakPath}", outputBakDirName)
    println("toFileDir==========>" + toFileDir)
    // 拷贝补丁文件到备份目录
    project.copy {
    from patchFile
    into toFileDir
    }
    // 压缩outputs文件夹并备份
    def patchOutputFile = project.file("${project.rootDir}/${project.name}/build/outputs")
    println("patchOutputFile==========>" + patchOutputFile)
    if (patchOutputFile.exists()){
    FileUtils.compressedFile(patchOutputFile.getAbsolutePath(), toFileDir.getAbsolutePath())
    }else{
    throw new RuntimeException("补丁outputs文件夹不存在:" + patchOutputFile)
    }
    }else{
    throw new RuntimeException("补丁文件不存在:" + bakPath)
    }
    }
    /**
    * 为正式包整理文件输出目录
    * @param outputBakDirName 备份文件夹名
    * @param outputBakPath 备份目录
    * @param buildType 当前编译模式类型 release/debug
    * @return
    */
    def adjustOutputForOfficial(Project project, outputBakDirName, outputBakPath, buildType){
    // 定义输入文件名
    def fromApkName = outputBakDirName + ".apk"
    def fromRName = outputBakDirName + "-R.txt"
    def fromMappingName = outputBakDirName + "-mapping.txt"
    def fromFileDir = findBuglyOutputFile(project, outputBakDirName, outputBakPath)
    println("fromFileDir==========>" + fromFileDir)
    // 目标文件目录
    def toFileDir = new File("${outputBakPath}", outputBakDirName)
    println("toFileDir==========>" + toFileDir)
    // 定义输出文件名
    def toFileNamePrefix = "${project.name}-${buildType}"
    def toApkName = toFileNamePrefix + ".apk"
    def toApkFile = new File(toFileDir, toApkName)
    def toRName = toFileNamePrefix + "-R.txt"
    def toMappingName = toFileNamePrefix + "-mapping.txt"
    // 拷贝源文件目录下所有资源到目标文件目录,并重命名
    project.copy {
    // 拷贝目录下所有文件
    from fromFileDir
    into toFileDir
    rename { String fileName ->
    fileName.replace(fromApkName, toApkName)
    }
    rename { String fileName ->
    fileName.replace(fromRName, toRName)
    }
    rename { String fileName ->
    fileName.replace(fromMappingName, toMappingName)
    }
    }
    // 删除源文件目录
    if (toApkFile.exists()){
    def deleteSuccess = project.file("${fromFileDir}").deleteDir()
    println("delete fromFileDir==========>" + deleteSuccess)
    }
    }
    /**
    * 查找bugly tinker打包输出的文件夹
    * @param project
    * @param outputBakDirName
    * @param outputBakPath
    * @return
    */
    def findBuglyOutputFile(Project project, outputBakDirName, outputBakPath){
    def fromApkName = outputBakDirName + ".apk"
    // 查找目标apk文件夹
    def tree = project.fileTree(outputBakPath) {
    include "**/${fromApkName}"
    }
    // 校验查找到的文件夹合法性
    if (tree.size() == 0){
    throw new RuntimeException("没有找到APK文件:" + fromApkName)
    }
    if (tree.size() > 1){
    throw new RuntimeException("存在多个APK文件:" + tree.toString())
    }
    // 源文件目录
    def fromFileDir = tree[0].getParentFile()
    return fromFileDir
    }
    }

插件使用

  • 在application类型模块下build.gradle中添加以下配置;

    1
    2
    3
    4
    5
    6
    7
    this.printlnLog("调整打包输出目录插件")
    apply plugin: com.zaozuo.plugins.adjustoutput.AdjustAssembleOutputPlugin
    adjustAssembleOutputConfig{
    outputBakPath "${project.bakPath}"
    outputBakDirName "${project.tinkerIdValue}"
    isPatchModelValue "${project.isPatchModel}".toBoolean()
    }
  • 使用插件后,每次打包后输出的目录及文件;

检查生成的APK

检查渠道配置

come soon!

检查tinker配置

come soon!

0%