Android 中 Context 详解类型、使用场景与限制一、Context 是什么Context 是 Android 系统的抽象接口它提供了访问系统资源、启动组件、获取系统服务等核心能力。可以把它理解为**“应用环境的句柄”**——没有它你就无法与 Android 系统交互。publicabstractclassContext{// 获取资源publicabstractResourcesgetResources();// 启动 ActivitypublicabstractvoidstartActivity(Intentintent);// 获取系统服务publicabstractObjectgetSystemService(Stringname);// 获取 SharedPreferencespublicabstractSharedPreferencesgetSharedPreferences(Stringname,intmode);// ... 大量其他方法}二、Context 的继承体系Context抽象类 │ ┌────────────────┼────────────────┐ │ │ │ ContextWrapper Application Service │ (getApplicationContext()) │ ┌────┴────┐ │ │ Activity ContextThemeWrapper │ AppCompatActivity核心实现类类型类名说明Applicationandroid.app.Application全局单例代表整个应用Activityandroid.app.Activity代表一个界面携带 UI 主题Serviceandroid.app.Service后台服务组件ContextWrapperandroid.content.ContextWrapper包装器可代理其他 Context三、四种 Context 详解1. Application Context应用上下文// 获取方式ContextappContextgetApplicationContext();// Activity/Service 中ContextappContextcontext.getApplicationContext();// 任意 Context 中ContextappContextMyApp.getInstance();// 自定义 Application 单例自定义 Application 单例示例publicclassMyAppextendsApplication{privatestaticMyAppinstance;OverridepublicvoidonCreate(){super.onCreate();instancethis;}publicstaticMyAppgetInstance(){returninstance;}}特点全局唯一生命周期与 Application 一致应用存活期间一直存在不持有 Activity 引用不会导致内存泄漏没有 UI 主题无法用于创建 Dialog、启动带主题的 Activity适用场景场景原因获取全局配置/资源getResources()、getString()启动 ServicestartService()发送广播sendBroadcast()获取系统服务getSystemService()操作数据库/Room生命周期长不会随 Activity 销毁而关闭操作 SharedPreferences同上单例类中持有 Context避免内存泄漏必须传 Application Context图片加载库初始化Glide、Coil 等需要长生命周期 Context网络请求库初始化Retrofit/OkHttp 配置中需要不适用场景场景原因创建 Dialog没有 UI 主题会崩溃启动 Activity标准方式没有 Activity 栈需要FLAG_ACTIVITY_NEW_TASK布局 Inflate带主题的没有主题样式会丢失显示 Toast虽然可以但推荐用 Activity Context2. Activity Context界面上下文// 获取方式ContextactivityContextthis;// 在 Activity 中ContextactivityContextrequireContext();// 在 Fragment 中特点生命周期与 Activity 绑定Activity 销毁后 Context 失效持有 Activity 引用滥用会导致内存泄漏携带 UI 主题ContextThemeWrapper继承而来适用场景场景原因创建 Dialog/AlertDialog需要依附于 Activity 窗口启动 Activity自动加入 Activity 返回栈布局 Inflate需要 Activity 的主题解析属性显示 Toast需要与当前界面关联请求权限requestPermissions()需要 Activity启动 ForResultstartActivityForResult()需要 Activity视图操作View.getContext()通常就是 Activity Context不适用场景场景原因单例中持有Activity 销毁后单例仍持有引用 → 内存泄漏异步回调中使用网络请求回调里用 Activity Context用户退出后崩溃后台任务中WorkManager、后台线程中不应持有 Activity Context内存泄漏典型示例// ❌ 错误单例持有 Activity ContextpublicclassSingletonManager{privatestaticContextcontext;publicstaticvoidinit(Contextctx){contextctx;// 持有 Activity 引用}}// 正确做法传 Application ContextSingletonManager.init(getApplicationContext());正确做法// ✅ 正确单例持有 Application ContextpublicclassSingletonManager{privatestaticContextappContext;publicstaticvoidinit(Contextctx){appContextctx.getApplicationContext();// 永远用 Application}publicstaticContextgetContext(){returnappContext;}}3. Service Context服务上下文// 获取方式ContextserviceContextthis;// 在 Service 中特点生命周期与 Service 绑定没有 UI 主题Service 本身无界面可以启动 Activity需加FLAG_ACTIVITY_NEW_TASK适用场景场景原因后台播放音乐服务中控制 MediaPlayer后台下载/上传服务中执行网络操作启动前台服务startForeground()发送通知NotificationManager需要 Context不适用场景场景原因创建 DialogService 没有窗口令牌无法显示 Dialog启动普通 Activity需要FLAG_ACTIVITY_NEW_TASK否则崩溃布局 Inflate带主题没有主题4. ContextWrapper包装上下文// 常见使用方式ContextWrapperwrappernewContextWrapper(baseContext);ContextThemeWrapperthemedContextnewContextThemeWrapper(baseContext,R.style.MyTheme);特点代理模式包装另一个 Context可修改主题、资源等Application、Service都继承自ContextWrapper适用场景场景原因动态切换主题ContextThemeWrapper给 View 换主题插件化/换肤代理 Resources 实现资源替换多语言切换代理getResources()返回特定 Locale 资源动态切换主题示例publicclassThemeHelper{publicstaticContextwrapContext(Contextcontext,intthemeResId){returnnewContextThemeWrapper(context,themeResId);}// 使用publicstaticvoidapplyTheme(Viewview,intthemeResId){ContextthemedContextnewContextThemeWrapper(view.getContext(),themeResId);LayoutInflaterinflaterLayoutInflater.from(themedContext);// 用 themedContext inflate 的布局会应用新主题}}四、Context 使用场景对照表操作ApplicationActivityService说明startActivity()⚠️ 需 NEW_TASK✅⚠️ 需 NEW_TASKActivity 最自然startService()✅✅✅都可以bindService()✅✅✅都可以sendBroadcast()✅✅✅都可以registerReceiver()✅✅✅都可以getResources()✅✅✅都可以getSharedPreferences()✅✅✅都可以但推荐 ApplicationgetSystemService()✅✅✅都可以createDeviceProtectedStorageContext()✅✅✅都可以创建 Dialog❌✅❌只有 Activity 有窗口布局 Inflate带主题❌✅❌只有 Activity 有主题启动 ForResult❌✅❌只有 Activity 有回调请求权限❌✅❌只有 Activity 有界面显示 PopWindow❌✅❌需要 Activity 窗口五、常见错误与最佳实践错误 1单例持有 Activity Context// ❌ 错误publicclassBadManager{privatestaticContextcontext;publicstaticvoidinit(Contextctx){contextctx;// 如果是 Activity就泄漏了}}// ✅ 正确publicclassGoodManager{privatestaticContextappContext;publicstaticvoidinit(Contextctx){appContextctx.getApplicationContext();// 永远用 Application}}错误 2异步回调中使用 Activity Context// ❌ 错误网络请求回调中直接使用 ActivitypublicclassMainActivityextendsAppCompatActivity{privateTextViewtextView;privateApiServiceapi;publicvoidfetchData(){api.getData().enqueue(newCallbackData(){OverridepublicvoidonResponse(CallDatacall,ResponseDataresponse){// 用户可能已经退出 Activity这里操作 UI 崩溃或泄漏textView.setText(response.body().toString());}OverridepublicvoidonFailure(CallDatacall,Throwablet){// 同样可能崩溃}});}}// ✅ 正确使用 Application Context 或判空publicclassMainActivityextendsAppCompatActivity{privateTextViewtextView;privateApiServiceapi;publicvoidfetchData(){api.getData().enqueue(newCallbackData(){OverridepublicvoidonResponse(CallDatacall,ResponseDataresponse){// 方式1使用 Application Context 做非 UI 操作saveToDB(getApplicationContext(),response.body());// 方式2判空后再操作 UIif(!isDestroyed()!isFinishing()){textView.setText(response.body().toString());}}OverridepublicvoidonFailure(CallDatacall,Throwablet){if(!isDestroyed()!isFinishing()){Toast.makeText(MainActivity.this,请求失败,Toast.LENGTH_SHORT).show();}}});}privatevoidsaveToDB(Contextcontext,Datadata){// 使用 Application Context 操作数据库}}错误 3Application Context 创建 Dialog// ❌ 崩溃android.view.WindowManager$BadTokenExceptionAlertDialog.BuilderbuildernewAlertDialog.Builder(getApplicationContext());AlertDialogdialogbuilder.create();dialog.show();// ✅ 正确使用 Activity ContextAlertDialog.BuilderbuildernewAlertDialog.Builder(MainActivity.this);AlertDialogdialogbuilder.create();dialog.show();错误 4Service 中启动 Activity 不加 FLAG// ❌ 崩溃Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flagIntentintentnewIntent(this,MainActivity.class);startActivity(intent);// ✅ 正确IntentintentnewIntent(this,MainActivity.class);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);startActivity(intent);错误 5在 BroadcastReceiver 中启动 Dialog// ❌ 错误BroadcastReceiver 没有窗口publicclassMyReceiverextendsBroadcastReceiver{OverridepublicvoidonReceive(Contextcontext,Intentintent){// 这里 context 是 Application Context 或 ReceiverRestrictedContextAlertDialog.BuilderbuildernewAlertDialog.Builder(context);builder.show();// 崩溃}}// ✅ 正确启动一个 Activity 来显示 DialogpublicclassMyReceiverextendsBroadcastReceiver{OverridepublicvoidonReceive(Contextcontext,Intentintent){IntentactivityIntentnewIntent(context,DialogActivity.class);activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);context.startActivity(activityIntent);}}六、Context 数量关系一个 App 中 Application Context 1 个全局唯一 Activity Context N 个有几个 Activity 实例就有几个 Service Context M 个有几个 Service 实例就有几个 总 Context 数 1 N M七、为什么创建 Dialog 不能使用 Application Context核心原因Dialog 必须依附于一个窗口Window而 Application Context 没有窗口。Dialog 的创建流程当你调用Dialog.show()时内部发生了这些// Dialog.java 简化版publicvoidshow(){// 1. 创建 WindowmWindownewPhoneWindow(mContext);// 需要 Context 来创建窗口// 2. 获取 WindowManagermWindowManager(WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);// 3. 把 Dialog 的 DecorView 添加到 WindowmWindowManager.addView(mDecor,params);// 这里需要 WindowToken}关键点在第 3 步WindowManager.addView()需要一个WindowToken。WindowToken 是什么WindowToken 是窗口的身份证Android 窗口管理系统WMS用它来识别和管控窗口。窗口层级关系 PhoneWindowActivity 的根窗口 ↓ WindowToken由 AMS 分配绑定到 Activity 的 token ↓ Dialog 的窗口子窗口必须挂在某个父窗口下Dialog 是子窗口Sub-Window它必须挂载在一个父窗口通常是 Activity 的 PhoneWindow下面。这个挂载关系靠WindowToken维系。Application Context 为什么不行Application 没有 WindowToken// Application 继承关系Application→ContextWrapper→Context// Activity 继承关系Activity→ContextThemeWrapper→ContextWrapper→Context↑ 这里多了ContextThemeWrapper携带了窗口信息Activity 在创建时AMS 会分配一个IBinder token给它这个 token 就是 WindowToken 的来源// ActivityThread.javafinalActivityClientRecordrnewActivityClientRecord();r.tokentoken;// 来自 AMS 的 WindowTokenApplication 没有参与 Activity 的启动流程AMS 不会给它分配 token。报错现场当你用 Application Context 创建 Dialog 并show()// 崩溃日志android.view.WindowManager$BadTokenException:Unabletoaddwindow--tokennullis not valid;is your activity running?token null指的就是 WindowToken 为 null。Activity Context 为什么可以Activity 的mToken在attach()方法中被设置// Activity.javafinalvoidattach(Contextcontext,ActivityThreadaThread,Instrumentationinstr,IBindertoken,// ← 这个 token 就是 WindowTokenApplicationapplication,Intentintent,...){mWindownewPhoneWindow(this);mWindow.setWindowManager(...,token,...);// 把 token 传给 WindowManagermTokentoken;}所以当你用 Activity Context 创建 DialogDialogdialognewDialog(activity);// activity 的 mToken 有值dialog.show();Dialog 内部会复用 Activity 的 WindowToken把自己作为子窗口挂载到 Activity 的窗口树下。如果非要用 Application Context 显示弹窗怎么办方案 1用 System Alert 窗口需要权限// AndroidManifest.xml 中声明权限uses-permission android:nameandroid.permission.SYSTEM_ALERT_WINDOW/// 代码DialogdialognewDialog(getApplicationContext());dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);dialog.show();这种窗口不依附于任何 Activity是系统级浮窗类似微信视频通话的小窗。方案 2用 Toast本身就是系统窗口Toast.makeText(getApplicationContext(),提示,Toast.LENGTH_SHORT).show();Toast 使用系统窗口TYPE_TOAST不需要 Activity Token。方案 3启动一个透明的 Activity// 用 Application Context 启动一个透明主题的 Activity再在里面显示 DialogIntentintentnewIntent(getApplicationContext(),TransparentDialogActivity.class);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);getApplicationContext().startActivity(intent);总结问题答案Dialog 为什么需要窗口因为 Dialog 是子窗口必须挂载到父窗口下父窗口怎么识别靠 WindowTokenAMS 分配的 IBinderActivity 有没有 WindowToken有AMS 在启动 Activity 时分配Application 有没有 WindowToken没有Application 不参与窗口管理不用 Activity 怎么显示弹窗System Alert 窗口、Toast、透明 Activity一句话Dialog 是窗中窗Application 不是窗所以挂不上。八、总结类型生命周期有 UI 主题主要用途防泄漏Application应用存活期间❌全局操作、单例、数据库最安全Activity界面存活期间✅UI 相关、Dialog、跳转最易泄漏Service服务存活期间❌后台任务、播放、下载较安全ContextWrapper取决于被包装者可配置主题切换、插件化取决于内部核心原则能用 Application Context 的地方绝不用 Activity Context。必须 UI 操作的地方确认生命周期后再用 Activity Context。单例、异步、后台任务一律传 Application Context。