之前App在提交測試和最終部署的過程中App打包一直是由開發人員來完成的,由於項目比較大, 再加上Android打包本身就比較慢,所以每次打包還是很耗時的。並且按照嚴格的研發流程來講,開發人員應該只負責提交代碼,測試和部署過程中的打包都不應該由開發人員來完成,所以我就想着給測試和運維人員搭建一個可以自動打包的環境。后來在網上看到很多網友分享使用Jenkins進行Android自動打包的文章,幾經嘗試終於把環境搭建起來了。
Jenkins安裝
Jenkins作為一個開源的持續集成工具,不僅可以用來進行Android打包,也可以用來進行iOS打包、NodeJs打包、Jave服務打包等。官方地址為:jenkins.io/。Jenkins是使用Java開發的,官方提供一個war包,並且自帶servlet容器,可以獨立運行也可以放在Tomcat中運行。我們這里使用獨立運行的方式。運行命令為:
java -jar jenkins.war
運行成功,打開瀏覽器訪問http://locahost:8080
,首次運行會要求輸入管理員密碼,Jenkins在首次運行時生成的,會在控制台打印出來或者按照頁面提示的文件路徑查看管理員密碼。控制台輸出的密碼:
*************************************************************
*************************************************************
*************************************************************
Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:
b7004e63acb940368e62a5dacaa2b246
This may also be found at: /Users/dmx/.jenkins/secrets/initialAdminPassword
第一次運行的頁面
輸入密碼之后點擊continue選擇要安裝的插件
由於Jenkins的插件之間存在依賴關系,並且Jenkins不會幫我們自動安裝依賴的插件,所以插件安裝過程比較容易出錯,所以我們建議自己選擇要安裝的插件,不選擇Jenkins建議安裝的插件。點擊Select plugins to install
進入下一個頁面
首先把默認選中的插件都取消掉,然后選擇我們要安裝的插件,對於Android打包來講一般需要的插件有
- Git plugin
- Gradle Plugin
- Email Extension Plugin
- description setter plugin
- build-name-setter
- user build vars plugin
- Post-Build Script Plug-in
- Branch API Plugin
- SSH plugin
- Scriptler
- Git Parameter Plug-In
- Gitlab plugin
如果插件安裝過程中由於依賴關系造成安裝失敗,可以根據錯誤信息先安裝依賴的插件再重新安裝需要的插件。
插件安裝完成之后按照提示創建一個管理員賬號即可使用,登錄之后進行首頁面。
配置環境變量
需要配置的環境變量有Android Home、JDK目錄、Gradle目錄。首先點擊系統管理=>系統設置,選中Environment variables
,然后新增Android Home環境變量
然后在系統管理=>Global Tool Configuration中配置JDK目錄和Gradle目錄
JDK和Gradle建議提前下載好放到服務器上,不要使用自動安裝,Jenkins自動下載安裝非常慢
配置打包腳本
Jenkins配置完成之后需要我們來完善我們的gradle腳本讓它能夠滿足我們的打包要求,既能支持在Jenkins中打包,也能支持我們使用Android Studio進行打包。首先我們需要一個變量IS_JENKINS
用來標識當前是在Jenkins中打包還是在Android Studio中打包,在不同環境下打包時證書的路徑和APK生成的路徑不同,我們定義一個函數來獲取證書路徑,然后在gradle中指定打包時使用的證書
def getMyStoreFile(){
if("true".equals(IS_JENKINS)){
return file("使用Jenkins打包時的證書路徑")
}else{
return file("使用Android Studio打包時證書路徑")
}
}
android{
signingConfigs {
release {
keyAlias '*****'
keyPassword '****'
storeFile getMyStoreFile()
storePassword '****'
}
}
buildTypes{
debug{
....
signingConfig signingConfigs.release
}
release{
....
signingConfig signingConfigs.release
}
}
....
}
然后配置不同打包環境下apk的生成路徑
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
//新名字
def newName
//輸出文件夾
def outDirectory
//是否為Jenkins打包,輸出路徑不同
if ("true".equals(IS_JENKINS)) {
//BUILD_PATH為服務器輸出路徑
outDirectory = BUILD_PATH
newName = "你的應用名稱" + "-" + defaultConfig.versionName + "-" + BUILD_TYPE + ".apk"
} else {
outDirectory = output.outputFile.getParent()
newName = "你的應用名稱" + "-" + defaultConfig.versionName + "-" + BUILD_TYPE + ".apk"
}
output.outputFile = new File(outDirectory, newName)
}
}
最終完成的gradle腳本為
apply plugin: 'com.android.application'
repositories {
flatDir {
dirs 'libs'
}
}
dependencies {
....
}
def getMyStoreFile(){
if("true".equals(IS_JENKINS)){
return file("使用Jenkins打包時的證書路徑")
}else{
return file("使用Android Studio打包時證書路徑")
}
}
android {
signingConfigs {
release {
keyAlias '*****'
keyPassword '****'
storeFile getMyStoreFile()
storePassword '****'
}
}
compileSdkVersion Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION)
buildToolsVersion project.ANDROID_BUILD_TOOLS_VERSION
dexOptions {
jumboMode true
}
defaultConfig {
applicationId project.APPLICATION_ID
minSdkVersion Integer.parseInt(project.ANDROID_BUILD_MIN_SDK_VERSION)
targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_TARGET_SDK_VERSION)
versionName project.APP_VERSION
versionCode Integer.parseInt(project.VERSION_CODE)
ndk {
abiFilters "armeabi", "armeabi-v7a", "arm64-v8a", "mips", "mips64", "x86", "x86_64"
}
// Enabling multidex support.
multiDexEnabled true
}
buildTypes {
debug {
minifyEnabled false
shrinkResources false
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
// 移除無用的resource文件
shrinkResources true
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
//新名字
def newName
//輸出文件夾
def outDirectory
//是否為Jenkins打包,輸出路徑不同
if ("true".equals(IS_JENKINS)) {
//BUILD_PATH為服務器輸出路徑
outDirectory = BUILD_PATH
newName = "你的app名字" + "-" + defaultConfig.versionName + "-" + BUILD_TYPE + ".apk"
} else {
outDirectory = output.outputFile.getParent()
newName = "你的app名字" + "-" + defaultConfig.versionName + "-" + BUILD_TYPE + ".apk"
}
output.outputFile = new File(outDirectory, newName)
}
}
flavorDimensions("channel")
productFlavors {
yingyongbao { dimension "channel" }
}
productFlavors.all {
flavor -> flavor.manifestPlaceholders = [CHANNEL_VALUE: name]
}
packagingOptions {
exclude 'META-INF/DEPENDENCIES.txt'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
exclude 'META-INF/dependencies.txt'
exclude 'META-INF/LGPL2.1'
}
}
gradle腳本中使用了在gradle.properties中定義的變量,gradle.properties內容如下
org.gradle.daemon=true
org.gradle.parallel=true
manifestmerger.enabled=true
android.useDeprecatedNdk=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4096m -XX\:MaxPermSize\=4096m -XX\:+HeapDumpOnOutOfMemoryError -Dfile.encoding\=UTF-8
ANDROID_BUILD_MIN_SDK_VERSION=14
ANDROID_BUILD_TOOLS_VERSION=25.0.1
ANDROID_BUILD_TARGET_SDK_VERSION=22
ANDROID_BUILD_SDK_VERSION=24
VERSION_CODE=176
APPLICATION_ID=你的applicationId
#jenkins中用到的變量
NODEJS_ADDRESS=app要訪問的服務器地址
API_VERSION=api版本號
APP_VERSION=app版本號
IS_JENKINS=false
BUILD_PATH=apk輸出路徑
BUILD_TYPE=Debug
創建Job
經過上面對gradle的配置我們已經做好了准備工作,現在需要在Jenkins上新建一個任務來完成對上面腳本的調用。
在Jenkins中點擊新建,輸入Job名字,由於Jenkins會根據Job名字生成目錄所以建議使用英文不要使用中文,然后選擇構建一個自由風格的軟件項目,然后點擊OK進入配置頁面
Job配置一共分為六個部分:General、源碼管理、構建觸發器、構建、構建后操作。
General
General中可以配置Job的基本信息,名字、描述等信息,我們需要關注的是關於構建的配置,如果服務器資源比較緊張可以選擇丟棄舊的構建,然后選中參數化構建過程,這樣就能夠在打包的時候輸入一些必要的參數,比如App版本號、打包類型、服務器地址、渠道等信息,這些輸入參數會在構建過程中替換掉gradle.properties中定義的變量。Jenkins中支持的參數類型有Boolean、Choice(下拉選擇形式的)、String、Git(需要安裝插件)。網上其他文章中提到的Dynamic Parameter Plug-in
由於安全性問題已經不再支持。下面看一下我們需要添加參數:
BUILD_TYPE表示構建版本是Release版還是Debug版,這樣可以區分App是正式版本還是內容測試版本。JS_JENKINS表示這是從Jenkins打包的,默認值為true
PRODUCT_FLAVORS表示App的渠道,我們目前只設置了應用寶這個一個渠道,如果渠道包多的話這樣打包效率比較低,需要一個專門進行多渠道打包的工具。APP_VERSION表示APP的版本號,這里添加這個參數是為了能夠讓運維人員在App發布時能夠指定發布的版本號。
GIT_TAG用於在打包時選擇使用倉庫上哪個分支或者TAG,其中Parameter Type可以選擇Tag、Branch、Branch or Tag或者revision,這里我們選擇Branch or Tag
NODEJS_ADDRESS表示服務器地址,這里可以配置上測試環境、生產環境地址,在打包時選擇要哪個后台服務。
REMARK用來描述本次打包的版本,比如這次打包使用來驗證哪個問題等等,要不然單憑版本號很難想起當時打包這個版本是用來干什么的。
源碼管理
我們公司使用Gitlab進行代碼管理,這里選擇git,然后輸入倉庫地址,並在Branch Specifier綁定GIT_TAG變量,這樣GIT_TAG會自動讀取倉庫上的分支和TAG列表。
構建觸發器
構建觸發器用來配置什么時候觸發構建,一般做法有手動觸發、定時觸發、或者提交代碼時觸發。提交代碼觸發需要在gitlab中添加webhook,我們這里使用手動觸發所以這里不做配置
構建環境
通過選中Set Build Name
設置構建名稱,我們這里設置名稱為
#${BUILD_NUMBER}_${BUILD_USER}_${APP_VERSION}_${BUILD_TYPE}
在Jenkins中${}表示引用變量,其中BUILD_NUMBER為構建編號,為Jenkins提供的變量;BUILD_USER為構建人,即當前登錄用戶,需要選中Set jenkins user build variables
;APP_VERSION為App版本號;BUILD_TYPE為構建類型。一個實際的構建名稱為#14_admin_1.2_Release
,表示第14次構建,構建人為admin,構建的App版本為1.2Release版本
構建
選中invoke gradle
通過調用gradle腳本進行構建,選擇在系統管理中配置的gradle的版本,這里為gradle4.0
然后在Tasks輸入打包命令
clean assemble${PRODUCT_FLAVORS}${BUILD_TYPE}
首先執行clean,然后執行assemble進行打包。以PRODUCT_FLAVORS選擇yingyongbao,BUILD_TYPE為Release為例,則實際執行的命令為
clean assembleYingyongbaoRelease
然后選中Pass job parameters as Gradle properties
這樣才能將我們自定義參數在打包時傳遞到gradle腳本中
這樣我們就能成功打包出apk了
實現二維碼下載
為了能夠更方便的使用,我們還應該提供一個二維碼功能,這樣手機掃描之后就能下載安裝。一般做法有兩個:一是選擇將打包出來的apk上傳到第三方平台;另一個是本地搭建一個服務,實現靜態文件服務器的功能。我們這里選擇在本地服務器搭建一個靜態文件服務,同時將文件地址生成一個二維碼展示出來。
在Excute Shell中輸入在構建完成之后執行的腳本,根據apk路徑生成一個二維碼
node /opt/jenkins_node/qr.js http://10.1.170.154:3000/apk/yundiangong-${APP_VERSION}-${BUILD_TYPE}.apk /opt/jenkins_node/apk/yundiangong-${APP_VERSION}-${BUILD_TYPE}.png
即通過node 執行/opt/jenkins_node(需要根據自己實際的目錄設置)下的qr.js文件,同時傳遞兩個參數,第一個參數文件apk文件訪問路徑,我在gradle打包腳本中設置apk輸出路徑為/opt/jenkins_node/apk目錄,通過靜態文件服務的訪問地址http://10.1.170.154:3000/apk/yundiangong-${APP_VERSION}-${BUILD_TYPE}.apk
(10.1.170.154為我們公司內部服務器,需要根據自己情況設置);第二個參數為生成二維碼的保存路徑,同樣為/opt/jenkins_node/apk目錄,這樣靜態文件服務既可以提供apk下載,也可以提供二維碼下載。
然后通過設置build description顯示二維碼功能,通過定義一個html片段,需要在系統管理=>Configure Global Security中將Markup Formatter
選擇為Safe HTML
<br> <a target="_blank" href="http://10.1.170.154:3000/apk/yundiangong-${APP_VERSION}-${BUILD_TYPE}.apk">點擊下載</a><p>${REMARK}</p>
這樣構建成功之后會展示一個二維碼,同時提供一個點擊下載的鏈接,並且還會展示該構建版本的描述信息
我們使用nodeJs實現一個靜態文件服務,通過nodejs啟動一個http服務,然后通過解析請求返回對應的apk文件。代碼如下
const http = require('http')
const path = require('path')
const url = require('url')
const fs = require('fs')
const mime = require('mime')
const port = '3000'
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.end('Hello World')
return
}
if (req.url === '/favicon.ico') return //不響應favicon請求
// 獲取url->patnname 即文件名
let pathname = path.join(__dirname, url.parse(req.url).pathname)
pathname = decodeURIComponent(pathname) // url解碼,防止中文路徑出錯
if (fs.existsSync(pathname)) {
if (!fs.statSync(pathname).isDirectory()) {
// 以binary讀取文件
fs.readFile(pathname, 'binary', (err, data) => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' })
res.end(JSON.stringify(err))
return false
}
res.writeHead(200, {
'Content-Type': `${mime.lookup(pathname)};charset:UTF-8`
})
res.write(data, 'binary')
res.end()
})
} else {
res.statusCode = 404;
res.end('Directory Not Support')
}
} else {
res.statusCode = 404;
res.end('File Not Found')
}
});
server.listen(port);
生成二維碼的小程序也是使用nodejs實現,通過使用qr-image模塊實現生成二維碼功能
const qr=require('qr-image')
const args = process.argv.splice(2);
const filePath=args[0]//源文件地址
const distPath=args[1]//目標文件地址
const img=qr.image(filePath,{size:5})//生成二維碼圖片
img.pipe(require('fs').createWriteStream(distPath));//保存圖片
代碼完整地址為:github.com/dumingxin/j…,首先需要安裝nodejs,然后在代碼目錄執行npm install
,最后執行node web.js
啟動靜態文件服務即可。如果想后台運行可以使用pm2啟動web.js
最后打包成功之后的效果