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

栏目: IOS · Android · 发布时间: 5年前

内容简介:相信各位都用过或听过使用一个用户体验比较好的更新通常有两个步骤,一个是询问服务器当前是否有更新,有更新后提醒用户,再由用户选择是否下载更新。检测更新这一步骤,我们就可以在我们的代码仓库中放一个文件,文件的内容是当前的版本信息,比如,可以使用json格式:

相信各位都用过或听过使用 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即可查看文件内容。

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

如图所示,上面的url就是你App可以直接通过HTTP访问的,从数据流读取数据后就可以得到字符串。可以把这个理解为后端提供的API:

https://raw.githubusercontent.com/${GitHub用户名}/${项目名}/${分支名}}/${相对于项目根目录的路径}}

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

通过这个接口,不仅能访问文本文件,还可以访问你 公开仓库 的任何文件,然后就可以下载到本地了。

注意:如果是私有仓库,无法用这个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任务:

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

可以看到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 时我

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)
        }
    }
}
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

A Common-Sense Guide to Data Structures and Algorithms

A Common-Sense Guide to Data Structures and Algorithms

Jay Wengrow / Pragmatic Bookshelf / 2017-8-13 / USD 45.95

If you last saw algorithms in a university course or at a job interview, you’re missing out on what they can do for your code. Learn different sorting and searching techniques, and when to use each. F......一起来看看 《A Common-Sense Guide to Data Structures and Algorithms》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换