阅读 1826

利用GitHub实现简单的个人App版本更新

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任务:

可以看到gradle执行了很多任务后才完成了apk的打包。而每个任务实际上又由多个Action组成。可以理解为Task中有一个Action队列,执行Task时会从队列一个一个取出Action执行。Task提供了doLast、doFirst等方法将一个Action加入队列。比如doLast时加入队列尾部。这里我们在build的最后一个任务assembleRelease的队尾中加入一个Action来做版本信息生成的工作。

生成必要信息的函数

首先,我们可以定义一个函数,用于将生成的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~

关注下面的标签,发现更多相似文章
评论