You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

未上架PlayStore的Android Studio应用如何通过源码设置自动更新

未上架Google Play的Android应用自实现自动更新方案

核心逻辑非常简单,不需要依赖任何第三方SDK,全流程自己可控:你自行托管更新配置文件和最新版APK安装包,客户端运行时拉取配置比对版本号,发现新版本后经用户确认下载APK,调起系统安装器完成覆盖安装。


具体实现步骤

  • 第一步:准备服务端更新配置
    你不需要搭专门的后端服务,随便找个支持直链访问的静态资源托管位(自己的云服务器、对象存储都可以),放两个文件:

    1. 命名为update_config.json的版本配置文件,内容参考:
    {
      "versionCode": 23,
      "versionName": "2.3.1",
      "updateContent": "1. 修复部分机型启动崩溃问题\n2. 新增本地文件导出功能\n3. 优化首页列表加载速度",
      "apkUrl": "你的APK文件直链访问地址",
      "forceUpdate": false
    }
    

    注意versionCode必须和你每次打包时build.gradle里配置的整数版本号一致,用来做版本大小比对;forceUpdate用来标记是否为强制更新版本。
    2. 每次发布新版本打包生成的正式签名APK,上传后把访问地址填到上面json的apkUrl字段里。

  • 第二步:配置客户端权限和FileProvider
    首先在AndroidManifest.xml里声明需要的权限,适配不同Android版本要求:

    <!-- 基础网络权限 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <!-- Android 8.0+ 未知来源应用安装权限 -->
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
    <!-- Android 12及以下版本APK写入存储权限,13+不需要 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
    

    同文件内配置FileProvider,解决Android 7.0以上私有文件访问限制问题:

    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="${applicationId}.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
    

    在res/xml目录下新建file_paths.xml,配置APK存放路径的访问权限:

    <?xml version="1.0" encoding="utf-8"?>
    <paths>
        <external-files-path name="apk_update" path="update/"/>
    </paths>
    
  • 第三步:实现版本比对逻辑
    先写工具方法获取当前安装应用的本地版本号:

    fun getLocalVersionCode(context: Context): Long {
        return try {
            val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                packageInfo.longVersionCode
            } else {
                @Suppress("DEPRECATION")
                packageInfo.versionCode.toLong()
            }
        } catch (e: Exception) {
            0L
        }
    }
    

    可以在应用启动页、或者设置页的「检查更新」按钮点击事件里,开子线程请求你之前托管的update_config.json,解析出远端的versionCode和本地值比对:如果远端值更大,就弹对话框展示更新说明,询问用户是否确认更新;如果是强制更新版本,弹框不设取消按钮,用户不更新就直接退出应用即可。

  • 第四步:实现APK下载逻辑
    直接用系统原生DownloadManager实现下载即可,不用自己写下载线程,自动支持通知栏进度展示、后台断点续传,稳定性高:

    fun downloadNewApk(context: Context, apkUrl: String, versionName: String) {
        val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        val request = DownloadManager.Request(Uri.parse(apkUrl))
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
        request.setTitle("应用更新 v$versionName")
        request.setDescription("安装包下载中...")
        // 下载的APK存在应用私有目录,不需要额外存储权限
        val saveFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app_v${versionName}.apk")
        val apkUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", saveFile)
        } else {
            Uri.fromFile(saveFile)
        }
        request.setDestinationUri(Uri.fromFile(saveFile))
        val downloadId = downloadManager.enqueue(request)
        // 注册广播监听下载完成事件
        val downloadCompleteReceiver = object : BroadcastReceiver() {
            override fun onReceive(ctx: Context?, intent: Intent?) {
                val completeId = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
                if (completeId == downloadId) {
                    installApk(ctx!!, apkUri)
                    ctx.unregisterReceiver(this)
                }
            }
        }
        context.registerReceiver(downloadCompleteReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
    }
    
  • 第五步:实现安装调起逻辑
    适配Android 8.0以上的未知来源安装权限要求,下载完成后直接调起系统安装器:

    fun installApk(context: Context, apkUri: Uri) {
        // 8.0以上需要先检查是否有安装未知来源应用的权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if (!context.packageManager.canRequestPackageInstalls()) {
                // 跳转到当前应用的权限设置页,引导用户开启
                val permissionIntent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
                permissionIntent.data = Uri.parse("package:${context.packageName}")
                context.startActivity(permissionIntent)
                return
            }
        }
        val installIntent = Intent(Intent.ACTION_VIEW)
        installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        installIntent.setDataAndType(apkUri, "application/vnd.android.package-archive")
        context.startActivity(installIntent)
    }
    

常见踩坑点

  • 托管APK和配置文件的地址必须是无鉴权、无防盗链拦截的GET直链,否则DownloadManager会下载失败,用户点安装会提示解析包错误。
  • 每次发新版本,必须先上传新APK,再更新update_config.json里的版本号和APK地址,否则会出现用户下载到旧包、反复提示更新的问题。
  • 非强制更新不要在应用冷启动的时候直接弹全屏对话框打断用户操作,可以给个通知栏弱提示,或者把检查更新入口放在设置页让用户主动触发。
  • 安装Intent必须加FLAG_GRANT_READ_URI_PERMISSION权限,否则Android高版本系统安装器没法读取你私有目录下的APK文件。

内容的提问来源于stack exchange,提问作者user19521700

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 02:51:21