linger机制

9 阅读9分钟

Android 网络 Linger 机制详解

一、什么是 Linger 机制?

Linger(逗留/保活)机制 是 Android 网络架构中的一个核心机制,用于在网络不再是最优选择时,延迟断开连接,给应用层提供足够的时间优雅地完成网络通信,避免突然断网导致的数据传输中断。

二、核心概念

1. 两种 Inactivity(不活跃)状态

// When ConnectivityService disconnects a network:
// -----------------------------------------------
// If a network is just connected, ConnectivityService will think it will be used soon, but might
// not be used. Thus, a 5s timer will be held to prevent the network being torn down immediately.
// This "nascent" state is implemented by the "lingering" logic below without relating to any
// request, and is used in some cases where network requests race with network establishment. The
// nascent state ends when the 5-second timer fires, or as soon as the network satisfies a
// request, whichever is earlier. In this state, the network is considered in the background.
//
// If a network has no chance of satisfying any requests (even if it were to become validated
// and enter state #5), ConnectivityService will disconnect the NetworkAgent's AsyncChannel.
//
// If the network was satisfying a foreground NetworkRequest (i.e. had been the highest scoring that
// satisfied the NetworkRequest's constraints), but is no longer the highest scoring network for any
// foreground NetworkRequest, then there will be a 30s pause to allow network communication to be
// wrapped up rather than abruptly terminated. During this pause the network is said to be
// "lingering". During this pause if the network begins satisfying a foreground NetworkRequest,
// ConnectivityService will cancel the future disconnection of the NetworkAgent's AsyncChannel, and
// the network is no longer considered "lingering". After the linger timer expires, if the network
// is satisfying one or more background NetworkRequests it is kept up in the background. If it is
// not, ConnectivityService disconnects the NetworkAgent's AsyncChannel.

根据代码注释,Android 定义了两种"不活跃"状态:

(1) Nascent(新生)状态
  • 触发条件:网络刚连接,但尚未满足任何请求
  • 目的:防止网络刚连上就被立即断开(避免竞态条件)
  • 时长5 秒DEFAULT_NASCENT_DELAY_MS
  • 结束条件
    • 满足了某个请求(提前结束)
    • 5 秒超时
(2) Lingering(逗留)状态
  • 触发条件:网络原本是某个 foreground 请求的最佳网络,但现在被更高分数的网络取代
  • 目的:给应用层时间完成当前传输,避免突然断网
  • 时长30 秒DEFAULT_LINGER_DELAY_MS
  • 结束条件
    • 重新成为某个 foreground 请求的最佳网络(取消 linger)
    • 30 秒超时后:
      • 如果仍满足 background 请求 → 保持后台运行
      • 否则 → 彻底断开

2. 时长配置

// Default to 30s linger time-out, and 5s for nascent network. Modifiable only for testing.
private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger";
private static final int DEFAULT_LINGER_DELAY_MS = 30_000;
private static final int DEFAULT_NASCENT_DELAY_MS = 5_000;

// Delimiter used when creating the broadcast delivery group for sending
// CONNECTIVITY_ACTION broadcast.
private static final char DELIVERY_GROUP_KEY_DELIMITER = ';';

// Network transition wakelock timeout in millis.
private static final int NETWORK_TRANSITION_WAKELOCK_TIMEOUT_MS = 1000;

// Timeout for NetworkFactory to be able to mark a network request pending.
private static final int PENDING_INTENT_TIMEOUT_MS = 5 * 60 * 1000;

private final PowerManager.WakeLock mNetTransitionWakeLock;
private final PowerManager.WakeLock mPendingIntentWakeLock;

static final int MAX_NETWORK_REQUESTS_PER_UID = 100;

@VisibleForTesting
static final int MAX_NETWORK_REQUESTS_PER_SYSTEM_UID = 250;

@VisibleForTesting
protected int mLingerDelayMs;  // Can't be final, or test subclass constructors can't change it.
@VisibleForTesting
protected int mNascentDelayMs;
  • Linger 时长:30 秒(可通过系统属性 persist.netmon.linger 调整)
  • Nascent 时长:5 秒(固定)

三、核心实现机制

1. InactivityTimer 数据结构

public static class InactivityTimer implements Comparable<InactivityTimer> {
    public final int requestId;
    public final long expiryMs;

    public InactivityTimer(int requestId, long expiryMs) {
        this.requestId = requestId;
        this.expiryMs = expiryMs;
    }
    public boolean equals(Object o) {
        if (!(o instanceof InactivityTimer)) return false;
        InactivityTimer other = (InactivityTimer) o;
        return (requestId == other.requestId) && (expiryMs == other.expiryMs);
    }
    public int hashCode() {
        return Objects.hash(requestId, expiryMs);
    }
    public int compareTo(InactivityTimer other) {
        return (expiryMs != other.expiryMs) ?
                Long.compare(expiryMs, other.expiryMs) :
                Integer.compare(requestId, other.requestId);
    }
    public String toString() {
        return String.format("%s, expires %dms", requestId,
                expiryMs - SystemClock.elapsedRealtime());
    }
}

每个网络的 NetworkAgentInfo 维护一个 InactivityTimer 集合TreeSet),用于跟踪每个请求的 linger 状态。

2. Linger 状态判断

public boolean isLingering() {
    return mInactive && !isNascent();
}

/**
 * Return whether the network satisfies no request, but is still being kept up
 * because it has just connected less than
 * {@code ConnectivityService#DEFAULT_NASCENT_DELAY_MS}ms ago and is thus still considered
 * nascent. Note that nascent mechanism uses inactivity timer which isn't
 * associated with a request. Thus, use {@link NetworkRequest#REQUEST_ID_NONE} to identify it.
 *
 */
public boolean isNascent() {
    return mInactive && mInactivityTimers.size() == 1
            && mInactivityTimers.first().requestId == NetworkRequest.REQUEST_ID_NONE;
}
  • NascentmInactive=true 且只有一个 REQUEST_ID_NONE 的 timer
  • LingeringmInactive=true 但不是 nascent 状态

3. 添加 Linger Timer

/**
 * Sets the specified requestId to linger on this network for the specified time. Called by
 * ConnectivityService when any request is moved to another network with a higher score, or
 * when a network is newly created.
 *
 * @param requestId The requestId of the request that no longer need to be served by this
 *                  network. Or {@link NetworkRequest#REQUEST_ID_NONE} if this is the
 *                  {@code InactivityTimer} for a newly created network.
 */
// TODO: Consider creating a dedicated function for nascent network, e.g. start/stopNascent.
public void lingerRequest(int requestId, long now, long duration) {
    if (mInactivityTimerForRequest.get(requestId) != null) {
        // Cannot happen. Once a request is lingering on a particular network, we cannot
        // re-linger it unless that network becomes the best for that request again, in which
        // case we should have unlingered it.
        Log.wtf(TAG, toShortString() + ": request " + requestId + " already lingered");
    }
    final long expiryMs = now + duration;
    InactivityTimer timer = new InactivityTimer(requestId, expiryMs);
    if (VDBG) Log.d(TAG, "Adding InactivityTimer " + timer + " to " + toShortString());
    mInactivityTimers.add(timer);
    mInactivityTimerForRequest.put(requestId, timer);
}

/**
 * Sets the specified requestId to linger on this network for the timeout set when
 * initializing or modified by {@link #setLingerDuration(int)}. Called by
 * ConnectivityService when any request is moved to another network with a higher score.
 *
 * @param requestId The requestId of the request that no longer need to be served by this
 *                  network.
 * @param now current system timestamp obtained by {@code SystemClock.elapsedRealtime}.
 */
public void lingerRequest(int requestId, long now) {
    lingerRequest(requestId, now, mLingerDurationMs);
}

当一个请求从网络 A 迁移到网络 B 时,ConnectivityService 会调用 A 的 lingerRequest()

4. 取消 Linger

/**
 * Cancel lingering. Called by ConnectivityService when a request is added to this network.
 * Returns true if the given requestId was lingering on this network, false otherwise.
 */
public boolean unlingerRequest(int requestId) {
    InactivityTimer timer = mInactivityTimerForRequest.get(requestId);
    if (timer != null) {
        if (VDBG) {
            Log.d(TAG, "Removing InactivityTimer " + timer + " from " + toShortString());
        }
        mInactivityTimers.remove(timer);
        mInactivityTimerForRequest.remove(requestId);
        return true;
    }
    return false;
}

如果网络重新成为最佳网络,调用 unlingerRequest() 取消 linger。

5. 定时器触发与清理

public void updateInactivityTimer() {
    long newExpiry = mInactivityTimers.isEmpty() ? 0 : mInactivityTimers.last().expiryMs;
    if (newExpiry == mInactivityExpiryMs) return;

    // Even if we're going to reschedule the timer, cancel it first. This is because the
    // semantics of WakeupMessage guarantee that if cancel is called then the alarm will
    // never call its callback (handleLingerComplete), even if it has already fired.
    // WakeupMessage makes no such guarantees about rescheduling a message, so if mLingerMessage
    // has already been dispatched, rescheduling to some time in the future won't stop it
    // from calling its callback immediately.
    if (mInactivityMessage != null) {
        mInactivityMessage.cancel();
        mInactivityMessage = null;
    }

    if (newExpiry > 0) {
        // If the newExpiry timestamp is in the past, the wakeup message will fire immediately.
        mInactivityMessage = new WakeupMessage(
                mContext, mHandler,
                "NETWORK_LINGER_COMPLETE." + network.getNetId() /* cmdName */,
                EVENT_NETWORK_LINGER_COMPLETE /* cmd */,
                0 /* arg1 (unused) */, 0 /* arg2 (unused) */,
                this /* obj (NetworkAgentInfo) */);
        mInactivityMessage.schedule(newExpiry);
    }

    mInactivityExpiryMs = newExpiry;
}

使用 WakeupMessage 调度定时器,超时后触发 EVENT_NETWORK_LINGER_COMPLETE 事件。

四、完整流程

场景 1:Nascent(新生网络)流程

sequenceDiagram
    participant NA as NetworkAgent
    participant CS as ConnectivityService
    participant NAI as NetworkAgentInfo
    
    NA->>CS: register()
    CS->>NAI: lingerRequest(REQUEST_ID_NONE, 5s)
    CS->>NAI: setInactive()
    Note over NAI: Nascent 状态开始
    
    alt 5 秒内满足请求
        CS->>NAI: 分配请求
        CS->>NAI: unlingerRequest(REQUEST_ID_NONE)
        CS->>NAI: unsetInactive()
        Note over NAI: 提前结束 Nascent
    else 5 秒超时
        NAI->>CS: EVENT_NETWORK_LINGER_COMPLETE
        CS->>CS: handleLingerComplete()
        CS->>CS: unneeded() 检查
        alt 无任何请求
            CS->>NA: teardownUnneededNetwork()
        else 有 background 请求
            CS->>NAI: 转为后台网络
        end
    end

代码体现

// Before first rematching networks, put an inactivity timer without any request, this
// allows {@code updateInactivityState} to update the state accordingly and prevent
// tearing down for any {@code unneeded} evaluation in this period.
// Note that the timer will not be rescheduled since the expiry time is
// fixed after connection regardless of the network satisfying other requests or not.
// But it will be removed as soon as the network satisfies a request for the first time.
networkAgent.lingerRequest(NetworkRequest.REQUEST_ID_NONE,
SystemClock.elapsedRealtime(), mNascentDelayMs);
networkAgent.setInactive();

场景 2:Lingering(网络切换)流程

sequenceDiagram
    participant WiFi as WiFi NetworkAgent
    participant Cell as Cellular NetworkAgent
    participant CS as ConnectivityService
    participant App as 应用层
    
    Note over WiFi: 当前默认网络 (score=70)
    Cell->>CS: 连接并验证成功 (score=80)
    CS->>CS: rematchAllNetworksAndRequests()
    Note over CS: Cell 成为新默认网络
    
    CS->>WiFi: lingerRequest(requestId, 30s)
    CS->>WiFi: setInactive()
    Note over WiFi: Lingering 状态
    
    CS->>App: onLosing(WiFi, lingerTime=30s)
    CS->>App: onAvailable(Cell)
    
    alt WiFi 信号恢复,score > Cell
        CS->>WiFi: unlingerRequest(requestId)
        CS->>WiFi: unsetInactive()
        CS->>App: WiFi 重新成为默认网络
    else 30 秒超时
        CS->>CS: handleLingerComplete()
        alt WiFi 仍有 background 请求
            CS->>WiFi: 保持后台运行
        else WiFi 无任何请求
            CS->>WiFi: teardownUnneededNetwork()
        end
    end

代码体现

/**
 * Updates the inactivity state from the network requests inside the NAI.
 * @param nai the agent info to update
 * @param now the timestamp of the event causing this update
 * @return whether the network was inactive as a result of this update
 */
private boolean updateInactivityState(@NonNull final NetworkAgentInfo nai, final long now) {
    // 1. Update the inactivity timer. If it's changed, reschedule or cancel the alarm.
    // 2. If the network was inactive and there are now requests, unset inactive.
    // 3. If this network is unneeded (which implies it is not lingering), and there is at least
    //    one lingered request, set inactive.
    nai.updateInactivityTimer();
    if (nai.isInactive() && nai.numForegroundNetworkRequests() > 0) {
        if (DBG) log("Unsetting inactive " + nai.toShortString());
        nai.unsetInactive();
        logNetworkEvent(nai, NetworkEvent.NETWORK_UNLINGER);
    } else if (unneeded(nai, UnneededFor.LINGER) && nai.getInactivityExpiry() > 0) {
        if (DBG) {
            final int lingerTime = (int) (nai.getInactivityExpiry() - now);
            log("Setting inactive " + nai.toShortString() + " for " + lingerTime + "ms");
        }
        nai.setInactive();
        logNetworkEvent(nai, NetworkEvent.NETWORK_LINGER);
        return true;
    }
    return false;
}

五、LingerMonitor(用户通知)

/**
 * Class that monitors default network linger events and possibly notifies the user of network
 * switches.
 *
 * This class is not thread-safe and all its methods must be called on the ConnectivityService
 * handler thread.
 */
public class LingerMonitor {

    private static final boolean DBG = true;
    private static final boolean VDBG = false;
    private static final String TAG = LingerMonitor.class.getSimpleName();

    public static final int DEFAULT_NOTIFICATION_DAILY_LIMIT = 3;
    public static final long DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS = DateUtils.MINUTE_IN_MILLIS;

    private static final HashMap<String, Integer> TRANSPORT_NAMES = makeTransportToNameMap();
    @VisibleForTesting
    public static final Intent CELLULAR_SETTINGS = new Intent().setComponent(new ComponentName(
            "com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity"));

    @VisibleForTesting
    public static final int NOTIFY_TYPE_NONE         = 0;
    public static final int NOTIFY_TYPE_NOTIFICATION = 1;
    public static final int NOTIFY_TYPE_TOAST        = 2;

    private static SparseArray<String> sNotifyTypeNames = MessageUtils.findMessageNames(
            new Class[] { LingerMonitor.class }, new String[]{ "NOTIFY_TYPE_" });

    private final Context mContext;
    final Resources mResources;
    private final NetworkNotificationManager mNotifier;
    private final int mDailyLimit;
    private final long mRateLimitMillis;

    private long mFirstNotificationMillis;
    private long mLastNotificationMillis;
    private int mNotificationCounter;

    /** Current notifications. Maps the netId we switched away from to the netId we switched to. */
    private final SparseIntArray mNotifications = new SparseIntArray();

    /** Whether we ever notified that we switched away from a particular network. */
    private final SparseBooleanArray mEverNotified = new SparseBooleanArray();

LingerMonitor 负责监控默认网络的 linger 事件,并在合适时机(如 WiFi→Cellular 切换)向用户展示通知或 Toast。

六、应用层回调

// Notify the requests on this NAI that the network is now lingered.
private void notifyNetworkLosing(@NonNull final NetworkAgentInfo nai, final long now) {
    final int lingerTime = (int) (nai.getInactivityExpiry() - now);
    notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING, lingerTime);
}

应用层通过 NetworkCallback.onLosing(Network network, int maxMsToLive) 接收通知,得知当前网络即将失效,建议迁移到新网络。

七、关键设计点

1. 为什么需要 Linger?

  • 避免突然断网:WiFi→Cellular 切换时,如果立即断开 WiFi,正在下载的大文件、视频流会突然中断。
  • 优雅降级:给应用层 30 秒缓冲期,完成当前传输或迁移到新网络。

2. 为什么需要 Nascent?

  • 避免竞态:网络刚连接时,ConnectivityService 的 rematch 逻辑可能还未完成,如果立即评估为"不需要"会误断开。
  • 保护新连接:给网络 5 秒时间等待请求匹配。

3. 动态调整 Linger 时长

/**
 * Set the linger duration for this NAI.
 * @param durationMs The new linger duration, in milliseconds.
 */
public void setLingerDuration(final int durationMs) {
    final long diff = durationMs - mLingerDurationMs;
    final ArrayList<InactivityTimer> newTimers = new ArrayList<>();
    for (final InactivityTimer timer : mInactivityTimers) {
        if (timer.requestId == NetworkRequest.REQUEST_ID_NONE) {
            // Don't touch nascent timer, re-add as is.
            newTimers.add(timer);
        } else {
            newTimers.add(new InactivityTimer(timer.requestId, timer.expiryMs + diff));
        }
    }
    mInactivityTimers.clear();
    mInactivityTimers.addAll(newTimers);
    updateInactivityTimer();
    mLingerDurationMs = durationMs;
}

NetworkAgent 可通过 setLingerDuration() 动态调整 linger 时长(如 WiFi 可能需要更长时间)。

八、与之前讨论的关联

之前我们分析了 createDefaultInternetRequestForTransport 创建的 BACKGROUND_REQUEST,这些请求与 Linger 机制密切相关:

  • BACKGROUND_REQUEST 保证了 Cellular/WiFi 即使不是默认网络也保持连接
  • 当网络进入 Lingering 状态后,如果仍有 BACKGROUND_REQUEST 满足它,网络不会被 teardown,而是转为后台运行
  • 这就是为什么关闭 WiFi 后,Cellular 能快速成为默认网络的原因——它一直在后台保活

九、总结

状态触发条件时长目的
Nascent网络刚连接,未满足请求5 秒避免竞态导致误断开
Lingering网络被更高分网络取代30 秒优雅完成当前传输

Linger 机制本质:在网络切换时提供 缓冲期,平衡了 快速切换连接稳定性,是 Android 网络架构中的关键用户体验优化设计。