Android 中 Context 详解:类型、使用场景与限制
一、Context 是什么
Context 是 Android 系统的抽象接口,它提供了访问系统资源、启动组件、获取系统服务等核心能力。可以把它理解为**“应用环境的句柄”**——没有它,你就无法与 Android 系统交互。
publicabstractclassContext{// 获取资源publicabstractResourcesgetResources();// 启动 ActivitypublicabstractvoidstartActivity(Intentintent);// 获取系统服务publicabstractObjectgetSystemService(Stringname);// 获取 SharedPreferencespublicabstractSharedPreferencesgetSharedPreferences(Stringname,intmode);// ... 大量其他方法}二、Context 的继承体系
Context(抽象类) │ ┌────────────────┼────────────────┐ │ │ │ ContextWrapper Application Service │ (getApplicationContext()) │ ┌────┴────┐ │ │ Activity ContextThemeWrapper │ AppCompatActivity核心实现类
| 类型 | 类名 | 说明 |
|---|---|---|
| Application | android.app.Application | 全局单例,代表整个应用 |
| Activity | android.app.Activity | 代表一个界面,携带 UI 主题 |
| Service | android.app.Service | 后台服务组件 |
| ContextWrapper | android.content.ContextWrapper | 包装器,可代理其他 Context |
三、四种 Context 详解
1. Application Context(应用上下文)
// 获取方式ContextappContext=getApplicationContext();// Activity/Service 中ContextappContext=context.getApplicationContext();// 任意 Context 中ContextappContext=MyApp.getInstance();// 自定义 Application 单例自定义 Application 单例示例:
publicclassMyAppextendsApplication{privatestaticMyAppinstance;@OverridepublicvoidonCreate(){super.onCreate();instance=this;}publicstaticMyAppgetInstance(){returninstance;}}特点:
- 全局唯一,生命周期与 Application 一致(应用存活期间一直存在)
- 不持有 Activity 引用,不会导致内存泄漏
- 没有 UI 主题(无法用于创建 Dialog、启动带主题的 Activity)
适用场景:
| 场景 | 原因 |
|---|---|
| 获取全局配置/资源 | getResources()、getString() |
| 启动 Service | startService() |
| 发送广播 | sendBroadcast() |
| 获取系统服务 | getSystemService() |
| 操作数据库/Room | 生命周期长,不会随 Activity 销毁而关闭 |
| 操作 SharedPreferences | 同上 |
| 单例类中持有 Context | 避免内存泄漏,必须传 Application Context |
| 图片加载库初始化 | Glide、Coil 等需要长生命周期 Context |
| 网络请求库初始化 | Retrofit/OkHttp 配置中需要 |
不适用场景:
| 场景 | 原因 |
|---|---|
| 创建 Dialog | 没有 UI 主题,会崩溃 |
| 启动 Activity(标准方式) | 没有 Activity 栈,需要FLAG_ACTIVITY_NEW_TASK |
| 布局 Inflate(带主题的) | 没有主题,样式会丢失 |
| 显示 Toast | 虽然可以,但推荐用 Activity Context |
2. Activity Context(界面上下文)
// 获取方式ContextactivityContext=this;// 在 Activity 中ContextactivityContext=requireContext();// 在 Fragment 中特点:
- 生命周期与 Activity 绑定,Activity 销毁后 Context 失效
- 持有 Activity 引用,滥用会导致内存泄漏
- 携带 UI 主题(
ContextThemeWrapper继承而来)
适用场景:
| 场景 | 原因 |
|---|---|
| 创建 Dialog/AlertDialog | 需要依附于 Activity 窗口 |
| 启动 Activity | 自动加入 Activity 返回栈 |
| 布局 Inflate | 需要 Activity 的主题解析属性 |
| 显示 Toast | 需要与当前界面关联 |
| 请求权限 | requestPermissions()需要 Activity |
| 启动 ForResult | startActivityForResult()需要 Activity |
| 视图操作 | View.getContext()通常就是 Activity Context |
不适用场景:
| 场景 | 原因 |
|---|---|
| 单例中持有 | Activity 销毁后单例仍持有引用 → 内存泄漏 |
| 异步回调中使用 | 网络请求回调里用 Activity Context,用户退出后崩溃 |
| 后台任务中 | WorkManager、后台线程中不应持有 Activity Context |
内存泄漏典型示例:
// ❌ 错误:单例持有 Activity ContextpublicclassSingletonManager{privatestaticContextcontext;publicstaticvoidinit(Contextctx){context=ctx;// 持有 Activity 引用!}}// 正确做法:传 Application ContextSingletonManager.init(getApplicationContext());正确做法:
// ✅ 正确:单例持有 Application ContextpublicclassSingletonManager{privatestaticContextappContext;publicstaticvoidinit(Contextctx){appContext=ctx.getApplicationContext();// 永远用 Application}publicstaticContextgetContext(){returnappContext;}}3. Service Context(服务上下文)
// 获取方式ContextserviceContext=this;// 在 Service 中特点:
- 生命周期与 Service 绑定
- 没有 UI 主题(Service 本身无界面)
- 可以启动 Activity(需加
FLAG_ACTIVITY_NEW_TASK)
适用场景:
| 场景 | 原因 |
|---|---|
| 后台播放音乐 | 服务中控制 MediaPlayer |
| 后台下载/上传 | 服务中执行网络操作 |
| 启动前台服务 | startForeground() |
| 发送通知 | NotificationManager需要 Context |
不适用场景:
| 场景 | 原因 |
|---|---|
| 创建 Dialog | Service 没有窗口令牌,无法显示 Dialog |
| 启动普通 Activity | 需要FLAG_ACTIVITY_NEW_TASK,否则崩溃 |
| 布局 Inflate(带主题) | 没有主题 |
4. ContextWrapper(包装上下文)
// 常见使用方式ContextWrapperwrapper=newContextWrapper(baseContext);ContextThemeWrapperthemedContext=newContextThemeWrapper(baseContext,R.style.MyTheme);特点:
- 代理模式,包装另一个 Context
- 可修改主题、资源等
Application、Service都继承自ContextWrapper
适用场景:
| 场景 | 原因 |
|---|---|
| 动态切换主题 | ContextThemeWrapper给 View 换主题 |
| 插件化/换肤 | 代理 Resources 实现资源替换 |
| 多语言切换 | 代理getResources()返回特定 Locale 资源 |
动态切换主题示例:
publicclassThemeHelper{publicstaticContextwrapContext(Contextcontext,intthemeResId){returnnewContextThemeWrapper(context,themeResId);}// 使用publicstaticvoidapplyTheme(Viewview,intthemeResId){ContextthemedContext=newContextThemeWrapper(view.getContext(),themeResId);LayoutInflaterinflater=LayoutInflater.from(themedContext);// 用 themedContext inflate 的布局会应用新主题}}四、Context 使用场景对照表
| 操作 | Application | Activity | Service | 说明 |
|---|---|---|---|---|
startActivity() | ⚠️ 需 NEW_TASK | ✅ | ⚠️ 需 NEW_TASK | Activity 最自然 |
startService() | ✅ | ✅ | ✅ | 都可以 |
bindService() | ✅ | ✅ | ✅ | 都可以 |
sendBroadcast() | ✅ | ✅ | ✅ | 都可以 |
registerReceiver() | ✅ | ✅ | ✅ | 都可以 |
getResources() | ✅ | ✅ | ✅ | 都可以 |
getSharedPreferences() | ✅ | ✅ | ✅ | 都可以,但推荐 Application |
getSystemService() | ✅ | ✅ | ✅ | 都可以 |
createDeviceProtectedStorageContext() | ✅ | ✅ | ✅ | 都可以 |
| 创建 Dialog | ❌ | ✅ | ❌ | 只有 Activity 有窗口 |
| 布局 Inflate(带主题) | ❌ | ✅ | ❌ | 只有 Activity 有主题 |
| 启动 ForResult | ❌ | ✅ | ❌ | 只有 Activity 有回调 |
| 请求权限 | ❌ | ✅ | ❌ | 只有 Activity 有界面 |
| 显示 PopWindow | ❌ | ✅ | ❌ | 需要 Activity 窗口 |
五、常见错误与最佳实践
错误 1:单例持有 Activity Context
// ❌ 错误publicclassBadManager{privatestaticContextcontext;publicstaticvoidinit(Contextctx){context=ctx;// 如果是 Activity,就泄漏了}}// ✅ 正确publicclassGoodManager{privatestaticContextappContext;publicstaticvoidinit(Contextctx){appContext=ctx.getApplicationContext();// 永远用 Application}}错误 2:异步回调中使用 Activity Context
// ❌ 错误:网络请求回调中直接使用 ActivitypublicclassMainActivityextendsAppCompatActivity{privateTextViewtextView;privateApiServiceapi;publicvoidfetchData(){api.getData().enqueue(newCallback<Data>(){@OverridepublicvoidonResponse(Call<Data>call,Response<Data>response){// 用户可能已经退出 Activity,这里操作 UI 崩溃或泄漏textView.setText(response.body().toString());}@OverridepublicvoidonFailure(Call<Data>call,Throwablet){// 同样可能崩溃}});}}// ✅ 正确:使用 Application Context 或判空publicclassMainActivityextendsAppCompatActivity{privateTextViewtextView;privateApiServiceapi;publicvoidfetchData(){api.getData().enqueue(newCallback<Data>(){@OverridepublicvoidonResponse(Call<Data>call,Response<Data>response){// 方式1:使用 Application Context 做非 UI 操作saveToDB(getApplicationContext(),response.body());// 方式2:判空后再操作 UIif(!isDestroyed()&&!isFinishing()){textView.setText(response.body().toString());}}@OverridepublicvoidonFailure(Call<Data>call,Throwablet){if(!isDestroyed()&&!isFinishing()){Toast.makeText(MainActivity.this,"请求失败",Toast.LENGTH_SHORT).show();}}});}privatevoidsaveToDB(Contextcontext,Datadata){// 使用 Application Context 操作数据库}}错误 3:Application Context 创建 Dialog
// ❌ 崩溃:android.view.WindowManager$BadTokenExceptionAlertDialog.Builderbuilder=newAlertDialog.Builder(getApplicationContext());AlertDialogdialog=builder.create();dialog.show();// ✅ 正确:使用 Activity ContextAlertDialog.Builderbuilder=newAlertDialog.Builder(MainActivity.this);AlertDialogdialog=builder.create();dialog.show();错误 4:Service 中启动 Activity 不加 FLAG
// ❌ 崩溃:Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flagIntentintent=newIntent(this,MainActivity.class);startActivity(intent);// ✅ 正确Intentintent=newIntent(this,MainActivity.class);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);startActivity(intent);错误 5:在 BroadcastReceiver 中启动 Dialog
// ❌ 错误:BroadcastReceiver 没有窗口publicclassMyReceiverextendsBroadcastReceiver{@OverridepublicvoidonReceive(Contextcontext,Intentintent){// 这里 context 是 Application Context 或 ReceiverRestrictedContextAlertDialog.Builderbuilder=newAlertDialog.Builder(context);builder.show();// 崩溃!}}// ✅ 正确:启动一个 Activity 来显示 DialogpublicclassMyReceiverextendsBroadcastReceiver{@OverridepublicvoidonReceive(Contextcontext,Intentintent){IntentactivityIntent=newIntent(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. 创建 WindowmWindow=newPhoneWindow(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)用它来识别和管控窗口。
窗口层级关系: PhoneWindow(Activity 的根窗口) ↓ 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.javafinalActivityClientRecordr=newActivityClientRecord();r.token=token;// 来自 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,...){mWindow=newPhoneWindow(this);mWindow.setWindowManager(...,token,...);// 把 token 传给 WindowManagermToken=token;}所以当你用 Activity Context 创建 Dialog:
Dialogdialog=newDialog(activity);// activity 的 mToken 有值dialog.show();Dialog 内部会复用 Activity 的 WindowToken,把自己作为子窗口挂载到 Activity 的窗口树下。
如果非要用 Application Context 显示弹窗怎么办
方案 1:用 System Alert 窗口(需要权限)
// AndroidManifest.xml 中声明权限<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>// 代码Dialogdialog=newDialog(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,再在里面显示 DialogIntentintent=newIntent(getApplicationContext(),TransparentDialogActivity.class);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);getApplicationContext().startActivity(intent);总结
| 问题 | 答案 |
|---|---|
| Dialog 为什么需要窗口 | 因为 Dialog 是子窗口,必须挂载到父窗口下 |
| 父窗口怎么识别 | 靠 WindowToken(AMS 分配的 IBinder) |
| Activity 有没有 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。