Android 在 Service 启动 Activity 和 Dialog

时间:2022-11-01 13:40:01

Service 启动 Activity

全文转载自《Android异常之Service启动Activity》

在 Activity 中其中 startActivity 这个大家应该是非常熟悉的。那么从 Service 里面调用 startActivity 话,会怎么样呢?

会出现下面的异常:

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 “FLAG_ACTIVITY_NEW_TASK”。

那么下面的话,我们将从下面几个方面分析这个问题。
1. 这个异常怎么产生的?
2. 解决这个异常后会出现问题?
3. 为什么 Activity.startActivity() 不会出现这个问题?

Context的继承关系图

首先来看一张图, 这张图表示了 Context 里面的基本继承关系。

Android 在 Service 启动 Activity 和 Dialog

  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

代码文件

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() 方法

Service 启动 Dialog

由于 Dialog 是依赖于 Activity 存在的,所以对于从 Service 启动 Dialog 主要有两种方法:

  1. 首先启动一个 半透明的 Activity,然后在 Activity 里启动 Dialog。(或者直接使用 Activity 来仿写一个 Activity)
  2. 使用 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();

权限

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

使用 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>

或者直接使用

@android:style/Theme.Dialog