多卡数据切换流程

13 阅读9分钟

1️⃣ 整体流程架构

┌─────────────────────────────────────────────────────────┐
│ 用户操作:在设置中选择默认数据卡                         │
└──────────────────┬──────────────────────────────────────┘
                   │ Settings.Global.putInt(
                   │ MULTI_SIM_DATA_CALL_SUBSCRIPTION, subId)
                   ▼
┌─────────────────────────────────────────────────────────┐
│ SubscriptionManagerService 监听 WatchedInt 变化         │
│ mDefaultDataSubId.set(newValue)                         │
│ → Settings.Global 同步更新                             │
└──────────────────┬──────────────────────────────────────┘
                   │ mDefaultDataSubIdChangedRegistrants
                   │ .notifyRegistrants()
                   ▼
┌─────────────────────────────────────────────────────────┐
│ MultiSimSettingController                              │
│ EVENT_DEFAULT_DATA_SUBSCRIPTION_CHANGED                 │
│ → onDefaultDataSettingChanged()                         │
└──────────────────┬──────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────┐
│ PhoneSwitcher                                           │
│ notifyDefaultDataSubChanged()                           │
│ → onEvaluate() 重新评估应该激活哪些 Modem              │
└──────────────────┬──────────────────────────────────────┘
                   │
         ┌─────────┴──────────────┐
         │                        │
         ▼                        ▼
    ┌──────────┐        ┌───────────────┐
    │ 旧数据卡  │        │ 新数据卡       │
    │ 停用      │        │ 激活           │
    │deactivate│        │activate        │
    └────┬─────┘        └────┬──────────┘
         │                    │
         ▼                    ▼
  ┌──────────────┐   ┌──────────────────┐
  │ setDataAllowed│  │ setDataAllowed    │
  │ (false)       │  │ (true)            │
  │ → RIL         │  │ → RIL             │
  └────┬─────────┘   └────┬─────────────┘
       │                   │
       ▼                   ▼
  ┌──────────────┐   ┌──────────────────┐
  │ 释放旧连接    │   │ 建立新连接        │
  │ PDP 去激活    │   │ PDP 激活          │
  │ 网络指示      │   │ 网络指示          │
  └────┬─────────┘   └────┬─────────────┘
       │                   │
       └─────────┬─────────┘
                 │
                 ▼
    ┌─────────────────────────┐
    │ ConnectivityService     │
    │ 更新默认网络            │
    │ 应用切换网络            │
    └─────────────────────────┘

2️⃣ 详细执行步骤

Step 1: 用户设置变化 (Settings 层)

// 位置:frameworks_opt_telephony/src/java/com/android/internal/telephony/
// subscription/SubscriptionManagerService.java:505-519

mDefaultDataSubId = new WatchedInt(
    Settings.Global.getInt(
        mContext.getContentResolver(),
        Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION,  ◄──── Key!
        SubscriptionManager.INVALID_SUBSCRIPTION_ID)) {
    @Override
    public boolean set(int newValue) {
        int oldValue = mValue;
        if (super.set(newValue)) {
            logl("Default data subId changed from " + oldValue + " to " + newValue);
            // 同步更新 Settings.Global
            Settings.Global.putInt(mContext.getContentResolver(),
                    Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION, newValue);
            return true;
        }
        return false;
    }
};

关键点

  • WatchedInt 是一个被 "监视" 的整数
  • 当值改变时会自动触发注册的 registrant
  • 同时同步更新到 Settings.Global 数据库

Step 2: MultiSimSettingController 响应 (协调器)

// 位置:MultiSimSettingController.java:319-350

public void notifyDefaultDataSubChanged() {
    obtainMessage(EVENT_DEFAULT_DATA_SUBSCRIPTION_CHANGED).sendToTarget();
}

@Override
public void handleMessage(Message msg) {
    switch (msg.what) {
        case EVENT_DEFAULT_DATA_SUBSCRIPTION_CHANGED:
            onDefaultDataSettingChanged();  ◄──── 处理默认数据卡变化
            break;
    }
}

private void onDefaultDataSettingChanged() {
    // 验证数据卡有效性
    // 同步关联订阅的设置(如数据漫游)
    int defaultDataSubId = mSubscriptionManagerService.getDefaultDataSubId();
    
    // 通知 PhoneSwitcher
    PhoneSwitcher.getInstance().notifyDefaultDataSubChanged();
}

职责

  • 协调多张卡的设置
  • 确保同组订阅的设置一致
  • 触发 PhoneSwitcher 的重新评估

Step 3: PhoneSwitcher 评估 (决策点) ⭐

// 位置:PhoneSwitcher.java:1430-1480

protected void updatePreferredDataPhoneId() {
    // 优先级决策:
    
    // 1️⃣ 紧急情况:优先级最高
    if (mEmergencyOverride != null) {
        mPreferredDataPhoneId = mEmergencyOverride.mPhoneId;
        return;
    }
    
    // 2️⃣ 有通话时:如果启用了 "通话期间数据切换"
    if (isAnyVoiceCallActiveOnDevice() && shouldSwitchDataDueToInCall()) {
        mPreferredDataPhoneId = mPhoneIdInVoiceCall;  // 切换到通话卡
        return;
    }
    
    // 3️⃣ 自动切换建议:CBRS 等
    if (mAutoSelectedDataSubId != DEFAULT_SUBSCRIPTION_ID) {
        mPreferredDataPhoneId = getPhoneIdBySubId(mAutoSelectedDataSubId);
        return;
    }
    
    // 4️⃣ 用户选择的默认数据卡:最终方案
    if (SubscriptionManager.isUsableSubIdValue(mPrimaryDataSubId)) {
        mPreferredDataPhoneId = mSubscriptionManagerService.getPhoneId(mPrimaryDataSubId);
    }
}

// 关键方法:评估是否需要改变激活的 Modem
protected boolean onEvaluate(boolean requestsChanged, String evaluationReason) {
    logl("onEvaluate: " + evaluationReason);
    
    if (updatesIfPhoneInVoiceCallChanged()      ◄──── 通话状态
        || updatesIfSubscriptionChanged()         ◄──── 订阅变化
        || updatesIfActiveModemCountChanged()) {  ◄──── Modem 数
        return true;
    }
    
    updatePreferredDataPhoneId();  ◄──── 更新偏好数据 Modem
    
    // ... 计算应该激活哪些 Modem ...
    for (int phoneId = 0; phoneId < mActiveModemCount; phoneId++) {
        if (!newActivePhones.contains(phoneId)) {
            deactivate(phoneId);   ◄──── 停用旧数据卡
        }
    }
    
    for (int phoneId : newActivePhones) {
        activate(phoneId);         ◄──── 激活新数据卡
    }
    
    return true;
}

决策流程(优先级从高到低):

  1. 紧急通话 - 必须优先
  2. 有语音通话且启用通话期间数据切换 - 防止数据丢失
  3. 自动切换建议(如 CBRS)
  4. 用户选择的默认数据卡

Step 4: 停用/激活 Modem (执行层)

// PhoneSwitcher.java:1307-1322

protected void activate(int phoneId) {
    switchPhone(phoneId, true);
}

protected void deactivate(int phoneId) {
    switchPhone(phoneId, false);
}

private void switchPhone(int phoneId, boolean active) {
    PhoneState state = mPhoneStates[phoneId];
    if (state.active == active) return;  ◄──── 避免重复操作
    
    state.active = active;
    logl((active ? "activate " : "deactivate ") + phoneId);
    state.lastRequested = System.currentTimeMillis();
    
    sendRilCommands(phoneId);  ◄──── 发送给 RIL
}

protected void sendRilCommands(int phoneId) {
    if (!SubscriptionManager.isValidPhoneId(phoneId)) return;
    
    Message message = Message.obtain(this, EVENT_MODEM_COMMAND_DONE, phoneId);
    
    if (mActiveModemCount > 1) {
        // 调用 RIL 的 setDataAllowed
        PhoneFactory.getPhone(phoneId)
            .mCi.setDataAllowed(isPhoneActive(phoneId), message);
                             ▲
                             │
                    ┌────────┴───────────┐
                    │                    │
                 true (激活)          false (停用)
    }
}

关键:setDataAllowed 的含义

  • true - 告诉 Modem:"允许这个 SIM 卡建立数据连接"
  • false - 告诉 Modem:"禁止这个 SIM 卡建立数据连接"

Step 5: RIL 处理 (通信层)

// RIL.java:4166-4182

@Override
public void setDataAllowed(boolean allowed, Message result) {
    RadioDataProxy dataProxy = getRadioServiceProxy(RadioDataProxy.class);
    if (!canMakeRequest("setDataAllowed", dataProxy, result, 
            RADIO_HAL_VERSION_1_4)) {
        return;
    }
    
    RILRequest rr = obtainRequest(RIL_REQUEST_ALLOW_DATA, result, 
            mRILDefaultWorkSource);
    
    riljLog(rr.serialString() + "> setDataAllowed allowed = " + allowed);
    
    // 通过 HIDL/AIDL 发送给 Radio HAL
    radioServiceInvokeHelper(HAL_SERVICE_DATA, rr, "setDataAllowed", () -> {
        dataProxy.setDataAllowed(rr.mSerial, allowed);
    });
}

Step 6: Modem 处理 (硬件层)

// hardware_ril/libril/ril_service.cpp:2359-2363

Return<void> RadioImpl::setDataAllowed(int32_t serial, bool allow) {
    RLOGD("setDataAllowed: serial %d", serial);
    
    // 发送 RIL 请求到 RIL daemon
    dispatchInts(serial, mSlotId, RIL_REQUEST_ALLOW_DATA, 
            1, BOOL_TO_INT(allow));
    return Void();
}

// Modem 接收到后:
// - allow = true:  PDP 激活条件满足,启动连接
// - allow = false: PDP 停用,释放连接,断开网络

3️⃣ 切换过程中避免网络断连的方案

问题分析

时间线:
0ms:    用户切换 SIM 卡
10ms:   旧 SIM 卡 → setDataAllowed(false)
20ms:   Modem 释放旧连接 ❌ 网络断连(<1s)
100ms:  新 SIM 卡 → setDataAllowed(true)
300ms:  新连接建立 ✓ 网络恢复

问题:中间的 80-280ms 内没有数据连接!
应用在这段时间的传输会失败。

优化方案 1: 快速序列(当前实现)

特点:依赖 Modem 的快速处理

时间线:
0ms:    旧 setDataAllowed(false)  + 新 setDataAllowed(true)
        ↓ 同步发出 (几乎同时)
10ms:   Modem 收到两个命令
20ms:   Modem 同时处理:
        - 旧连接 DETACH
        - 新连接 ATTACH
        这个瞬间,新连接可能已经初始化!

优点:速度最快,断连时间 < 100ms
缺点:依赖 Modem 实现,不同厂商行为不同

代码实现(推荐)

// 在 PhoneSwitcher 中修改 switchPhone 逻辑
private void switchPhoneOptimized(int oldPhoneId, int newPhoneId) {
    // 同步发送两个命令,而不是顺序发送
    Message message1 = Message.obtain(this, EVENT_MODEM_COMMAND_DONE, oldPhoneId);
    Message message2 = Message.obtain(this, EVENT_MODEM_COMMAND_DONE, newPhoneId);
    
    PhoneFactory.getPhone(oldPhoneId).mCi.setDataAllowed(false, message1);
    PhoneFactory.getPhone(newPhoneId).mCi.setDataAllowed(true, message2);
    
    logl("Simultaneous switch: " + oldPhoneId + " -> " + newPhoneId);
}

优化方案 2: 带验证的切换

特点:确保新卡网络有效后再断开旧卡

流程:
0ms:    激活新 SIM 卡 → setDataAllowed(true)
100ms:  等待 ConnectivityService 验证网络
300ms:  验证成功 ✓
400ms:  此时停用旧卡 → setDataAllowed(false)

优点:保证网络连续性,无断连
缺点:延迟增加(通常 400-500ms)
       短期内有两个连接(费电)

实现位置:PhoneSwitcher.java
// 使用 CellularNetworkValidator 验证网络
private void switchPhoneWithValidation(int oldPhoneId, int newPhoneId) {
    // 1️⃣ 先激活新卡
    activate(newPhoneId);
    
    // 2️⃣ 启动网络验证
    mValidator.validate(getSubIdByPhoneId(newPhoneId),
            true,  // needValidation
            DataSwitch.Reason.DATA_SWITCH_REASON_USER,
            (validated, subId) -> {
                if (validated) {
                    // 3️⃣ 验证成功,停用旧卡
                    logl("Network validated, deactivating old phone: " + oldPhoneId);
                    deactivate(oldPhoneId);
                } else {
                    // ❌ 验证失败,回滚到旧卡
                    logl("Validation failed, reverting to old phone: " + oldPhoneId);
                    deactivate(newPhoneId);
                    activate(oldPhoneId);
                }
            });
}

优化方案 3: 应用缓冲

在应用层实现缓冲,减少切换影响:

1️⃣ 检测到网络切换(ConnectivityManager.NetworkCallback)
2️⃣ 缓冲待发送数据(100-300ms)
3️⃣ 新网络建立后,发送缓冲数据
4️⃣ 提高用户体验

优点:对系统无侵入,应用自主控制
缺点:每个应用都需要实现

4️⃣ 应用正在传输数据时的切换处理

场景分析

应用场景:用户在下载时切换 SIM 卡

时间线:
0ms:    下载 file.bin (SIM1, 10MB, 进度 30%)
100ms:  用户切换到 SIM2
110ms:  旧连接断开 ❌
120ms:  新连接建立
130ms:  下载继续 (从头?从断点?)

问题:连接中断,下载暂停甚至失败!

Android 系统处理机制

ConnectivityService 的自动恢复:

事件                    系统行为
─────────────────────────────────────────
1. 数据连接丢失         DetectableConnectivityError
                        → 回调应用 onLost()
                        
2. 应用收到 onLost()    应该:
                        - 暂停所有数据传输
                        - 保存进度信息
                        - 等待新网络
                        
3. 新网络可用           onAvailable()
                        → 应用恢复传输(从断点)
                        
4. ConnectivityManager
   validateNetwork()     确保新网络真的可用

推荐的应用处理方式

// 位置:应用代码

class DataDownloadManager(context: Context) {
    private val connectivityManager = 
        context.getSystemService<ConnectivityManager>()!!
    
    private val networkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onLost(network: Network) {
            // 1️⃣ 检测到网络丢失
            logg("Network lost, pausing download")
            pauseAllDownloads()  // 暂停下载
            saveProgress()       // 保存进度
        }
        
        override fun onAvailable(network: Network) {
            // 2️⃣ 新网络可用
            logg("Network available, resuming download")
            resumeDownloads()    // 从断点恢复
        }
        
        override fun onCapabilitiesChanged(
            network: Network,
            caps: NetworkCapabilities) {
            // 3️⃣ 检查网络是否已验证
            if (caps.hasCapability(NET_CAPABILITY_VALIDATED)) {
                logg("Network validated")
                // 现在安全发送数据
            }
        }
    }
    
    fun startMonitoring() {
        val request = NetworkRequest.Builder()
            .addCapability(NET_CAPABILITY_INTERNET)
            .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
            .build()
        
        connectivityManager.registerNetworkCallback(request, networkCallback)
    }
    
    private fun pauseAllDownloads() {
        // 暂停所有下载任务
        // 记录当前位置
        downloads.forEach { it.pause() }
    }
    
    private fun saveProgress() {
        // 保存进度到数据库或文件
        downloads.forEach { download ->
            database.updateProgress(download.id, download.currentBytes)
        }
    }
    
    private fun resumeDownloads() {
        // 从保存的断点恢复
        downloads.forEach { download ->
            val savedProgress = database.getProgress(download.id)
            download.resume(savedProgress)
        }
    }
}

5️⃣ 两张卡都是 4G 时的切换延迟分析

延迟来源分解

┌─────────────────────────────────────────────────────────┐
│ 多卡数据切换总延迟 = Δt_total                          │
└─────────────────────────────────────────────────────────┘

Δt_total 包含以下部分:

1. 应用层感知 (10-50ms) 
   Settings.Global 变化 → SubscriptionManagerService

2. MultiSimSettingController 处理 (5-20ms)
   事件分发 → 有效性校验

3. PhoneSwitcher 评估 (20-100ms) ⭐⭐⭐ 最长
   - onEvaluate() 遍历所有网络请求
   - 更新所有优先级
   - 决策激活/停用 Modem
   
4. RIL 命令发送 (5-10ms)
   Java → C++ → HIDL/AIDL 消息传递

5. Modem DETACH (旧卡) (200-500ms) ⭐⭐⭐ 关键
   - 通知网络(NW:释放连接)
   - 等待网络确认
   - 关闭 PDP 上下文
   - 两张都是 4G 时,这通常很快 (200-300ms)

6. Modem ATTACH (新卡) (200-800ms) ⭐⭐⭐ 最不确定
   两张卡都是 4G 时的关键:
   
   a) 搜网 (50-200ms)
      - 如果已经驻留,可能 0-50ms
      - 如果需要搜网,可能 100-200ms
      
   b) 认证/附着 (100-300ms)
      - MME/HSS 认证
      - GUTI 分配等
      
   c) PDN 建立 (100-300ms)
      - PDP 激活
      - 获取 IP 地址
      
   总计:200-800ms
   
   两张 4G 时优化:
   - 如果两张都已驻留:可降至 200-300ms
   - 如果切换到信号弱的卡:可能 500-800ms

7. ConnectivityService 检测 (0-100ms)
   onAvailable() 回调

8. 应用感知 (0-1000ms+)
   取决于应用是否监听 NetworkCallback

────────────────────────────────────────────────

总合(最优情况):
10 + 5 + 50 + 5 + 250 + 250 + 50 + 100 = 720ms

总合(最坏情况):
50 + 20 + 100 + 10 + 500 + 800 + 100 + 1000 = 2580ms

两张 4G 时的期望值:500-800ms

可接受的延迟基准

应用类型               可接受延迟        说明
─────────────────────────────────────────────────
1. 网页浏览            < 3 秒           用户感知不强
2. 实时通话(VoIP)      < 1 秒           时延敏感,需验证
3. 在线游戏            < 500ms          关键,需回滚机制
4. 视频流播放          < 2 秒           需缓冲策略
5. 文件下载            无限制           应用缓冲处理
6. 短视频(抖音)        < 2 秒           用户容忍度低

系统推荐:< 1000ms (1秒)

优化建议(两张 4G)

// 方案 1: 预附着优化(最激进)
// 在停用旧卡前,新卡已完全附着
// 优点:0ms 断连
// 缺点:短期两个活跃连接(耗电)

private void optimizedSwitchWith4G(int oldPhoneId, int newPhoneId) {
    // 不需要等待验证,直接激活
    logl("4G → 4G switch, using fast track");
    
    // 时刻 0ms:激活新卡,同时停用旧卡(并发)
    PhoneFactory.getPhone(newPhoneId).mCi.setDataAllowed(true, msg1);
    PhoneFactory.getPhone(oldPhoneId).mCi.setDataAllowed(false, msg2);
    
    // 预期延迟:200-300ms(纯 ATTACH 时间)
}

// 方案 2: 串联优化(平衡)
// 新卡 ATTACH 成功后再 DETACH 旧卡

private void balancedSwitch4G(int oldPhoneId, int newPhoneId) {
    PhoneFactory.getPhone(newPhoneId).mCi.setDataAllowed(true, 
        new Message() {
            @Override
            public void handleCallback() {
                // 新卡附着成功
                logl("New SIM attached, detaching old");
                PhoneFactory.getPhone(oldPhoneId)
                    .mCi.setDataAllowed(false, msg);
            }
        });
    
    // 预期延迟:200-300ms (新) + 200-300ms (旧) = 400-600ms
    // 但提供了 100% 的连续性保障
}

// 方案 3: 验证优化(最安全)
private void validatedSwitch4G(int oldPhoneId, int newPhoneId) {
    mValidator.setValidationCallback((validated, subId) -> {
        if (validated && subId == getSubIdByPhoneId(newPhoneId)) {
            logl("Network fully validated, safe to detach old");
            PhoneFactory.getPhone(oldPhoneId)
                .mCi.setDataAllowed(false, msg);
        }
    });
    
    // 预期延迟:200-300ms (ATTACH) + 100-200ms (验证) = 300-500ms
    // 最大的延迟,但提供最强的保障
}

6️⃣ 性能指标监控

// 监控数据切换延迟的实现

class DataSwitchMetrics {
    
    private fun logSwitchEvent(
        oldSubId: Int,
        newSubId: Int,
        startTime: Long,
        endTime: Long
    ) {
        val duration = endTime - startTime
        
        logg("""
            Data Switch Metrics:
            Old SubId: $oldSubId
            New SubId: $newSubId
            Duration: ${duration}ms
            Network Continuous: ${duration < 1000}
            
            Recommendation:
            ${when {
                duration < 300 -> "⭐⭐⭐ Excellent"
                duration < 500 -> "⭐⭐ Good"
                duration < 1000 -> "⭐ Acceptable"
                else -> "❌ Poor - Review Modem"
            }}
        """.trimIndent())
        
        // 上报到 metrics 系统
        TelephonyMetrics.getInstance().writeDataSwitchEvent(
            mPhoneId,
            oldSubId,
            newSubId,
            duration,
            "success"
        )
    }
}

总结表格

方面指标说明
两张 4G 最优延迟200-300ms仅 ATTACH 时间
推荐延迟< 1000ms用户基本无感
不可接受延迟> 3000ms需要优化系统
可接受无网时间< 500ms大多数应用可容忍
关键优化点Modem ATTACH占总延迟 30-50%
推荐切换方案验证+串联平衡性能和安全

这就是完整的多卡数据切换流程及优化方案