在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