从一个崩溃再谈Context

本文深入分析了在Android中使用TextView的分享功能时出现的一个常见异常:在非Activity上下文中启动Activity未添加FLAG_ACTIVITY_NEW_TASK标志。文章通过示例代码展示了如何复现此问题,并提供了正确的LayoutInflater使用方式以避免此类崩溃。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

异常信息

AndroidRuntime: FATAL EXCEPTION: main Process: com.aspook.contexttest,
PID: 22578
android.util.AndroidRuntimeException: Calling startActivity() from
outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK
flag. Is this really what you want?
at android.app.ContextImpl.startActivity(ContextImpl.java:672)
at android.app.ContextImpl.startActivity(ContextImpl.java:659)
at
android.content.ContextWrapper.startActivity(ContextWrapper.java:331)
at android.widget.TextView.shareSelectedText(TextView.java:9493)
at android.widget.TextView.onTextContextMenuItem(TextView.java:9211)
at
android.widget.Editor TextActionModeCallback.onActionItemClicked(Editor.java:3249)atcom.android.internal.policy.PhoneWindow DecorView ActionModeCallback2Wrapper.onActionItemClicked(PhoneWindow.java:3540)atcom.android.internal.view.FloatingActionMode 3.onMenuItemSelected(FloatingActionMode.java:85)
at
com.android.internal.view.menu.MenuBuilder.dispatchMenuItemSelected(MenuBuilder.java:761)
at
com.android.internal.view.menu.MenuItemImpl.invoke(MenuItemImpl.java:152)
at
com.android.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:904)
at
com.android.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:894)
at
com.android.internal.view.FloatingActionMode 4.onMenuItemClick(FloatingActionMode.java:111)atcom.android.internal.widget.FloatingToolbar FloatingToolbarMainPanel 1.onClick(FloatingToolbar.java:1015)atandroid.view.View.performClick(View.java:5204)atandroid.view.View PerformClick.run(View.java:21155)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5422)
at java.lang.reflect.Method.invoke(Native Method)
at
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

上面是整个异常的堆栈信息,我们先来看主要的部分:

android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

这个异常相信大家都见过,也知道其发生的原因以及如何修复。不过我猜测大家遇到此异常多是因为在自己的代码里没有用Activity的上下文去startActivity且没有添加FLAG_ACTIVITY_NEW_TASK这个flag。而我遇到的这个异常却不是由于人为使用错误的Context去startActivity造成的,而是系统自己调用startActivity引起的。

原因分析

其实从用代码去启动Activity的层面上讲,自己写的话不会犯上述错误。情况是这样的,对于一个EditText,长按其内容可以弹出系统的上下文菜单,支持复制、全选等,然后在新的系统里面开始支持分享了。当选中一些文本,点击分享的时候,本应该调起系统的分享界面,而这时候就报此异常了,异常堆栈信息也指出了该事实:

at android.widget.TextView.shareSelectedText(TextView.java:9493)

由于老版本的Android不支持分享功能,所以一直没有发现该异常,最根本原因还是由于代码写的有问题。

下面就来复现一下这个异常。

  1. 界面很简单,就一个EditText

  2. 界面布局利用LayoutInflater引入,代码如下

    @Override
    protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
    
       LayoutInflater mLayoutInflater = LayoutInflater.from(getApplicationContext());
       View root = mLayoutInflater.inflate(R.layout.activity_second, null);
       setContentView(root);
    
    }

最终效果如下图:

这里写图片描述

上述代码乍一看没什么问题,如果不使用长按分享功能,也一切正常。但选中文本后进行分享,当调起系统分享界面时就发生了崩溃。我们可能有以下经验:

在Activity中通过startActivity(intent)可以正常启动另一Activity,而通过getApplicationContext().startActivity(intent)则会报上述相同错误(如果没设置FLAG_ACTIVITY_NEW_TASK的话)。因此可以判断原因必定是由于使用了错误的Context引起。仔细检查上述代码,并没有显式调用startActivity的代码,只有

LayoutInflater mLayoutInflater = LayoutInflater.from(getApplicationContext());

这一句引入了Context对象,因此可以推断是通过LayoutInflater引入布局时错用了Application的Context,而这里应该使用Activity的上下文对象。

将上面错误代码改写如下后,果然不再报错。

LayoutInflater mLayoutInflater = LayoutInflater.from(this);

关于Context的分析及使用,网上已有太多文章,这里就不再分析,请参考Context都没弄明白,还怎么做Android开发?或者自己去看源码,下面也是来自这篇文章的一张总结图片:

这里写图片描述

刨根问底

既然我们知道原因是由于LayoutInflater时传入了错误的Context,那么LayoutInflater中的Context是怎样传到TextView(EditView继承自TextView)中的呢?

从错误日志信息

at android.widget.TextView.shareSelectedText(TextView.java:9493)

定位到TextView的源码:

private void shareSelectedText() {
    String selectedText = getSelectedText();
    if (selectedText != null && !selectedText.isEmpty()) {
        Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
        sharingIntent.setType("text/plain");
        sharingIntent.removeExtra(android.content.Intent.EXTRA_TEXT);
        sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, selectedText);
        getContext().startActivity(Intent.createChooser(sharingIntent, null));
        stopTextActionMode();
    }
}

倒数第二行getContext().startActivity(Intent.createChooser(sharingIntent, null));就是这里调用的系统分享界面。

接着看getContext(),定位到View.java源码中

/**
 * Returns the context the view is running in, through which it can
 * access the current theme, resources, etc.
 *
 * @return The view's Context.
 */
@ViewDebug.CapturedViewProperty
public final Context getContext() {
    return mContext;
}

继续跟踪发现,mContext是在View的构造函数中赋值的:

/**
 * Simple constructor to use when creating a view from code.
 *
 * @param context The Context the view is running in, through which it can
 *        access the current theme, resources, etc.
 */
public View(Context context) {
    mContext = context;
    ……
}

那么我们接下来就是要分析LayoutInflater生成View的过程了,主要代码都在LayoutInflater.java中,逻辑细节比较繁琐,但最终是通过反射调用View的构造函数创建一个View对象,因此Context就传到了View中。如果对LayoutInflater生成View的流程感兴趣,可以参考LayoutInflater&LayoutInflaterCompat源码解析,这里不再分析,对于LayoutInflater生成View的需求,建议使用Activity的上下文对象,除了可以避免上述隐藏的崩溃外,还可以让视图的主题跟Activity保持一致,如果使用Application的上下文,则视图的主题为应用的主题,可能会造成不一致。

<think>我们正在讨论的是Android中的Context。根据引用[3],Application是一个单例实例,运行在应用程序进程中。无论从哪里访问,你都会得到同一个实例。 用户的问题:如何在Android中创建一个唯一的Context实例? 重要提示:在Android中,Context一个抽象类,我们不能直接实例化它。系统会提供Context的实现(如Activity、Service、Application等),并且我们只能通过这些组件来获取Context。 但是,用户可能想要一个全局唯一的Context实例(通常指的是Application Context),因为它在整个应用生命周期内都是唯一的,并且不会因为Activity的销毁而改变。 因此,我们可以这样理解用户的需求:如何获取一个全局唯一的Context实例(即Application Context)? 步骤: 1. 自定义Application类,并在其中维护一个静态的Application实例(单例模式)。 2.AndroidManifest.xml中注册这个自定义Application类。 3. 通过这个自定义Application类来获取全局唯一的Context。 具体实现: 第一步:创建自定义Application类 ```java public class MyApp extends Application { private static MyApp instance; @Override public void onCreate() { super.onCreate(); instance = this; } public static Context getContext() { return instance.getApplicationContext(); } } ``` 第二步:在AndroidManifest.xml中指定Application类 ```xml <manifest ...> <application android:name=".MyApp" ...> ... </application> </manifest> ``` 第三步:在任何地方使用 ```java Context appContext = MyApp.getContext(); // 获取全局唯一的Application Context ``` 注意:这样获取的Context是Application Context,它和Activity Context的区别在于: - Application Context的生命周期是整个应用,而Activity Context的生命周期与Activity绑定。 - 使用Application Context不会导致内存泄漏(因为它不会持有Activity的引用)。 - 但是,有些操作必须使用Activity Context(例如显示对话框、启动Activity等需要UI相关操作)。 因此,用户所说的“唯一”可能是指在整个应用生命周期内唯一的Context实例,那么Application Context就是最佳选择。 另外,根据引用[3]:Application是一个单例实例,运行在应用程序进程中。无论通过Activity或Service的getApplication(),还是其他继承自Context的对象的getApplicationContext(),你都会得到同一个实例。 所以,我们也可以直接通过Activity的getApplicationContext()来获取,但自定义Application类的方式可以方便在非Activity类中获取。 回答用户问题:在Android中,你不能直接创建Context实例,但可以通过自定义Application类来获取一个全局唯一的Application Context实例。 相关问题: 1. 如何避免在静态变量中持有Context导致的内存泄漏? 2. Application Context和Activity Context有什么区别? 3. 在工具类中如何安全地使用Context? 4. 哪些场景必须使用Activity Context? 5. 如何理解Android中的Context?</think>在 Android 开发中,**不能直接创建 `Context` 的实例**,因为它是系统管理的核心对象。但可以通过以下方式获取全局唯一的 `Application Context` 实例(整个应用生命周期内唯一): ### 一、获取全局唯一 Context 的步骤 1. **创建自定义 Application 类** ```java public class MyApp extends Application { private static MyApp instance; @Override public void onCreate() { super.onCreate(); instance = this; // 初始化单例实例 } // 全局获取 Application Context public static Context getAppContext() { return instance.getApplicationContext(); } } ``` 2. **在 `AndroidManifest.xml` 中注册** ```xml <application android:name=".MyApp" <!-- 关键:指定自定义类 --> ... > <activity ... /> </application> ``` 3. **全局使用** ```java // 在任何地方获取唯一 Context Context appContext = MyApp.getAppContext(); // 使用示例:访问资源 String appName = appContext.getString(R.string.app_name); ``` ### 二、关键特性说明 1. **唯一性保证** `Application Context` 是进程级别的单例对象,通过 `getApplicationContext()` 获取的始终是**同一个实例**[^3]。 2. **生命周期** - 从应用启动创建,到进程终止销毁 - 比 `Activity Context` 生命周期更长且稳定 3. **适用场景** | 场景 | 推荐 Context 类型 | |--------------------------|-------------------| | 全局资源访问(字符串/颜色) | Application Context ✅ | | 系统服务(定位/存储) | Application Context ✅ | | 启动 Activity/显示对话框 | Activity Context ❗ | ### 三、注意事项 1. **禁止内存泄漏** ```java // 错误:静态变量持有 Activity Context public class Util { static Context context; // 会导致内存泄漏 } // 正确:使用 Application Context public class Util { static Context getContext() { return MyApp.getAppContext(); // 安全 } } ``` 2. **UI 操作限制** `Application Context` **不能用于 UI 相关操作**: ```java // 错误:用 Application Context 显示 Toast Toast.makeText(MyApp.getAppContext(), "Hello", Toast.LENGTH_SHORT).show(); // 可能崩溃 // 正确:使用 Activity Context Toast.makeText(activityContext, "Hello", Toast.LENGTH_SHORT).show(); ``` 3. **多进程应用** 如果应用配置了多进程(`android:process`),每个进程会有独立的 `Application` 实例,此时需通过进程通信解决唯一性问题。 > **最佳实践**:工具类、后台服务、广播接收器等非 UI 组件优先使用 `Application Context`,避免持有 `Activity` 引用导致内存泄漏[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值