分为三步。
- 在本地实现脱离IDE直接脚本打包。
- 分离敏感资源,确定在远程部署这些的方式方法。
- 在Github Actions机器上实现脚本打包并发布。
本地脚本打包
- 实际上IDE在创建工程时已经将基本的脚本什么的写好了
gradlew
- Linuxgradlew.bat
- Windows- 配置完毕以后直接运行脚本
chmod +x gradlew #linux需要先授权, windows不需要 ./gradlew assembleRelease
- 混淆(可选)
- 修改
app/build.gradle
文件buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } }
- 根据需要配置
app/proguard-rules.pro
文件proguard-rules.pro
# Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ############################################# # # 基本指令区域(没什么别的需求不需要动) # ############################################# # 代码混淆压缩比,在0~7之间,默认为5,一般不做修改 -optimizationpasses 5 # 混合时不使用大小写混合,混合后的类名为小写 -dontusemixedcaseclassnames # 指定不去忽略非公共库的类 -dontskipnonpubliclibraryclasses # 这句话能够使我们的项目混淆后产生映射文件 # 包含有类名->混淆后类名的映射关系 -verbose # 指定不去忽略非公共库的类成员 -dontskipnonpubliclibraryclassmembers # 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。 -dontpreverify # 保留Annotation不混淆 -keepattributes *Annotation*,InnerClasses # 避免混淆泛型 -keepattributes Signature # 抛出异常时保留代码行号 -keepattributes SourceFile,LineNumberTable # 指定混淆是采用的算法,后面的参数是一个过滤器 # 这个过滤器是谷歌推荐的算法,一般不做更改 -optimizations !code/simplification/cast,!field/*,!class/merging/* ############################################# # # Android开发中一些需要保留的公共部分(没什么别的需求不需要动) # ############################################# # 保留我们使用的四大组件,自定义的Application等等这些类不被混淆 # 因为这些子类都有可能被外部调用 -keep public class * extends android.app.Activity -keep public class * extends android.app.Appliction -keep public class * extends android.app.Service -keep public class * extends android.content.BroadcastReceiver -keep public class * extends android.content.ContentProvider -keep public class * extends android.app.backup.BackupAgentHelper -keep public class * extends android.preference.Preference -keep public class * extends android.view.View -keep public class com.android.vending.licensing.ILicensingService # 保留support下的所有类及其内部类 -keep class android.support.** {*;} # 保留继承的 -keep public class * extends androidx.** -keep public class * extends android.support.v4.** -keep public class * extends android.support.v7.** -keep public class * extends android.support.annotation.** # 保留R下面的资源 -keep class **.R$* {*;} # 保留本地native方法不被混淆 -keepclasseswithmembernames class * { native ; } # 保留在Activity中的方法参数是view的方法, # 这样以来我们在layout中写的onClick就不会被影响 -keepclassmembers class * extends android.app.Activity{ public void *(android.view.View); } # 保留枚举类不被混淆 -keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); } # 保留我们自定义控件(继承自View)不被混淆 -keep public class * extends android.view.View{ *** get*(); void set*(***); public (android.content.Context); public (android.content.Context, android.util.AttributeSet); public (android.content.Context, android.util.AttributeSet, int); } # 保留Parcelable序列化类不被混淆 -keep class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator *; } # 保留Serializable序列化的类不被混淆 -keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; private static final java.io.ObjectStreamField[] serialPersistentFields; !static !transient ; !private ; !private ; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); } # 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆 -keepclassmembers class * { void *(**On*Event); void *(**On*Listener); } # webView处理,项目中没有使用到webView忽略即可 #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} #-keepclassmembers class * extends android.webkit.webViewClient { # public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap); # public boolean *(android.webkit.WebView, java.lang.String); #} #-keepclassmembers class * extends android.webkit.webViewClient { # public void *(android.webkit.webView, jav.lang.String); #} # 移除Log类打印各个等级日志的代码,打正式包的时候可以做为禁log使用,这里可以作为禁止log打印的功能使用 # 记得proguard-android.txt中一定不要加-dontoptimize才起作用 # 另外的一种实现方案是通过BuildConfig.DEBUG的变量来控制 #-assumenosideeffects class android.util.Log { # public static int v(...); # public static int i(...); # public static int w(...); # public static int d(...); # public static int e(...); # ############################################# # # 项目中特殊处理部分 # ############################################# #-----------处理反射类--------------- #-dontwarn aaa.bbb.ccc.model.** -keep class aaa.bbb.ccc.model.** {*;} #-----------处理js交互--------------- #-----------处理实体类--------------- # 在开发的时候我们可以将所有的实体类放在一个包内,这样我们写一次混淆就行了。 #-keep class com.ghs.ghspm.bean.** { *; } #-----------处理第三方依赖库--------- -keep class io.netty.** { *;} -dontwarn io.netty.** </blockcode> </pre> </details>
- 修改
- 签名
-
修改
app/build.gradle
文件signingConfigs { release{ keyAlias 这里填写keyAlias keyPassword 这里填写keyPassword storeFile file(这里填写xxx.jks的路径) storePassword 这里填写storePassword } } buildTypes { // 这里debug和release使用同样的配置 release { signingConfig signingConfigs.release } debug{ signingConfig signingConfigs.release } }
-
分离敏感资源
- 主要是签名文件和相关密码等,可以将其统一放到额外的配置文件中,它不上传的公共云端。
此处将敏感信息放入了signing.properties
- app/signing.properties
KEYSTORE_FILE = C:\\Users\\xxx\\abc.jks KEYSTORE_PASSWORD = pwd1 KEY_ALIAS = alias_xxxxx KEY_PASSWORD = pwd2
-
app/build.gradle
// 加载signing.properties 读取配置 Properties props = new Properties() props.load(new FileInputStream(file("signing.properties"))) android { compileSdkVersion 29 buildToolsVersion "29.0.3" defaultConfig { applicationId "a.b.c" minSdkVersion 24 targetSdkVersion 29 versionCode 10 versionName "1.2.0r" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } signingConfigs { release{ keyAlias props['KEY_ALIAS'] keyPassword props['KEY_PASSWORD'] storeFile file(props['KEYSTORE_FILE']) storePassword props['KEYSTORE_PASSWORD'] } } buildTypes { release { // ...省略其他配置 signingConfig signingConfigs.release } debug{ signingConfig signingConfigs.release } } } // ...省略其他配置
- app/signing.properties
- 如何在云端部署
signing.properties
和abc.jks
- signing.properties
- 机密信息只有
KEYSTORE_PASSWORD
、KEY_ALIAS
、KEY_PASSWORD
。 - 我们可以将它们放到Github Repo的secrets里面,然后直接echo写进配置文件
- 机密信息只有
- abc.jks
- 我们可以将它上传到自己私密服务器上,通过wget临时下载至Github Actions的机器上。
将可以实现该功能的单行shell命令放到Github Repo的secrets里面,其名为BASH_DOWNLOAD_JKS
。 - 举个例子,我们可以将它上传到我们Github的私有repo里面,并申请一个有接触权限的token。
此时,BASH_DOWNLOAD_JKS
可以是这样子,里面的变量自己去替换:
wget https://raw.githubusercontent.com/{user}/{repo}/{branch}/{path}/abc.jks --header="Authorization: token {token}" -O abc.jks
- 我们可以将它上传到自己私密服务器上,通过wget临时下载至Github Actions的机器上。
-
Github Action 脚本
- name: Generate signing.properties run: | rm -rf xxx.jks ${{ secrets.BASH_DOWNLOAD_JKS }} echo "KEYSTORE_FILE = ${{github.workspace}}/xxx.jks" > app/signing.properties echo "KEYSTORE_PASSWORD = ${{ secrets.KEYSTORE_PASSWORD }}" >> app/signing.properties echo "KEY_ALIAS = ${{ secrets.KEY_ALIAS }}" >> app/signing.properties echo "KEY_PASSWORD = ${{ secrets.KEY_PASSWORD }}" >> app/signing.properties
- signing.properties
Github Actions 打包并发布
- 可以参考一次GitHub Action自动化发布集成部署(Java 篇)
- 最终实现的例子freedom4NG
- Workflow的yaml配置如下,当我修改
release.json
的信息时,会触发构建。
name: android master CI
on:
push:
branches: [ master ]
paths:
# Trigger only when src/** changes
- ".github/release.json"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Read tag_latest
id: tag_latest
uses: ashley-taylor/[email protected]
with:
path: ./.github/release.json
property: tag_latest
- name: Read description
id: description
uses: juliangruber/read-file-action@v1
with:
path: ./.github/release.info
- name: Generate signing.properties
run: |
rm -rf freedom.jks
${{ secrets.BASH_DOWNLOAD_JKS }}
echo "KEYSTORE_FILE = ${{github.workspace}}/freedom.jks" > app/signing.properties
echo "KEYSTORE_PASSWORD = ${{ secrets.KEYSTORE_PASSWORD }}" >> app/signing.properties
echo "KEY_ALIAS = ${{ secrets.KEY_ALIAS }}" >> app/signing.properties
echo "KEY_PASSWORD = ${{ secrets.KEY_PASSWORD }}" >> app/signing.properties
cat app/signing.properties
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew assembleRelease
- name: Create Release
id: create_release
uses: actions/create-release@latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{steps.tag_latest.outputs.value}}
release_name: Freedom - v${{steps.tag_latest.outputs.value}}
body: |
${{steps.description.outputs.content}}
draft: false
prerelease: false
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./app/build/outputs/apk/release/app-release.apk
asset_name: Freedom.${{steps.tag_latest.outputs.value}}.apk
asset_content_type: application/vnd.android.package-archive