背景与痛点
把 ChatGPT 能力装进 Android 再推到 Google Play,看似只是“打包上架”,真正踩坑才知道:
Google Play 的审核机器人比真人还较真,版本号写错一位都能打回;API 级别低于 34 直接拒审;OpenAI SDK 里某个 OkHttp 4.10 依赖与 Play 目标 SDK 冲突,导致启动崩溃;再加上“后台定位”“读取已安装应用”这类权限哪怕在 Manifest 里写一行,也会被判定为“过度收集”。
结果反复申诉,时间线拉到两周,评分先掉 0.3。本文把最近两次踩坑记录拆成可复用的 checklist,帮你一次过审。
技术选型对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| A. 官方 openai-java SDK + Retrofit | 接口新,代码简洁 | 最低 OkHttp 4.12,与 Play 强制 targetSdk 34 的 security-crypto 1.1.0 冲突 | 快速 PoC,不追求上架 |
| B. 自行封装 Retrofit + 自定义拦截器 | 依赖干净,可插日志/重试 | 需自己维护模型类 | 正式发版首选 |
| C. 直接套 WebView 调 ChatGPT Web | 无 SDK 冲突 | 违反 OpenAI ToS;审核“Webview 模拟”红线 | 千万别用 |
结论:选 B,同时把 targetSdk 固定 34,compileSdk 35,留一个版本缓冲。
核心实现细节
gradle 版本对齐
在gradle/libs.versions.toml里统一定义,防止依赖漂移:[versions] compileSdk = "35" targetSdk = "34" minSdk = "24" okhttp = "4.12.0" retrofit = "2.11.0"在模块
build.gradle.kts中强制覆盖传递性依赖:configurations.all { resolutionStrategy { force("com.squareup.okhttp3:okhttp:${libs.versions.okhttp.get()}") } }Manifest 权限最小化
只保留网络与震动(用于语音交互提示),其余动态申请:<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.VIBRATE"/>把
android:exported显式写 true/false,避免合并冲突导致“默认导出”警告。API 级别运行时检查
对 13 以上新运行时通知权限做分支:if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) handlePostNotification()Proguard 保留规则
开放 AI 的 DTO 类被混淆后 Gson 解析失败,需加:-keep class com.yourapp.openai.** { *; }
代码示例
下面片段演示“动态权限 + 版本兼容”双保险,Kotlin + AndroidX:
class ChatActivity : AppCompatActivity() { companion object { private const val REQ_RECORD = 1 private const val MIN_SDK_FOR_V2 = 28 // 使用新版语音转写 } override fun onCreate(savedInstanceState savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_chat) if (hasRecordPermission()) startVoiceInput() else requestRecordPermission() } private fun hasRecordPermission(): Boolean = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED private fun requestRecordPermission() = ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), REQ_RECORD) override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == REQ_RECORD && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) { startVoiceInput() } else { toast("需要麦克风权限才能语音对话") } } private fun startVoiceInput() { // 根据系统版本选择转写器 val recognizer = if (Build.VERSION.SDK_INT >= MIN_SDK_FOR_V2) { VoiceRecognizerV2() // 使用平台 AI 本地 API } else { VoiceRecognizerCompat() // 回退到云端 } recognizer.start(this) } }要点:
- 权限申请与业务逻辑完全解耦,方便单元测试。
- 运行时根据 SDK_INT 选择实现,兼顾低版本设备与审核政策。
性能与安全性考量
性能
targetSdk 升到 34 后,前台服务必须加FOREGROUND_SERVICE_TYPE_*类别,否则 5 秒崩溃;把长连接语音流放在mediaPerformance类别,CPU 锁频降低 8%。安全
- 所有网络层强制 TLS 1.3,禁用 cipher suite
TLS_RSA_*; - API Key 放 Google Play 的Play App Signing证书下
BuildConfig字段,避免硬编码; - 用户对话数据先写进EncryptedFile(security-crypto 1.1.0),再上传,符合“数据最小化”审核要点。
- 所有网络层强制 TLS 1.3,禁用 cipher suite
生产环境避坑指南
坑 1:在
AndroidManifest.xml里写QUERY_ALL_PACKAGES权限,审核秒拒。
解:如果只为检查浏览器是否存在,用<queries><intent>...</intent></queries>具体声明。坑 2:OpenAI 返回 SSE 流,Retrofit 未设置
responseBodyOkio,导致大模型回答一长就 OOM。
解:加callFactory使用OkHttpClient.Builder().readTimeout(0, TimeUnit.SECONDS).build()。坑 3:版本号
versionCode忘记递增,Google Play 报“已上传更高 APK”。
解:在 CI 里用git commit count自动赋值,永不重复。坑 4:混淆后
gson.fromJson返回 null,审核复现崩溃。
解:在proguard-rules.pro加-keepattributes Signature -keepattributes *Annotation*。
互动与思考
Google Play 政策每个季度都会更新,下一步可能强制 PhotoPicker 替代文件权限、要求 AI 生成内容加“举报”按钮。
建议把政策 diff 订阅到 Slack,发版前跑一次Policy Scanner(Android Studio Hedgehog 内置),提前修掉警告。
你在实际交付中还遇到哪些诡异驳回?欢迎在评论区贴出 rejection 截图与解决方案,一起把“过审”做成自动化流水线。
把 ChatGPT 装进手机只是第一步,让它“听得懂、说得出、能上架”才是一次完整交付。
如果你想亲手跑通 ASR→LLM→TTS 全链路,又懒得自己搭网关,可以试试这个动手实验:从0打造个人豆包实时通话AI。
我跟着教程 90 分钟就把 Demo 跑通,火山引擎的流式语音接口已经做好兼容性封装,targetSdk 34 直接编译通过,比自己踩坑快得多。小白也能顺利体验,建议本地调通后再合并到生产分支,省时省力。