在客户反馈中遇到一个问题,Home键把应用置于后台,外部通过对接协议广播的方式,应用内接收到广播之后通过startActivity把应用从后台调起到前台,只是普通的逻辑调用,但是应用在后台停留了10S之后,应用才回到前台,并先出现了黑屏,之后才显示应用界面的问题。
从Activity的生命周期Log分析,在调用了startActivity之后,已经走到了onResume,但是过了5S之后,应用才显示出来。
一开始是怀疑系统方的问题,系统方分析之后,单单startActivity就花费了3S时间,之后还有应用内的Activity切换又耗费额外的2S,这5S时间可以看作是主线程的卡顿导致。但另外的5S是在onResume之后,过了5S才显示界面,排查代码并没有在onResume里面做耗时的逻辑处理。
问了同事刚好有遇到类似的问题,在配置了STOP_APP_SWITCHES权限之后,此问题消失。
<uses-permission android:name=\"android.permission.STOP_APP_SWITCHES\"/>
一、STOP_APP_SWITCHES权限说明
在网上找的博客中,说这个权限这样的设计是为了避免在用户毫不知情的情况下突然中断用户正在进行的工作,博文中贴的官网链接地址已失效,在Android官网中搜索也没找到这个权限相关的说明。
那就直接看这个权限在源码当中的注释比较恰当点(以下从6.0 framework源码中截取),注释说明的功能是:允许一个应用通知ActivityManager暂停应用的切换,把应用置于一个特殊的模式下,避免应用在一些关键的UI场景下被快速地切换掉,比如按Home键显示界面的时候。
<!-- @SystemApi Allows an application to tell the activity manager to temporarily stop application switches, putting it into a special mode that prevents applications from immediately switching away from some critical UI such as the home screen. @hide --> <permission android:name="android.permission.STOP_APP_SWITCHES" android:protectionLevel="signature|privileged" />
二、STOP_APP_SWITCHES权限源码解析(基于Android6.0)
1. PhoneWindowManager.java中stopAppSwitches调用
从PhoneWindowManager类中stopAppSwitches调用的地方可以看到,当按Home键或其他热键回桌面的时候就会调用ActivityManagerService类中的stopAppSwitches方法。
/** * A home key -> launch home action was detected. Take the appropriate action * given the situation with the keyguard. */ void launchHomeFromHotKey(final boolean awakenFromDreams, final boolean respectKeyguard) { if (respectKeyguard) { if (!mHideLockScreen && mKeyguardDelegate.isInputRestricted()) { // when in keyguard restricted mode, must first verify unlock // before launching home mKeyguardDelegate.verifyUnlock(new OnKeyguardExitResult() { @Override public void onKeyguardExitResult(boolean success) { if (success) { try { ActivityManagerNative.getDefault().stopAppSwitches(); } catch (RemoteException e) { } sendCloseSystemWindows(SYSTEM_DIALOG_REASON_HOME_KEY); startDockOrHome(true /*fromHomeKey*/, awakenFromDreams); } } }); return; } } // no keyguard stuff to worry about, just launch home! try { ActivityManagerNative.getDefault().stopAppSwitches(); } catch (RemoteException e) { } }
/** * goes to the home screen * @return whether it did anything */ boolean goHome() { if (false) { // This code always brings home to the front. try { ActivityManagerNative.getDefault().stopAppSwitches(); } catch (RemoteException e) { } } else { // This code brings home to the front or, if it is already // at the front, puts the device to sleep. try { if (SystemProperties.getInt("persist.sys.uts-test-mode", 0) == 1) { } else { ActivityManagerNative.getDefault().stopAppSwitches(); } } catch (RemoteException ex) { // bummer, the activity manager, which is in this process, is dead } } return true; }
2. ActivityManagerService.java 类中延迟五秒的处理逻辑
在调用stopAppSwitches之后,通过Handler延迟5s启动延时PendingActivityLaunch数组轮询。
// Amount of time after a call to stopAppSwitches() during which we will // prevent further untrusted switches from happening. static final long APP_SWITCH_DELAY_TIME = 5*1000; @Override public void stopAppSwitches() { if (checkCallingPermission(Manifest.permission.STOP_APP_SWITCHES) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires permission " + Manifest.permission.STOP_APP_SWITCHES); } synchronized(this) { mAppSwitchesAllowedTime = SystemClock.uptimeMillis() + APP_SWITCH_DELAY_TIME; mDidAppSwitch = false; mHandler.removeMessages(DO_PENDING_ACTIVITY_LAUNCHES_MSG); Message msg = mHandler.obtainMessage(DO_PENDING_ACTIVITY_LAUNCHES_MSG); mHandler.sendMessageDelayed(msg, APP_SWITCH_DELAY_TIME); } } //延迟5s消息执行之后,调用ActivityStackSupervisor类中的doPendingActivityLaunchesLocked方法 case DO_PENDING_ACTIVITY_LAUNCHES_MSG: { synchronized (ActivityManagerService.this) { mStackSupervisor.doPendingActivityLaunchesLocked(true); } } break; //ActivityStackSupervisor类中调用的STOP_APP_SWITCHES权限检查 boolean checkAppSwitchAllowedLocked(int sourcePid, int sourceUid, int callingPid, int callingUid, String name) { if (mAppSwitchesAllowedTime < SystemClock.uptimeMillis()) { return true; } int perm = checkComponentPermission( Manifest.permission.STOP_APP_SWITCHES, sourcePid, sourceUid, -1, true); if (perm == PackageManager.PERMISSION_GRANTED) { return true; } // If the actual IPC caller is different from the logical source, then // also see if they are allowed to control app switches. if (callingUid != -1 && callingUid != sourceUid) { perm = checkComponentPermission( Manifest.permission.STOP_APP_SWITCHES, callingPid, callingUid, -1, true); if (perm == PackageManager.PERMISSION_GRANTED) { return true; } } Slog.w(TAG, name + " request from " + sourceUid + " stopped"); return false; }
3. ActivityStackSupervisor类中的逻辑处理
startActivityUncheckedLocked是实际执行启动页面的地方,这边就不贴代码,startActivityLocked是在startActivityUncheckedLocked之前调用的,直接通过代码startActivity启动页面或者通过Pending的方式启动Activity都会先走startActivityLocked。
如果所启动的App没有配置STOP_APP_SWITCHES权限,就会把要启动的Activity加入到延时的mPendingActivityLaunches数组中。
final void doPendingActivityLaunchesLocked(boolean doResume) { while (!mPendingActivityLaunches.isEmpty()) { PendingActivityLaunch pal = mPendingActivityLaunches.remove(0); startActivityUncheckedLocked(pal.r, pal.sourceRecord, null, null, pal.startFlags, doResume && mPendingActivityLaunches.isEmpty(), null, null); } }
final int startActivityLocked(IApplicationThread caller, Intent intent, String resolvedType, ActivityInfo aInfo, IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid, String callingPackage, int realCallingPid, int realCallingUid, int startFlags, Bundle options, boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity, ActivityContainer container, TaskRecord inTask) {
if (voiceSession == null && (stack.mResumedActivity == null || stack.mResumedActivity.info.applicationInfo.uid != callingUid)) { // 调用ActivityManagerService中的方法进行权限检查 if (!mService.checkAppSwitchAllowedLocked(callingPid, callingUid, realCallingPid, realCallingUid, "Activity start")) { PendingActivityLaunch pal = new PendingActivityLaunch(r, sourceRecord, startFlags, stack); mPendingActivityLaunches.add(pal); ActivityOptions.abort(options); return ActivityManager.START_SWITCHES_CANCELED; } } }
三、STOP_APP_SWITCHES权限相关问题的解决方法
1. 集成到system/app/目录下的系统级别应用
既然是权限问题,直接在AndroidManifest.xml文件中适配权限即可:
<uses-permission android:name=\"android.permission.STOP_APP_SWITCHES\"/>
2. 第三方应用
修改原有的startActivity实现:
Intent intent = new Intent(context, A.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent);
把上面的逻辑改为用PendingIntent实现:
Intent intent = new Intent(context, A.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); try { pendingIntent.send(); } catch (PendingIntent.CanceledException e) { e.printStackTrace(); }
TIPS:为什么用PendingIntent可以避开STOP_APP_SWITCHES权限限制?
从ActivityManagerService类中checkAppSwitchAllowedLocked判断逻辑中可以看出拿sourcePid和callingUid来做权限检查,直接看PendingIntent类中getActivity的源码实现,可以看到拿了UserHandle.myUserId()来做参数,也就是系统进程的UID参数。这就相当于借用系统的权限来做启动Activity的操作。
public static PendingIntent getActivity(Context context, int requestCode, @NonNull Intent intent, @Flags int flags, @Nullable Bundle options) { String packageName = context.getPackageName(); String resolvedType = intent != null ? intent.resolveTypeIfNeeded( context.getContentResolver()) : null; try { intent.migrateExtraStreamToClipData(); intent.prepareToLeaveProcess(); IIntentSender target = ActivityManagerNative.getDefault().getIntentSender( ActivityManager.INTENT_SENDER_ACTIVITY, packageName, null, null, requestCode, new Intent[] { intent }, resolvedType != null ? new String[] { resolvedType } : null, flags, options, UserHandle.myUserId()); return target != null ? new PendingIntent(target) : null; } catch (RemoteException e) { } return null; }
四、参考资料
Start activity from service takes too long
Starting an activity from a service after HOME button pressed without the 5 seconds delay
关于在 Service 或 BroadcastReceiver 中 startActivity 的问题
相关阅读:
转载请注明出处:陈文管的博客 – Android Home键之后后台启动Activity延迟5秒
扫码或搜索:文呓
微信公众号 扫一扫关注