美文网首页
监听Home键之后启动一个后台Activity延迟5s

监听Home键之后启动一个后台Activity延迟5s

作者: 福later | 来源:发表于2019-08-27 15:55 被阅读0次

    问题引入

    最近做项目时遇到这样一个问题,原本需求是这样的:
    在一个播放界面,播放时退出当前界面,或者点击home键时,窗口上会显示一个小的悬浮窗,点击这个悬浮窗,就会跳转至播放界面。
    很显然这个悬浮窗是全局的,即时程序退至后台,依然坚挺的显示在界面只上。
    然后测试发现这样一个问题:

    当点击home之后,立即点击悬浮窗跳转,会有一段时间的延迟才会跳转。如果放置一段时间再进行点击,则能立即跳转,没有问题。
    当点击返回,finish掉当前播放页,不管是立即还是放置一会儿再点击,都是ok的。
    • 附上跳转代码
    Intent intent = new Intent(applicationContext, PlayActivity.class);
    intent.putExtra(InteractionFmMainActivity.INFO_ID_KEY, PlayActivity.sParamsIdKey);
    intent.putExtra(InteractionFmMainActivity.INFO_TYPE_KEY,PlayActivity.sParamsInfoTypeKey);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
    applicationContext.startActivity(intent);
    
    

    WHY

    通过搜索引擎,得知这原来是Google官方就是这么设置的!

    • 不从后台启动 Activity 准则
      在谷歌的 Android API Guides 中,特意提醒开发者不要在后台启动 activity,包括在 Service 和 BroadcastReceiver 中,这样的设计是为了避免在用户毫不知情的情况下突然中断用户正在进行的工作,在 http://developer.android.com/guide/practices/seamlessness.html#interrupt 中有如下解释:

    That is, don't call startActivity() from BroadcastReceivers or Services running in the background. Doing so will interrupt whatever application is currently running, and result in an annoyed user. Perhaps even worse, your Activity may become a "keystroke bandit" and receive some of the input the user was in the middle of providing to the previous Activity. Depending on what your application does, this could be bad news.

    • 需要违反“不从后台启动 Activity”准则的特例
      特例:即便如此,手机厂商的开发者们在开发基于系统级的应用的时候,可能仍然需要有从 Service 或 BroadcastReceiver 中 startActivity 的需求,往往这样的前提是连这样的 Service 或 BroadcastReceiver 也是由用户的某些操作而触发的,Service 或 BroadcastReceiver 只是充当了即将启动 activity 之前的一些代理参数检查工作以便决定是否需要 start 该 activity。
      除非是上述笔者所述的特殊情况,应用开发者都应该遵循 “不要从后台启动 Activity”准则。
      一个需要特别注意的问题是,特例中所述的情况还会遇到一个问题,就是当通过 home 键将当前 activity 置于后台时,任何在后台startActivity 的操作都将会延迟 5 秒,除非该应用获取了 "android.permission.STOP_APP_SWITCHES" 权限。
      关于延迟 5 秒的操作在 com.android.server.am.ActivityManagerService 中的 stopAppSwitches() 方法中,系统级的应用当获取了 "android.permission.STOP_APP_SWITCHES" 后将不会调用到这个方法来延迟通过后台启动 activity 的操作,事实上 android 原生的 Phone 应用就是这样的情况,它是一个获取了"android.permission.STOP_APP_SWITCHES" 权限的系统级应用,当有来电时,一个从后台启动的 activity 将突然出现在用户的面前,警醒用户有新的来电,这样的设计是合理的。
      所以,当你需要开发类似 Phone 这样的应用时,需要做如下工作:
      1. root 你的手机;
      2. 在 AndroidManifest.xml 中添加 "android.permission.STOP_APP_SWITCHES" 用户权限;
      3. 将你开发的应用程序 push 到手机的 /system/app 目录中。

    解决方案

    显然上述的解决方案是行不通的,光是要求root手机,就有点过分了,试问哪个用户会为了你一个app大费周章去root手机,简直是得不偿失。那么真的就无计可施了吗?
    答案当然是NO,不然鄙人也不会在这做这个问题记录

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

    他将intent用PendingIntent包裹后,进行启动,于是我也按照这方法修改了自己的代码:

    Intent intent = new Intent(applicationContext, PlayActivity.class);
    intent.putExtra(InteractionFmMainActivity.INFO_ID_KEY, PlayActivity.sParamsIdKey);
    intent.putExtra(InteractionFmMainActivity.INFO_TYPE_KEY,PlayActivity.sParamsInfoTypeKey);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
    try {
          PendingIntent pendingIntent = PendingIntent.getActivity(ApplicationGlobal.getGlobalContext(), 0, intent, 0);
                            pendingIntent.send();
         } catch (Exception e) {
                            e.printStackTrace();
         }
    
    

    经测试后确实是完美解决了问题,感谢这位大神的解答!

    刨根问底

    那么究竟为什么会有这个问题呢,下面我们从源码的角度进行剖析。

    1.事件分发前的拦截过程
    Home事件在分发前的关键拦截过程:

    ......
    if (keyCode == KeyEvent.KEYCODE_HOME) {
                if (!down) {
                    .........
                    launchHomeFromHotKey();
                    return -1;
    
                }
    
            ........
    
            }
    
        }
    
    void launchHomeFromHotKey() {
            .....
             try {
                    ActivityManagerNative.getDefault().stopAppSwitches();
                    } catch (RemoteException e) {
                    }
             .....
    
     }
    
    

    最后会走到ActivityManagerService的stopAppSwitches()方法

    public void stopAppSwitches() {
            if (checkCallingPermission(android.Manifest.permission.STOP_APP_SWITCHES)
                    != PackageManager.PERMISSION_GRANTED) {
                throw new SecurityException("Requires permission "
                        + android.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);
            }
    
        }
    
    

    2.startActivity的启动流程
    关于启动流程,网上已经有很多的相关资料,在这里我们只分析ActivityStackSupervisor类的startActivityLocked的方法,在此方法内我们可以发现在执行下个流程的startActivityUncheckedLocked方法前,会有个条件判断,如下:

    final ActivityStack stack = getFocusedStack();
            if (stack.mResumedActivity == null
                    || stack.mResumedActivity.info.applicationInfo.uid != callingUid) {
                if (!mService.checkAppSwitchAllowedLocked(callingPid, callingUid, "Activity start")) {
                    PendingActivityLaunch pal =
                            new PendingActivityLaunch(r, sourceRecord, startFlags, stack);
                    mService.mPendingActivityLaunches.add(pal);
                    setDismissKeyguard(false);
                    ActivityOptions.abort(options);
                    return ActivityManager.START_SWITCHES_CANCELED;
                }
    
            }
    
    

    由于是后台服务启动的Activity,所以stack.mResumedActivity.info.applicationInfo.uid != callingUid的值肯定为true,其中callingUid为后台服务的UID,stack.mResumedActivity.info.applicationInfo.uid为当前前台显示Activity的UID。

    继续分析ActivityManagerService类的checkAppSwitchAllowedLocked的方法:

    int checkComponentPermission(String permission, int pid, int uid,
                int owningUid, boolean exported) {
            ...
            return ActivityManager.checkComponentPermission(permission, uid,
                    owningUid, exported);
        }
    
    

    最后分析ActivityManager类的checkComponentPermission的方法。

    public static int checkComponentPermission(String permission, int uid,
                int owningUid, boolean exported) {
            if (uid == 0 || uid == Process.SYSTEM_UID) {
                return PackageManager.PERMISSION_GRANTED;
            }
    
            ....
    
            try {
                return AppGlobals.getPackageManager()
                        .checkUidPermission(permission, uid);
            } catch (RemoteException e) {
                Slog.e(TAG, "PackageManager is dead?!?", e);
            }
            return PackageManager.PERMISSION_DENIED;
        }
    
    

    由上可以发现后台服务的UID如果为Process.SYSTEM_UID,或者启动的Activity具有android.Manifest.permission.STOP_APP_SWITCHES权限,就不会进入延时5s启动Activity流程,而是进入startActivityUncheckedLocked方法正常启动Activity。

    3.原因分析
    经过一、二的分析,再在关键地方加入日志,把callingUid的值打印出来,最后发现在应用中点击悬浮窗进行跳转操作时(或者直接home后,点击应用图标),callingUid的值为1000,与Process.SYSTEM_UID相等,这种情况是完全ok的,Activity会立即启动。而从桌面点击悬浮窗按钮进行跳转时,callingUid的值为50122,进入到延时5s启动Activity的流程。

    相关文章

      网友评论

          本文标题:监听Home键之后启动一个后台Activity延迟5s

          本文链接:https://www.haomeiwen.com/subject/rwveectx.html