iOS Resource Monitor

通常我们只能通过抓包的方式来抓取和查看手机设备上发送的网络请求。

抓包这种方式,必须保证电脑和手机设备处于同一个网络。

比如常遇到的运营商HTTP劫持(如果只在个别运营商网络下才会出现问题,常常就是遇到了运营商HTTP劫持),这种情况下要通过抓包的方式来定位问题是非常麻烦的。

目前我们能使用的解决方法,通常是通过手机设备打开网络热点,电脑通过该热点来连接网络,再进行抓包。另外每次都要通过电脑才能查看,也是非常的麻烦。

方案

在iOS设备上监听所有的网络请求,展示所有请求的基本信息,包括请求的类型、url和参数,以及响应的状态码、耗时和返回值。开发者通过这些信息可以基本了解请求的整个生命周期的情况。

实现

在iOS上,监听大部分的请求(除了自建了session的请求),最简单暴力的方法是通过NSURLProtocol来监听。由于大部分的请求,包括H5网页,都是通过默认的session,所以这个方案是满足需求的。因此这里实现新的自定义NSURLProtocol,并且在应用启动的时候注册。

c++
1
2
3
#ifdef DEBUG
[NSURLProtocol registerClass:[RSMResourceMonitorURLProtocol class]];
#endif

这里有两点需要注意下:

1.由于主要是用来进行调试,所以最好先判断是否测试包,再进行注册
2.NSURLProtocol查询的次序与注册的次序相反,所以最好是在第一个请求发送之前再进行注册来保证优先级。

c++
1
2
3
4
5
6
7
8
9
10
11
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
return [NSURLProtocol propertyForKey:RSMResourceURLProtocolStartTimeKey inRequest:request] == nil;
}

- (void)startLoading {
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
//标示改request已经处理过了,防止无限循环
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
[NSURLProtocol setProperty:@(startTime) forKey:RSMResourceURLProtocolStartTimeKey inRequest:mutableReqeust];
self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];
}

因为我们是要监听所有的请求,但防止死循环,我们只需要在+canInitWithRequest:方法中判断自定义的RSMResourceURLProtocolStartTimeKey来控制,然后在startLoading方法中标记RSMResourceURLProtocolS。

在请求结束的回调中,记录下请求整体花费的时间、请求本身(请求响应可以通过NSURLRequest获取)和错误信息,以便后面进行查看:

c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSURLRequest *request = connection.currentRequest;

NSNumber *startTimeInterval = [NSURLProtocol propertyForKey:RSMResourceURLProtocolStartTimeKey inRequest:request];
if (startTimeInterval != nil) {
NSTimeInterval endTimeInterval = [[NSDate date] timeIntervalSince1970];
NSTimeInterval duration = endTimeInterval - [startTimeInterval doubleValue];
dispatch_async(dispatch_get_main_queue(), ^(void) {
RSMResorceEntity *entity = [RSMResorceEntity new];
entity.duration = @(duration);
entity.request = request;
entity.error = error;
[[RSMResourceConsoler consoler] addLogEnttiy:entity];
});
}

[self.client URLProtocol:self didFailWithError:error];
}

获取了数据之后,后面的展示就比较简单了,这里就不详细说明,只提一点,就是网络传输的时候,有时候为了减少数据量,服务端有可能会对数据先进行压缩再发送给客户端,所以在展示之前先判断Content-Encoding字段,如果是gzip压缩,则先解压再展示:

c++
1
2
3
4
NSString *encoding = httpResponse.allHeaderFields[@"Content-Encoding"];
if ([encoding isEqualToString:@"gzip"]) {
data = [data rsm_dataByGZipDecompressing] ? : data;
}

目前实现通过请求状态和响应内容的类型来筛选请求,对大部分的情况也基本足够。

最终效果

项目地址:https://github.com/yang2012/iOS-Resource-Monitor

Requests
Request Detail
Request Filter

可优化的点

1.当响应内容较大的时候,直接展示会容易卡住主线程,导致应用有段时间无响应

2.因为目前是通过+[NSURLProtocol registerClass:]的方式来注册自定义的NSURLProtocol,而这个方法是注册到默认的session中,因此如果是自建了NSURLSession,则无法捕获。