feat(ios): M3 PoC — PacketTunnelProvider + entitlement + 内存监控 [tsk_nsobbj_rJdy0]

### 新增文件
- client/ios/PacketTunnel/MemoryMonitor.swift
  周期打点 NE 进程内存(os_proc_available_memory iOS13+ / mach_task_basic_info 兼容
  iOS12);每 10s 采样一次;availableMemory < 5MB 时输出 ⚠️ 警告;提供
  summaryString() 供 handleAppMessage/getMemory IPC 返回。

- client/ios/PacketTunnel/PacketTunnel.entitlements
  Extension 的 NE + App Group entitlement 文件;申请说明写入注释。

- client/ios/Runner/Runner.entitlements
  主 App 的 NE + App Group entitlement 文件;申请说明写入注释。

- doc/ne-entitlement-申请指引.md
  完整申请操作手册:Bundle ID 清单、Apple Developer Portal 步骤、
  专项审批英文申请文案(已脱敏,无红线词)、审批 lead time 说明。

### 修改文件
- client/ios/PacketTunnel/PacketTunnelProvider.swift
  完整 M3 实现(替换原骨架 TODO):
  · startTunnel:解析 configJson(options/App Group 两路回退)→
    buildNetworkSettings → setTunnelNetworkSettings → setupTunBridge
    → startLibbox → startPacketBridging → memoryMonitor.start()
  · stopTunnel:memoryMonitor.stop() → stopLibbox → closeTunBridge
  · handleAppMessage:JSON IPC 协议(getStatus/getMemory/selectOutbound)
  · LibboxPlatformInterface 扩展(#if canImport(Libbox)):
    openTun 返回 socketpair libbox 侧 fd,autoDetectInterfaceControl no-op,
    writeLog 转 os.log,useProcFS 返回 false
  · packetFlow 双向桥接(readFromPacketFlow/writeToPacketFlow 循环)
  · parseTunAddress/parseDNSServers 从 JSON 提取网络参数

- client/ios/Runner/VpnManager.swift
  · localizedDescription "穿山甲 Pangolin VPN" → "Pangolin 加速"(红线词修复)
  · 新增 appGroup 常量(group.com.pangolin.pangolinVpn)
  · start() 写入 App Group UserDefaults 缓存 configJson

- client/ios/Runner/Info.plist
  NSVPNUsageDescription "穿山甲使用 VPN…" → "Pangolin 使用网络加速通道…"(红线词修复)

- client/ios/PacketTunnel/Info.plist
  CFBundleDisplayName "穿山甲 Tunnel" → "Pangolin 加速通道"(红线词修复)

- client/ios/Runner.xcodeproj/project.pbxproj
  · Runner + PacketTunnel 所有 build config 添加 CODE_SIGN_ENTITLEMENTS
  · 新增 MemoryMonitor.swift 到 PacketTunnel Sources 构建阶段
  · 新增 Runner.entitlements / PacketTunnel.entitlements / MemoryMonitor.swift
    文件引用及分组

- app/kernel/build-ios.sh
  添加 M3 内存裁剪决策记录:gVisor→grpc→QUIC 优先裁减顺序及内存估算,
  等待真机 10min 压测结果后更新最终结论

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-13 17:44:11 +08:00
parent ed5eabea58
commit e129f094c2
10 changed files with 889 additions and 41 deletions
+140
View File
@@ -0,0 +1,140 @@
// MemoryMonitor.swift NE
//
//
// 1. os_proc_available_memory()iOS 13+ NE
// 2. task_info(MACH_TASK_BASIC_INFO) resident size iOS 12
// 3. availableMemory < warningThreshold
// 4. 10 M3
//
// 使
// let monitor = MemoryMonitor()
// monitor.start() //
// monitor.stop() //
// monitor.peakResidentBytes //
//
// NE jetsam
// A9~15 MB residual limit
// A12~50 MB residual limit
// 线availableMemory < 5 MB
import Darwin
import Foundation
import os.log
// os_proc_available_memory iOS 13+ libSystem 退 mach
// Swift os/proc.h @_silgen_name
@_silgen_name("os_proc_available_memory")
private func _os_proc_available_memory() -> UInt64
// MARK: MemoryMonitor
final class MemoryMonitor {
//
/// 10
let interval: TimeInterval
/// 5 MB
let warningThreshold: UInt64
/// os.log
private let log: OSLog
//
private var timer: DispatchSourceTimer?
private let queue = DispatchQueue(label: "com.pangolin.memory-monitor", qos: .utility)
private(set) var peakResidentBytes: UInt64 = 0
private(set) var minAvailableBytes: UInt64 = UInt64.max
private(set) var sampleCount: Int = 0
init(
interval: TimeInterval = 10,
warningThreshold: UInt64 = 5 * 1024 * 1024,
subsystem: String = "com.pangolin.pangolinVpn.PacketTunnel"
) {
self.interval = interval
self.warningThreshold = warningThreshold
self.log = OSLog(subsystem: subsystem, category: "MemoryMonitor")
}
// MARK:
func start() {
stop()
let t = DispatchSource.makeTimerSource(queue: queue)
t.schedule(deadline: .now(), repeating: interval, leeway: .seconds(1))
t.setEventHandler { [weak self] in self?.tick() }
timer = t
t.resume()
os_log("MemoryMonitor: 开始打点,间隔=%.0fs", log: log, type: .info, interval)
}
func stop() {
timer?.cancel()
timer = nil
os_log(
"MemoryMonitor: 停止打点。总采样 %d 次 | 峰值 RSS=%.1f MB | 最小可用=%.1f MB",
log: log, type: .info,
sampleCount,
Double(peakResidentBytes) / 1_048_576,
minAvailableBytes == UInt64.max ? -1.0 : Double(minAvailableBytes) / 1_048_576
)
}
// MARK:
private func tick() {
let resident = residentBytes()
let available = availableBytes()
sampleCount += 1
if resident > peakResidentBytes { peakResidentBytes = resident }
if available < minAvailableBytes { minAvailableBytes = available }
let level: OSLogType = available < warningThreshold ? .error : .info
os_log(
"MemoryMonitor [#%d] RSS=%.1f MB | 可用=%.1f MB%{public}@",
log: log, type: level,
sampleCount,
Double(resident) / 1_048_576,
Double(available) / 1_048_576,
available < warningThreshold ? " ⚠️ 接近上限,建议裁剪 build tags" : ""
)
}
// MARK:
/// resident sizeRSSiOS 12+
func residentBytes() -> UInt64 {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size / MemoryLayout<integer_t>.size)
let result: kern_return_t = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
return result == KERN_SUCCESS ? UInt64(info.resident_size) : 0
}
/// NE
/// iOS 13+ os_proc_available_memory()
/// iOS 12退 MAX_RSS - resident
func availableBytes() -> UInt64 {
if #available(iOS 13.0, *) {
return _os_proc_available_memory()
} else {
// iOS 12 退 15 MB - RSS
let conservativeLimit: UInt64 = 15 * 1024 * 1024
let resident = residentBytes()
return resident < conservativeLimit ? conservativeLimit - resident : 0
}
}
/// handleAppMessage/getStats
func summaryString() -> String {
let rss = Double(residentBytes()) / 1_048_576
let avail = Double(availableBytes()) / 1_048_576
return String(format: "RSS=%.1fMB avail=%.1fMB peak=%.1fMB samples=%d",
rss, avail,
Double(peakResidentBytes) / 1_048_576,
sampleCount)
}
}