docs: APM 监控部分

This commit is contained in:
杭城小刘
2020-04-06 22:35:27 +08:00
parent fb73c0acf4
commit 32d90b27fc
34 changed files with 2702 additions and 126 deletions

439
Chapter1 - iOS/1.83.md Normal file
View File

@@ -0,0 +1,439 @@
# NSURLProtocol 应用场景
> 在做 Hybrid 的时候就使用到 NSURLProtocol对于网络监控依旧可以使用它所以本文就总结下 NSURLProtocol 的应用场景和如何用
## 一、 NSURLProtocol 是什么
NSURLProtocol 是 Foundation 框架中 URL Loading System 的一部分。它可以让开发者可以在不修改应用内原始请求代码的情况下,去改变 URL 加载的全部细节。换句话说NSURLProtocol 是一个被 Apple 默许的中间人攻击。
虽然 NSURLProtocol 叫“Protocol”却不是协议而是一个抽象类。
既然 NSURLProtocol 是一个抽象类,说明它无法被实例化,那么它又是如何实现网络请求拦截的?
答案就是通过子类化来定义新的或是已经存在的 URL 加载行为。如果当前的网络请求是可以被拦截的,那么开发者只需要将一个自定义的 NSURLProtocol 子类注册到 App 中,在这个子类中就可以拦截到所有请求并进行修改。
## 二、NSURLProtocol 使用场景
### 1. 技术层面
NSURLProtocol 是 URL Loading System 的一部分,所以它可以拦截所有基于 URL Loading System 的网络请求:
- NSURLSession
- NSURLConnection
- NSURLDownload
- NSURLResponse
- NSHTTPURLResponse
- NSURLRequest
- NSMutableURLRequest
所以,基础这些基础技术开发的网络框架比如 AFNetworking、Alamofire 也可以拦截。
想到了2种场景不能拦截
- 早期使用 CFNetwork 实现的 ASIHTTPRequest 框架就无法拦截
- UIWebView 也是可以被拦截的。但是 WKWebView 是基于 webkit不走底层 c socket。
### 2. 需求层面
#### 2.1 Hybrid
- 对 webview 上运行的资源进行监控(大小、时间等)
- Native 代理 WebView 上面的图片资源,使用和客户端一致的图片管理策略(比如 SDWebImage 管理)
- 访问提速。App 在 cd 阶段打包内置了项目技术栈的框架库、样式库、一些业务频道的基础包。配合一定的资源更新策略就可以给 Hybrid 提速
- 代理 WebView 上的网络请求。Native 针对网络请求具备更高的安全性和灵活性、网络请求收口策略
#### 2.2 针对网络进行监控
- 对 App 内的网络请求进行重定向,解决 DNS 域名劫持问题
- 针对全局网络请求设置。比如缓存管理、请求地址修改、header
- App 网络安全性。设置 App 网络白名单
- 自定义网络请求,过滤垃圾内容
- H5 加速,请求走本地离线包
## 三、NSURLProtocol 的相关方法
创建协议对象
```Objective-c
// 创建一个 URL 协议实例来处理 request 请求
- (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client;
// 创建一个 URL 协议实例来处理 session task 请求
- (instancetype)initWithTask:(NSURLSessionTask *)task cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client;
```
注册和注销协议类
```Objective-c
// 尝试注册 NSURLProtocol 的子类,使之在 URL 加载系统中可见
+ (BOOL)registerClass:(Class)protocolClass;
// 注销 NSURLProtocol 的指定子类
+ (void)unregisterClass:(Class)protocolClass;
```
确定子类是否可以处理请求
子类化 NSProtocol 的首要任务就是告知它,需要控制什么类型的网络请求。
```objective-c
// 确定协议子类是否可以处理指定的 request 请求,如果返回 YES请求会被其控制返回 NO 则直接跳入下一个 protocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
// 确定协议子类是否可以处理指定的 task 请求
+ (BOOL)canInitWithTask:(NSURLSessionTask *)task;
```
获取和设置请求属性
NSURLProtocol 允许开发者去获取、添加、删除 request 对象的任意元数据。这几个方法常用来处理请求无限循环的问题。
```Objective-c
// 在指定的请求中获取与指定键关联的属性
+ (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;
// 设置与指定请求中的指定键关联的属性
+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
// 删除与指定请求中的指定键关联的属性
+ (void)removePropertyForKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
```
提供请求的规范版本
如果你想要用特定的某个方式来修改请求,可以用下面这个方法。
```Objective-c
// 返回指定请求的规范版本
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
```
确定请求是否相同
```Objective-c
// 判断两个请求是否相同,如果相同可以使用缓存数据,通常只需要调用父类的实现
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
```
启动和停止加载
这是子类中最重要的两个方法,不同的自定义子类在调用这两个方法时会传入不同的内容,但共同点都是围绕 protocol 客户端进行操作。
```Objective-c
// 开始加载
- (void)startLoading;
// 停止加载
- (void)stopLoading;
```
获取协议属性
```Objective-c
// 获取协议接收者的缓存
- (NSCachedURLResponse *)cachedResponse;
// 接受者用来与 URL 加载系统通信的对象,每个 NSProtocol 的子类实例都拥有它
- (id<NSURLProtocolClient>)client;
// 接收方的请求
- (NSURLRequest *)request;
// 接收方的任务
- (NSURLSessionTask *)task;
```
## 四、 如何利用 NSProtocol 拦截网络请求
NSURLProtocol 在实际应用中,主要是完成两步:拦截 URL 和 URL 转发。先来看如何拦截网络请求。
**创建 NSURLProtocol 子类**
这里创建一个名为 HTCustomURLProtocol 的子类。
```Objective-c
@interface HTCustomURLProtocol : NSURLProtocol
@end
```
**注册 NSURLProtocol 的子类**
在合适的位置注册这个子类。对基于 NSURLConnection 或者使用 [NSURLSession sharedSession] 初始化对象创建的网络请求,调用 registerClass 方法即可。
```objective-c
[NSURLProtocol registerClass:[NSClassFromString(@"CustomURLProtocol") class]];
// or
// [NSURLProtocol registerClass:[HTCustomURLProtocol class]];
```
如果需要全局监听,可以设置在 `AppDelegate.m` 的 `didFinishLaunchingWithOptions` 方法中。如果只需要在单个 UIViewController 中使用,记得在合适的时机注销监听:
```objective-c
[NSURLProtocol unregisterClass:[NSClassFromString(@"HTCustomURLProtocol") class]];
```
如果是基于 `NSURLSession` 的网络请求,且不是通过 `[NSURLSession sharedSession]` 方式创建的,就得配置 `NSURLSessionConfiguration` 对象的 `protocolClasses` 属性。
```objective-c
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.protocolClasses = @[[NSClassFromString(@"CustomProtocol") class]];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
```
**实现 NSURLProtocol 子类**
> 注册 → 拦截 → 转发 → 回调 → 结束
以拦截 UIWebView 为例,这里需要重写父类的这五个核心方法。
```objective-c
// 定义一个协议 key
static NSString * const HTCustomURLProtocolHandledKey = @"HTCustomURLProtocolHandledKey";
// 在拓展中定义一个 NSURLConnection 属性。通过 NSURLSession 也可以拦截,这里只是以 NSURLConnection 为例。
@property (nonatomic, strong) NSURLConnection *connection;
// 定义一个可变的请求返回值,
@property (nonatomic, strong) NSMutableData *responseData;
// 方法 1在拦截到网络请求后会调用这一方法可以再次处理拦截的逻辑比如设置只针对 http 和 https 的请求进行处理。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
// 只处理 http 和 https 请求
NSString *scheme = [[request URL] scheme];
if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
[scheme caseInsensitiveCompare:@"https"] == NSOrderedSame)) {
// 看看是否已经处理过了,防止无限循环
if ([NSURLProtocol propertyForKey:HTCustomURLProtocolHandledKey inRequest:request]) {
return NO;
}
// 如果还需要截取 DNS 解析请求中的链接,可以继续加判断,是否为拦截域名请求的链接,如果是返回 NO
return YES;
}
return NO;
}
// 方法 2【关键方法】可以在此对 request 进行处理,比如修改地址、提取请求信息、设置请求头等。
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
// 可以打印出所有的请求链接包括 CSS 和 Ajax 请求等
NSLog(@"request.URL.absoluteString = %@",request.URL.absoluteString);
NSMutableURLRequest *mutableRequest = [request mutableCopy];
return mutableRequest;
}
// 方法 3【关键方法】在这里设置网络代理重新创建一个对象将处理过的 request 转发出去。这里对应的回调方法对应 <NSURLProtocolClient> 协议方法
- (void)startLoading {
// 可以修改 request 请求
NSMutableURLRequest *mutableRequest = [[self request] mutableCopy];
// 打 tag防止递归调用
[NSURLProtocol setProperty:@YES forKey:HTCustomURLProtocolHandledKey inRequest:mutableRequest];
// 也可以在这里检查缓存
// 将 request 转发,对于 NSURLConnection 来说,就是创建一个 NSURLConnection 对象;对于 NSURLSession 来说,就是发起一个 NSURLSessionTask。
self.connection = [NSURLConnection connectionWithRequest:mutableRequest delegate:self];
}
// 方法 4主要判断两个 request 是否相同,如果相同的话可以使用缓存数据,通常只需要调用父类的实现。
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
return [super requestIsCacheEquivalent:a toRequest:b];
}
// 方法 5处理结束后停止相应请求清空 connection 或 session
- (void)stopLoading {
if (self.connection != nil) {
[self.connection cancel];
self.connection = nil;
}
}
// 按照在上面的方法中做的自定义需求,看情况对转发出来的请求在恰当的时机进行回调处理。
#pragma mark- NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self.client URLProtocol:self didFailWithError:error];
}
#pragma mark - NSURLConnectionDataDelegate
// 当接收到服务器的响应(连通了服务器)时会调用
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
self.responseData = [[NSMutableData alloc] init];
self.internalResponse = response;
// 可以处理不同的 statusCode 场景
// NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
// 可以设置 Cookie
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
// 接收到服务器的数据时会调用,可能会被调用多次,每次只传递部分数据
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.responseData appendData:data];
[self.client URLProtocol:self didLoadData:data];
}
// 服务器的数据加载完毕后调用
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.client URLProtocolDidFinishLoading:self];
}
// 请求错误(失败)的时候调用,比如出现请求超时、断网,一般指客户端错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self.client URLProtocol:self didFailWithError:error];
}
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response {
if (response != nil) {
[[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
}
return request;
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection {
return YES;
}
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
[[challenge sender] useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
[self.client URLProtocol:self didReceiveAuthenticationChallenge:challenge];
}
- (void) connection:(NSURLConnection *)connection
didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
[self.client URLProtocol:self
didCancelAuthenticationChallenge:challenge];
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse *)cachedResponse {
return cachedResponse;
}
```
注意NSURLConnection 已经被废弃,推荐使用 NSURLSession 进行网络请求,它好处多多,具体的自行查阅官方介绍。
## 五、 补充内容
### 1. 使用 NSURLSession 时的注意事项
如果在 NSURLProtocol 中使用 NSURLSession需要注意
• 拦截到的 request 请求的 HTTPBody 为 nil但可以借助 HTTPBodyStream 来获取 body
• 如果要用 registerClass 注册,只能通过 ` [NSURLSession sharedSession] `的方式创建网络请求。
### 2. 注册多个 NSURLProtocol 子类
当有多个自定义 NSURLProtocol 子类注册到系统中的话,会按照他们注册的反向顺序依次调用 URL 加载流程,也就是最后注册的 NSURLProtocol 会被优先判断。
对于通过配置 NSURLSessionConfiguration 对象的 protocolClasses 属性来注册的情况protocolClasses 数组中只有第一个 NSURLProtocol 会起作用,后续的 NSURLProtocol 就无法拦截到了。
### 3. 如何拦截 WKWebview
WKWebView 在独立于 app 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在 WKWebView 上直接使用 NSURLProtocol 无法拦截请求。苹果开源的 webKit2 源码暴露了 私有API
```objective-c
+ [WKBrowsingContextController registerSchemeForCustomProtocol:]
```
通过注册 http(s) scheme 后 WKWebView 将可以使用 NSURLProtocol 拦截 http(s) 请求。涉及到的私有 api `WKBrowsingContextControoler` 、`registerSchemeForCustomProtocol`
```objective-c
Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([cls respondsToSelector:sel]) {
// 通过 http 和 https 的请求,同理可通过其他的 Scheme 但是要满足 URL Loading System
[cls performSelector:sel withObject:@"http"];
[cls performSelector:sel withObject:@"https"];
}
```
因为使用了私有 api所以会无法过审我们可以对字符串进行处理比如对方法名进行加密。
该方案还存在2个严重缺陷
1. post 请求 body 数据被清空
由于 WKWebview 在独立的进程中执行网络请求,一旦注册 registerSchemeForCustomProtocol httphttps scheme 后,网络请求将从 Network Process 发送到 App Process这样 NSURLProtocol 才能拦截网络请求。在 Webkit2 的设计里使用 **MessageQueue** 进行进程间通信, Network Process 会将请求 encode 成一个 message然后通过 IPC 发送给 App Process出于性能角度的考虑encode 的时候 HTTPBody 和 HTTPBodyStream 这2个字段被丢弃掉了。可以查看 [webkit2 源码](https://github.com/WebKit/webkit/blob/fe39539b83d28751e86077b173abd5b7872ce3f9/Source/WebKit2/Shared/mac/WebCoreArgumentCodersMac.mm#L61-L88)。
2. 对 ATS 支持不足
info.plist 中打开 ATS 开关,设置 Allow Arbitrary Loads 选项为 NO设置 registerSchemeForCustomProtocol 注册了 httphttps schemeWKWebView 发起的所有 http 网络请求将被阻塞(即使 Allow Arbitrary Loads in Web Content 选项为 YES
WKWebView 可以注册 customScheme比如自定义 scheme`Hybrid://`,因此使用离线包但不使用 post 方式的请求可以通过 customScheme 发起。比如 `Hybrid://www.xxx.com/`,然后在 App 进程被 NSURLProtocol 拦截这个请求,然后加载离线包资源。
不足:使用 post 方式的请求需要修改 h5 侧代码scheme
### 4. WKWebView loadRequest 问题
在 WKWebView 上通过 loadRequest 发起的 post 请求,会丢失 body 数据。
```objective-c
//同样是由于进程间通信性能问题HTTPBody字段被丢弃
[request setHTTPMethod:@"POST"];
[request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];
[wkwebview loadRequest: request];
```
通过上面的基础条件,其实可以麻烦一点解决 post 请求的 body 丢失问题。
假如 WKWebView loadRequest 要加载 post 请求 request1: http://h5.xxx.com/mobile/index。步骤如下
1. 子类继承自抽象类 NSURLProtocol并向 App 注册。
2. 将 request1 的 scheme 进行替换,生成新的请求 request2`post://h5.xxx.com/mobile/index`。同时将 rquest1 的 body 字段添加到 header 信息中webkit 不会丢弃 header 信息)。
3. WKWebView 加载新的 request2。 `[WKWebView loadRequest:request2];`
4. 通过 `[WKBrowsingContextController registerSchemeForCustomProtocol:]` 注册 scheme**post://**。
5. NSURLProtocl 拦截请求 ``post://h5.xxx.com/mobile/index`。做 scheme 还原的操作,也就是替换 scheme。生成新的请求 request3 `http://h5.xxx.com/mobile/index`,同时将 request2 header 中的 body 字段添加到 request3 的 body 中,并使用 NSURLConnection 加载 request3
6. 网络请求完成后,通过 NetworkProtocolClient 将请求结果返回给 WKWebView。
1.
关于 WKWebview 的各种问题可以查看这篇[文章](https://www.tuicool.com/articles/QbE3Mb7)。