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;
}
- Nascent:
mInactive=true且只有一个REQUEST_ID_NONE的 timer - Lingering:
mInactive=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 网络架构中的关键用户体验优化设计。