【源码剖析2】framework 源码 5——UserController之多用户的切换流程(中)

上一章讲解了,第三方app要切换用户ActivityManagerService的SwtichUser方法最终走到切换用户的最关键方法: UserController里面的startUser方法。前面叫做切换用户,而当前这一步这叫做启动新用户。其实切换到新用户就是启动新用户的过程。

本章将详细讲解startUser方法。

boolean startUser(

            final int userId,

            final boolean foreground,

            @Nullable IProgressListener unlockListener) {

        if (mInjector.checkCallingPermission(INTERACT_ACROSS_USERS_FULL)

                != PackageManager.PERMISSION_GRANTED) {

            String msg = "Permission Denial: switchUser() from pid="

                    + Binder.getCallingPid()

                    + ", uid=" + Binder.getCallingUid()

                    + " requires " + INTERACT_ACROSS_USERS_FULL;

            Slog.w(TAG, msg);

            throw new SecurityException(msg);

        }

//首先是加个打印,一共查询

        Slog.i(TAG, "Starting userid:" + userId + " fg:" + foreground);

        final long ident = Binder.clearCallingIdentity();

 

//核心功能第一步,再次查询,目标用户是否满足被切换的条件,逻辑代码和SwtichUser有重合的地方。

        try {

            final int oldUserId = getCurrentUserId();

   //如果目标用户就是当前用户则不启动目标用户

            if (oldUserId == userId) {

                return true;

            }

 

            if (foreground) {

                mInjector.clearAllLockedTasks("startUser");

            }

            //如果目标用户不存在则不启动目标用户

            final UserInfo userInfo = getUserInfo(userId);

            if (userInfo == null) {

                Slog.w(TAG, "No user info for user #" + userId);

                return false;

            }

//如果目标用户不支持切换则不启动目标用户

            if (foreground && userInfo.isManagedProfile()) {

                Slog.w(TAG, "Cannot switch to User #" + userId + ": not a full user");

                return false;

            }

 //核心代码第二步,如果采用有提示的切换方式则冻屏。这里很多人不理解为什么要冻屏。这是为了保证dialog的显示时间和切换用户的时间保持一致。

//切换用户的时间是不确定的,受CPU等因素影响,每次切换用户时间都不太一样。我们既不希望用户还没切换的时候,提示的dialog就消失了,也不希望用户已经切换过去,dialog还在继续显示。

//那么最好的方案就是在dialog显示的时候把屏幕冻住,而后在用户切完结束的时候在把屏幕激活。

//所以,实际的dialog很早就调用了dismiss,但是由于屏幕冻住,所以dialog的图片还显示在window上,但是实际上dialog在程序里面已经消失了。于是在屏幕激活,刷新的一瞬间就会同时出发dialog消失和切换用户,达到dialog消失和切换用户同步的效果

            if (foreground && mUserSwitchUiEnabled) {

                mInjector.getWindowManager().startFreezingScreen(

                        R.anim.screen_user_exit, R.anim.screen_user_enter);

            }

 

            boolean needStart = false;

            boolean updateUmState = false;

            UserState uss;

            //开始记录一些信息,mStartedUsers记录启动过的用户,mUserLru也类似。可以用来查询被启动过的用户,或者当前正在运行的用户。凡是被启动过的用户,在未删除的情况下,都会一直处于启动状态。

            synchronized (mLock) {

                uss = mStartedUsers.get(userId);

                if (uss == null) {

                    uss = new UserState(UserHandle.of(userId));

                    uss.mUnlockProgress.addListener(new UserProgressListener());

                    mStartedUsers.put(userId, uss);

                    updateStartedUserArrayLU();

                    needStart = true;

                    updateUmState = true;

                } else if (uss.state == UserState.STATE_SHUTDOWN && !isCallingOnHandlerThread()) {

                    Slog.i(TAG, "User #" + userId

                            + " is shutting down - will start after full stop");

                    mHandler.post(() -> startUser(userId, foreground, unlockListener));

                    return true;

                }

                final Integer userIdInt = userId;

                mUserLru.remove(userIdInt);

                mUserLru.add(userIdInt);

            }

            if (unlockListener != null) {

                uss.mUnlockProgress.addListener(unlockListener);

            }

            if (updateUmState) {

                mInjector.getUserManagerInternal().setUserState(userId, uss.state);

            }

            //这里有个概念,我启动这个用户是否要把用户切换到前台用户。这是因为startUser不仅仅被switchUser调用。可以这么说,切换用户的核心是把目标用户启动到前台。而startUser则包括启动到前台和启动到后台。

            if (foreground) {

                // 如果是启动到前台,那么这个时候就切换currentUser了。currentUser是一个参数,用来表示当前用户是哪一个

 

                mInjector.reportGlobalUsageEventLocked(UsageEvents.Event.SCREEN_NON_INTERACTIVE);

                synchronized (mLock) {

 // 如果是启动到前台,则更换当前mCurrentUserId 至目标Userid,这个时候如果UserController类里面的方法查询当前用户,则查询结果是切换后的结果

                    mCurrentUserId = userId;

                    mTargetUserId = UserHandle.USER_NULL; // reset, mCurrentUserId has caught up

                }

 //而后把mCurrentUserId的切换同步到整个系统,从而确保如windowManager可以获取到正确的预期的userId

                mInjector.updateUserConfiguration();

                updateCurrentProfileIds();

                mInjector.getWindowManager().setCurrentUser(userId, getCurrentProfileIds());

                mInjector.reportCurWakefulnessUsageEvent();

                // Once the internal notion of the active user has switched, we lock the device

                // with the option to show the user switcher on the keyguard.

                if (mUserSwitchUiEnabled) {

//容易被忽略掉一处核心代码第三步:lockNow,进入锁屏状态,我们知道当我们正常切换到目标用户的时候,新用户是处于锁屏状态的。所以普通切换就是锁屏的。

//那么有哪些情况是不锁屏的,常见有两种情况,一种是首次切换,此时切换过去就是开机向导,这种状态没必要锁屏,第二种是目前用户不是独立界面,这种状态也不用锁屏。 比如,应用克隆,实质就是一个非独立界面的用户。

                    mInjector.getWindowManager().setSwitchingUser(true);

                    mInjector.getWindowManager().lockNow(null);

                }

            } else {

                final Integer currentUserIdInt = mCurrentUserId;

                updateCurrentProfileIds();

                mInjector.getWindowManager().setCurrentProfileIds(getCurrentProfileIds());

                synchronized (mLock) {

                    mUserLru.remove(currentUserIdInt);

                    mUserLru.add(currentUserIdInt);

                }

            }

 

//接下来是对多用户状态的一个确认,其中正确的状态是STATE_START,也就是正常流程是不会走下面3种分支的

            if (uss.state == UserState.STATE_STOPPING) {

                uss.setState(uss.lastState);

                mInjector.getUserManagerInternal().setUserState(userId, uss.state);

                synchronized (mLock) {

                    updateStartedUserArrayLU();

                }

                needStart = true;

            } else if (uss.state == UserState.STATE_SHUTDOWN) {

                uss.setState(UserState.STATE_BOOTING);

                mInjector.getUserManagerInternal().setUserState(userId, uss.state);

                synchronized (mLock) {

                    updateStartedUserArrayLU();

                }

                needStart = true;

            }

 

            if (uss.state == UserState.STATE_BOOTING) {

                mInjector.getUserManager().onBeforeStartUser(userId);

                mHandler.sendMessage(

                        mHandler.obtainMessage(SYSTEM_USER_START_MSG, userId, 0));

            }

 

// 以上三种分支基本可以忽略,因为正常流程不走,接下来,首先是通知系统服务(systemService)我们切换用户啦,

//其次核心代码第四步是开始进行切换用户的进行用户切换的工作,其核心是调用dispatchUserSwitch()方法来完成。

            if (foreground) {

                mHandler.sendMessage(mHandler.obtainMessage(SYSTEM_USER_CURRENT_MSG, userId,

                        oldUserId));

                mHandler.removeMessages(REPORT_USER_SWITCH_MSG);

                mHandler.removeMessages(USER_SWITCH_TIMEOUT_MSG);

                mHandler.sendMessage(mHandler.obtainMessage(REPORT_USER_SWITCH_MSG,

                        oldUserId, userId, uss));

                mHandler.sendMessageDelayed(mHandler.obtainMessage(USER_SWITCH_TIMEOUT_MSG,

                        oldUserId, userId, uss), USER_SWITCH_TIMEOUT_MS);

            }

 

            if (needStart) {

                // 发送广播告知user启动

                Intent intent = new Intent(Intent.ACTION_USER_STARTED);

                intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY

                        | Intent.FLAG_RECEIVER_FOREGROUND);

                intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);

                mInjector.broadcastIntent(intent,

                        null, null, 0, null, null, null, AppOpsManager.OP_NONE,

                        null, false, false, MY_PID, SYSTEM_UID, userId);

            }

 //核心代码第5步,启动目标用户的应用,也就是启动目标用户的初始应用,该核心通过调用moveUserToForeground()方法完成。

//什么叫做初始应用,我们在首次开机的时候,初始应用是开机向导,而走过开机向导之后,后面再重启初始应用就是桌面。所以初始应用就是首次进入该用户时候的应用。

//对于多用1户来说,具备应用记忆功能,也就是我们离开这个用户的时候是哪个应用,则进去还可以是那个应用。当然默认首先是开机向导,走过开机向导就是桌面,而后如果已经在该用户启动过其应用,且在其他应用的使用过程中,切换离开,那么再次回到此用户的时候,就可以启动离开前的那个应用。

            if (foreground) {

                moveUserToForeground(uss, oldUserId, userId);

            } else {

                finishUserBoot(uss);

            }

 

            if (needStart) {

                Intent intent = new Intent(Intent.ACTION_USER_STARTING);

                intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);

                intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);

                mInjector.broadcastIntent(intent,

                        null, new IIntentReceiver.Stub() {

                            @Override

                            public void performReceive(Intent intent, int resultCode,

                                    String data, Bundle extras, boolean ordered,

                                    boolean sticky,

                                    int sendingUser) throws RemoteException {

                            }

                        }, 0, null, null,

                        new String[]{INTERACT_ACROSS_USERS}, AppOpsManager.OP_NONE,

                        null, true, false, MY_PID, SYSTEM_UID, UserHandle.USER_ALL);

            }

        } finally {

            Binder.restoreCallingIdentity(ident);

        }

 

        return true;

    }

 

至此就是整个startUser的代码。

在进行一次总结,核心代码如下:

1:检查是否能够切换

2:冻屏

3:锁屏

4:分发切换事件

5:启动目标应用。

 

对于后面3种常见都是调用其他的应用,值得展开细讲。

这里进行一个简单的描述,后面再详细讲解具体实现流程。

 

锁屏操作主要是在systemUI里面完成,systemUI不是普通应用,也不在应用堆栈里面,systemUI是一个永远活跃的应用。

systemUI又称状态栏,平常使用的时候,在顶部显示时间,电池等小图标的地方。

systemUI有3个显示状态,我简单称之为顶部状态、下拉状态、锁屏状态。

顶部状态则是在顶部的时候,而下拉状态则是可以有一些快捷开关,显示一些通知内容,而最后则是锁屏状态,锁屏的时候,其实也就是状态栏全屏显示的时候,状态栏全屏你的一切操作都被状态栏拦截,因此也就操作不到桌面以及其他应用上。

systemUI有一个核心模块叫做keyguard,顾名思义密码。systemUI的锁屏模式下,只有keyguard通过校验才会缩小状态栏进入顶部状态。这就是我们的锁屏。

在切换用户的过程中,我们会申请systemUI去进行锁屏操作,锁屏的过程中,需要为解锁做准备所以比较花时间且流程复杂,这就是startUser的第三个核心步骤。

 

分发切换事件有几大功能,比如onUserSwitched的调用,在多用户的切换过程中,有很多回调,大体流程是用户切换,通过广播和回调通知到各个模块,然后各个模块立刻联动起来,做各自模块应该做的切换的事情,比如wallpaperManager就会接到切换用户的回调,然后修改当前壁纸。

当然在这个流程中也包括解除冻屏操作。

正如之前所说,为了保证切换的一致性,通过冻屏来控制dialog的显示时间,会在切换完用户的时候解除冻屏。什么时候是切换完用户的时候,答案是当各个回调都结束的时候,这里采用木桶法则,所有回调中最后一个完成时,算是用户切换完成,切换用户回调大约20多个。

 

启动目标应用,此方法是涉及堆栈的一个方法,因此很复杂。在启动目标应用的时候首先进行一个判断,判断是启动默认应用还是启动历史应用,而后在根据堆栈等启动对应应用。此方法主要通过ActivityManagerService完成。

 

 

 

总结,整个切换流程大概分为:

1:检查是否能够切换

2:显示dialog

3:冻屏

4:锁屏

5:分发切换事件

6:启动目标应用

你可能感兴趣的:(源码剖析)