Android应用内增量更新

单于经纬
2023-12-01

#Android 增量更新实现
原本来腾讯课堂中了解到了一些关于增量更新的知识,特地写一篇博客来记录下。

  • 手机端实现 https://github.com/ItsFated/DiffPatch-app
  • 服务端实现 https://github.com/ItsFated/DiffPatch-server
    源码都在Github上开源,这里就只是记录下实现思路。
    ##准备材料
  1. Bsdiff(差分/合并工具)下载地址: http://www.daemonology.net/bsdiff/
  2. Bsdiff 依赖的bzip2下载地址: https://github.com/cnSchwarzer/bsdiff-win/tree/master/bzip2-1.0.6 (这是别人下载好放在GitHub上的,官网貌似不见了?)
    ##手机端实现
    具体思路
  3. 通过JNI实现差分合并
  4. 每次APP启动就获取最新版本号
  5. 判断是否为最新版本,如果不是。通过DownloadManager下载当前版本和最新版本的差分文件。
  6. 通过差分文件和当前的原始APK文件合并成最新的APK文件
  7. 调用系统API安装最新的APK

通过JNI实现差分、合并

object BsDiff {
    init {
        System.loadLibrary("bsdiff")
    }
    external fun bsdiff(oldFile: String, newFile: String, patchFile: String): Int
    external fun bspatch(oldFile: String, newFile: String, patchFile: String): Int
}

这个是Kotlin代码。那么如何给Kotlin代码实现JNI方法呢?

  1. 先写好Java类
  2. 通过javah命令,给Java源文件生成C/C++头文件
  3. 将Java转成Kotlin代码(AndroidStudio的自带工具 Code>Convert Java File to Kotlin File)
    这样只需要实现头文件的方法就可以了。

通过DownloadManager下载差分文件

object HttpUtils {
    const val HOST_URL = "http://192.168.18.38:8080"
    const val GET_LATEST_VERSION = "/app/latest-version"
    const val GET_LATEST_PATCH = "/app/get-apk-patch"

    /**
     * 获取服务器当前版本信息,JSON格式:
     * {"versionCode":2,"versionName":"DiffPatch 1.1"}
     */
    fun getLatestVersion(): JSONObject {
        val json: JSONObject
        val url = URL("$HOST_URL$GET_LATEST_VERSION")
        val urlConn: HttpURLConnection = url.openConnection() as HttpURLConnection
        urlConn.doOutput = true
        urlConn.doInput = true
        val reader = BufferedReader(InputStreamReader(urlConn.inputStream))
        urlConn.connect()
        json = JSONObject(reader.readLine())
        Log.i(App.TAG, json.toString())
        reader.close()
        urlConn.disconnect()
        return json
    }

    /**
     * 通过DownloadManager下载Patch文件(注意:URL中需要附加当前版本号),如:
     * http://192.168.18.38:8080/app/get-apk-patch?current-version-code=1
     */
    fun downloadLatestPatch(context: Context, patchFileName: String): Long {
        // 注意:URL中需要附加当前版本号
        val request = DownloadManager.Request(Uri.parse("$HOST_URL$GET_LATEST_PATCH?current-version-code=${App.versionCode}"))
        request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, patchFileName)
        request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI)
        request.setTitle(context.getString(R.string.app_name))
        request.setDescription(context.getString(R.string.format_patch_file, patchFileName))
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
        request.setVisibleInDownloadsUi(true)
        //7.0以上的系统适配
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            request.setRequiresDeviceIdle(false)
            request.setRequiresCharging(false)
        }
        return (context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager).enqueue(request)
    }
}

第一个方法是获取最新版本。
第二个方法是通过Android系统的DownloadManager下载差分文件。
注:下载目录如果改为Context.getExternalCacheDir()可以不用读写权限,参考博客 https://blog.csdn.net/wl724120268/article/details/78275686
合并成最新APK,并安装
合并代码:

class UpdateApkBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        Log.i(App.TAG, "onReceive: ${intent.action}")
        when (intent.action) {
            DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
                val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
                installApk(context, id)
            }
            DownloadManager.ACTION_VIEW_DOWNLOADS,
            DownloadManager.ACTION_NOTIFICATION_CLICKED -> {
                val viewDownloadIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
                viewDownloadIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                context.startActivity(viewDownloadIntent)
            }
        }
    }
    private fun installApk(context: Context, downloadId: Long) {
        val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        val uri = downloadManager.getUriForDownloadedFile(downloadId)
        val file = App.getRealFilePath(context, uri)
        if (uri != null && file != null) {
            val newApk = "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)}${File.separator}DiffPatch-2.apk"
            Log.i(App.TAG, "Start Activity: $uri")
            Log.i(App.TAG, newApk)
            BsDiff.bspatch(context.applicationInfo.sourceDir, newApk, file)
            //以下两行代码可以让下载的apk文件被直接安装而不用使用Fileprovider,系统7.0或者以上才启动。
            //https://www.jianshu.com/p/6b7bd2a59096
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                val localBuilder = StrictMode.VmPolicy.Builder()
                StrictMode.setVmPolicy(localBuilder.build())
            }
            val install = Intent(Intent.ACTION_VIEW)
            install.data = Uri.fromFile(File(newApk))
            install.setDataAndType(install.data, "application/vnd.android.package-archive")
            install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            context.startActivity(install)
        } else {
            Log.i(App.TAG, "Start Activity: failure")
        }
    }
}

安装未知来源权限,需要在Android 8.0上单独请求:

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

##服务端实现
具体思路

  1. 通过ServletContext记录最新的版本信息。
  2. 一个接口实现获取最新版本信息。
  3. 一个接口实现根据版本号,获取该版本号和最新版本的差分文件。
    在下也没有完全实现第二个接口,主要是因为bsdiff没有在Windows上编译通过,只有使用别人编译好的工具 https://github.com/cnSchwarzer/bsdiff-win/tree/master 以后编译通过了再更新。
 类似资料: