Android 在Service中啓動Activity的大坑

在Activity中其中startActivity這個你們應該是很是熟悉的;那麼從service裏面調用startActivity話,會怎麼樣呢?java

會出現下面的異常:android

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

也就是在service裏面啓動Activity的話,必須添加FLAG_ACTIVITY_NEW_TASK flag。app

    那麼下面的話,咱們將從下面幾個方面分析這個問題。ide

    1.    這個異常怎麼產生的?
    2.    解決這個異常後會出現問題?
    3.    爲何Activity.startActivity()不會出現這個問題?
    4.    Android 爲何要這麼設計?ui

下面,一一分析
一.    Context的繼承關係圖this

首先來看一張圖, 這張圖表示了Context裏面的基本繼承關係。spa

​ img.net

1.    最上面的是Context.java,它實際上是一個抽象類,它有兩個重要的子類ContextImpl和ContextWrapper設計

2.    ContextImpl,是Context功能實現的主要類,

3.    ContextWrapper,顧名思義,它只是一個包裝而已。主要功能實現都是經過調用ContextImpl去實現的。

4.    ContextThemeWrapper,包括一些主題的包裝,因爲Service沒有主題,因此直接繼承ContextWrapper;可是Activity就須要繼承ContextThemeWrapper
二.    異常如何產生

找到報錯的代碼

文件:

    frameworks\base\core\Java\Android\app\ContextImpl.java

代碼:

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


在下面的if條件判斷,若是不包含FLAG_ACTIVITY_NEW_TASK就會報這個錯誤

if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
    ...
}    


那麼service.startActivity(Intent intent)怎麼會調用這裏來的呢?

要回答這個問題,咱們分析下service.startActivity()作了什麼,其實,service.startActivity調用的是ContextWrapper.startActivity(),由於service繼承自ContextWrapper

2.  代碼文件

    frameworks\base\core\java\android\content\ContextWrapper.java

代碼:

public void startActivity(Intent intent, Bundle options) {
    mBase.startActivity(intent, options);
}

ContextWrapper.startActivity的話,是直接調用的

mBase.startActivity(intent, options);

那麼這個mBase是什麼呢?又是何時賦值的呢?其實mBase是在ContextWrapper的attachBaseContext的時候初始化的。以下:

protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }

那又是誰調用attachBaseContext的呢?

是在service建立的時候,在ActivityThread裏面調用,以下:

    代碼文件

    frameworks\base\core\java\android\app\ActivityThread.java

代碼:

private void handleCreateService(CreateServiceData data) {
        LoadedApk packageInfo = getPackageInfoNoCheck(
                data.info.applicationInfo, data.compatInfo);
        Service service = null;
        try {
            java.lang.ClassLoader cl = packageInfo.getClassLoader();
            service = (Service) cl.loadClass(data.info.name).newInstance();
        } catch (Exception e) {
            ....
        }
        try {
            if (localLOGV) Slog.v(TAG, "Creating service " + data.info.name);
            ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
            context.setOuterContext(service);
            Application app = packageInfo.makeApplication(false, mInstrumentation);
            service.attach(context, this, data.info.name, data.token, app,
                    ActivityManagerNative.getDefault());
            service.onCreate();
            mServices.put(data.token, service);
            ....
        } catch (Exception e) {
            ...
        }
    }

抽出主要代碼分析ActivityThread. handleCreateService()方法裏面主要作這幾件事

    1 經過pms找到要啓動的Service配置信息,而後經過反射生成Service對象
    2 建立ContextImpl對象,而後調用service.attach方法設置到ContextWrapper.java的mBaseContext變量裏面。

那如今就明白了,service.startActivity()->ContextWrapper.startActivity()->ContextImpl.startActivity()

而後再ContextImpl.startActivity裏面會檢查Intent的參數是否包含FLAG_ACTIVITY_NEW_TASK,從而出現這個異常。
三.    解決這個異常後會出現問題?

有些同窗就會說了,在Service裏面啓動Activity必需要有FLAG_ACTIVITY_NEW_TASK參數,那麼咱們添加上不就能夠了?以下:

intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

那麼這樣會帶來什麼問題呢?

這樣帶來的問題就是在最近任務列表裏面會出現兩個相同的應用程序,好比你是在電話本里面啓動的,那麼最近任務列表就會出現兩個電話本;由於有兩個Task嘛!

那怎麼解決呢?其實也很是好解決,只要在新的Task裏面的Activity裏面配置android:excludeFromRecents=」true」就能夠了。表示這個Activity不會顯示在最近列表裏面。
四.    Activity.startActivity()爲何不出現這個異常呢?

要回答這個問題,須要看下Activity.startActivity()調用到哪裏去了

代碼文件:

    frameworks\base\core\java\android\app\Activity.java

代碼:

public void startActivity(Intent intent) {
   this.startActivity(intent, null);
}

接下來會調用startActivityForResult()->而後一路調用到Ams去啓動Activity;

原來如此,Activity重寫了startActivity()方法…
五.    Android 爲何要這麼設計?

那如今來回答這個問題,爲何Android在Service 裏面啓動Activity要強制規定使用參數FLAG_ACTIVITY_NEW_TASK呢?

咱們能夠來作這樣一個假設,咱們有這樣一個需求:

咱們在電話本里面啓動一個Service,而後它執行5分鐘後,啓動一個Activity

那麼頗有可能用戶在5分鐘後已經不在電話本程序裏面操做了,有可能去上網,打開瀏覽器程序了。

5分鐘後,此時當前的Task是瀏覽器的task,那麼彈出Activity,若是這個Activity在當前Task的話,也就是瀏覽器的Task;那麼用戶就會以爲莫名其妙;由於彈出的Activity和瀏覽器在一個Task,原本這個Activity應該屬於電話本的。

因此,對於Service而言,乾脆強制定義啓動的Activity要建立一個新的Task.

這種設計,我以爲仍是比較合理的。
六. 樣例代碼:

Intent dialogIntent = new Intent(getBaseContext(), YourActivity.class);   
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);   
getApplication().startActivity(dialogIntent);
Service 啓動 Dialog

因爲 Dialog 是依賴於 Activity 存在的,因此對於從 Service 啓動 Dialog 主要有兩種方法:

    首先啓動一個 半透明的 Activity,而後在 Activity 裏啓動 Dialog。(或者直接使用 Activity 來仿寫一個 Activity)
    使用 WindowManager 實現

使用 WindowManager 時須要注意,此時的 Dialog 是 SYSTEM 級別的,若是程序在後臺時啓動這個 Dialog,Dialog 會浮在桌面上。(使用小米等有本身權限管理的系統時,須要申請必定權限才能夠在桌面顯示這個 Dialog,不然只能在本身 APP 前臺時才顯示)
使用 WindowManager 實現

AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage("是否接受文件?")
.setPositiveButton("是", new DialogInterface.OnClickListener() {
@Override
publicvoid onClick(DialogInterface dialog, int which) {
}
}).setNegativeButton("否", new OnClickListener() {
@Override
publicvoid onClick(DialogInterface dialog, int which) {
}
});
AlertDialog ad = builder.create();
// ad.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG); //系統中關機對話框就是這個屬性
ad.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
ad.setCanceledOnTouchOutside(false); //點擊外面區域不會讓dialog消失
ad.show();1234567891011121314151612345678910111213141516


權限

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />11

    1

使用 Activity 實現

Activity 半透明主題

<style name="DialogTransparent" parent="@android:style/Theme.Dialog">
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowAnimationStyle">@android:style/Animation</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:windowIsFloating">false</item>
        <item name="android:windowIsTranslucent">true</item>
    </style>1234567812345678

 

或者直接使用

@android:style/Theme.Dialog

相關文章
相關標籤/搜索