目录:
- Web和客户端如何交互
- DeepLink
- Https抓包内容可见控制
- KVOController的简易使用
- 一道偶尔看到的面试题
- TCP优化
- 重复点击问题
1. Web和客户端如何交互
首先如何自己搭建一个本地网页:https://blog.csdn.net/u011456337/article/details/50704331/
关于如何实现web调用iOS以及iOS调用web可以参考:https://blog.csdn.net/dolacmeng/article/details/79623708
大概实现方式有四种:
- 拦截url(适用于UIWebView和WKWebView)
- JavaScriptCore(只适用于UIWebView,iOS7+)
- WKScriptMessageHandler(只适用于WKWebView,iOS8+)
- WebViewJavascriptBridge(适用于UIWebView和WKWebView,属于第三方框架)
鉴于WKWebView比UIWebView好很多,并且我们项目中用的是WKScriptMessageHandler,这里就只演示第三种啦~ 关于WKWebView的使用可以参考https://blog.csdn.net/u013983033/article/details/84027078
下面正式搞起来~ 先按照上面的方式自己搭一个node服务器,只要有一个页面就行,下面是index.html文件,放入webapp文件夹下:
<!DOCTYPE html>
<html>
<head>
<script>
function changePtext() {
document.getElementById("demo").innerHTML = "Changed";
}
function sendRequestToIOS() {
window.webkit.messageHandlers.changeText.postMessage(null);
}
</script>
</head>
<body>
<h2>Head JavaScript</h2>
<p id="demo">A paragraph</p>
<button type="button" onclick="sendRequestToIOS()">Try change text</button>
</body>
</html>
然后就可以打开页面:http://localhost:3000/index.html
啦~
这里的window.webkit.messageHandlers.changeText.postMessage(null);
其实就是web调用了手机侧的changeText
方法,注意哦,这里的postMessage(null)
如果没有参数也必须写null,不可以postMessage()
哦!
然后客户端需要一个webview以及处理js调用客户端的方法:
#import <WebKit/WebKit.h>
#import "WebInteractionViewController.h"
@interface WebInteractionViewController () <WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler>
@property (strong, nonatomic) WKWebView *webView;
@end
@implementation WebInteractionViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSString * urlS = [NSString stringWithFormat:@"http://127.0.0.1:3000/index.html"];
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:urlS]]];
}
- (WKWebView *)webView {
if (!_webView) {
// 进行配置控制器
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
// 实例化对象
configuration.userContentController = [WKUserContentController new];
// 调用JS方法
[configuration.userContentController addScriptMessageHandler:self name:@"changeText"];
// 进行偏好设置
WKPreferences *preferences = [WKPreferences new];
preferences.javaScriptEnabled = YES;
preferences.javaScriptCanOpenWindowsAutomatically = YES;
preferences.minimumFontSize = 40.0;
configuration.preferences = preferences;
_webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
_webView.navigationDelegate = self;
_webView.opaque = NO;
_webView.backgroundColor = [UIColor whiteColor];
if (@available(ios 11.0,*)){ _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;}
[self.view addSubview:_webView];
}
return _webView;
}
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"changeText"]) {
[self.webView evaluateJavaScript:@"changePtext()" completionHandler:nil];
return;
}
}
#pragma mark - WKNavigationDelegate
// 页面开始加载时调用
-(void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation{
NSLog(@"页面开始加载时调用");
}
// 当内容开始返回时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation{
NSLog(@"当内容开始返回时调用");
}
// 页面加载完成之后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{//这里修改导航栏的标题,动态改变
NSLog(@"页面加载完成之后调用");
}
// 页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation{
NSLog(@"页面加载失败时调用");
}
// 接收到服务器跳转请求之后再执行
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation{
NSLog(@"接收到服务器跳转请求之后再执行");
}
// 在收到响应后,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
NSLog(@"在收到响应后,决定是否跳转");
NSLog(@"%@",navigationResponse);
WKNavigationResponsePolicy actionPolicy = WKNavigationResponsePolicyAllow;
//这句是必须加上的,不然会异常
decisionHandler(actionPolicy);
}
// 在发送请求之前,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
NSLog(@"在发送请求之前,决定是否跳转");
decisionHandler(WKNavigationActionPolicyAllow); // 必须实现 加载
}
#pragma mark - WKUIDelegate
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
}
//弹出一个输入框(与JS交互的)
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullable result))completionHandler{
}
@end
代码中的[configuration.userContentController addScriptMessageHandler:self name:@"changeText"];
就是设置了当web调用了手机侧的changeText方法的时候,self会去处理,处理的方式就是WKScriptMessageHandler的- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
方法会被调用。
在didReceiveScriptMessage
里面我们可以拿到message也就是调用的方法名,然后通过判断不同的方法名执行不同的操作,这个例子里面我让收到changeText
以后就调用web的方法,通过最简单的[self.webView evaluateJavaScript:@"changePtext()" completionHandler:nil];
即可。
上面就实现了点击一下网页中的button,button会触发发changeText消息给客户端,客户端收到以后调用了webview的jschangePtext()
来改变文字,注意调用JS的时候也要写括号吼~
2. DeepLink
Deeplink,简单讲,就是你在手机上点击一个链接之后,可以直接链接到app内部的某个页面,而不是app正常打开时显示的首页。
这项技术主要是为了方便广告跳转而产生的,最大的例子就是淘宝天猫京东等购物APP。在第三方APP中点击广告链接直接跳转到对应的客户端的商品的详情中。
可参考:https://blog.csdn.net/Keep_Dream/article/details/56842806
① 如何打开别人的客户端
这里先以taobao为例看如何做到点击自己的app里面的一个按钮打开淘宝吧~
首先你需要在阿里的平台注册为开发者,并且添加你自己的应用,拿到API Key。https://console.baichuan.taobao.com/applications.htm?spm=0.0.0.0#create
然后需要在客户端加两个配置,缺一不可哦!首先在如下位置添加 URL Type:
其中 identifier 写为 taobao
字样(自定义),URL Scheme 中填写的格式是 tbopen{AppKey}
,就是在阿里百川上申请的 App 对应的 AppKey。
由于 iOS 限制了APP打开类型,所以需要在 info.plist 中添加LSApplicationQueriesSchemes
,在其中添加和 URL Types 中一致的 taobao
字符串即可,需要其中的注意元素类型:
然后就可以写代码啦~
- (IBAction)turnToTaobao:(id)sender {
NSString *urlString = @"taobao://s.taobao.com/?q=iphone";
if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:urlString]]) {
//若安装了需要跳转的app->跳转到APP
NSURL * url = [NSURL URLWithString:[urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]];
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}else{
//若未安装需要跳转的app->跳转到APP的下载界面,这里用了淘宝ipad哈
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"itms-apps://itunes.apple.com/cn/app/appname/id438865278"] options:@{} completionHandler:nil];
//或者直接显示web端的页面
}
}
② 如何让其他app跳转至自己app
可参考:http://www.cocoachina.com/articles/31815
这个就比较简单啦,我们只要给自己的app配置一下url schema:
这里identifier需要填写bundle id,然后schema就是我们打开app的前缀,例如taobao://,可以自定义的。
然后在safari里面打开[schema://identifier]
即可跳转至我们的app,例如ex1://xxx.Example1
。
并且在app被打开的时候,会回调:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
NSLog(@"openUrl: %@", url);
return YES;
}
在app被打开的时候就会输出:
2020-01-12 15:19:45.547004+0800 Example1[3277:626851] openUrl: ex1://xxx.Example1
我们也就可以通过检查url来打开不同的页面啦~
最后有篇木有看懂但感觉很厉害的文章也推荐一下~//www.greatytc.com/p/43f8a81dd8ca
3. Https抓包内容可见控制
可参考://www.greatytc.com/p/833c560a8470 或//www.greatytc.com/p/4682aecf162d 或//www.greatytc.com/p/23545f8d36d2
关于Charles如何实现抓包的可以参考我之前的网络篇~ 今天只是聊一下关于Https内容加密。
按理说如果使用Charles可以抓到所有HTTP的请求,但这周测试小帅哥问我为什么xcode的debug包可以,但release包就不能看到请求内容,而Jenkins的release包可以呢?
后来优秀的小哥哥在Jenkins上找到了一段代码类似:
# 禁用SSL Pinning
perl -i -pe 's/shouldConfigPinnedCertificatesForRequest\:\(Request \*\)request {/shouldConfigPinnedCertificatesForRequest\:\(Request \*\)request { return NO;/' Source/……/NetworkSecurityPolicyPlugin.m
这里其实就是替换了NetworkSecurityPolicyPlugin.m文件里面的shouldConfigPinnedCertificatesForRequest
,关于perl可以参考:https://blog.csdn.net/sdustliyang/article/details/7578730
所以原来Jenkins的release包和Xcode的shouldConfigPinnedCertificatesForRequest
的实现是不同的,然后我看了下shouldConfigPinnedCertificatesForRequest
的调用,如果设为NO,request.securityPolicy = [AFSecurityPolicy defaultPolicy];
+ (instancetype)defaultPolicy {
AFSecurityPolicy *securityPolicy = [[self alloc] init];
securityPolicy.SSLPinningMode = AFSSLPinningModeNone;
return securityPolicy;
}
那么SSLPinningMode
是啥呢?
SSL Pinning,即SSL证书绑定。通过SSL证书绑定来验证服务器身份,防止应用被抓包。
AFSecurityPolicy是AFNetworking中网络通信安全策略模块。它提供三种SSL Pinning Mode:
enum {
AFSSLPinningModeNone,
AFSSLPinningModePublicKey,
AFSSLPinningModeCertificate,
}
`AFSSLPinningModeNone`
Do not used pinned certificates to validate servers.
`AFSSLPinningModePublicKey`
Validate host certificates against public keys of pinned certificates.
`AFSSLPinningModeCertificate`
Validate host certificates against pinned certificates.
判断证书是不是要信任就是下图紫色的部分,所以如果设置AFSSLPinningMode为AFSSLPinningModeNone
,客户端就会信任Charles的证书;反正如果不是none,那么会和本地的公钥对比(AFSSLPinningModePublicKey)或者全部对比(AFSSLPinningModeCertificate),此时就使得Charles的假证书没有被信任,于是也就无法解析加密请求了哦。
4. KVOController的简易使用
我之前用KVOController的时候为了保证可以持续监听,就把KVOController作为一个属性存给了VC,如果没有强引用其实监听不会被触发哦!
但其实FB提供了一个category专门用于获取KVOController的NSObject+FBKVOController
,所以我们可以用category替代属性KVOController:
#import <Foundation/Foundation.h>
#import "FBKVOController.h"
@interface NSObject (FBKVOController)
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end
它提供懒加载的FBKVOController,并且还提供了
KVOControllerNonRetaining
,这个controller是没有强持有被观察者的,防止被观察者自身持有controller,controller又持有了被观察者形成了retain cycle。
它里面的实现就是通过init传入retainObserved为no来做的:
- (FBKVOController *)KVOControllerNonRetaining
{
id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey);
if (nil == controller) {
controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO];
self.KVOControllerNonRetaining = controller;
}
return controller;
}
虽然KVOControllerNonRetaining能够对observe中存入的参数弱引用来打破循环引用,但是自动解除观察者这个特性却变得无法实现。因为KVOController的MapTable弱引用observe,而弱引用的指针,会在dealloc方法走到时,已经变成nil。
即便我们在dealloc方法里面,使用[self.KVOControllerNonRetaining unobserveAll]; 依旧会崩溃,因为 unobserveAll也是去MapTable寻找保存的信息来做移除,弱引用的指针已经被释放,所以无法移除任何KVO。
上面这段是之前小哥哥给我看的为啥不能self.KVOControllerNonRetaining
然后不removeObserver,但是对象的dealloc方法也就是我们覆写的部分其实是先于析构和清weak执行的,在我们的dealloc方法里面其实weak表啥的还没清呢哦,所以上面的part我是有一点疑惑的欢迎探讨
5. 一道偶尔看到的面试题
可参考://www.greatytc.com/p/f2a1518a42b8
题目是酱紫的:
@interface FJFPerson : NSObject
// name
@property (nonatomic, copy) NSString *name;
- (void)print;
@end
@implementation FJFPerson
- (void)print {
NSLog(@"my name is %@", self.name);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [FJFPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}
打印出来是神马呢?
my name is <WebInteractionViewController: 0x7fd91d70f5f0>
还记得NSObject的结构咩~ NSObject持有一个isa指针,指向它的class结构体。而上面的[FJFPerson class]
就是class结构体,cls是指向[FJFPerson class]
的指针,而obj是指向cls的指针。
有木有感觉和NSObject对象的指向方式很相似:
其实obj其实就是类似我们init alloc创建出来的对象指针,所以当我们调用[(__bridge id)obj print]
的时候其实就是调用了对象的print方法,所以上面的代码不会crash也不会compile error。
然后就是为什么print出来的是vc了?
将print加一下self以及name的地址来看下:
@implementation FJFPerson
- (void)print {
NSLog(@"self: %p", self);
NSLog(@"self.name: %p", &_name);
NSLog(@"my name is %@", self.name);
}
@end
输出:
2020-01-11 20:36:14.977915+0800 Example1[20066:923821] self: 0x7ffee3e83fd8
2020-01-11 20:36:14.978034+0800 Example1[20066:923821] self.name: 0x7ffee3e83fe0
所以指向_name的指针的地址 - 8 = self的地址
。然后我们尝试改一下viewDidLoad的代码:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSString *str = @"11111";
NSLog(@"str: %p", &str);
id cls = [FJFPerson class];
void *obj = &cls;
[(__bridge id)obj print];
NSLog(@"cls: %p", &cls);
NSLog(@"obj: %p", &obj);
}
输出:
2020-01-11 20:58:27.162163+0800 Example1[20519:988180] str: 0x7ffedfd48fd8
2020-01-11 20:58:27.162467+0800 Example1[20519:988180] my name is 11111
2020-01-11 20:58:27.162522+0800 Example1[20519:988180] cls: 0x7ffedfd48fd0
2020-01-11 20:58:27.162596+0800 Example1[20519:988180] obj: 0x7ffedfd48fc8
注意哦,cls打印出来的是这个指针指向的地方,而&cls才是这个指针存在了哪里。字符串是类似的,str是这个字符串字面量的地址,而&str才是指向这个字面量的指针保存的地址。
有木有发现很神奇,str指针的地址 - 8 = cls指针保存的地址
,并且cls指针的地址 - 8 = obj指针保存的地址
。所以打印的时候会打出比cls指针+8的地址的指针所指向的地方,也就是str(11111)。
其实这个是因为函数调用采用栈的形式,栈的地址是从高地址到低地址。
所以其实这个问题最后打印出来的是VC就是因为VC恰好是比cls高8位地址,具体上面的文章分析了一下汇编代码,总结了下面的图,虽然高低顺序反了,地址也错了但是大意是对的QAQ:
6. TCP优化
可参考:https://blog.csdn.net/fred1653/article/details/51689617/
TCP三次握手完成后,客户端与服务器就可以通信了。客户端在发送ACK分组后就可以立即发送数据,服务器则必须等待接收到ACK分组后才能够发送数据。
三次握手带来的延迟使得每次创建一个新TCP连接都要付出巨大代价,所以这里是提升TCP应用性能的关键。
Google研究发现TCP三次握手是页面延迟时间的重要组成部分,所以他们提出了TFO:在TCP握手期间交换数据,这样可以减少一次RTT。根据测试数据,TFO可以减少15%的HTTP传输延迟,全页面的下载时间平均节省10%,最高可达40%。
TCP Fast Open 简称 TFO,其目的是缩短 TCP 三次握手的时间。通过加入 cookie,在握手阶段就可以传输数据包,从而将三次握手的延时降低到最低。比较适用于网络延时比较长的场景。参考://www.greatytc.com/p/24bcaa99bb02
首次请求
客户端发送 syn,并且字段里面请求 cookie (tfo request)
服务端发送 syn+ack以及cookie
客户端保存cookie并发送ack
客户端发送数据后续请求
客户端发送 syn、数据以及 cookie
服务端验证 cookie 并发送 syn+ack
服务端不必等客户端ack开始发送数据(在TFO cookie超时之前)
客户端发送 ack
7. 重复点击问题
如果想让button在一定time(例如5秒)时间内不论调用多少次,只执行一次肿么破呢?其实就是用户可能连续点一个button,被触发好几次就不好了~
可以参考:https://blog.csdn.net/erice_e/article/details/72858720
- 方案一:用bool判断是不是已经做完事情了
在点击以后先判断当前是不是已经做完了任务,如果木有就return,如果已经做完就可以开始下一次的干活,然后干完活以后设置bool为已经做完了的状态。
- (IBAction)buttonClicked:(UIbutton *)sender {
if (doingSomeThing) return;
doingSomeThing = YES;
//doSomething
doingSomeThing = NO;
}
但这种对于push一个页面那种会比较麻烦,需要下一个页面出现以后设置上一个页面的flag。
- 方案二:delay几秒以后再还原flag
- (IBAction)buttonClicked:(UIbutton *)sender {
if (doingSomeThing) return;
doingSomeThing = YES;
//doSomething
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
doingSomeThing = NO;
});
}
如果我们希望所有点击事件都可以有这样的特性,不用单独给每个click写这样的方法,就可以通过swizzle来tricky一下的hook系统方法~
- 方案三:每次点击的时候delay执行任务,并先取消之前安排触发的任务
- (void)addaction{
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(doSomething) object:nil];
[self performSelector:@selector(doSomething) withObject:nil afterDelay:5];
}
- (void)doSomething{
}
这样做的问题是,用户点击以后5秒才会真的执行操作,并且如果中间又点击了,这个时间会再往后拖5秒。