Android随笔-Context

📅 2026/7/15 21:43:24 👁️ 阅读次数 📝 编程学习
Android随笔-Context

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(应用上下文)

// 获取方式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()
启动 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 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
启动 ForResultstartActivityForResult()需要 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

不适用场景:

场景原因
创建 DialogService 没有窗口令牌,无法显示 Dialog
启动普通 Activity需要FLAG_ACTIVITY_NEW_TASK,否则崩溃
布局 Inflate(带主题)没有主题

4. ContextWrapper(包装上下文)

// 常见使用方式ContextWrapperwrapper=newContextWrapper(baseContext);ContextThemeWrapperthemedContext=newContextThemeWrapper(baseContext,R.style.MyTheme);

特点:

  • 代理模式,包装另一个 Context
  • 可修改主题、资源等
  • ApplicationService都继承自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 使用场景对照表

操作ApplicationActivityService说明
startActivity()⚠️ 需 NEW_TASK⚠️ 需 NEW_TASKActivity 最自然
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 继承关系:ApplicationContextWrapperContext// Activity 继承关系:ActivityContextThemeWrapperContextWrapperContext↑ 这里多了ContextThemeWrapper,携带了窗口信息

Activity 在创建时,AMS 会分配一个IBinder token给它,这个 token 就是 WindowToken 的来源:

// ActivityThread.javafinalActivityClientRecordr=newActivityClientRecord();r.token=token;// 来自 AMS 的 WindowToken

Application 没有参与 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 的mTokenattach()方法中被设置:

// 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。