Android Home鍵之後啟動Activity延遲5s

weixin_34290000發表於2018-09-10

問題引入

最近做專案時遇到這樣一個問題,原本需求是這樣的:
在一個播放介面,播放時退出當前介面,或者點選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的流程。

參考資料

關於在 Service 或 BroadcastReceiver 中 startActivity 的問題
按Home鍵後,後臺服務啟動Activity要延時5s左右才會啟動原理分析

相關文章