Android 双卡功能深度解析

0 阅读12分钟

第一阶段:双卡模式架构

1.1 双卡模式分类(MultiSimVariants)

public enum MultiSimVariants {
    DSDS,    // Dual SIM Dual Standby(双卡双待)
    DSDA,    // Dual SIM Dual Active(双卡双活)
    TSTS,    // Triple SIM Triple Standby(三卡三待)
    UNKNOWN
}

三种模式对比:

模式特性语音能力数据能力应用场景
DSDS一个卡主动,另一个卡待机1 个卡只能接听1 个卡数据入门 / 中端机型
DSDA两个卡都主动2 个卡同时语音2 个卡同时数据高端 / 特定运营商
TSTS三个卡,一个待机1-2 个卡语音1-2 个卡数据少见
DSDS 工作模式:
┌──────────────────┐     ┌──────────────────┐
│   SIM Slot 1     │     │   SIM Slot 2     │
│  (主动)          │     │  (待机)          │
│                  │     │                  │
│ 语音: 可接听     │ <→  │ 语音: 无法接听   │
│ 数据: 可使用     │     │ 数据: 无         │
│ 信号显示: 强     │     │ 信号显示: 弱     │
└──────────────────┘     └──────────────────┘

DSDA 工作模式:
┌──────────────────┐     ┌──────────────────┐
│   SIM Slot 1     │     │   SIM Slot 2     │
│  (主动)          │     │  (主动)          │
│                  │     │                  │
│ 语音: 可通话     │     │ 语音: 可通话     │
│ 数据: 可使用     │ ←→  │ 数据: 可使用     │
│ 信号显示: 强     │     │ 信号显示: 强     │
└──────────────────┘     └──────────────────┘

1.2 在 Android 中查询双卡模式

// 获取当前设备的双卡模式
TelephonyManager tm = context.getSystemService(TelephonyManager.class);

// 方法 1:获取多卡配置
MultiSimVariants config = tm.getMultiSimConfiguration();
// 返回:DSDS / DSDA / TSTS / UNKNOWN

// 方法 2:获取活跃 Modem 数量
int activeModemCount = tm.getActiveModemCount();
// 返回:1(单卡)、2(双卡)、3(三卡)

// 方法 3:获取硬件支持的最大 Modem 数
int maxModemCount = tm.getSupportedModemCount();
// 返回:2(支持双卡但可能运行在单卡模式)

// 方法 4:获取最大同时活跃卡数
int maxActiveVoiceSubId = tm.getMaxNumberOfSimultaneouslyActiveSims();
// 返回:1(DSDS/TSTS)或 2(DSDA)

// 方法 5:查询是否支持多卡
int multiSimSupported = tm.isMultiSimSupported();
// 返回:MULTISIM_ALLOWED 或 MULTISIM_NOT_SUPPORTED_BY_HARDWARE

1.3 设备属性配置

# ro.telephony.sim_config 属性决定双卡模式
# 在 device.mk 中设置
ro.telephony.sim_config=dsds    # 双卡双待
ro.telephony.sim_config=dsda    # 双卡双活
ro.telephony.sim_config=tsts    # 三卡三待

# ro.telephony.max_active_modems 属性
# 指定最多可同时激活多少个 Modem
ro.telephony.max_active_modems=2

# 运行时模式切换
ro.telephony.dsds_mode=2        # 2=always on DSDS(不能切换回单卡)

第二阶段:多Phone实例与多Modem管理

2.1 Phone 实例创建(PhoneFactory)

public class PhoneFactory {
    // 全局 Phone 实例数组
    private static Phone[] sPhones;                    // Phone 对象数组
    private static RIL[] sCommandsInterfaces;          // RIL 接口数组
    private static UiccController sUiccController;     // UICC 控制器
    
    // 初始化双卡
    public static void makeDefaultPhones(Context context) {
        synchronized (sLockProxyPhones) {
            // 获取活跃 Modem 数量
            int numPhones = TelephonyManager.getDefault().getActiveModemCount();
            
            // 创建对应数量的 Phone 实例
            sPhones = new Phone[numPhones];                          // 每个卡一个 Phone
            sCommandsInterfaces = new RIL[numPhones];               // 每个卡一个 RIL
            sTelephonyNetworkFactories = new TelephonyNetworkFactory[numPhones];
            
            for (int i = 0; i < numPhones; i++) {
                // 为每个 Modem 创建 RIL 接口
                sCommandsInterfaces[i] = new RIL(context,
                        RadioAccessFamily.getRafFromNetworkType(networkModes[i]),
                        cdmaSubscription,
                        i,              // Modem index
                        featureFlags);
                
                // 为每个 Modem 创建 Phone 对象
                sPhones[i] = createPhone(context, i);
                
                // 创建 ImsPhone(支持 VoLTE)
                if (hasImsSupport) {
                    sPhones[i].createImsPhone();
                }
            }
            
            // 初始化 UICC 控制器(管理 SIM 卡)
            sUiccController = UiccController.make(context);
            
            // 初始化 PhoneSwitcher(数据卡切换)
            PhoneSwitcher.make(maxDataAttachModemCount, context, ...);
        }
    }
}

关键概念:

  • PhoneId:物理卡槽索引(0 = 卡槽 1,1 = 卡槽 2)
  • SubId:订阅 ID(逻辑概念,可能映射到任何卡槽)
  • Slot Index:物理卡槽位置
// 获取 Phone 实例
Phone phone0 = PhoneFactory.getPhone(0);  // 卡槽 0 对应的 Phone
Phone phone1 = PhoneFactory.getPhone(1);  // 卡槽 1 对应的 Phone

// SubId 与 PhoneId 映射
int phoneId = SubscriptionManager.getPhoneId(subId);
int subId = SubscriptionManager.getSubscriptionId(phoneId);

2.2 动态切换单卡/双卡模式

// PhoneFactory 动态调整
public static void onMultiSimConfigChanged(Context context, int activeModemCount) {
    synchronized (sLockProxyPhones) {
        int prevActiveModemCount = sPhones.length;
        
        // 如果数量相同,无需操作
        if (prevActiveModemCount == activeModemCount) return;
        
        // 仅支持增加,不支持减少(减少需要重启)
        if (prevActiveModemCount > activeModemCount) return;
        
        // 扩展数组
        sPhones = Arrays.copyOf(sPhones, activeModemCount);
        sCommandsInterfaces = Arrays.copyOf(sCommandsInterfaces, activeModemCount);
        
        // 为新增的 Modem 创建对象
        for (int i = prevActiveModemCount; i < activeModemCount; i++) {
            // 创建新的 RIL 接口
            sCommandsInterfaces[i] = new RIL(context, ...);
            
            // 创建新的 Phone 对象
            sPhones[i] = createPhone(context, i);
            
            // 如果支持 IMS,创建 ImsPhone
            if (hasImsSupport) {
                sPhones[i].createImsPhone();
            }
        }
    }
}

2.3 多 Phone 实例的业务逻辑

// 例:获取两个卡的服务状态
Phone phone0 = PhoneFactory.getPhone(0);
Phone phone1 = PhoneFactory.getPhone(1);

ServiceState ss0 = phone0.getServiceState();
ServiceState ss1 = phone1.getServiceState();

// 例:监听两个卡的呼叫状态
phone0.registerForPreciseCallStateChanged(handler, EVENT_CALL_STATE_CHANGED_0, null);
phone1.registerForPreciseCallStateChanged(handler, EVENT_CALL_STATE_CHANGED_1, null);

第三阶段:订阅管理与默认卡选择

3.1 订阅概念(SubscriptionInfo)

public class SubscriptionInfo {
    private int mId;                    // 订阅 ID(SubId)
    private int mIccId;                 // ICC ID(UICC 唯一标识)
    private int mSimSlotIndex;          // 卡槽索引
    private String mDisplayName;        // 显示名称
    private String mCarrierName;        // 运营商名称
    private int mColor;                 // 颜色标记(UI)
    private boolean mIsEmbedded;        // 是否为 eSIM
    private int mIconTint;              // 图标色调
    private String mNumber;             // 电话号码
    private int mDataRoaming;           // 漫游状态
    private int mMnc;                   // MNC(移动网络代码)
    private int mMcc;                   // MCC(移动国家代码)
}

3.2 默认卡设置(三层默认)

在双卡场景下,需要分别设置:

// 1. 默认数据卡(用于互联网)
SubscriptionManager.setDefaultDataSubId(subId);
int defaultDataSubId = SubscriptionManager.getDefaultDataSubscriptionId();

// 2. 默认语音卡(用于拨号)
SubscriptionManager.setDefaultVoiceSubId(subId);
int defaultVoiceSubId = SubscriptionManager.getDefaultVoiceSubscriptionId();

// 3. 默认短信卡(用于发短信)
SubscriptionManager.setDefaultSmsSubId(subId);
int defaultSmsSubId = SubscriptionManager.getDefaultSmsSubscriptionId();

设置时机:

// SubscriptionManagerService 维护这些设置
public class SubscriptionManagerService extends Handler {
    private WatchedInt mDefaultVoiceSubId;  // 默认语音卡
    private WatchedInt mDefaultDataSubId;   // 默认数据卡
    private WatchedInt mDefaultSmsSubId;    // 默认短信卡
}

// 从系统设置中读取
Settings.Global.MULTI_SIM_VOICE_CALL_SUBSCRIPTION    // 语音设置
Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION     // 数据设置
Settings.Global.MULTI_SIM_SMS_SUBSCRIPTION           // 短信设置

3.3 订阅组(SubscriptionGroup)

// 某些运营商允许将多个订阅分组
public class SubscriptionGroup {
    private String mGroupUuid;
    private List<Integer> mSubscriptionIds;  // 组内的订阅 ID 列表
}

// 获取订阅所在的组
SubscriptionInfo[] groupedSubs = 
    SubscriptionManager.getSubscriptionsInGroup(groupUuid);

// 组内订阅共享设置
// - MOBILE_DATA:整组共用一个开关
// - DATA_ROAMING:整组共用一个开关
// - 默认数据卡:只有一个

第四阶段:PhoneSwitcher 数据卡切换逻辑

4.1 PhoneSwitcher 职责

public class PhoneSwitcher extends Handler {
    /**
     * PhoneSwitcher 是双卡模式下最核心的组件,负责:
     * 1. 监听订阅变化和网络请求
     * 2. 决定哪个卡作为数据卡
     * 3. 控制 Modem 的 PS 附着状态
     * 4. 处理语音通话时的自动数据切换
     * 5. 处理紧急呼叫时的数据卡切换
     */
}

4.2 数据卡选择优先级

// PhoneSwitcher 中的关键变量
protected int mPrimaryDataSubId;        // 用户设置的主数据卡
protected int mPreferredDataPhoneId;    // 当前活跃的数据 Phone ID
protected int mAutoSelectedDataSubId;   // 自动选择的数据卡(用于 CBRS)
protected int mOpptDataSubId;           // 机会性数据卡(CBRS)

// 数据卡选择顺序:
// 1. 紧急呼叫进行中 → 切换到有信号的卡(EmergencyStateTracker)
// 2. 语音通话进行中 → 自动切换到通话卡(shouldSwitchDataDueToInCall)
// 3. 机会性数据卡可用 → 使用机会性卡(CBRS/优先网络)
// 4. 自动数据切换 → 根据信号强度自动切换
// 5. 用户设置 → 使用用户选择的主卡

4.3 onEvaluate 数据卡选择逻辑

public class PhoneSwitcher {
    protected boolean onEvaluate(boolean requestsChanged, String reason) {
        StringBuilder sb = new StringBuilder(reason);
        
        // 1. 检查用户设置的主数据卡是否改变
        int primaryDataSubId = mSubscriptionManagerService.getDefaultDataSubId();
        if (primaryDataSubId != mPrimaryDataSubId) {
            mPrimaryDataSubId = primaryDataSubId;
            mLastSwitchPreferredDataReason = DATA_SWITCH_REASON_MANUAL;
        }
        
        // 2. 检查是否有活跃的语音呼叫(DSDS 下自动切换)
        if (updatesIfPhoneInVoiceCallChanged()) {
            // 如果有通话,检查是否需要切换数据卡到通话卡
            if (shouldSwitchDataDueToInCall()) {
                // 切换到通话 Phone
                switchDataToPhoneInCall();
            }
        }
        
        // 3. 检查订阅是否变化
        for (int i = 0; i < mActiveModemCount; i++) {
            int sub = SubscriptionManager.getSubscriptionId(i);
            if (sub != mPhoneSubscriptions[i]) {
                // 订阅已改变,需要重新评估
                mPhoneSubscriptions[i] = sub;
                notifySubscriptionsMappingChanged();
            }
        }
        
        // 4. 评估是否需要立即切换(无需等待验证)
        evaluateIfImmediateDataSwitchIsNeeded();
        
        // 5. 发送 ALLOW_DATA 命令到 Modem
        updateAllowedModems();
        
        return true;  // 数据卡已改变
    }
}

4.4 ALLOW_DATA 命令

在 DSDS 模式下,Modem 只允许一个卡进行 PS(数据)附着:

┌─────────────────────────────────────────┐
│      ALLOW_DATA 命令(RIL 命令)        │
├─────────────────────────────────────────┤
│ 发送:Phone 0 - ALLOW_DATA (1)         │  允许卡 0 数据
│       Phone 1 - ALLOW_DATA (0)         │  禁止卡 1 数据
│                                        │
│ 结果:Phase switch 发生(约 1 秒)     │
│       旧数据卡断开连接                 │
│       新数据卡建立连接                 │
└─────────────────────────────────────────┘

HAL 命令(新方式):
setPreferredDataModem(phoneId)  // 直接设置首选 Modem

第五阶段:紧急呼叫的双卡处理

5.1 紧急呼叫时的卡选择

public class EmergencyStateTracker {
    // 紧急呼叫优先级选择:
    // 1. 有有效订阅的卡 → 优先选择该卡
    // 2. 无任何订阅 → 选择 Phone 0(默认)
    // 3. 如果选中的卡无信号 → 切换到有信号的卡
    
    public boolean needToSwitchPhone(Phone phone) {
        int subId = phone.getSubId();
        int phoneId = phone.getPhoneId();
        
        // 检查当前卡是否有效
        if (!isSimReady(phoneId, subId)) {
            // SIM 不可用
            if (!SubscriptionManager.isValidSubscriptionId(subId)) {
                // 无订阅 → 尝试切换到有订阅的其他卡
                if (phoneId != 0 || isThereOtherPhone(phoneId, true)) {
                    return true;  // 需要切换
                }
            } else {
                // 有订阅但 SIM 未就绪 → 尝试切换到其他卡
                if (isThereOtherPhone(phoneId, false)) {
                    return true;  // 需要切换
                }
            }
        }
        return false;  // 不需要切换
    }
}

5.2 紧急呼叫时的数据卡切换

public class EmergencyStateTracker {
    // 紧急呼叫时需要:
    // 1. 确保数据卡能进行 PS 附着(可能需要切换)
    // 2. 通知系统进入紧急模式
    // 3. 在紧急呼叫结束后恢复原来的设置
    
    private CompletableFuture<Boolean> 
            possiblyOverrideDefaultDataForEmergencyCall(Phone phone) {
        
        // 获取当前数据卡
        int oldDds = PhoneSwitcher.getInstance().getPreferredDataPhoneId();
        int emergencyPhoneId = phone.getPhoneId();
        
        // 如果紧急呼叫卡不是当前数据卡,切换
        if (emergencyPhoneId != oldDds) {
            // 1. 即时切换数据卡到紧急呼叫卡
            PhoneSwitcher.getInstance().overrideDefaultDataPhoneId(
                    emergencyPhoneId);
            
            // 2. 等待数据连接建立
            // 3. 紧急呼叫结束后恢复原来的数据卡
        }
        
        return completableFuture;  // 返回切换完成信号
    }
}

第六阶段:多卡场景下的业务流程

6.1 来电处理

┌─────────────────────────────────────────────────────────────┐
│              双卡设备接收来电(DSDS 模式)                  │
└─────────────────────────────────────────────────────────────┘

网络发来呼叫信息
        ↓
卡 0 RRC 接收(数据卡)→ 处理下行数据
卡 1 RRC 接收(待机卡)→ 处理下行语音信号
        ↓
来电事件通知(在卡 1 上)
        ↓
系统查询默认语音卡设置
        ├→ 如果来电卡 = 默认语音卡
        │   → 直接呼叫响起
        ├→ 如果来电卡 ≠ 默认语音卡
        │   → 根据配置播放所有来电或仅默认卡来电
        └→ 用户可选择接听或拒绝
        ↓
接听来电
        ├→ 切换到语音卡的优先级提升
        ├→ 数据可能中断或降级(DSDS 只有一个 Modem 工作)
        └→ 数据卡自动切换到语音卡(可选功能)

6.2 拨号处理

┌─────────────────────────────────────────────────────────────┐
│              双卡设备拨出呼叫(DSDS 模式)                  │
└─────────────────────────────────────────────────────────────┘

用户拨号或选择卡
        ↓
应用查询默认语音卡或用户选择
        ├→ 无特殊选择 → 使用默认语音卡
        ├→ 用户指定 → 使用指定卡
        └→ 多个卡都可用 → 询问用户选择
        ↓
在选定卡上建立语音连接
        ├→ 切换待机卡到该卡
        ├→ 如果该卡不是数据卡 → 自动切换数据到该卡
        │  (可能导致原数据卡连接断开)
        └→ 通话接通
        ↓
通话进行中
        ├→ 数据功能受限(只有通话卡可用)
        ├→ 如果需要数据 → 使用通话卡的数据
        └→ 其他卡无法使用
        ↓
通话结束
        ├→ 数据卡恢复到用户设置
        ├→ 通话卡回到待机状态
        └→ 系统恢复正常

6.3 发短信处理

┌─────────────────────────────────────────────────────────────┐
│           双卡设备发送短信(DSDS 模式)                    │
└─────────────────────────────────────────────────────────────┘

应用调用 sendTextMessage()
        ↓
系统查询默认短信卡
        ├→ 已设置 → 使用该卡
        ├→ 未设置 → 使用默认语音卡
        └→ 如果启用"每次询问" → 弹窗让用户选择
        ↓
在选定卡上发送短信
        ├→ 如果该卡不是当前活跃卡 → 可能需要切换
        └→ CS 域短信发送
        ↓
短信发送完成
        ├→ 如果数据卡没改变 → 继续使用原数据卡
        ├→ 如果因短信改变了卡 → 恢复数据卡设置
        └→ 系统恢复正常

第七阶段:关键数据结构

7.1 PhoneState(PhoneSwitcher 内部状态)

private class PhoneState {
    public int mPhoneId;
    public int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
    
    // 该 Phone 的呼叫状态
    public Call.State mForegroundCall = Call.State.IDLE;
    public Call.State mBackgroundCall = Call.State.IDLE;
    public Call.State mRingingCall = Call.State.IDLE;
    
    // IMS 相关
    public boolean mImsConnected = false;
}

7.2 MultiSimSettingController(多卡设置协调)

public class MultiSimSettingController extends Handler {
    /**
     * 协调多卡场景下的设置规则:
     * 1. 订阅分组:组内共享 MOBILE_DATA 和 DATA_ROAMING 设置
     * 2. 默认设置:自动继承或清除
     * 3. 主卡数据:只有主卡允许开启数据
     */
}

第八阶段:双卡模式切换(DSDS 动态开关)

8.1 单卡 ↔ 双卡切换

// 用户在设置中切换单卡/双卡模式
public void switchMultiSimConfig(int numOfSims) {
    // numOfSims: 1(单卡)或 2(双卡)
    
    mPhoneConfigurationManager.switchMultiSimConfig(numOfSims);
    
    // 流程:
    // 1. 设置系统属性:ro.telephony.sim_config
    // 2. 触发 Modem 重配置
    // 3. 通知所有组件:onMultiSimConfigChanged()
    //    ├→ PhoneFactory: 调整 Phone 实例
    //    ├→ PhoneSwitcher: 调整数据卡管理
    //    ├→ UiccController: 重新扫描 SIM 卡
    //    └→ SubscriptionManagerService: 调整订阅
    // 4. 通常需要重启
}

8.2 模式切换时的业务处理

单卡 → 双卡切换:
┌───────────────────────────────────────┐
│ 1. 创建第二个 Phone 实例              │
│ 2. 创建第二个 RIL 通道                │
│ 3. 扫描第二个卡槽的 SIM 卡            │
│ 4. 创建新的订阅信息                   │
│ 5. 设置默认卡(语音/数据/短信)       │
│ 6. 启动 PhoneSwitcher 数据卡切换      │
│ 7. 通知系统 UI 更新(显示两个信号)   │
└───────────────────────────────────────┘

双卡 → 单卡切换:
┌───────────────────────────────────────┐
│ 1. 选择主卡(通常是卡 0)             │
│ 2. 禁用第二个 Modem                   │
│ 3. 删除第二个订阅                     │
│ 4. 销毁 PhoneSwitcher 数据卡管理      │
│ 5. 清理第二个 Phone 实例              │
│ 6. 通知系统 UI 更新(仅显示一个信号)│
└───────────────────────────────────────┘

第九阶段:双卡中的特殊场景

9.1 DSDS 下的语音 + 数据限制

DSDS 设备只有一个 RF 模块和一个 Modem,因此:

┌────────────────────────────────────────────┐
│     场景 1:在卡 0 通话中,用户打开数据    │
├────────────────────────────────────────────┤
│ 结果:数据自动切换到卡 0(通话卡)        │
│       卡 1 无法使用数据                   │
│       通话优先级更高                     │
└────────────────────────────────────────────┘

┌────────────────────────────────────────────┐
│   场景 2:在卡 1 通话中,卡 0 有来电      │
├────────────────────────────────────────────┤
│ 结果:当前通话继续在卡 1 上               │
│       卡 0 来电进入等待或被拒绝           │
│       等待 SRVCC(语音转换)机制          │
└────────────────────────────────────────────┘

9.2 DSDA 下的双语音 + 双数据

DSDA 设备有两个独立的 RF 模块和 Modem,因此:

┌────────────────────────────────────────────┐
│    卡 0 通话(语音)+ 卡 1 数据            │
├────────────────────────────────────────────┤
│ ✓ 支持:同时进行(独立的无线通道)        │
│   - 卡 0 可以通话                         │
│   - 卡 1 可以同时使用数据                 │
│   - 用户体验最佳                         │
└────────────────────────────────────────────┘

┌────────────────────────────────────────────┐
│    卡 0 通话 + 卡 1 通话(并发呼叫)      │
├────────────────────────────────────────────┤
│ ✓ 支持:两个通话可同时进行                │
│   - 卡 0 通话进行中                       │
│   - 卡 1 来电铃声响起                     │
│   - 用户可选择接听卡 1 的来电             │
│   - 形成会议通话或三方通话               │
└────────────────────────────────────────────┘

现在让我更新 TODO 进度:


总结:Android 双卡功能核心知识点

我已经为你详细讲解了 Android 双卡功能的完整体系。让我总结关键要点:

1. 双卡模式架构(DSDS/DSDA)

  • DSDS:一个卡主动,一个卡待机(入门/中端)
  • DSDA:两个卡都主动(高端/特定运营商)
  • 通过 TelephonyManager.getMultiSimConfiguration() 查询

2. 多 Phone 实例管理

  • PhoneFactory 为每个卡槽创建独立的 Phone 实例
  • 每个 Phone 对应一个 RIL 通道和一个 Modem
  • 支持动态切换单卡/双卡模式(通常需要重启)

3. 订阅与默认卡

  • 三层默认设置:语音卡、数据卡、短信卡
  • SubscriptionId (SubId):逻辑概念,映射到物理卡槽
  • 订阅分组:运营商可将多个 SubId 分组,组内共享设置

4. PhoneSwitcher 数据卡切换

  • 监听所有订阅和网络请求的变化
  • 决定哪个卡作为数据卡(数据优先级排序)
  • 向 Modem 发送 ALLOW_DATA 命令控制 PS 附着
  • 语音通话时自动切换数据卡到通话卡

5. 紧急呼叫处理

  • EmergencyStateTracker 管理紧急呼叫
  • 紧急呼叫时自动切换到有信号的卡
  • 紧急呼叫进行中,可能需要切换数据卡以确保连接
  • 紧急呼叫结束后恢复原设置

6. DSDS 下的限制

  • 只有一个 Modem:不能同时语音 + 数据
  • 解决方案:利用 VoLTE(IMS)在数据卡上进行 VoIP 通话
  • 通话时数据自动切换到通话卡

7. DSDA 的优势

  • 两个独立 Modem:可同时语音 + 数据
  • 支持双通话(并发通话)
  • 最佳用户体验,但成本更高

这是面试中关于 Android 多卡/双卡功能 的完整知识体系。涵盖了从硬件架构、系统设计、订阅管理、卡切换逻辑,到特殊场景处理的所有方面!