如何获取 iOS 的网络流量数据

最近在做直播相关的一些内容,其中一个需求就是在直播时,能够实时地显示当前的网络状况,包括上下行的速度与累计使用的流量。遂做了一些相关的研究,发现所有的检索结果都指向了 ifaddrs.h 。依照文件头部的版权申明, ifaddrs.h 来自 FreeBSD 的项目,版权属于 Berkeley Software Design, Inc.

FreeBSD 是一种自由的类 Unix 操作系统,它起源于 AT&T Unix,是经过 BSD 、 386BSD 和 4.4BSD 发展而来的类 Unix 的一个重要分支。
—— 摘自维基百科

当然追根溯源并不是今天的重点,而且网上一堆 OC 的现成例子,抄了就能用。不过作为一名 Swift 老司机,怎么用 Swift 做实现才是一名好司机的关键。下面我们发车!

如何自己将 C.h 封装成一个 Module

首先,基于扁平与模块化的思想,直接将 ifaddrs.h 放到 Objective-C bridging header 做桥接肯定是不妥的,而且如果要将其加到一个 framework 中,这样也是不允许的。

Swift Complier - Search Path - Import Path

在你的项目中,定位到你 PROJECT 的 Build Setting,过滤器中可以输入一个 import,然后找到 Swift Complier - Search Path 大项中的 Import Path。这里你可以按照平台划分,来加入一些 modulemap 的检索路径。你实际输入的时候可能是这样的 $(SRCROOT)/SystemModule/ifaddrs/iphoneos
而 module.modulemap 文件的内容,此处以 iphoneos 平台为例,至少需要同时支持 iphoneos 与 iphonesimulator,不同平台的具体路径可以依葫芦画瓢,检索一下即可:

1
2
3
4
module ifaddrs {
header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/ifaddrs.h"
export *
}

然后,你就可以在你的项目中直接 import ifaddrs 了。

getifaddrs(_:) -> Int32 函数的一些使用说明与 Swift 指针的初见

该函数可以获取所有系统的网络接口的信息,不仅仅是全局的联网数据,同时 IP 地址也可以从这里获取。
此时你已经可以看到 getifaddrs(_:) -> Int32 在 Swift 下面的具体方法签名了

1
public func getifaddrs(_: UnsafeMutablePointer<UnsafeMutablePointer<ifaddrs>?>!) -> Int32

少年,回想起当年第一次面对 C 语言的指针、指针的指针的恐惧了吗?UnsafeMutablePointer 即为 Swift 下可变指针的具体类型,至少比星号看起来舒服多了。此处需要构造一个指针的指针,实际类型为 ifaddrs

1
2
3
4
var addrsPointer: UnsafeMutablePointer<ifaddrs>? = nil
if getifaddrs(&addrsPointer) == 0 {
// Do something
}

getifaddrs(_:) -> Int32 函数会创建一个链表,链表上的每个节点都是一个 ifaddrs 结构体,并返回链表第一个元素的指针。成功返回 0 , 失败返回 -1 。并在最后使用 freeifaddrs(_:) 函数来释放申请的内存空间。

1
2
3
4
5
6
var pointer = addrsPointer
while pointer != nil {
// Do something
pointer = pointer?.pointee.ifa_next
}
freeifaddrs(addrsPointer)

注意:在 Swift3 中,指针取其实际的对象的方法已从 memory 变成了 pointee ,其具体的签名为:

1
public var pointee: Pointee { get nonmutating set }

通过判断每一个 ifaddrs 结构体的 ifa_addr 属性的 sa_family 字段是否为 AF_LINK 来过滤进行流量监控内容

1
2
3
4
5
6
if let addrs = pointer?.pointee {
let name = String(cString: addrs.ifa_name)
if addrs.ifa_addr.pointee.sa_family == UInt8(AF_LINK) {
// Do something
}
}

最后根据其 name 来判断流量属于 Wi-Fi 还是 WWAN。
这里还有一个小坑,ifaddrs 结构体的 ifa_data 字段的类型是 UnsafeMutableRawPointer! 。而目标需要使用的类型,或者说它实际的类型是 if_data 。如果你直接使用 if let 编译器会告诉你这是不相关的类型,无法成功转换。此处需要使用 Swift 标准库中的 unsafeBitCast 的方法,其具体签名为:

1
public func unsafeBitCast<T, U>(_ x: T, to: U.Type) -> U

引用 王巍 @onevcat 的原话:

unsafeBitCast 是非常危险的操作,它会将一个指针指向的内存强制按位转换为目标的类型。因为这种转换是在 Swift 的类型管理之外进行的,因此编译器无法确保得到的类型是否确实正确,你必须明确地知道你在做什么。
—— 原文《Swift 中的指针使用》

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if getifaddrs(&addrsPointer) == 0 {
var pointer = addrsPointer
while pointer != nil {
if let addrs = pointer?.pointee {
let name = String(cString: addrs.ifa_name)
if addrs.ifa_addr.pointee.sa_family == UInt8(AF_LINK) {
if name.hasPrefix("en") { // Wifi
let networkData = unsafeBitCast(addrs.ifa_data, to: UnsafeMutablePointer<if_data>.self)
result.wifi.received += networkData.pointee.ifi_ibytes
result.wifi.sent += networkData.pointee.ifi_obytes
} else if name.hasPrefix("pdp_ip") { // WWAN
let networkData = unsafeBitCast(addrs.ifa_data, to: UnsafeMutablePointer<if_data>.self)
result.wwan.received += networkData.pointee.ifi_ibytes
result.wwan.sent += networkData.pointee.ifi_obytes
}
}
}
pointer = pointer?.pointee.ifa_next
}
freeifaddrs(addrsPointer)
}

结语

至此,你已经拿到全局级别的网络数据,注意单位是 bytes,至于怎么转化为最终使用的 1.024 kb/s 或是 已使用 10.24 MB ,我相信已经难不倒各位老司机了。当然,如果你是一名只专注上车的乘客,不如试一下我已经做好的封装 TrafficPolice 。其实,当我第一次看到 Traffic 一词有流量的意思,我也是表示,英语已经全还给老师了,囧。当然,上车注意请刷卡(加星!)。暂时仅支持 Carthage 部署,不是我懒,的确是想为这么好的工具打一次硬广,您就试一下呗。至于如何支持 CocoaPod 那就又是下一话了。