Objective-C

[iOS] 跟着大神们学习代码(7)

2020-01-12  本文已影响0人  木小易Ying

目录:

  1. Web和客户端如何交互
  2. DeepLink
  3. jenkins如何自动打包
  4. Https抓包内容可见控制
  5. KVOController的简易使用
  6. 一道偶尔看到的面试题
  7. TCP优化

1. Web和客户端如何交互

首先如何自己搭建一个本地网页:https://blog.csdn.net/u011456337/article/details/50704331/

关于如何实现web调用iOS以及iOS调用web可以参考:https://blog.csdn.net/dolacmeng/article/details/79623708

大概实现方式有四种:

  1. 拦截url(适用于UIWebView和WKWebView)
  2. JavaScriptCore(只适用于UIWebView,iOS7+)
  3. WKScriptMessageHandler(只适用于WKWebView,iOS8+)
  4. 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

API Key

然后需要在客户端加两个配置,缺一不可哦!首先在如下位置添加 URL Type:

image

其中 identifier 写为 taobao 字样(自定义),URL Scheme 中填写的格式是 tbopen{AppKey},就是在阿里百川上申请的 App 对应的 AppKey。

由于 iOS 限制了APP打开类型,所以需要在 info.plist 中添加LSApplicationQueriesSchemes,在其中添加和 URL Types 中一致的 taobao 字符串即可,需要其中的注意元素类型:

image

然后就可以写代码啦~

- (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:


设置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来打开不同的页面啦~

最后有篇木有看懂但感觉很厉害的文章也推荐一下~https://www.jianshu.com/p/43f8a81dd8ca


3. jenkins如何自动打包

可参考:https://www.jianshu.com/p/3668979476ad 以及 https://www.jianshu.com/p/5af631ba513d

这部分我也没细看,就是大概看了一下,本地需要搭一个Jenkins环境我就太懒了,之前很好奇怎么搞的,有机会下次自己搞个试一下~


4. Https抓包内容可见控制

可参考:https://www.jianshu.com/p/833c560a8470https://www.jianshu.com/p/4682aecf162dhttps://www.jianshu.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的假证书没有被信任,于是也就无法解析加密请求了哦。

Charles抓https原理
5. 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。


6. 一道偶尔看到的面试题

可参考:https://www.jianshu.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对象的指向方式很相似:


image

其实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:

image
7. 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,在握手阶段就可以传输数据包,从而将三次握手的延时降低到最低。比较适用于网络延时比较长的场景。参考:https://www.jianshu.com/p/24bcaa99bb02

上一篇 下一篇

猜你喜欢

热点阅读