0x01 前言
相信各位都用过或听过使用GitHub作为远程代码仓库。但GitHub的功能可不仅仅是管理存放代码,你可以把任何文件放在GitHub上,甚至可以把它当作网盘来使用。所以,作为没有服务器 (没钱) 的学生和懒得 (不会) 自己动手搭后台的我,尝试使用GitHub来实现简单的App版本更新。
0x02 更新流程
一个用户体验比较好的更新通常有两个步骤,一个是询问服务器当前是否有更新,有更新后提醒用户,再由用户选择是否下载更新。
检测更新
检测更新这一步骤,我们就可以在我们的代码仓库中放一个文件,文件的内容是当前的版本信息,比如,可以使用json格式:
{
"code": 2,
"name": "0.2.0",
"filename": "EveryDownload-release-0.2.0.apk",
"url": "https://raw.githubusercontent.com/SirLYC/EveryDownload/master/update/EveryDownload-release-0.2.0.apk",
"time": 1562244720615,
"des": "1. \u6dfb\u52a0\u68c0\u67e5\u66f4\u65b0\u529f\u80fd\n2. \u7f8e\u5316`\u5173\u4e8e\u9875\u9762`",
"size": 3339869,
"md5": "09283d79f77e9a162b8edc6811ecfe42"
}
怎么获取到文件内容?在你的代码仓库查看文件处点击RAW即可查看文件内容。
如图所示,上面的url就是你App可以直接通过HTTP访问的,从数据流读取数据后就可以得到字符串。可以把这个理解为后端提供的API:
https://raw.githubusercontent.com/${GitHub用户名}/${项目名}/${分支名}}/${相对于项目根目录的路径}}
通过这个接口,不仅能访问文本文件,还可以访问你公开仓库的任何文件,然后就可以下载到本地了。
注意:如果是私有仓库,无法用这个API访问,有一个
token
参数,具体可以看GitHub的开发者API文档。
这里的例子中,code就是build时的versionCode,App读取json后与当前versionCode比较即可知道是否需要更新(比较方式不唯一,使用versionCode是比较常见的)
if (info.code > BuildConfig.VERSION_CODE) {
// do update
} else {
// already updated
}
下载更新
更新就相对来讲比较简单了。既然可以把GitHub作为“网盘”,我们一样可以在Git中将打包好的apk文件也commit push上去,然后找到对应路径就可以下载下来。
0x03 打包时生成版本信息
按照上面的思路,每次更新都需要修改versionCode,versionName,然后还要处理json文件,apk文件...万一push弄错了一次,有强迫症的我又要amend commit -> force push
一气呵成了 (逃)。作为一个智慧 (懒惰) 的程序员,这种任务肯定就应该交给程序去做啦~
在阅读下文之前,希望各位看官先行了解一下gradle中Task、Action的概念。我也是现学现卖,有不对的地方也请各位指正~
Task&Actio
我们平常不管是点击运行还是Build或者Clean,实际背后都是由一堆Gradle的Task
来完成这些事,只是Android Studio将这些过程图形化了,没去专门了解的话可能感受不到。
可以简单的看一下在buildType为release
时build project执行的gradle任务:
生成必要信息的函数
首先,我们可以定义一个函数,用于将生成的apk移动到目标文件夹,并生成更新时查询的json:
// 计算apk的md5
static String generateMD5(File file) {
if (!file.exists() || !file.isFile()) {
return null
}
def digest = MessageDigest.getInstance("MD5")
file.withInputStream() { is ->
byte[] buffer = new byte[8192]
int read
while ((read = is.read(buffer)) > 0) {
digest.update(buffer, 0, read)
}
}
return digest.digest().encodeHex().toString()
}
void generateUpdateInfo(String apkName) {
println("------------------ Generating version info ------------------")
// 把apk文件从build目录复制到根项目的update文件夹下
def apkFile = project.file("build/outputs/apk/release/$apkName")
if (!apkFile.exists()) {
throw new GradleScriptException("apk file not exist!")
}
def toDir = rootProject.file(buildInfo.updatePath)
String apkHash = generateMD5(apkFile)
def updateJsonFile = new File(toDir, buildInfo.updateInfoFilename)
def writeNewFile = true
// 如果有以前的json文件,检查这次打包是否有改变
if (updateJsonFile.exists()) {
try {
def oldUpdateInfo = new JsonSlurper().parse(updateJsonFile)
if (buildInfo.versionCode <= oldUpdateInfo.code && apkHash == oldUpdateInfo.md5) {
writeNewFile = false
}
} catch (Exception e) {
writeNewFile = true
e.printStackTrace()
updateJsonFile.delete()
}
}
if (writeNewFile) {
def oldFiles = toDir.listFiles()
oldFiles.each {
if (!it.delete()) {
it.deleteOnExit()
}
}
copy {
from(apkFile)
into(toDir)
}
// 创建json的实体类
// Expando可以简单理解为Map
def updateInfo = new Expando(
code: buildInfo.versionCode,
name: buildInfo.versionName,
filename: apkFile.name,
url: "${buildInfo.updateBaseUrl}${apkFile.name}",
time: System.currentTimeMillis(),
des: buildInfo.versionDes,
size: apkFile.length(),
md5: apkHash
)
String newApkHash = generateMD5(new File(toDir, apkName))
println("new apk md5: $newApkHash")
def outputJson = new JsonBuilder(updateInfo).toPrettyString()
println(outputJson)
// 将json写入文件中,用于查询更新
updateJsonFile.write(outputJson)
} else {
// 不需要更新
println("This version is already released.\n" +
"VersionCode = ${buildInfo.versionCode}\n" +
"Skip generateUpdateInfo.")
}
println("------------------ Finish Generating version info ------------------")
}
添加到Task assembleRelease中
因为只在release包时需要生成版本信息,所以在buildType为release
时才需要生成版本信息,所以这里要做一下判断,只在release时将函数的添加到Task:
applicationVariants.each { variant ->
// 同一App名,方便操作
def apkName = "EveryDownload-${variant.buildType.name}-${defaultConfig.versionName}.apk"
variant.outputs.all {
outputFileName = apkName
}
// 只在release添加
if (variant.buildType.name == "release") {
直接添加到Task的Action队尾,build执行完成后就可以执行这个函数
variant.assembleProvider.get().doLast {
generateUpdateInfo(apkName)
}
}
}
运行结果
使用命令行构建,方便看到输出结果:
./gradlew app:assembleRelease
可以看到函数打印结果:
然后根目录有了这两个文件:
其他不动,再build一次,可以看到没有重新生成:
到这里,就成功解放双手啦~
0x04 有关更新的实现
这个方案直接用在了我的下载器中(项目传送门),欢迎star~