context.startActivity(intent) 7.0以下與7.0及以上的區別

相信Android開發都遇到過這樣一個報錯信息

04-09 15:55:08.165: E/AndroidRuntime(3403): 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的Context調用startActivity方法時,Intent添加一個FLAG_ACTIVITY_NEW_TASK的flag,代碼如下

intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

 

但是細心的同學會發現Android7.0及以上的機型即使沒有添加以上代碼,也不會崩潰。這是怎麼回事呢?

這就涉及到activity的startActivity和context的startActivity方法之間的區別,Activity也是Context的子類,但是Activity本身實現了startActivity方法,所以兩個是不同的。

 

activity的startActivity方法最終會調用Activity類的startActivityForResult方法,代碼如下

 public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
        if (mParent == null) {
            options = transferSpringboardActivityOptions(options);
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
            if (ar != null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            }
            if (requestCode >= 0) {
                // If this start is requesting a result, we can avoid making
                // the activity visible until the result is received.  Setting
                // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
                // activity hidden during this time, to avoid flickering.
                // This can only be done when a result is requested because
                // that guarantees we will get information back when the
                // activity is finished, no matter what happens to it.
                mStartedActivity = true;
            }
​
            cancelInputsAndStartExitTransition(options);
            // TODO Consider clearing/flushing other event sources and events for child windows.
        } else {
            if (options != null) {
                mParent.startActivityFromChild(this, intent, requestCode, options);
            } else {
                // Note we want to go through this method for compatibility with
                // existing applications that may have overridden it.
                mParent.startActivityFromChild(this, intent, requestCode);
            }
        }
    }

 

context的 startActivity方法最終會調用ContextImpl的startActivity方法,代碼如下

@Override
public void startActivity(Intent intent, Bundle options) {
    warnIfCallingFromSystemProcess();
    if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
        throw new AndroidRuntimeException(
                "Calling startActivity() from outside of an Activity "
                + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                + " Is this really what you want?");
    }
    mMainThread.getInstrumentation().execStartActivity(
            getOuterContext(), mMainThread.getApplicationThread(), null,
            (Activity) null, intent, -1, options);
}

7.0及以下

@Override
public void startActivity(Intent intent, Bundle options) {
    warnIfCallingFromSystemProcess();
​
    // Calling start activity from outside an activity without FLAG_ACTIVITY_NEW_TASK is
    // generally not allowed, except if the caller specifies the task id the activity should
    // be launched in.
    if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0
            && options != null && ActivityOptions.fromBundle(options).getLaunchTaskId() == -1) {
        throw new AndroidRuntimeException(
                "Calling startActivity() from outside of an Activity "
                + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                + " Is this really what you want?");
    }
    mMainThread.getInstrumentation().execStartActivity(
            getOuterContext(), mMainThread.getApplicationThread(), null,
            (Activity) null, intent, -1, options);
}

 

從上面代碼可以看出,Activity的startActivity方法是不會拋出文章開始說的那個錯的;問題集中在非Activity的Context調用startActivity方法,在7.0之前如果Intent沒有添加flag:FLAG_ACTIVITY_NEW_TASK,程序是必崩的,7.0及以上如果調用startActivity(intent)方法,是不會崩的,因爲此時options爲空。一般爲了兼容7.0以下版本,我們還是得添加flag的。

startActivity(intent, null);

 

注意7.0增加了兩個判斷條件

if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0
        && options != null && ActivityOptions.fromBundle(options).getLaunchTaskId() == -1) {
    throw new AndroidRuntimeException(
            "Calling startActivity() from outside of an Activity "
            + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
            + " Is this really what you want?");
}

 

到這裏基本上文章就該結束了,但是我們項目在線上還是遇到文章開始說的那個問題,有一些崩潰,都是7.0以下的機型。經過一番尋找,終於定位到有問題的代碼,原因是我們用到了Intent的一個方法createChooser(Intent target, CharSequence title)

Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_TEXT, text);
shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
shareIntent.setType("text/plain");
Utils.getContext().startActivity(Intent.createChooser(shareIntent, "分享到"));

 

從代碼上看到我們明明給Intent添加了flag,還是會崩潰

shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

 

我們跟進Intent.createChooser方法裏看看

public static Intent createChooser(Intent target, CharSequence title, IntentSender sender) {
    Intent intent = new Intent(ACTION_CHOOSER);
    intent.putExtra(EXTRA_INTENT, target);
    if (title != null) {
        intent.putExtra(EXTRA_TITLE, title);
    }
​
    if (sender != null) {
        intent.putExtra(EXTRA_CHOSEN_COMPONENT_INTENT_SENDER, sender);
    }
​
    // Migrate any clip data and flags from target.
    int permFlags = target.getFlags() & (FLAG_GRANT_READ_URI_PERMISSION
            | FLAG_GRANT_WRITE_URI_PERMISSION | FLAG_GRANT_PERSISTABLE_URI_PERMISSION
            | FLAG_GRANT_PREFIX_URI_PERMISSION);
    if (permFlags != 0) {
        ClipData targetClipData = target.getClipData();
        if (targetClipData == null && target.getData() != null) {
            ClipData.Item item = new ClipData.Item(target.getData());
            String[] mimeTypes;
            if (target.getType() != null) {
                mimeTypes = new String[] { target.getType() };
            } else {
                mimeTypes = new String[] { };
            }
            targetClipData = new ClipData(null, mimeTypes, item);
        }
        if (targetClipData != null) {
            intent.setClipData(targetClipData);
            intent.addFlags(permFlags);
        }
    }
​
    return intent;
}

 

首先這個方法先把原Intent存到新Intent的extra上,

intent.putExtra(EXTRA_INTENT, target);

 

然後permFlags值爲0,就這樣,新Intent的flag就沒了,所以返回來的Intent的flag值爲0,雖然extra裏的Intent的flag值爲Intent.FLAG_ACTIVITY_NEW_TASK,但是沒有,取flag值時不是從extra裏的Intent取值。

int permFlags = target.getFlags() & (FLAG_GRANT_READ_URI_PERMISSION
        | FLAG_GRANT_WRITE_URI_PERMISSION | FLAG_GRANT_PERSISTABLE_URI_PERMISSION
        | FLAG_GRANT_PREFIX_URI_PERMISSION);

 

有點坑吧,所以如果使用了Intent.createChooser()方法後還得加上addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

Utils.getContext().startActivity(Intent.createChooser(shareIntent, "分享到").addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));

建議以後如果有非Activity的Context調用startActivity()方法,一定要用7.0以下的機型測試一下,我們手上的測試大部分是7.0的,加上那段代碼的使用場景很少,所以導致了線上出現這個bug。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章