Android锁屏下启动应用卡屏5秒的原因分析

Android锁屏下启动应用卡屏5秒的原因分析

最近分析一个问题,在锁屏窗口中启动应用会出现卡顿5秒,比如拨打电话,启动Google日历等。

拿拨打电话来举例,启动的action为 android.intent.action.CALL,对应处理的Activity在Telecom中:

//packages/services/Telecomm/AndroidManifest.xml:

<activity android:name=".components.UserCallActivity"
        android:label="@string/userCallActivityLabel"
        android:theme="@style/Theme.Telecomm.Transparent"
        android:permission="android.permission.CALL_PHONE"
        android:excludeFromRecents="true"
        android:process=":ui">
    <!-- CALL action intent filters for the various ways of initiating an outgoing call. -->
    <intent-filter>
        <action android:name="android.intent.action.CALL" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="tel" />
    </intent-filter>
</activity>

出现卡屏的UserCallActivity比较特别,没有界面,启动之后就会在onCreate里面直接finish,其他的没有出现这个问题快捷方式都是在启动的Activity直接显示。

WMS出现卡屏5秒的原因分析

启动Activity com.android.server.telecom/.components.UserCallActivity的时候调用AMS的方法:

com/android/server/am/ActivityStackSupervisor.java:

final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,
                                    boolean andResume, boolean checkConfig) throws RemoteException {
//…
    if (mKeyguardController.isKeyguardLocked()) {//锁屏状态下调用
        r.notifyUnknownVisibilityLaunched();
    }
//…
}

接着调用ActivityRecord# notifyUnknownVisibilityLaunched方法:

com/android/server/am/ActivityRecord.java:

void notifyUnknownVisibilityLaunched() {

   // No display activities never add a window, so there is no point in waiting them for
   // relayout.
      if (!noDisplay) {
            mWindowContainerController.notifyUnknownVisibilityLaunched();
      }
}

最终调用到UnknownAppVisibilityController#notifyLaunched方法,UnknownAppVisibilityController里面的成员变量mUnknownApps记录了锁屏状态下调用AppWindowToken的状态列表,它有几个状态:

UNKNOWN_STATE_WAITING_RESUME 等待执行完onResume
UNKNOWN_STATE_WAITING_RELAYOUT 等待执行完layout
UNKNOWN_STATE_WAITING_VISIBILITY_UPDATE 等待可见性的更新

目前看来这个类的目的就是控制锁屏界面下启动的Activity显示,要等待锁屏下开启的Activity完全显示以后才能显示其他的Activity,如果一直不显示也会其他被启动的Acitivity也有一个5s的超时被强制显示。

com/android/server/wm/UnknownAppVisibilityController.java

/**
 * Manages the set of {@link AppWindowToken}s for which we don't know yet whether it's visible or
 * not. This happens when starting an activity while the lockscreen is showing. In that case, the
 * keyguard flags an app might set influence it's visibility, so we wait until this is resolved to
 * start the transition to avoid flickers.(防止闪烁先显示Launcer,然后又快速地切换到显示目标Activity)
 */
class UnknownAppVisibilityController {
    /**
     * Notifies that {@param appWindow} has been launched behind Keyguard, and we need to wait * until it is resumed and relaid out to resolve the visibility.
     * Keyguard状态下启动Activity的时候调用
     */

void notifyLaunched(@NonNull AppWindowToken appWindow) {
if (DEBUG_UNKNOWN_APP_VISIBILITY) {
            Slog.d(TAG, "App launched appWindow=" + appWindow);
        }
        mUnknownApps.put(appWindow, UNKNOWN_STATE_WAITING_RESUME); 
}


/**
 * Notifies that {@param appWindow} has finished resuming.  Acitivty#onResume完成调用
 */
void notifyAppResumedFinished(@NonNull AppWindowToken appWindow) {
    if (mUnknownApps.containsKey(appWindow)
            && mUnknownApps.get(appWindow) == UNKNOWN_STATE_WAITING_RESUME) {
        if (DEBUG_UNKNOWN_APP_VISIBILITY) {
            Slog.d(TAG, "App resume finished appWindow=" + appWindow);
        }
        mUnknownApps.put(appWindow, UNKNOWN_STATE_WAITING_RELAYOUT);
    }
}

    /**
     * Notifies that {@param appWindow} has relaid out.
     * layout完成
     */
void notifyRelayouted(@NonNull AppWindowToken appWindow) {
     if (!mUnknownApps.containsKey(appWindow)) {
         return;
     }
     if (DEBUG_UNKNOWN_APP_VISIBILITY) {
         Slog.d(TAG, "App relayouted appWindow=" + appWindow);
     } 
     int state = mUnknownApps.get(appWindow);
        if (state == UNKNOWN_STATE_WAITING_RELAYOUT) { //当layout完成并且可见,也会从集合里移除
            mUnknownApps.put(appWindow, UNKNOWN_STATE_WAITING_VISIBILITY_UPDATE);
            mService.notifyKeyguardFlagsChanged(this::notifyVisibilitiesUpdated);
    }
} 
    //在Activity#onDestroy方法执行完成通知AMS的时候才会调用,mUnknownApps移除对应的appWindow
    void appRemovedOrHidden(@NonNull AppWindowToken appWindow) {
        if (DEBUG_UNKNOWN_APP_VISIBILITY) {
            Slog.d(TAG, "App removed or hidden appWindow=" + appWindow);
        }
        mUnknownApps.remove(appWindow);
    }
    //这个方法在AppTrasition的时候后判断是否有mUnknownApps存在
    boolean allResolved() {
        return mUnknownApps.isEmpty();
    }
}

下面是UnknownAppVisibility的日志,可以看到UserCallActivity的AppWindowToken被放到了mUnknownApps里面,过了5s之后才执行UserCallActivity#onDestory方法,从mUnknownApps里面移除。

LOG:
06-02 09:54:44.352 D/UnknownAppVisibility(  777): App launched appWindow=AppWindowToken{99d9676 token=Token{85511 ActivityRecord{5db03d9 u0 com.android.server.telecom/.components.UserCallActivity t288}}}

06-02 09:54:45.263 D/UnknownAppVisibility(  777): App resume finished appWindow=AppWindowToken{99d9676 token=Token{85511 ActivityRecord{5db03d9 u0 com.android.server.telecom/.components.UserCallActivity t288}}}

06-02 09:54:51.455 V/ActivityManagerService_Switch(  777): ACTIVITY DESTROYED: Token{85511 ActivityRecord{5db03d9 u0 com.android.server.telecom/.components.UserCallActivity t288 f}} [这个地方的onDestroy不是正常finish后调用的,而是AMS的超时机制触发]

06-02 09:54:51.460 D/UnknownAppVisibility(  777): App removed or hidden appWindow=AppWindowToken{99d9676 token=Token{85511 ActivityRecord{5db03d9 u0 com.android.server.telecom/.components.UserCallActivity t288}}}

在启动Activity的时候,会调用continueSurfaceLayout:

com/android/server/am/ActivityStarter.java:

private int startActivity(final ActivityRecord r, ActivityRecord sourceRecord,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
                        int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask, ActivityRecord[] outActivity) {
//…
try {
    mService.mWindowManager.deferSurfaceLayout();
    result = startActivityUnchecked(r, sourceRecord, voiceSession, voiceInteractor,
            startFlags, doResume, options, inTask, outActivity);
} finally {
    mService.mWindowManager.continueSurfaceLayout();
}
    return result;
}

最终会调用到WindowSurfacePlacer # transitionGoodToGo这个方法,判断是否准备好可以执行transition:

com/android/server/wm/WindowSurfacePlacer.java:

int handleAppTransitionReadyLocked() {
   int appsCount = mService.mOpeningApps.size();
   if (!transitionGoodToGo(appsCount, mTempTransitionReasons)) {
      return 0;
}
//…
if (DEBUG_APP_TRANSITIONS) Slog.v(TAG, "**** GOOD TO GO");
 mService.mH.removeMessages(H.APP_TRANSITION_TIMEOUT);//如果成功,从handler移除AppTransition超时处理
//…
}

//下面是个关键的地方, UnknownAppVisibilityController里面保存了在锁屏情况下启动Activity的AppWindowToken,导致不能正常的transition
private boolean transitionGoodToGo(int appsCount, SparseIntArray outReasons) {
    //...
    if (!mService.mAppTransition.isTimeout()) {
        //...
        if (!mService.mUnknownAppVisibilityController.allResolved()) {
            if (DEBUG_APP_TRANSITIONS) {
                Slog.v(TAG, "unknownApps is not empty: "
                        + mService.mUnknownAppVisibilityController.getDebugMessage());
            }
            return false;
        }
        //....
        return false;
    }
    return true;
}
LOG
06-02 09:54:45.229 V/WindowSurfacePlacer(  777): unknownApps is not empty: app=AppWindowToken{99d9676 token=Token{85511 ActivityRecord{5db03d9 u0 com.android.server.telecom/.components.UserCallActivity t288}}} state=1
//….
06-02 09:54:45.278 V/WindowSurfacePlacer(  777): unknownApps is not empty: app=AppWindowToken{99d9676 token=Token{85511 ActivityRecord{5db03d9 u0 com.android.server.telecom/.components.UserCallActivity t288}}} state=2
//…
    //private static final int UNKNOWN_STATE_WAITING_RESUME = 1;
    //private static final int UNKNOWN_STATE_WAITING_RELAYOUT = 2;

可以看到日志UserCallActivity的state一直处于1和2的状态,阻塞了不能正常的AppTrasition

尝试在加上条件测试看是否还会出现卡屏5s的现象:

com/android/server/wm/WindowSurfacePlacer.java:
if (!mService.mUnknownAppVisibilityController.allResolved()&&
!mService.mUnknownAppVisibilityController.getDebugMessage()
.contains(“com.android.server.telecom/.components.UserCallActivity”)) {
    //…
    return false;
}

加上这个测试条件后就没有了卡屏5秒的情况,但是有了新问题,会出现flickers(闪烁),先显示Launcer,然后又快速地切换到显示InCallActivity。如果FUNC先解锁操作再执行拨打电话,也会出现同样的问题。这正是UnknownAppVisibilityController解决的问题。下面看UserCallActivity finish不掉的原因。

AMS finish没有立即触发onDestroy的原因分析

正常的情况下,在Activity#onCreate方法中直接调用finish(),在之后的onPause方法调用AMS的activityPaused,就会直接调用IApplicationThread#scheduleDestroyActivity的方法通知Activity执行onDestroy,然后执行AMS的activityDestroyed,将UnknownAppVisibilityController里面的AppWindow移除。然而锁屏的的情况有区别:

com/android/server/am/ActivityManagerService.java:
@Override
public final void activityPaused(IBinder token) {
   //…
   ActivityStack stack = ActivityRecord.getStackLocked(token);
   if (stack != null) {
      stack.activityPausedLocked(token, false);
   }
}
com/android/server/am/ActivityStack.java:

final void activityPausedLocked(IBinder token, boolean timeout) {
    final ActivityRecord r = isInStackLocked(token);
    mHandler.removeMessages(PAUSE_TIMEOUT_MSG, r);
    //…
    completePauseLocked(true /* resumeNext */, null /* resumingActivity */);
   //…
}

private void completePauseLocked(boolean resumeNext, ActivityRecord resuming) {
    ActivityRecord prev = mPausingActivity;
    if (DEBUG_PAUSE) Slog.v(TAG_PAUSE, "Complete pause: " + prev);

    if (prev != null) {
        final boolean wasStopping = prev.state == STOPPING;
        prev.state = ActivityState.PAUSED;
        if (prev.finishing) {
            if (DEBUG_PAUSE) Slog.v(TAG_PAUSE, "Executing finish of activity: " + prev);
            prev = finishCurrentActivityLocked(prev, FINISH_AFTER_VISIBLE, false);
        } else{//...}
        
    }
}

final ActivityRecord finishCurrentActivityLocked(ActivityRecord r, int mode, boolean oomAdj) {
    // First things first: if this activity is currently visible,
    // and the resumed activity is not yet visible, then hold off on
    // finishing until the resumed one becomes visible.
    //这里的next是Launcher, r为UserCallActivity
    final ActivityRecord next = mStackSupervisor.topRunningActivityLocked();
    //可以看到上面的注释就是我们的这种场景 
    //在锁屏的情况下会走到分支mode为FINISH_AFTER_VISIBLE,r.visible为true
    //关键的是next.nowVisible为false,即Launcher现在也是不可见的
    //所以就会将UserCallActivity放到 mStoppingActivities里面
    if (mode == FINISH_AFTER_VISIBLE && (r.visible || r.nowVisible)
            && next != null && !next.nowVisible) {
        if (!mStackSupervisor.mStoppingActivities.contains(r)) {
            addToStopping(r, false /* scheduleIdle */, false /* idleDelayed */);
        }
        if (DEBUG_STATES) Slog.v(TAG_STATES,
                "Moving to STOPPING: "+ r + " (finish requested)");
        r.state = STOPPING;
        return r;
    }
     //…
    //非锁屏下走这个分支,这里就会scheduleDestroyActivity

    if (mode == FINISH_IMMEDIATELY
            || (prevState == ActivityState.PAUSED
            && (mode == FINISH_AFTER_PAUSE || mStackId == PINNED_STACK_ID))
            || finishingActivityInNonFocusedStack
            || prevState == STOPPING
            || prevState == STOPPED
            || prevState == ActivityState.INITIALIZING) {
             boolean activityRemoved = destroyActivityLocked(r, true, "finish-imm");
        //....
        return activityRemoved ? null : r;
    }
}

这个时候dumpsys activity的信息如下,UserCallActivity被加到了waittingToStop列表里,没有执行scheduleDestroyActivity方法:

adb shell dumpsys activity:
Activities waiting to stop:
    TaskRecord{28306a9 #433 A=com.android.server.telecom U=0 StackId=1 sz=1}
      Stop #0: ActivityRecord{f676a30 u0 com.android.server.telecom/.components.UserCallActivity t433 f}

Activities waiting for another to become visible:
    TaskRecord{28306a9 #433 A=com.android.server.telecom U=0 StackId=1 sz=1}
      Wait #0: ActivityRecord{f676a30 u0 com.android.server.telecom/.components.UserCallActivity t433 f}
// InCallActivity已经Resume等待显示
ResumedActivity: ActivityRecord{4cd5d55 u0 com.tct.dialer/com.android.incallui.InCallActivity t432}

之后的finish操作在ActivityManagerService#activityIdle方法中处理: (每次有Activity Resume完成,就会在主线程MessageQueue的Idler中调用,也有可能会在SystemServer中闲时调用)

com/android/server/am/ActivityManagerService.java:
@Override
public final void activityIdle(IBinder token, Configuration config, boolean stopProfiling) {
    synchronized (this) {
        ActivityStack stack = ActivityRecord.getStackLocked(token);
        if (stack != null) {
            ActivityRecord r =
                    mStackSupervisor.activityIdleInternalLocked(token, false /* fromTimeout */,
                            false /* processPausingActivities */, config);
        }//….
    }
}

接着调用ActivityStackSupervisor# activityIdleInternalLocked

final ActivityRecord activityIdleInternalLocked(final IBinder token, boolean fromTimeout,
                                        boolean processPausingActivities, Configuration config) {
    if (DEBUG_ALL) Slog.v(TAG, "Activity idle: " + token);

        // Atomically retrieve all of the other things to do.
    //查找waitingToStop的Activity,如果这里能够找出来,那么在后面finish,调用Activity#onDestory
    final ArrayList<ActivityRecord> stops = processStoppingActivitiesLocked(r,
            true /* remove */, processPausingActivities);
    NS = stops != null ? stops.size() : 0;
    //…
    // Stop any activities that are scheduled to do so but have been
    // waiting for the next one to start.
    for (int i = 0; i < NS; i++) {
        r = stops.get(i);
        final ActivityStack stack = r.getStack();
        if (stack != null) {
            if (r.finishing) {
                stack.finishCurrentActivityLocked(r, ActivityStack.FINISH_IMMEDIATELY, false);
            } else {
                stack.stopActivityLocked(r);
            }
        }
    }
    //...
    return r;
}

//查找操作waitingStop符合条件finish的
final ArrayList<ActivityRecord> processStoppingActivitiesLocked(ActivityRecord idleActivity,
                                    boolean remove, boolean processPausingActivities) {
    ArrayList<ActivityRecord> stops = null;
//这个时候没有Activity可见,nowVisible为false
    final boolean nowVisible = allResumedActivitiesVisible();
    for (int activityNdx = mStoppingActivities.size() - 1; activityNdx >= 0; --activityNdx) {
        ActivityRecord s = mStoppingActivities.get(activityNdx);//找到UserCallActivity
        //waitingVisible为true,一直等待显示
        boolean waitingVisible = mActivitiesWaitingForVisibleActivity.contains(s);
        if (DEBUG_STATES) Slog.v(TAG, "Stopping " + s + ": nowVisible=" + nowVisible
                + " waitingVisible=" + waitingVisible + " finishing=" + s.finishing);
        if (waitingVisible && nowVisible) {//不会进入这个分支
            mActivitiesWaitingForVisibleActivity.remove(s);
            waitingVisible = false;//当InCallActivity show出来,置false,
            if (s.finishing) {
                //...
                s.setVisibility(false);
            }
        }
        if (remove) {   
            final ActivityStack stack = s.getStack();
            final boolean shouldSleepOrShutDown = stack != null
                    ? stack.shouldSleepOrShutDownActivities()
                    : mService.isSleepingOrShuttingDownLocked();
            //waitingVisible为true,进入不了这个分支
            if (!waitingVisible || shouldSleepOrShutDown) {
if (!processPausingActivities && s.state == PAUSING) {
                    //...
                    continue;
                }
                if (DEBUG_STATES) Slog.v(TAG, "Ready to stop: " + s);
                if (stops == null) {
                    stops = new ArrayList<>();
                }
                stops.add(s);
                mStoppingActivities.remove(activityNdx);
            }
        }
    }
    return stops;
}

由于此时的topActivity (InCallActivity)一直没有show出来,就会导致UserCallActivity一直处于waitingToStop的状态,直到InCallActivity show出来之后,才会destroy UserCallActivity。

下面是日志,验证上面的分析:

LOG
06-02 09:54:44.464 V/ActivityStackSupervisor(  777): Stopping ActivityRecord{5db03d9 u0 com.android.server.telecom/.components.UserCal
lActivity t288}: nowVisible=false waitingVisible=false finishing=false

06-02 09:54:45.073 V/ActivityStack_States(  777): Moving to STOPPING: ActivityRecord{5db03d9 u0 com.android.server.telecom/.components.
UserCallActivity t288 f} (finish requested)

06-02 09:54:45.283 V/ActivityStackSupervisor(  777): Stopping ActivityRecord{5db03d9 u0 com.android.server.telecom/.components.
UserCallActivity t288 f}: nowVisible=false waitingVisible=true finishing=true

06-02 09:54:47.607 V/ActivityStackSupervisor(  777): Stopping ActivityRecord{5db03d9 u0 com.android.server.telecom/.components.
UserCallActivity t288 f}: nowVisible=false waitingVisible=true finishing=true

06-02 09:54:51.186 V/ActivityStackSupervisor(  777): Stopping ActivityRecord{5db03d9 u0 com.android.server.telecom/.components.
UserCallActivity t288 f}: nowVisible=true waitingVisible=false finishing=true

06-02 09:54:51.187 V/ActivityStackSupervisor(  777): Ready to stop: ActivityRecord{5db03d9 u0 com.android.server.telecom/.components.
UserCallActivity t288 f}

06-02 09:54:51.287 V/ActivityStack_States(  777): Moving to FINISHING: ActivityRecord{5db03d9 u0 com.android.server.telecom/.components.
UserCallActivity t288 f}

结论

由于在锁屏界面启动Activity导致触发了WMS的 UnknownAppVisibilityController防止闪烁(显示Launcher之后又快速切换到目标Activity)的机制, UserCall Activity不能及时finish,就会导致一直处于卡屏状态,直到AppTransition超时机制才强制显示InCallActivity。

UserCallActivity是Telecomm服务里面的组件,但是它又不是真正显示UI的Acivity,针对这种情况可以使用Telecom的placeCall接口可以规避掉这种情况。但是,不能保证所有启动的Acitivty都直接显示,比如原生的Google日历也会出现卡屏5秒现象,目前看来是原生Android的bug,对于这种情况,finish之后没有将不显示的窗口及时地从UnknownAppVisibilityController移除。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348

推荐阅读更多精彩内容