利用GitHub實現簡單的我的App版本更新

0x01 前言

相信各位都用過或聽過使用GitHub做爲遠程代碼倉庫。但GitHub的功能可不只僅是管理存放代碼,你能夠把任何文件放在GitHub上,甚至能夠把它看成網盤來使用。因此,做爲沒有服務器 (沒錢) 的學生和懶得 (不會) 本身動手搭後臺的我,嘗試使用GitHub來實現簡單的App版本更新。java

0x02 更新流程

一個用戶體驗比較好的更新一般有兩個步驟,一個是詢問服務器當前是否有更新,有更新後提醒用戶,再由用戶選擇是否下載更新。git

檢測更新

檢測更新這一步驟,咱們就能夠在咱們的代碼倉庫中放一個文件,文件的內容是當前的版本信息,好比,可使用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

如圖所示,上面的url就是你App能夠直接經過HTTP訪問的,從數據流讀取數據後就能夠獲得字符串。能夠把這個理解爲後端提供的API:json

https://raw.githubusercontent.com/${GitHub用戶名}/${項目名}/${分支名}}/${相對於項目根目錄的路徑}} 後端

經過這個接口,不只能訪問文本文件,還能夠訪問你公開倉庫的任何文件,而後就能夠下載到本地了。bash

注意:若是是私有倉庫,沒法用這個API訪問,有一個token參數,具體能夠看GitHub的開發者API文檔服務器

這裏的例子中,code就是build時的versionCode,App讀取json後與當前versionCode比較便可知道是否須要更新(比較方式不惟一,使用versionCode是比較常見的)app

if (info.code > BuildConfig.VERSION_CODE) {
    // do update
} else {
    // already updated
}
複製代碼

下載更新

更新就相對來說比較簡單了。既然能夠把GitHub做爲「網盤」,咱們同樣能夠在Git中將打包好的apk文件也commit push上去,而後找到對應路徑就能夠下載下來。ide

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~

相關文章
相關標籤/搜索