01-18-08 废弃API的处理方式
01-18-08 废弃API的处理方式
API废弃概述
API废弃(Deprecation)是软件演进的必然过程,Android通过渐进式废弃策略保证生态系统平稳过渡。
废弃原因:
1. 安全问题 - 存在安全漏洞 - 无法修复的设计缺陷 2. 性能问题 - 低效的实现 - 资源浪费 3. 设计改进 - 更好的替代方案 - 接口简化 4. 平台演进 - 新特性支持 - 架构调整@Deprecated注解
基本用法
/** * @Deprecated注解使用 * frameworks/base/core/java/android/app/ActivityManager.java */publicclassActivityManager{/** * Android 1.0 原始API * Android 5.0 废弃 * * @deprecated 从API 21开始废弃,使用{@link #getRunningAppProcesses()}替代 */@DeprecatedpublicList<RunningTaskInfo>getRunningTasks(intmaxNum)throwsSecurityException{try{returngetService().getTasks(maxNum);}catch(RemoteExceptione){throwe.rethrowFromSystemServer();}}/** * 推荐的替代方法 */publicList<RunningAppProcessInfo>getRunningAppProcesses(){try{returngetService().getRunningAppProcesses();}catch(RemoteExceptione){throwe.rethrowFromSystemServer();}}}Kotlin @Deprecated
/** * Kotlin废弃注解 */classExample{/** * 废弃方法 - 提供替代方案 */@Deprecated(message="Use newMethod() instead",replaceWith=ReplaceWith("newMethod()"),level=DeprecationLevel.WARNING)funoldMethod(){// 旧实现}/** * 新方法 */funnewMethod(){// 新实现}/** * 强制废弃 - ERROR级别 */@Deprecated(message="This method is no longer supported",level=DeprecationLevel.ERROR)funobsoleteMethod(){throwUnsupportedOperationException("Method is obsolete")}/** * 隐藏废弃 - HIDDEN级别 */@Deprecated(message="Internal use only",level=DeprecationLevel.HIDDEN)funinternalMethod(){// 仅内部使用}}/** * DeprecationLevel级别: * - WARNING: 编译警告,代码仍可用 * - ERROR: 编译错误,但可通过@Suppress抑制 * - HIDDEN: 完全隐藏,无法使用 */渐进式废弃策略
废弃时间线
阶段0: 正常API(API N) ├─ API稳定 ├─ 文档完善 └─ 广泛使用 阶段1: 标记废弃(API N+1) ├─ 添加@Deprecated注解 ├─ 更新JavaDoc说明废弃原因 ├─ 提供替代方案 └─ IDE显示删除线 阶段2: 行为限制(API N+3) ├─ targetSdk >= N+3: 限制功能或返回默认值 ├─ targetSdk < N+3: 保持原有行为(兼容性) └─ 运行时日志警告 阶段3: 移除API(API N+6+) ├─ 完全移除代码 ├─ 抛出UnsupportedOperationException └─ 更新兼容性文档 典型时间跨度:6-8年(约3-4个Android大版本)实际案例:getExternalStorageDirectory
/** * 存储API废弃演进 * frameworks/base/core/java/android/os/Environment.java */publicclassEnvironment{/** * 阶段1: Android 10 (API 29) 标记废弃 * @deprecated 使用Context#getExternalFilesDir(String)替代 */@Deprecated@NonNullpublicstaticFilegetExternalStorageDirectory(){returnsCurrentUser.getExternalDirs()[0];}/** * 阶段2: Android 10 行为变更 * targetSdk >= 29: 启用分区存储 */privatestaticFilegetExternalStorageDirectoryInternal(){ApplicationInfoappInfo=ActivityThread.currentApplication().getApplicationInfo();if(appInfo.targetSdkVersion>=Build.VERSION_CODES.Q){// 分区存储模式:限制访问Log.w(TAG,"getExternalStorageDirectory() is deprecated, "+"use getExternalFilesDir() instead");// 返回应用私有目录returnsCurrentUser.getExternalDirs()[0];}else{// 兼容模式:返回公共存储returngetDataDirectory();}}}HttpClient废弃案例
/** * Apache HttpClient废弃 * Android 6.0 (API 23) 移除 */// Android 5.1及以下:可用importorg.apache.http.client.HttpClient;importorg.apache.http.impl.client.DefaultHttpClient;HttpClientclient=newDefaultHttpClient();// Android 6.0及以上:移除// 编译错误:Cannot resolve symbol 'DefaultHttpClient'// 替代方案1:HttpURLConnectionURLurl=newURL("https://example.com");HttpURLConnectionurlConnection=(HttpURLConnection)url.openConnection();try{InputStreamin=urlConnection.getInputStream();// 读取数据}finally{urlConnection.disconnect();}// 替代方案2:OkHttp(推荐)OkHttpClientclient=newOkHttpClient();Requestrequest=newRequest.Builder().url("https://example.com").build();Responseresponse=client.newCall(request).execute();targetSdkVersion行为限制
废弃API的targetSdk检查
/** * 基于targetSdk的行为调整 * frameworks/base/core/java/android/app/ApplicationPackageManager.java */publicclassApplicationPackageManagerextendsPackageManager{/** * 获取已安装应用列表 * Android 11 (API 30): 包可见性限制 */@OverridepublicList<ApplicationInfo>getInstalledApplications(intflags){ApplicationInfoappInfo=mContext.getApplicationInfo();// targetSdk >= 30: 包可见性过滤if(appInfo.targetSdkVersion>=Build.VERSION_CODES.R){// 只返回可见的应用returngetInstalledApplicationsFiltered(flags);}else{// targetSdk < 30: 返回所有应用(兼容性)returngetInstalledApplicationsUnfiltered(flags);}}/** * 获取应用签名 * Android 9.0 (API 28): GET_SIGNATURES废弃 */@OverridepublicPackageInfogetPackageInfo(StringpackageName,intflags)throwsNameNotFoundException{ApplicationInfoappInfo=mContext.getApplicationInfo();// 检查是否使用废弃的GET_SIGNATURESif((flags&GET_SIGNATURES)!=0){if(appInfo.targetSdkVersion>=Build.VERSION_CODES.P){// targetSdk >= 28: 记录警告Log.w(TAG,"Use GET_SIGNING_CERTIFICATES instead of GET_SIGNATURES");}}returngetPackageInfoAsUser(packageName,flags,getUserId());}}Notification API废弃
/** * Notification API演进 * frameworks/base/core/java/android/app/Notification.java */publicclassNotificationimplementsParcelable{/** * Android 4.1之前的方式(废弃) * @deprecated 使用{@link Builder}替代 */@DeprecatedpublicNotification(inticon,CharSequencetickerText,longwhen){this.icon=icon;this.tickerText=tickerText;this.when=when;}/** * Android 4.1+ 推荐方式:Builder模式 */publicstaticclassBuilder{privateContextmContext;privateNotificationmN;publicBuilder(Contextcontext){this(context,null);}/** * Android 8.0+ 必须指定Channel */publicBuilder(Contextcontext,StringchannelId){mContext=context;mN=newNotification();// Android 8.0+: 检查targetSdkApplicationInfoappInfo=context.getApplicationInfo();if(appInfo.targetSdkVersion>=Build.VERSION_CODES.O){// 必须设置Channelif(TextUtils.isEmpty(channelId)){thrownewIllegalArgumentException("Must specify notification channel for targetSdk >= 26");}}mN.channelId=channelId;}publicBuildersetSmallIcon(inticon){mN.icon=icon;returnthis;}publicBuildersetContentTitle(CharSequencetitle){mN.extras.putCharSequence(EXTRA_TITLE,title);returnthis;}publicNotificationbuild(){returnmN;}}}废弃API的文档处理
JavaDoc废弃标记
/** * JavaDoc废弃文档示例 */publicclassApiExample{/** * 获取设备ID * * <p>此方法在Android 10 (API 29)中废弃,出于隐私保护考虑。 * * <p><strong>废弃原因:</strong> * <ul> * <li>可被用于跟踪用户</li> * <li>无法重置</li> * <li>违反用户隐私</li> * </ul> * * <p><strong>替代方案:</strong> * <ul> * <li>使用{@link java.util.UUID#randomUUID()}生成应用级唯一ID</li> * <li>使用{@link android.provider.Settings.Secure#ANDROID_ID}获取设备ID(可重置)</li> * <li>使用{@link com.google.android.gms.ads.identifier.AdvertisingIdClient}获取广告ID</li> * </ul> * * <p><strong>迁移指南:</strong> * <pre>{@code * // 旧代码(已废弃) * String deviceId = telephonyManager.getDeviceId(); * * // 新代码(推荐) * String androidId = Settings.Secure.getString( * context.getContentResolver(), * Settings.Secure.ANDROID_ID * ); * }</pre> * * @return 设备ID * @throws SecurityException 如果应用没有READ_PHONE_STATE权限 * @deprecated 从API 29开始废弃,使用替代方案 * @see android.provider.Settings.Secure#ANDROID_ID */@Deprecated@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)publicStringgetDeviceId(){ApplicationInfoappInfo=mContext.getApplicationInfo();if(appInfo.targetSdkVersion>=Build.VERSION_CODES.Q){// API 29+: 抛出异常thrownewSecurityException("getDeviceId is no longer supported for targetSdk >= 29");}// API 28-: 返回实际值(兼容性)returngetDeviceIdInternal();}}API差异文档
Android API差异文档(API 29 → API 30) ==== 废弃的API ==== 1. android.telephony.TelephonyManager - getDeviceId() [废弃] 原因:隐私保护 替代:Settings.Secure.ANDROID_ID - getSimSerialNumber() [废弃] 原因:隐私保护 替代:无替代方案(需用户授权) 2. android.os.Environment - getExternalStorageDirectory() [废弃] 原因:分区存储 替代:Context.getExternalFilesDir() 3. android.content.pm.PackageManager - getInstalledApplications() [行为变更] 原因:包可见性 影响:targetSdk >= 30 只返回可见应用 迁移:声明<queries>标签 ==== 新增的API ==== 4. android.window.WindowMetrics + getBounds() [新增] 用途:获取窗口边界 替代:Display.getSize()(已废弃) ==== 行为变更 ==== 5. 后台位置权限 - 需要单独请求ACCESS_BACKGROUND_LOCATION - 不能与前台位置权限一起请求Lint检查规则
自定义Lint检测废弃API
/** * 自定义Lint检测器:检测废弃API使用 */classDeprecatedApiDetector:Detector(),SourceCodeScanner{overridefungetApplicableMethodNames():List<String>{returnlistOf("getDeviceId","getExternalStorageDirectory","getRunningTasks")}overridefunvisitMethodCall(context:JavaContext,node:UCallExpression,method:PsiMethod){valannotation=method.getAnnotation("java.lang.Deprecated")if(annotation!=null){// 检查targetSdkvalproject=context.projectvaltargetSdk=project.targetSdkif(targetSdk>=29){context.report(DEPRECATED_API,node,context.getLocation(node),"Deprecated API should not be used with targetSdk >= 29",createQuickFix(method))}}}privatefuncreateQuickFix(method:PsiMethod):LintFix?{// 根据废弃的方法提供快速修复returnwhen(method.name){"getDeviceId"->{LintFix.create().replace().text(method.name).with("Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID)").build()}else->null}}companionobject{valDEPRECATED_API=Issue.create(id="DeprecatedApi",briefDescription="Using deprecated API",explanation="This API has been deprecated and should not be used",category=Category.CORRECTNESS,priority=6,severity=Severity.WARNING,implementation=Implementation(DeprecatedApiDetector::class.java,Scope.JAVA_FILE_SCOPE))}}废弃API的运行时处理
异常抛出
/** * 废弃API运行时异常 */publicclassTelephonyManager{/** * 获取设备ID - 废弃API */@Deprecated@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)publicStringgetDeviceId(){ApplicationInfoappInfo=mContext.getApplicationInfo();if(appInfo.targetSdkVersion>=Build.VERSION_CODES.Q){// targetSdk >= 29: 抛出异常thrownewUnsupportedOperationException("This method is no longer supported. "+"See https://developer.android.com/training/articles/user-data-ids");}// targetSdk < 29: 记录警告Log.w(TAG,"getDeviceId() is deprecated, "+"see https://developer.android.com/training/articles/user-data-ids");// 返回实际值(兼容性)returngetDeviceIdInternal();}}日志警告
/** * 废弃API调用日志 * frameworks/base/core/java/android/util/Log.java */publicfinalclassLog{/** * 记录废弃API调用 */publicstaticvoidwarnDeprecatedApi(Stringapi,Stringreplacement){ApplicationInfoappInfo=ActivityThread.currentApplication().getApplicationInfo();Stringmessage=String.format("Application %s (targetSdk=%d) calls deprecated API: %s, "+"use %s instead",appInfo.packageName,appInfo.targetSdkVersion,api,replacement);Log.w("DeprecatedApi",message);// StrictMode检测if(StrictMode.getVmPolicy().penaltyListener!=null){StrictMode.onDeprecatedApiUsed(api);}}}StrictMode检测
/** * StrictMode检测废弃API */objectDeprecatedApiMonitor{/** * 启用废弃API检测 */funenableDetection(){if(BuildConfig.DEBUG){StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder().detectAll().penaltyLog().penaltyListener(Executors.newSingleThreadExecutor()){violation->Log.e("StrictMode","VM policy violation",violation)// 检测废弃API使用if(violationisDeprecatedApiViolation){handleDeprecatedApiViolation(violation)}}.build())}}privatefunhandleDeprecatedApiViolation(violation:DeprecatedApiViolation){valmessage=""" Deprecated API used: API:${violation.apiName}Stack:${violation.stackTrace}Replacement:${violation.replacement}""".trimIndent()Log.w("DeprecatedApi",message)// 上报到监控系统reportToAnalytics(violation)}privatefunreportToAnalytics(violation:DeprecatedApiViolation){// 上报到Firebase/Sentry等}}总结
废弃API处理原则
- 渐进式废弃:分阶段执行(标记→限制→移除)
- 充分通知:JavaDoc、Release Notes、开发者文档
- 提供替代:明确指出替代方案和迁移路径
- 兼容性保证:targetSdk < N 保持原有行为
- 运行时提示:日志警告、异常信息
- 工具支持:Lint检查、IDE提示
废弃时间线
API N: 发布新API API N+1: 标记旧API为@Deprecated API N+3: targetSdk >= N+3 限制功能 API N+6+: 完全移除旧API 时间跨度: 6-8年最佳实践
- 及时更新targetSdk
- 关注API废弃通知
- 使用Lint检测废弃API
- 测试不同Android版本
- 提供降级方案
Android 16 (API 36) 废弃API变化
Android 16 新增废弃API
后台启动Activity完全废弃
// Android 16:targetSdk >= 36 时,以下方法受限// startActivity() 从后台调用将抛出异常@Deprecated(message="从后台启动Activity在Android 16被完全限制",level=DeprecationLevel.WARNING)funstartFromBackground(context:Context,intent:Intent){if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.BAKLAVA){// 使用 PendingIntent 或通知启动valpendingIntent=PendingIntent.getActivity(context,0,intent,PendingIntent.FLAG_UPDATE_CURRENTorPendingIntent.FLAG_IMMUTABLE)// 通过通知栏触发}}旧版前台服务API废弃
// Android 16:不带类型的前台服务完全废弃@Deprecated(message="使用 startForeground(type, notification) 替代",replaceWith=ReplaceWith("startForeground(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC, notification)"),level=DeprecationLevel.ERROR// targetSdk 36 直接编译错误)funstartForegroundServiceLegacy(){// 旧方式:不再允许}// 新方式funstartForegroundServiceModern(){startForeground(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,notification)}StrictMode 废弃API检测增强
// Android 16:StrictMode 对废弃API的检测更严格StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder().detectAll().penaltyLog()// Android 16 新增:废弃API使用可配置为崩溃.penaltyDeathOnDeprecatedApiUsage()// 调试模式.build())关键要点:API废弃是Android演进的必然,通过渐进式策略和targetSdk检查确保生态系统平稳过渡
