如何为旧版本 Android 提供降级功能而非直接崩溃?

解读

面试官真正想考察的是“兼容性工程化能力”。国内存量机型 5.x-8.x 仍占 15% 以上,直接 minSdkVersion 飙高等于放弃这部分用户;而强行调用高版本 API 又会在运行时触发 NoSuchMethodError/VerifyError,被系统直接 Kill。因此“降级”不是简单 try-catch,而是一套“感知-路由-替代-埋点”的闭环方案,要让业务在旧系统上“无感降级”,同时让研发和测试“有据可查”。

知识点

  1. 版本语义与 API 级别:targetSdkVersion、compileSdkVersion、minSdkVersion 的区别与对运行时行为的影响。
  2. 反射与链接:直接引用导致编译期链接,反射调用可延迟到运行时,但性能与混淆风险需权衡。
  3. 兼容性注解:@RequiresApi、@TargetApi、@ChecksSdkIntAtLeast 在 Lint 阶段强制提醒。
  4. 运行时分支:Build.VERSION.SDK_INT、Build.VERSION.CODENAME 的可靠性与国内魔改 ROM 的陷阱。
  5. 兼容库与扩展:AndroidX Core、AppCompat、Activity-Compose 版本对照表,以及 Google 在国内无法访问时的本地 Maven 代理。
  6. 资源与功能降级:矢量图渲染、夜间模式、存储权限、后台定位、通知渠道、Scoped Storage、SplashScreen 等新特性在旧系统的 fallback 策略。
  7. 编译期代码剔除:Gradle 的 sourceSets、flavorDimensions、consumerProguardFiles 与 R8 的 if (SDK_INT >= X) 静态优化。
  8. 灰度与监控:Firebase/友盟/字节埋点如何区分“真降级”与“未命中新特性”,避免把兼容逻辑误判为“功能缺失”。

答案

“我们团队把降级拆成四层:编译隔离、运行时路由、UI 兼容、数据补偿,保证旧机不崩、新机用好。

  1. 编译隔离
    在 Gradle 里定义两个 flavor:‘legacy’ 与 ‘modern’,共用同一套业务接口。modern 的 sourceSet 里直接调用 Android 12 的 SplashScreen API;legacy 里只留空实现,通过 R8 的“静态条件剔除”把新 API 完全从 dex 中剪掉,避免 5.x 设备运行时链接到未知符号直接闪退。

  2. 运行时路由
    统一封装 SystemCompat 类,内部用 Build.VERSION.SDK_INT 做路由。例如设置通知渠道:
    if (SDK_INT >= Build.VERSION_CODES.O) {
    NotificationChannel channel = new NotificationChannel(…);
    nm.createNotificationChannel(channel);
    } else {
    nm.notify(id, new NotificationCompat.Builder(ctx, “legacy”).build());
    }
    所有业务层只依赖 SystemCompat,不直接感知版本差异。

  3. UI 兼容
    对矢量图、圆角、波纹动画,我们在 res/drawable-v24 下放矢量,drawable 下放 png fallback;Compose 侧用 LocalInspectionMode 或自定义 CompositionLocal 提供两套实现,5.x 走 View 体系,12 以上走 Compose 全屏。

  4. 数据补偿
    当新系统有“隐私沙盒”或“照片选择器”返回 URI 时,legacy 设备通过 Storage Access Framework 模拟相同 URI 结构,再回写至本地 MediaStore,保证上层 Repository 拿到的数据模型一致,无需业务二次适配。

  5. 自动化验证
    在 CI 里用 Firebase Test Lab 与国内云测平台各跑 5.x/6.x/7.x 真机,脚本断言‘启动无崩溃、核心路径可点、埋点字段存在’,一旦新增 API 未加版本判断即拒绝合并。

通过这套方案,我们把 minSdkVersion 压在 21,但 95% 的新特性在 Android 12 上可全量开启,5.x 用户也能正常使用核心功能,崩溃率始终低于 0.1%。”

拓展思考

  1. 如果未来把 targetSdkVersion 抬到 34,而国内厂商 ROM 对通知权限采用“二次弹窗”策略,如何在不降低到达率的前提下,对 8.x 以下设备做“无权限降级”?
  2. 当模块采用 KSP 与 Compose Compiler,其生成的代码会依赖高版本 androidx.compose.runtime 符号,怎样在 legacy flavor 中彻底剔除这些生成类,防止 5.x 设备因类校验失败而 install_failed_dexopt?
  3. 对于车载、Wear 这类硬编 SDK 的定制系统,Build.VERSION.SDK_INT 被厂商锁定为 29,但实际已回移植 32 的蓝牙 API,如何设计“厂商白名单”机制,让同一套 APK 在车载上走新实现,手机上走旧实现?