diff --git a/Chapter1 - iOS/1.74.md b/Chapter1 - iOS/1.74.md index 106aa10..40ec969 100644 --- a/Chapter1 - iOS/1.74.md +++ b/Chapter1 - iOS/1.74.md @@ -1861,7 +1861,7 @@ iOS 网络现状是由4层组成的:最底层的 BSD Sockets、SecureTransport -#### 2.1 NSURLProtocol 监控 App 网络请求 +#### 2.1 NSURLProtocol 监控 App 网络请求 NSURLProtocol 作为上层接口,使用较为简单,但 NSURLProtocol 属于 URL Loading System 体系中。应用协议的支持程度有限,支持 FTP、HTTP、HTTPS 等几个应用层协议,对于其他的协议则无法监控,存在一定的局限性。如果监控底层网络库 CFNetwork 则没有这个限制。 @@ -2341,9 +2341,9 @@ API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) -#### 2.2 骚操作篇 +#### 2.2 骚操作篇 -在 2.1 分析到了 NSURLSessionTaskMetrics 由于兼容性问题,对于网络监控来说似乎不太完美,但是自后在搜资料的时候看到了一篇[文章](https://www.jianshu.com/p/1c34147030d1)。文章在分析 WebView 的网络监控的时候分析 Webkit 源码的时候发现了下面代码 +文章上面 [2.1 ](#network-2.1)分析到了 NSURLSessionTaskMetrics 由于兼容性问题,对于网络监控来说似乎不太完美,但是自后在搜资料的时候看到了一篇[文章](https://www.jianshu.com/p/1c34147030d1)。文章在分析 WebView 的网络监控的时候分析 Webkit 源码的时候发现了下面代码 ```objective-c #if !HAVE(TIMINGDATAOPTIONS) @@ -2441,9 +2441,9 @@ NSURLSession 在 iOS9 之前使用 `_setCollectsTimingData:` 就可以使用 Tim 在 iOS 中 AOP 的实现是基于 Runtime 机制,目前由3种方式:Method Swizzling、NSProxy、FishHook(主要用用于 hook c 代码)。 -2.1 讨论了满足大多数的需求的场景,NSURLProtocol 监控了 NSURLConnection、NSURLSession 的网络请求,自身代理后可以发起网络请求并得到诸如请求开始时间、请求结束时间、header 信息等,但是无法得到非常详细的网络性能数据,比如 DNS 开始解析时间、DNS 解析用了多久、reponse 开始返回的时间、返回了多久等。 iOS10 之后 NSURLSessionTaskDelegate 增加了一个代理方法 `- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));`,可以获取到精确的各项网络数据。但是具有兼容性。2.2 讨论了一个 Webkit 源码中得到的信息,通过私有方法 `_setCollectsTimingData:` 、`_timingData` 可以获取到 TimingData。 +文章上面 [2.1 ](#network-2.1)讨论了满足大多数的需求的场景,NSURLProtocol 监控了 NSURLConnection、NSURLSession 的网络请求,自身代理后可以发起网络请求并得到诸如请求开始时间、请求结束时间、header 信息等,但是无法得到非常详细的网络性能数据,比如 DNS 开始解析时间、DNS 解析用了多久、reponse 开始返回的时间、返回了多久等。 iOS10 之后 NSURLSessionTaskDelegate 增加了一个代理方法 `- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));`,可以获取到精确的各项网络数据。但是具有兼容性。文章上面 [2.2 ](#network-2.2)讨论了从 Webkit 源码中得到的信息,通过私有方法 `_setCollectsTimingData:` 、`_timingData` 可以获取到 TimingData。 -但是如果需要监全部的网络请求就不能满足需求了,查阅资料后发现了阿里百川有 APM 的解决方案,对于网络监控需要做如下的 hook +但是如果需要监全部的网络请求就不能满足需求了,查阅资料后发现了阿里百川有 APM 的解决方案,于是有了方案3,对于网络监控需要做如下的处理 ![network hook](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-04-network_monitor.png) @@ -2546,7 +2546,292 @@ void printResponseData (CFDataRef responseData) { } ``` -Method Swizzling 方法替换需要知道类名,我们在开发阶段使用 NSURLConnection、NSURLSession 都需要指定代理对象,且代理对象在该阶段没有办法确定,所以在此处的思路是在 NSURLConnection、NSURLSession 设置代理的时候替换掉代理对象, +我们知道 NSURLSession、NSURLConnection、CFNetwork 的使用都需要调用一堆方法进行设置然后需要设置代理对象,实现代理方法。所以针对这种情况进行监控首先想到的是使用 runtime hook 掉方法层级。但是针对设置的代理对象的代理方法没办法 hook,因为不知道代理对象是哪个类。所以想办法可以 hook 设置代理对象这个步骤,将代理对象替换成我们设计好的某个类,然后让这个类去实现 NSURLConnection、NSURLSession、CFNetwork 相关的代理方法。然后在这些方法的内部都去调用一下原代理对象的方法实现。所以我们的需求得以满足,我们在相应的方法里面可以拿到监控数据,比如请求开始时间、结束时间、状态码、内容大小等。 + +具体步骤及其关键代码如下,以 NSURLConnection 举例 + +- 因为要 Hook 挺多地方,所以写一个 method swizzling 的工具类 + + ```objective-c + #import + + NS_ASSUME_NONNULL_BEGIN + + @interface NSObject (hook) + + /** + hook对象方法 + + @param originalSelector 需要hook的原始对象方法 + @param swizzledSelector 需要替换的对象方法 + */ + + (void)apm_swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector; + + /** + hook类方法 + + @param originalSelector 需要hook的原始类方法 + @param swizzledSelector 需要替换的类方法 + */ + + (void)apm_swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector; + + @end + + NS_ASSUME_NONNULL_END + + + (void)apm_swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector + { + class_swizzleInstanceMethod(self, originalSelector, swizzledSelector); + } + + + (void)apm_swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector + { + //类方法实际上是储存在类对象的类(即元类)中,即类方法相当于元类的实例方法,所以只需要把元类传入,其他逻辑和交互实例方法一样。 + Class class2 = object_getClass(self); + class_swizzleInstanceMethod(class2, originalSelector, swizzledSelector); + } + + void class_swizzleInstanceMethod(Class class, SEL originalSEL, SEL replacementSEL) + { + Method originMethod = class_getInstanceMethod(class, originalSEL); + Method replaceMethod = class_getInstanceMethod(class, replacementSEL); + + if(class_addMethod(class, originalSEL, method_getImplementation(replaceMethod),method_getTypeEncoding(replaceMethod))) + { + class_replaceMethod(class,replacementSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod)); + }else { + method_exchangeImplementations(originMethod, replaceMethod); + } + } + ``` + +- 建立一个继承自 NSProxy 抽象类的类,实现相应方法。 + + ```objective-c + #import + + NS_ASSUME_NONNULL_BEGIN + + // 为 NSURLConnection、NSURLSession、CFNetwork 代理设置代理转发 + @interface NetworkDelegateProxy : NSProxy + + + (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate; + + @end + + NS_ASSUME_NONNULL_END + + // .m + @interface NetworkDelegateProxy () { + id _originalTarget; + id _NewDelegate; + } + + @end + + + @implementation NetworkDelegateProxy + + #pragma mark - life cycle + + + (instancetype)sharedInstance { + static NetworkDelegateProxy *_sharedInstance = nil; + + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + _sharedInstance = [NetworkDelegateProxy alloc]; + }); + + return _sharedInstance; + } + + + #pragma mark - public Method + + + (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate + { + NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance]; + instance->_originalTarget = originalTarget; + instance->_NewDelegate = newDelegate; + return instance; + } + + - (void)forwardInvocation:(NSInvocation *)invocation + { + if ([_originalTarget respondsToSelector:invocation.selector]) { + [invocation invokeWithTarget:_originalTarget]; + [((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation]; + } + } + + - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel + { + return [_originalTarget methodSignatureForSelector:sel]; + } + + @end + ``` + +- 创建一个对象,实现 NSURLConnection、NSURLSession、NSIuputStream 代理方法 + + ```objective-c + // NetworkImplementor.m + + #pragma mark-NSURLConnectionDelegate + - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { + NSLog(@"%s", __func__); + } + + - (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response { + NSLog(@"%s", __func__); + return request; + } + + #pragma mark-NSURLConnectionDataDelegate + - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { + NSLog(@"%s", __func__); + } + + - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { + NSLog(@"%s", __func__); + } + + - (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten + totalBytesWritten:(NSInteger)totalBytesWritten + totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite { + NSLog(@"%s", __func__); + } + + - (void)connectionDidFinishLoading:(NSURLConnection *)connection { + NSLog(@"%s", __func__); + } + + #pragma mark-NSURLConnectionDownloadDelegate + - (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes { + NSLog(@"%s", __func__); + } + + - (void)connectionDidResumeDownloading:(NSURLConnection *)connection totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes { + NSLog(@"%s", __func__); + } + + - (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL { + NSLog(@"%s", __func__); + } + // 根据需求自己去写需要监控的数据项 + ``` + +- 给 NSURLConnection 添加 Category,专门设置 hook 代理对象、hook NSURLConnection 对象方法 + + ```objective-c + // NSURLConnection+Monitor.m + @implementation NSURLConnection (Monitor) + + + (void)load + { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + @autoreleasepool { + [[self class] apm_swizzleMethod:@selector(apm_initWithRequest:delegate:) swizzledSelector:@selector(initWithRequest: delegate:)]; + } + }); + } + + - (_Nonnull instancetype)apm_initWithRequest:(NSURLRequest *)request delegate:(nullable id)delegate + { + /* + 1. 在设置 Delegate 的时候替换 delegate。 + 2. 因为要在每个代理方法里面,监控数据,所以需要将代理方法都 hook 下 + 3. 在原代理方法执行的时候,让新的代理对象里面,去执行方法的转发, + */ + NSString *traceId = @"traceId"; + NSMutableURLRequest *rq = [request mutableCopy]; + NSString *preTraceId = [request.allHTTPHeaderFields valueForKey:@"head_key_traceid"]; + if (preTraceId) { + // 调用 hook 之前的初始化方法,返回 NSURLConnection + return [self apm_initWithRequest:rq delegate:delegate]; + } else { + [rq setValue:traceId forHTTPHeaderField:@"head_key_traceid"]; + + NSURLSessionAndConnectionImplementor *mockDelegate = [NSURLSessionAndConnectionImplementor new]; + [self registerDelegateMethod:@"connection:didFailWithError:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@@"]; + + [self registerDelegateMethod:@"connection:didReceiveResponse:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@@"]; + [self registerDelegateMethod:@"connection:didReceiveData:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@@"]; + [self registerDelegateMethod:@"connection:didFailWithError:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@@"]; + + [self registerDelegateMethod:@"connectionDidFinishLoading:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@"]; + [self registerDelegateMethod:@"connection:willSendRequest:redirectResponse:" originalDelegate:delegate newDelegate:mockDelegate flag:"@@:@@"]; + delegate = [NetworkDelegateProxy setProxyForObject:delegate withNewDelegate:mockDelegate]; + + // 调用 hook 之前的初始化方法,返回 NSURLConnection + return [self apm_initWithRequest:rq delegate:delegate]; + } + } + + - (void)registerDelegateMethod:(NSString *)methodName originalDelegate:(id)originalDelegate newDelegate:(NSURLSessionAndConnectionImplementor *)newDelegate flag:(const char *)flag + { + if ([originalDelegate respondsToSelector:NSSelectorFromString(methodName)]) { + IMP originalMethodImp = class_getMethodImplementation([originalDelegate class], NSSelectorFromString(methodName)); + IMP newMethodImp = class_getMethodImplementation([newDelegate class], NSSelectorFromString(methodName)); + if (originalMethodImp != newMethodImp) { + [newDelegate registerSelector: methodName]; + NSLog(@""); + } + } else { + class_addMethod([originalDelegate class], NSSelectorFromString(methodName), class_getMethodImplementation([newDelegate class], NSSelectorFromString(methodName)), flag); + } + } + + @end + ``` + + + +这样下来就是可以监控到网络信息了,然后将数据交给数据上报 SDK,按照下发的数据上报策略去上报数据。 + +顺道说一句,上面针对 NSURLConnection、NSURLSession、NSInputStream 代理对象的 hook 之后,利用 NSProxy 实现代理对象方法的转发,有另一种方法可以实现,那就是 **isa swizzling**。 + +- Method swizzling 原理 + + ```objective-c + struct old_method { + SEL method_name; + char *method_types; + IMP method_imp; + }; + ``` + + ![method swizzling](./../assets/2020-04-09-methodSwizzling.png) + + method swizzling 改进版如下 + + ```objective-c + Method originalMethod = class_getInstanceMethod(aClass, aSEL); + IMP originalIMP = method_getImplementation(originalMethod); + char *cd = method_getTypeEncoding(originalMethod); + IMP newIMP = imp_implementationWithBlock(^(id self) { + void (*tmp)(id self, SEL _cmd) = originalIMP; + tmp(self, aSEL); + }); + class_replaceMethod(aClass, aSEL, newIMP, cd); + ``` + +- isa swizzling + + ```objective-c + /// Represents an instance of a class. + struct objc_object { + Class _Nonnull isa OBJC_ISA_AVAILABILITY; + }; + + /// A pointer to an instance of a class. + typedef struct objc_object *id; + + ``` + + ![method swizzling](./../assets/2020-04-09-methodSwizzling.png) @@ -2556,7 +2841,7 @@ Method Swizzling 方法替换需要知道类名,我们在开发阶段使用 NS -#### 2.4 监控 App 常见网络请求 +#### 2.4 监控 App 常见网络请求 本着成本的原因,由于现在大多数的项目的网络能力都是通过 [AFNetworking](https://github.com/AFNetworking/AFNetworking) 完成的,所以本文的网络监控可以快速完成。 diff --git a/Chapter1 - iOS/1.83.md b/Chapter1 - iOS/1.83.md index 0e6bed0..f8162d9 100644 --- a/Chapter1 - iOS/1.83.md +++ b/Chapter1 - iOS/1.83.md @@ -407,10 +407,10 @@ if ([cls respondsToSelector:sel]) { 6. 网络请求完成后,通过 NetworkProtocolClient 将请求结果返回给 WKWebView。 +### 5. 拦截 WebView 内 Ajax 请求 - - - +其实上述的方法也是可行,不过使用私有 API 的方式不是很推荐,一般在穷途末路的时候才选择私有 API,所以另一种思路是 hook Web 端的 ajax 请求。在执行 hook 后的 ajax 请求的时候将 ajax 的请求相关信息(请求方式、header、body 等)以 messageHandler 的方式告诉 Native,然后起到监控的效果。 +参考: https://www.jianshu.com/p/7337ac624b8e;https://github.com/wendux/Ajax-hook diff --git a/Chapter3 - Server/3.9.md b/Chapter3 - Server/3.9.md new file mode 100644 index 0000000..2854542 --- /dev/null +++ b/Chapter3 - Server/3.9.md @@ -0,0 +1,106 @@ +# 爬取疫情数据并用 Markdown 预览 + +> 周五不困,无聊写了一个 Python 脚本,功能很简单:获取新浪关于各个国家疫情数据,并写入 md 文件并预览,定时去获取数据,有新数据则生成新的 markdown 内容拼接在文件最后。 + + + + +## 一、 代码 + +由于功能和代码都很简单,直接上代码 + +```Python +# -*-coding:utf8-*- +import re,requests,json,pprint,time +import os + +pattern=re.compile(r'^try{sinajp_15844213244528328543098388435\((.*?)\);}catch\(e\){};') +lasttimes='00:00:00' + +while True: + res=requests.get('https://gwpre.sina.cn/ncp/foreign?_=1584421324452&callback=sinajp_15844213244528328543098388435') + match=pattern.search(res.text) + + if match: + obj=json.loads(match.group(1)) + resultObj=obj['result'] + times=resultObj['times'] # 截止时间 + timesMatch=re.search(r'截至(\d{2})月(\d{2})日(\d{2})时(\d{2})分',times) + if timesMatch: + times=timesMatch.group(1)+'月'+timesMatch.group(2)+'日 '+timesMatch.group(3)+':'+timesMatch.group(4) + + if times==lasttimes: + continue + else: + lasttimes=times + totalObj=resultObj['total'] + certain=totalObj['certain'] # 累计确诊 + die=totalObj['die'] # 死亡 + recure=totalObj['recure'] # 治愈 + certain_inc=totalObj['certain_inc'] # 确诊增加 + die_inc=totalObj['die_inc'] # 死亡增加 + recure_inc=totalObj['recure_inc'] # 治愈增加 + # 各国数据列表 + worldlistArr=resultObj['worldlist'] + worldlistArr.sort(key=lambda x: int(x.get('conNum','0')),reverse=True) + + fo=open('./coronavirus.md','a') + fo.writelines('\n# '+times+'\n') + fo.writelines('感染国家总数:'+str(len(worldlistArr))+'\n') + fo.writelines('```\n累计确诊:'+certain.rjust(10,' ')+' 较昨日:'+certain_inc+'\n'+'累计死亡:'+die.rjust(10,' ')+' 较昨日:'+die_inc+'\n'+'累计治愈:'+recure.rjust(10,' ')+' 较昨日:'+recure_inc+'\n```\n') + + fo.writelines('|国家|新增确诊|累计确诊|新增死亡|累计死亡|累计治愈|'+'\n') + fo.writelines('|:--:|---:|---:|---:|---:|---:|'+'\n') + + top15=worldlistArr[:15] + pattient_countrys=['澳大利亚','加拿大','巴西','印度','丹麦','越南','新加坡','俄罗斯','塞尔维亚','巴基斯坦',] + pattient=[c for c in worldlistArr if c['name'] in pattient_countrys] + + for countryObj in top15: + name=countryObj['name'] # 国家 + if name=='中国': + continue + conadd=countryObj['conadd'] # 新增确诊 + conNum=countryObj['conNum'] # 累计确诊 + deathadd=countryObj['deathadd'] # 新增死亡 + deathNum=countryObj['deathNum'] # 累计死亡 + cureNum=countryObj['cureNum'] # 累计治愈 + fo.writelines('|'+name+'|'+conadd+'|'+conNum+'|'+deathadd+'|'+deathNum+'|'+cureNum+'|\n') + + fo.writelines('\n特别关心'+'\n') + fo.writelines('|国家|新增确诊|累计确诊|新增死亡|累计死亡|累计治愈|'+'\n') + fo.writelines('|:--:|---:|---:|---:|---:|---:|'+'\n') + for countryObj in pattient: + name=countryObj['name'] # 国家 + conadd=countryObj['conadd'] # 新增确诊 + conNum=countryObj['conNum'] # 累计确诊 + deathadd=countryObj['deathadd'] # 新增死亡 + deathNum=countryObj['deathNum'] # 累计死亡 + cureNum=countryObj['cureNum'] # 累计治愈 + fo.writelines('|'+name+'|'+conadd+'|'+conNum+'|'+deathadd+'|'+deathNum+'|'+cureNum+'|\n') + fo.close() + + # 用 Markdown IDE 打开 .md 文件进行预览 + os.system('open -a "/Applications/Typora.app" ./coronavirus.md') + + for i in range(1,61): + time.sleep(10) + print(i*10)2 +``` + + + + +## 二、 如何使用 + +1. 安装 requests +```shell +pip3 install requests +``` + +2. 修改 Markdown 的打开方式。由于我电脑安装 Marodown 编辑器是 `Typora`,所以脚本是 `open -a "/Applications/Typora.app" ./coronavirus.md`。修改这里的 `***.app` 为自己的 ide + +3. 终端运行即可 +```shell +python3 coronavirus.py +``` diff --git a/Chapter3 - Server/chapter3.md b/Chapter3 - Server/chapter3.md index c8d1ef5..34368a6 100644 --- a/Chapter3 - Server/chapter3.md +++ b/Chapter3 - Server/chapter3.md @@ -11,3 +11,4 @@ * [6、YAML](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.6.md) * [7、Node单元测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.7.md) * [8、数据安全(反爬虫)之「防重放」策略](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.8.md) +* [9、爬取疫情数据并用 Markdown 预览](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.9.md) \ No newline at end of file diff --git a/SUMMARY.md b/SUMMARY.md index 7c80e6b..f4435a4 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -138,6 +138,7 @@ * [6、YAML](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.6.md) * [7、Node单元测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.7.md) * [8、数据安全(反爬虫)之「防重放」策略](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.8.md) + * [9、爬取疫情数据并用 Markdown 预览](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.9.md) diff --git a/assets/2020-04-09-methodSwizzling.png b/assets/2020-04-09-methodSwizzling.png new file mode 100644 index 0000000..4d3f303 Binary files /dev/null and b/assets/2020-04-09-methodSwizzling.png differ