天天看點

從一個崩潰再談Context

異常資訊

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);
    
    }
               

最終效果如下圖:

從一個崩潰再談Context

上述代碼乍一看沒什麼問題,如果不使用長按分享功能,也一切正常。但選中文本後進行分享,當調起系統分享界面時就發生了崩潰。我們可能有以下經驗:

在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開發?或者自己去看源碼,下面也是來自這篇文章的一張總結圖檔:

從一個崩潰再談Context

刨根問底

既然我們知道原因是由于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的上下文,則視圖的主題為應用的主題,可能會造成不一緻。