android 通过命令行自动生成打包单webview应用安装包
之前在一个网站上见到这么一个功能:输入一个网址,这个网站会自动生成一个以该网址为首页的单webview的应用的安装包给你。自己试着捣鼓了一下,实现出来了,在这里分享一下。
基本原理是先安装好android studio的开发环境,打包用到的工具都是用现成的,然后建立一个基本的android单webview应用的项目,就是那种一打开就显示一个webview的简单项目,将这个项目作为模板项目。然后在这个项目的build.gradle中定义一个task,这个task能够接收命令行传入的参数如网址、app名称等,然后根据这些参数修改模板项目,最后自动生成证书证书,并打包生成安装包。
实现并不难,涉及了gradle和groove的知识,下面介绍一下。
项目源码 https://github.com/eeeyuerrrr/AutoGenApk ,可结合源码了解。
一、 模板项目
这个模板项目是个简单的单WebView应用,打开后显示启动图片n秒,然后就是一个Webview。项目中的app名字、图标、启动图、web网址等value写在res/value文件中,比如自己创建的一个config_args.xml。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">mAppName</string>
<string name="web_home_url">http://www.sina.com</string>
<drawable name="app_launcher">@mipmap/m_app_launcher</drawable>
<drawable name="splash_img">@mipmap/m_splash_img</drawable>
</resources>
为了自动生成证书,在app module的 build.gradle中跟证书有关的参数写成从keystore_info.xml文件中读取,这个keystore_info .xml文件 是自己创建的,里面保存了跟证书有关的信息。
build.gralde
//从keystore_info.xml读取签名文件的信息
def keystoreFile = rootProject.file("keystore_info.xml")
def signInfo = new XmlParser().parse(keystoreFile)
android {
...
// 签名
signingConfigs {
release {
// 证书文件
storeFile file( signInfo.storeFile.text() )
// 证书库的密码
storePassword signInfo.storePassword.text()
// 证书密码
keyPassword signInfo.keyPassword.text()
// 别名
keyAlias signInfo.keyAlias.text()
}
}
...
}
keystore_info .xml
<?xml version="1.0" encoding="utf-8"?>
<signInfo>
<storeFile>F:/keystore/demo.keystore</storeFile>
<storePassword>123456</storePassword>
<keyPassword>123456</keyPassword>
<keyAlias>mKeyStore</keyAlias>
</signInfo>
二、 在gradle中定义一个task
android中项目的编译打包基于项目模型,gradle提供了构建项目的框架,项目构建的具体内容由插件来完成。gradle是一种领域相关语言(Domain Specific Language),相当于某个行业的“行话”,所以有很多特定的术语,同时基于groovy,所以需要了解groovy的语法。
gradle中的两个主要对象是project和task,project即android项目中的一个module,为task提供执行的上下文。一个task表示一个逻辑上较为独立的执行过程,比如编译android源码,打包等。
编译过程分为三个阶段:
1. 初始化阶段:创建 Project 对象,如果有多个build.gradle,也会创建多个project.
2. 配置阶段:在这个阶段,会执行所有的编译脚本,同时还会创建project的所有的task,为后一个阶段做准备。
3. 执行阶段:在这个阶段,gradle 会根据传入的参数决定如何执行这些task,真正action的执行代码就在这里
task在module的build.gradle中定义,如定义一个简单的task
task helloWorld << {
println "Hello World!"
}
<< 表示执行阶段
由于我们要编译出的apk是app module中的,所以自定义的task写在app module的build.gradle中
三、 utils.gradle
为了方便开发,将一些常用的“方法”放在了项目根目录下的utils.gradle中。
utils.gradle中的“方法”其实是闭包,通过在闭包名前加ext使其成为成员变量供其他代码使用。
如,在utils.gradle中定义myHello的闭包:
ext.myHello = { a ->
println "hello $a"
}
a表示传入的参数,$a表示参数的值
在app module的build.gradle中引入
apply from: "../utils.gradle"
然后即可像方法一样使用:
myHello("world")
则会打印出hello world
utils.gradle中封装了一些常用功能,如:执行命令行 等等
/*
* 执行命令行
*/
ext.exeCmd = { cmd ->
Process p = cmd.execute()
p.consumeProcessOutput( System.out ,System.err )
p.waitFor()
// println "${p.text}"
return !p.exitValue()
}
/*
* 将打包前要改变的参数放到resource value文件中
*/
ext.writeArgToResourceValue = { appName, webHomeUrl, appLauncher, splashImg ->
String xml =
"""<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">$appName</string>
<string name="web_home_url">$webHomeUrl</string>
<drawable name="app_launcher">$appLauncher</drawable>
<drawable name="splash_img">$splashImg</drawable>
</resources>
"""
File file = new File("app/src/main/res/values/config_args.xml")
if (!file.exists()) { file.createNewFile() }
file.write(xml)
}
/*
* 将证书相关的信息写入文件keystore_info.xml
* */
ext.writeSignInfo = { storeFile, storePassword, keyPassword, keyAlias ->
String xml =
"""<?xml version="1.0" encoding="utf-8"?>
<signInfo>
<storeFile>$storeFile</storeFile>
<storePassword>$storePassword</storePassword>
<keyPassword>$keyPassword</keyPassword>
<keyAlias>$keyAlias</keyAlias>
</signInfo>
"""
File file = new File("keystore_info.xml")
if (!file.exists()) { file.createNewFile() }
file.write(xml)
}
/**
* 复制文件
* */
ext.copyFile = { File inputFile, File outputFile, boolean toOverwrite = false ->
if (!inputFile.exists()) {
throw new FileNotFoundException("Template file does not exist: ${inputFile.getAbsolutePath()}")
}
outputFile.getParentFile().mkdirs()
if (toOverwrite || !outputFile.exists()) {
outputFile.bytes = inputFile.readBytes()
}
}
/**
* 删除文件或文件夹
* */
ext.removeFile = { File f ->
if (!f.exists()){
throw new FileNotFoundException("file does not exist: ${f.getAbsolutePath()}")
}
if (f.isDirectory()) { f.listFiles().each { removeFile(it) } }
f.delete()
}
四、 从命令行执行task并传入参数的方法
命令行的环境目录是在模板项目下,可在android studio的命令行窗口用gradle来执行,或者在windows系统里定位到该目录用命令窗口调用gradle.bat来执行。
在app module的build.gradle中定义一个task,将传入的参数显示出来,只需要这样子写:
task show << {
println a
}
命令行输入为
gradlew -P a=hello show app:show
app表示app module,show表示要执行的task-P表示后面为一个参数,task会自动根据参数名来匹配参数,如会将命令行中的a=hello传入给task中的a
若要传入多个参数,则每个参数前都要加-P,如
gradlew -P a=hello -P b=world show app:show
五、 在app module的build.gradle中自定义一个task来根据参数打包apk
task inputArgsToPackage << {
println "input args:"
println "appName=$appName"
println "webHomeUrl=$webHomeUrl"
println "appLauncherImgPath=$appLauncherImgPath"
println "splashImgPath=$splashImgPath"
println "storeFile=$storeFile"
println "storePassword=$storePassword"
println "keyPassword=$keyPassword"
println "keyAlias=$keyAlias"
// 复制app图标到mipmap文件夹
def appLauncherFileName = "m_app_launcher"
println "copy app launcher file..."
def sourceFile = new File(appLauncherImgPath)
def desFile = new File("app/src/main/res/mipmap/${appLauncherFileName}.png")
copyFile(sourceFile, desFile, true)
println "copy succeed."
// 复制启动图片到mipmap文件夹
def splashImgFileName = "m_splash_img"
println "copy splash img..."
copyFile(new File(splashImgPath), new File("app/src/main/res/mipmap/${splashImgFileName}.png"), true)
println "copy succeed."
// 将参数appName,webHomeUrl,appLauncher,splashImg写入resource value文件
println "write args to resource value..."
writeArgToResourceValue(appName, webHomeUrl, "@mipmap/${appLauncherFileName}", "@mipmap/${splashImgFileName}")
println "write succeed."
// 将签名相关的参数写入keystore.xml
println "write sign info to keystore.xml"
writeSignInfo(storeFile, storePassword, keyPassword, keyAlias)
println "write succeed."
// 清除上一次打包的结果
// println "cleaning the last package"
// exeCmd("gradlew.bat clean")
// println "clean done"
// 执行打包命令
println "packaging..."
// String result = exeCmd("gradlew.bat packageDebug")
def isExeSucceed = exeCmd("gradlew.bat assembleRelease")
if( isExeSucceed ){
println "package succeed !"
}else{
println "package fail !"
}
}
命令行执行这个task
gradlew -q -P appName=mAppName -P webHomeUrl=http://www.sina.com -P appLauncherImgPath=F:/img/mAppLauncher.png -P splashImgPath=F:/img/mSplashImg.png -P storeFile=F:/keystore/demo.keystore -P storePassword=123456 -P keyPassword=123456 -P keyAlias=mKeyStore app:inputArgsToPackage
脚本执行完毕后即自动生成了apk,注意图片的路径要修改成自己本地电脑上的一张图片的路径
六、查看签名信息的方法
查看签名信息
jarsigner -verbose -verify -certs app-release.apk
如下则表示有证书
七、自动生成证书介绍
命令行自动生成证书的命令行:
keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000 -dname "cn=Mark Jones, ou=JavaSoft, o=Sun, c=US" -storepass 123456 -keypass 123456
参数说明:
-keystore 证书文件名
-alias 别名
-keyalg 加密算法
-keysize **长度
-validity 证书有效期
-dname 用户信息(cn名字与姓氏,ou组织单位名称,o组织名称,c国家地区代码)
-storepass 证书库密码
-keypass 证书密码
生成证书的task
//gradlew -q -P keystore=mKeystore.keystore -P alias=minstone -P username=minstone -P company=minstone -P organization=minstone -P countrycode=cn -P storepass=123456 -P keypass=123456 app:genKeystore
task genKeystore << {
String cmd = "keytool -genkey -v -keystore $keystore -alias $alias -keyalg RSA -keysize 2048 -validity 10000 -dname \"cn=$username, ou=$company, o=$organization, c=$countrycode\" -storepass $storepass -keypass $keypass"
def isExeSucceed = exeCmd(cmd)
if (isExeSucceed) {
println "package succeed !"
} else {
println "package fail !"
}
}
在执行自动打包的task时,先执行生成证书的task,再把自动打包过程的证书的信息改成生成的证书的信息,即可自动生成证书并打包。
八、项目源码
上一篇: 关于无限分类的10篇文章推荐