XCode 9.3 新的编译选项,优化 Swift 编译生成代码的尺寸

From: https://swiftcafe.io/post/swift-optimization

XCode 9.3 新的编译选项,优化 Swift 编译生成代码的尺寸

swift 发布于 2018年04月08日


在 Swift 4.1 的编译器中,提供了一个新的优化选项。可以减少代码生成尺寸。 对生成的代码进行优化,是很多编译器都提供的功能,Swift 之前版本的编译器其实也提供了优化功能,如果你打开 XCode 项目的 Build Settings, 就可以找到一个叫做 Optimization Level 的选项,默认情况下,XCode 会对 Release 下的项目默认开启编译器优化:

这个选项在实际编译的时候,会给编译器传递一个 -O 参数,它就代表进行性能优化。

新增的编译选项

XCode 9.3,除了上述的性能优化之外,还提供了另外一个新的选项 -Osize, 对代码尺寸进行优化:

从上图中可以看到 -O-Osize 两个参数是互斥的,只能选一个。 -O 前面我们说过了,是对代码的执行速度进行优化,但执行速度提升了,就会牺牲一部分代码空间。

反之 -Osize 是专门为节省代码空间这个目的而来的优化选项。 那么他们分别能带来多少收益和损失呢? 这点在 swift.org 的官方 Blog 也有介绍:

-Osize 根据项目不同,大致可以优化掉 5% - 30% 的代码空间占用。 相比 -0 来说,会损失大概 5% 的运行时性能。 如果你的项目对运行速度不是特别敏感,并且可以接受轻微的性能损失,那么 -Osize 就值得一用。

Single File 和 Whole Module

除了 -O-Osize, 还有另外一个概念也值得说一下。 就是 Single FileWhole Module 。 在之前的 XCode 版本,这两个选项和 -O 是连在一起设置的,Xcode 9.3 中,将他们分离出来,可以独立设置:

Single FileWhole Module 这两个模式分别对应编译器以什么方式处理优化操作。

Single File 是逐个文件进行优化,它的好处是对于增量编译的项目来说,它可以减少编译时间,对没有更改的源文件,不用每次都重新编译。并且可以充分利用多核 CPU,并行优化多个文件,提高编译速度。

但它的缺点就是对于一些需要跨文件的优化操作,它没办法处理。如果某个文件被多次引用,那么对这些引用方文件进行优化的时候,会反复的重新处理这个被引用的文件,如果你项目中类似的交叉引用比较多,就会影响性能。

Whole Module 则是将项目所有的文件看做一个整体,不会产生 Single File 模式对同一个文件反复处理的问题,并且可以进行最大限度的优化,包括跨文件的优化操作。

缺点是,不能充分利用多核处理器的性能,并且对于增量编译,每次也都需要重新编译整个项目。

XCode 的默认设置使用的是 Whole Module 模式。 我特意搜索了一下,在 Stack Overflow 上面找到了一个比较好的总结,也给大家贴出来参考一下:

这篇帖子的地址是 https://stackoverflow.com/questions/47998583/whats-the-difference-between-single-file-optimization-vs-whole-module-optimizat

对于这个选项,我的理解是,如果没有特殊情况,使用默认的 Whole Module 优化即可。 它会牺牲部分编译性能,但的优化结果是最好的。

何为编译优化

关于编译优化,这话话题其实不小。 简单来说,现在我们用高级语言写出的代码,更大程度上是基于我们人的思维逻辑。 然后通过编译器,变成机器的逻辑。比如现在如果大家开发一个项目,更关注业务逻辑的实现,比如点击购买按钮,能不能正常调用下订单的函数。或者当用户完成某个功能,能不能按照预定的要求弹出评价提示等。

我们现在越来越少的会为怎么写一行代码能够减少电量消耗,或者如何提高多核 CPU 的利用率这类的问题花费精力。编译优化就是在一定程度上帮助我们处理这类问题的功能。

用 swift.org 中一篇 Blog 来举个例子:

struct X {
var x: Int { return 27 }
}

比如上面这个代码,定义了一个属性 x, 它通过一个函数返回一个整数 27。 如果你开启了编译优化,编译器就有可能将这个属性优化为 inline 函数(因为这个函数体相对简单)。所谓 inline 函数,就是在调用它的地方直接把它展开成代码。比如这样:

let ins = X()
print(ins.x)

上述代码实际在编译优化后,就成了这样:

let ins = X()
print(27)

那么这样替换能带来什么好处呢,因为在程序真正执行的时候,函数调用的开销要比直接执行某段代码大很多,所以将一些比较小的函数直接优化成 inline 的,就肯定会提高程序运行的效率了。

这就是编译优化的一个例子,上面说的 inline 替换是对性能进行优化,可想而知如果你的代码中多次调用了 ins.x 这个属性,那么他们就都会被替换,我们这个例子中这个函数体还比较简单,如果函数体稍微复杂一些,你的代码总量必然会被编译优化增大。 过多的 inline 虽然会对性能提升有帮助,但无疑会增大代码的尺寸。

这也是程序设计中守恒的一个定律,同样的条件下,空间和性能不可能兼得,需要取舍。

相信通过这个解释,大家应该更能理解 -O-Osize 的区别了。 以官方 Blog 的解释,-O 更着重于优化性能,同时会带来代码空间的增大。 -Osize 着重于代码尺寸,比如官方 Blog 上面就有一点明确的说明,-Osize 对于 inline 函数的优化标准就比 -O 谨慎很多。 从这个角度看, inline 优化少了,代码尺寸自然会变小,同样的运行性能就会稍微降低。

当然,上面只是通过 inline 优化给大家举个例子,目的是帮助大家更好的理解编译优化的运作原理。 实际的编译优化操作,要远比我们这里描述的复杂。

总结

XCode 9.3 新增的 -Osize 编译选项,给大家提供了一个新的选择。 这篇文章也通过对它的介绍,给大家分享了关于编译优化知识的一些基本概念,也许会帮你在讨论问题的时候多一些谈资。同样,现在其实有不少项目对空间尺寸的优化需求在增多,我想这也是 -Osize 这个新的编译选项提供出来的原因之一吧。 如果你的项目恰好也有这方面的需求,不妨可以试一下它。

如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。


使用WKWebView进行性能调优

From: http://xibhe.com/2018/02/03/WKWebView-disabuse/
使用WKWebView进行性能调优
最近一周,用户频繁反应一个问题:切换到某个功能页面后,加载H5页面相应时间过长,当H5页面未展示出来时,此时,再切换到其他页面,App会卡死。我们试着在公司的网络环境下复现这个问题,但并未复现。

错误的尝试

最开始时并没有意识到是webView的原因,反而因为前几天刚解决了一个UI线程的bug,将这个卡顿问题主观上当做线程问题去解决。基于此做了以下操作:

  1. 增加webView加载失败的代理方法;
  2. 在加载完成和加载失败时,取消加载进度动画的展示;
  3. 在将项目中的页面替换为 WKWebView 后,发现在访问下个H5页面时,无法共享 Cookie 的问题(下面会详细说下这个问题是如何解决的),导致无法获取到已经验证成功的用户登录信息。

先期采用方法1和方法2,但测试时还是会造成卡顿。后期替换为 WKWebView 后,亟待解决 Cookie 无法共享的问题,想着能不能在每次加载H5页面时,都在请求链接后面拼上用户信息的各种参数,经测试,这样做仍然无法解决页面跳转后读取用户信息的bug。而且还因每次访问页面频繁与服务器进行验证,给服务器带来了性能压力。

问题的复现

这时考虑到用户应该是在弱网环境下进行操作,遇到的问题。于是,使用网络封包分析工具Charles模拟慢速网络。选择Throttle present:56 kbps Modem。此时,再切换页面,先切换到那个加载H5的页面,然后再来回切换其他几个页面,就会出现APP卡死的情况。(这里需要说明的是其他切换的页面有4个同样是加载H5页面,一共有8个主界面)。

现在问题基本可以明确了,每次加载H5页面时都要初始化webView导致了程序内存消耗过大,造成APP卡死。

控制台报错

调试时,在程序频繁切换刷新页面直至卡死阶段,控制台一直报错,主要报错如下:

1. Domain=NSURLErrorDomain Code=-999

<LZoutsourceViewController.m : 226> -[LZoutsourceViewController webView:didFailProvisionalNavigation:withError:]
2018-01-31 21:02:22.084257+0800 CloudOfficeTest[9230:4603782] error:Error Domain=NSURLErrorDomain Code=-999 "(null)" UserInfo={NSErrorFailingURLStringKey=http://test.net/h5/zskt/spkt.html? _WKRecoveryAttempterErrorKey=<WKReloadFrameErrorRecoveryAttempter: 0x1c0822d20>}

2. NSURLConnection finished with error - code -1002

2018-01-31 21:35:56.144596+0800 CloudOfficeTest[9301:4618465] NSURLConnection finished with error - code -1002
2018-01-31 21:36:02.742996+0800 CloudOfficeTest[9301:4618815] TIC TCP Conn Failed [14:0x1c41702c0]: 3:-9802 Err(-9802)

3. failed to return after waiting 10 seconds. main run loop mode: kCFRunLoopDefaultMode

2018-02-01 11:09:01.952689+0800 CloudOfficeTest[614:68129] void SendDelegateMessage(NSInvocation *): delegate (webView:decidePolicyForNavigationAction:request:frame:decisionListener:) failed to return after waiting 10 seconds. main run loop mode: kCFRunLoopDefaultMode

其中,前两个错误都有错误码,分别对应

Code=-999,NSURLErrorCancelled

code -1002,NSURLErrorUnsupportedURL

-999的错误,是因为webView在之前的请求还没有加载完成,就发起了下一个请求,此时webView会取消之前的请求,因此会回调的请求失败这里。

这里使用的是WKWebView,因此,需要在WKWebView加载失败的代理方法里拦截掉被取消的请求。

- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error
{
    // code = -999,被取消什么也不干
    if ([error code] == NSURLErrorCancelled) {
        return;
    }
    NSLog(@"error:%@",error);

    // 失败后的后续处理.....
}

第3个错误中看到了main run loop的字样,感觉很有可能是造成卡顿的元凶了。又在项目中全局搜了一下报错的这个方法,发现是使用的js与oc交互框架—WebViewJavaScriptBridge中的方法。

// WebViewJavascriptBridge.m

- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener

这个方法是框架WebViewJavascriptBridge中的方法,主要用于处理UIWebView与JS交互。到目前为止,仍然不能定位到究竟是UIWebView与JS交互时发生了什么?才导致报这个错误。只是隐隐的感觉到可能和初始化UIWebView时的内存消耗有关,毕竟WKWebView的内存消耗相比UIWebView低了一个数量级。于是,将加载会卡顿的页面替换为WKWebView来加载H5页面,通过降低频繁初始化消耗的内存,减少页面卡死的概率。但在替换后遇到一些比较棘手的问题。

具体替换步骤

  • 引入WKWebView的代理,生成WKWebViewJavascriptBridge桥接对象

    #import “WKWebViewJavascriptBridge.h”
    #import “LZWKWebKitSupport.h”

    @interface LZPartnerMainViewController ()

    @property WKWebViewJavascriptBridge jsBridge;
    /WKWebView/
    @property (nonatomic, strong) WKWebView
    wkWebView;

  • 初始化WKWebView

    • (void)viewDidLoad
      {
      _wkWebView = [LZWKWebKitSupport createSharableWKWebView:YES isShowNav:YES];
      [_wkWebView addObserver:self forKeyPath:@”estimatedProgress” options:NSKeyValueObservingOptionNew context:nil];
      [self.view addSubview:_wkWebView];

    self.jsBridge = [WKWebViewJavascriptBridge bridgeForWebView:_wkWebView];
    [self.jsBridge setWebViewDelegate:self];

    // 使用WKWebViewJavascriptBridge进行桥接,OC端注册方法,由js端进行调用

    [_jsBridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"data:%@",data);
        NSString *urlStr = nil;
        NSString *processIsTop = nil;
        if ([data isKindOfClass:[NSString class]]) {
            urlStr = data;
        }else{
            NSDictionary *dic = data;
            urlStr = dic[@"url"];
            processIsTop = dic[@"processIsTop"];
        }
        responseCallback(@"Response from testObjcCallback");
    }];
    

    }

注意,这里通过LZWKWebKitSupport来初始化一个WkWebView是为了同步Cookie,后面会具体说到为什么要同步Cookie及如何同步。

  • 设置WkWebView的代理方法

    #pragma mark - WKNavigationDelegate
    // 开始加载

    • (void)webView:(WKWebView )webView didCommitNavigation:(WKNavigation )navigation
      {
      self.loadingView.hidden = NO;
      NSLog(@”didCommitNavigation”);
      }

    // 加载完成

    • (void)webView:(WKWebView )webView didFinishNavigation:(WKNavigation )navigation
      {
      self.loadingView.hidden = YES;
      NSLog(@”didFinishNavigation”);
      }

    // 加载失败

    • (void)webView:(WKWebView )webView didFailProvisionalNavigation:(WKNavigation )navigation withError:(NSError *)error
      {
      // code = -999
      if ([error code] == NSURLErrorCancelled) {
      return;
      
      }
      NSLog(@”didFailProvisionalNavigation error.code = %ld”,error.code);
      }

    #pragma mark - wkwebviewDelegate

    • (void)webView:(WKWebView )webView decidePolicyForNavigationAction:(WKNavigationAction )navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
      {
      decisionHandler(WKNavigationActionPolicyAllow);
      }

    //接收到服务器响应 后决定是否允许跳转,主要用来处理请求失败的情况。

    • (void)webView:(WKWebView )webView decidePolicyForNavigationResponse:(WKNavigationResponse )navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
      {
      decisionHandler(WKNavigationResponsePolicyAllow);
      NSHTTPURLResponse response = (NSHTTPURLResponse )navigationResponse.response;

      // 读取cookies
      NSArray cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
      for (NSHTTPCookie
      cookie in cookies) {

      [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
      

      }

      if (response.statusCode && response.statusCode != 200) {

      LZErrorHintType type = LZErrorHintType404;
      if (![[Singleton shareInstance] hasNet]) {
          type = LZErrorHintTypeNet;
      }
      __weak typeof(self) weakSelf = self;
      if (!_errorView) {
          //弹出错误界面,点击刷新按钮刷新界面
          LZErrorHintView *errorView = [[LZErrorHintView alloc] initWithFrame:self.view.bounds type:type refreshBlock:^{
              [weakSelf loadHTMLPage];
          }];
          weakSelf.errorView = errorView;
          [self.view addSubview:errorView];
      }
      return;
      

      }
      if ([[Singleton shareInstance] hasNet]) {

      if (_errorView) {
          [_errorView removeFromSuperview];
          _errorView = nil;
      }
      

      }else{

      if(!_errorView){
          __weak typeof(self) weakSelf = self;
          LZErrorHintView *errorView = [[LZErrorHintView alloc] initWithFrame:self.view.bounds type:LZErrorHintTypeNet refreshBlock:^{
              [weakSelf viewWillAppear:YES];
          }];
          _errorView = errorView;
          errorView.tag = 2200;
          [self.view addSubview:errorView];
      }
      

      }
      }

替换UIWebView为WKWebView后遇到的问题及解决方法

1. 使用WKWebViewJavascriptBridge进行桥接时,加载H5页面闪退。

这里需要更新WebViewJavaScriptBridge桥接框架中WKWebView的桥接方法,

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;

    if ([_base isWebViewJavascriptBridgeURL:url]) {
        if ([_base isBridgeLoadedURL:url]) {
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {
            [self WKFlushMessageQueue];
        } else {
            [_base logUnkownMessage:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return; 
        // 对比之前的方法,这个地方多了一个return
    }

    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

2. WKWebView加载完网页后,点击里面的按钮,不跳转的问题。

设置WKWebView的另一个代理WKUIDelegate,从名称能看出它是webView在user interface上的代理,

// 创建新的webView
// 可以指定配置对象、导航动作对象、window特性。如果没用实现这个方法,不会加载链接,如果返回的是原webview会崩溃。
-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
{
if (!navigationAction.targetFrame.isMainFrame) {
    [webView loadRequest:navigationAction.request];
}
    return nil;
}

要调用下面的方法是有条件的,WKNavigationDelegate中的该方法是用户点击网页上的链接,需打开新页面时,将先调,是否允许跳转到链接。

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
 WKFrameInfo *sFrame = navigationAction.sourceFrame;//navigationAction的出处
 WKFrameInfo *tFrame = navigationAction.targetFrame;//navigationAction的目标
//只有当  tFrame.mainFrame == NO;时,表明这个 WKNavigationAction 将会新开一个页面。
// 才会调用createWebViewWithConfiguration这个代理方法。
}

这样就新开一个webView,如果我们只是显示网页,这样会消耗性能,没有必要。

3. 如何同步WKWebView的Cookie

在将UIWebView替换为WKWebView后加载速度提高了,页面卡死的问题基本没有再出现过。但遇到了一个更加棘手的问题,之前使用的是UIWebView,它会对首次加载H5页面后的用户登录信息进行同步,这样我由当前的H5页面跳转到一个新的UIWebView进行请求时,会自动找到上个页面同步的用户信息,从而加载当前用户对应的内容。

WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie。

因此,如何实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie)数据。是决定能否继续使用WKWebView的关键。如果不能解决这个问题,就只能再继续使用之前的 UIWebView 了,之前所做的一切都没有用处了。

解决多个 WKWebView 之间共享 Cookie 的问题,首先要弄明白三个问题?

  1. WKWebViewwebViewCookie 设置,读取上有什么不同?
  2. WKWebView 会将对应的 Cookie 存在什么地方?
  3. 如何取到 WKWebViewCookie 并将其注入到要访问的下一个 WKWebView 中?

结合以上三个问题,在网上搜索很多关于 WKWebViewCookie 存储在什么地方? 这些资料普遍认为 WKWebView 拥有自己的私有存储,不会将 Cookie 存入到标准的 Cookie 容器 NSHTTPCookieStorage 中。但在实际项目中,却发现 WKWebView 实例可以读取到存储于 NSHTTPCookieStorage 中的 Cookie。最后,看到了腾讯Bugly的一篇技术文章 —- WKWebView 那些坑,也印证了我的观点。

实践发现 WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,在iOS 8上,当页面跳转的时候,当前页面的 Cookie 会写入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中。

看来以后搜索技术文章,不能太片面了,一定要结合一些大厂的权威技术文章来具体分析。

下一步,就是如何在发起请求时注入 通过 NSHTTPCookieStorage 获取的Cookie。网上关于 WKWebViewCookie 注入方法有以下几种:

  1. JS注入 —- 在初始化 WKWebView 的时候,通过 WKUserScript 设置,使用javascript 注入 Cookie,一开始发送 NSMutableURLRequest 请求的时候也要加上 Cookie,并且保证两个地方的设置的cookie一致。参考 — Can I set the cookies to be used by a WKWebView?
  2. WKHTTPCookieStore —- 利用 iOS11 API WKHTTPCookieStore 解决 WKWebView 首次请求不携带 Cookie 的问题。参考 — iOS WebView 中的 Cookie 处理业务场景“IP直连”方案说明
  3. 利用 iOS11 之前的 API 解决 WKWebView 首次请求不携带 Cookie 的问题。参考 — iOS WebView 中的 Cookie 处理业务场景“IP直连”方案说明
  4. 通过让所有 WKWebView 共享同一个 WKProcessPool 实例,可以实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie) 数据。不过 WKWebView WKProcessPool 实例在 app 杀进程重启后会被重置,导致 WKProcessPool 中的 Cookiesession Cookie 数据丢失,目前也无法实现 WKProcessPool 实例本地化保存。

方法1,经过测试行不通,可能是后台读取 Cookie 的方式有问题;方法2,是 iOS 11API ,不具有普适性;方法3,在测试时无法通过 url 匹配到 Cookie;最后,只剩下方法4了,需要注意在特殊场景下 Cookie 丢失的情况:

app 杀进程重启后会被重置,导致 WKProcessPool 中的 Cookiesession Cookie 数据丢失,目前也无法实现 WKProcessPool 实例本地化保存。

但以我们的应用为例,哪怕是主动杀进程,重新打开应用;还是应用突然闪退,重新打开应用。首次加载某个含有用户登录验证的H5页面时,需要在发起请求的地方拼上用户特定信息的参数,因此,即使之前存储的 Cookie 数据丢失了,也会在首次加载时重新获取。如下:

Singleton *sin = [Singleton shareInstance];
NSString *baseIpPort = [LZUserDefaults objectForKey:PreferenceKey_SystemInit_ZyPartnerIPPort];
NSString *urlString = [[NSString stringWithFormat:@"%@/test1/test2?Id=%@&Name=%@&Pid=%@",baseIpPort,sin.clinicId,[LZUserDefaults objectForKey:PreferenceKey_Name],[LZUserDefaults objectForKey:PreferenceKey_Pid]] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
request.HTTPMethod = @"POST";
request.timeoutInterval = 15.0f;
[_wkWebView loadRequest:request];

因此,对于 APP 重启后 Cookie 数据可能丢失的情况,难道不可以在首次加载H5页面时,重新获取一下用户登录信息的 Cookie 吗?对我而言,现在的项目就是这样做的。

1. 新建一个名为 LZWKWebKitSupport 的类,用于生成一个统一的,全局使用同一个 WKProcessPoolWKWebView 对象。

// LZWKWebKitSupport.h

#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>

@interface LZWKWebKitSupport : NSObject
@property (nonatomic, strong,readonly) WKProcessPool *processPool;
+ (instancetype)sharedSupport;
+ (WKWebView *)createSharableWKWebView:(BOOL)isFullScreen isShowNav:(BOOL)showNav;
@end


// LZWKWebKitSupport.m
#import "LZWKWebKitSupport.h"
@interface LZWKWebKitSupport()
@end

@implementation LZWKWebKitSupport
+ (instancetype)sharedSupport {
    static LZWKWebKitSupport *_instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [LZWKWebKitSupport new];
    });
    return  _instance;
}

- (instancetype)init {
    if (self = [super init]) {
        self.processPool = [WKProcessPool new];
    }
    return self;
}

+ (WKWebView *)createSharableWKWebView:(BOOL)isFullScreen isShowNav:(BOOL)showNav
{
    WKUserContentController* userContentController = [WKUserContentController new];
    NSMutableString *cookies = [NSMutableString string];
    WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[cookies copy]                                                        injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
    [userContentController addUserScript:cookieScript];    
    WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
    // 一下两个属性是允许H5视频自动播放,并且全屏,可忽略
    configuration.allowsInlineMediaPlayback = YES;
    configuration.mediaPlaybackRequiresUserAction = NO;
    // 全局使用同一个processPool
    configuration.processPool = [[LZWKWebKitSupport sharedSupport] processPool];
    configuration.userContentController = userContentController;
    // 考虑到左侧菜单栏,需要设置webView的不同frame
    WKWebView *wk_webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, y, width, height) configuration:configuration];

    return wk_webView;
}
@end

2. 在加载H5的地方初始化 LZWKWebKitSupport,并在 WKNavigationDelegate 中获取 cookie,并设置到本地。

// 初始化LZWKWebKitSupport
- (void)viewDidLoad{
_wkWebView = [LZWKWebKitSupport createSharableWKWebView:YES isShowNav:YES];
[self.view addSubview:_wkWebView];
}


#pragma mark - wkwebviewDelegate
//接收到服务器响应 后决定是否允许跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
{
    decisionHandler(WKNavigationResponsePolicyAllow);
    NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
    // 读取cookie,并设置到本地
    NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
    for (NSHTTPCookie *cookie in cookies) {
        [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
    }
}

3. 在从第一个H5页面跳转至第二个H5页面时,在发起请求时注入Cookie。

这里以跳转到 LZDetailViewController 页面为例,先是通过LZWKWebKitSupport 初始化一个 WKWebView

// LZDetailViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    //初始化视图
    [self setUpSubViews];   
}

- (void)setUpSubViews{
    _wkWebView = [LZWKWebKitSupport createSharableWKWebView:YES isShowNav:NO];
    _wkWebView.UIDelegate = self;
    [self.view addSubview:_wkWebView];
    _jsBridge = [WKWebViewJavascriptBridge bridgeForWebView:_wkWebView];
    [_jsBridge setWebViewDelegate:self];
}

然后在加载请求时,注入之前设置的 Cookie

- (void)loadUrl{
    if (!_urlStr) {
        return;
    }

    NSURL *url = [NSURL URLWithString:_urlStr];
    NSMutableString *cookies = [NSMutableString string];
    NSMutableURLRequest *requestObj = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15.0];
    // 一般都只需要同步BJSESSIONID,可视不同需求自己做更改
    NSString * BJSESSIONID;
    // 获取本地所有的Cookie
    NSArray *tmp = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
    for (NSHTTPCookie * cookie in tmp) {
        if ([cookie.name isEqualToString:@"BJSESSIONID"]) {
            BJSESSIONID = cookie.value;
            break;
        }
    }
    if (BJSESSIONID.length) {
        // 格式化Cookie
        [cookies appendFormat:@"BJSESSIONID=%@;",BJSESSIONID];
    }
    // 注入Cookie
    [requestObj setValue:cookies forHTTPHeaderField:@"Cookie"];
    // 加载请求
    [self.wkWebView loadRequest:requestObj];
}

通过以上三步就可以达到同步 Cookie 的目的,现在看来之前通过 JS脚本 注入 Cookie 失败,可能是由于后台需要同步 BJSESSIONID,而BJSESSIONIDHtppOnly,不允许通过js脚本修改。

最后,需要特别注意的一点是:考虑在加载H5页前,是否需要清除某些H5页面的 Cookie ?

这里对于我们的项目而言,加载的需要验证用户身份信息的H5页面,是需要清除 Cookie 的,因为用户的权限不同,所看到的界面就不同,在同一台设备下切换不同的用户时,如果不清除之前的 Cookie,所展示的就是上一个用户的信息。

- (void)deleteWKCookies
{
    // 清除WKWebView缓存的cookie(根据ip)
    if ([[UIDevice currentDevice].systemVersion floatValue] >= 9.0){

        NSString *iPPort = [LZUserDefaults objectForKey:PreferenceKey_SystemInit_ZyIPPort];
        NSArray *iPPortArray = [iPPort componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/"]];
        NSString *recordIP;
        if ([iPPortArray count] > 2) {
            recordIP = partnerIPPortArray[2];
        }

        WKWebsiteDataStore *dateStore = [WKWebsiteDataStore defaultDataStore];
        [dateStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] completionHandler:^(NSArray<WKWebsiteDataRecord *> * __nonnull records) {
            for (WKWebsiteDataRecord *record  in records)
            {
                // 以www.baidu.com为例,是否包含baidu.com
                if ([recordIP containsString:record.displayName])
                {
                    [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes forDataRecords:@[record] completionHandler:^{
                        NSLog(@"Cookies for %@ deleted successfully",record.displayName);
                    }];
                }
            }
        }];
    }
}

WebView性能优化总结

一个加载网页的过程中,native、网络、后端处理、CPU都会参与,各自都有必要的工作和依赖关系;让他们相互并行处理而不是相互阻塞才可以让网页加载更快:

  • WebView初始化慢,可以在初始化同时先请求数据,让后端和网络不要闲着。
  • 后端处理慢,可以让服务器分trunk输出,在后端计算的同时前端也加载网络静态资源。
  • 脚本执行慢,就让脚本在最后运行,不阻塞页面解析。
  • 同时,合理的预加载、预缓存可以让加载速度的瓶颈更小。
  • WebView初始化慢,就随时初始化好一个WebView待用。
  • DNS和链接慢,想办法复用客户端使用的域名和链接。
  • 脚本执行慢,可以把框架代码拆分出来,在请求页面之前就执行好。

上面是美团点评技术团队关于WebView性能优化的总结。

对比我们项目中有哪些页面用到了 UIWebView,哪些用到了 WKWebView,发现当前程序中一共有8个主要模块,其中,一共有4个主要模块是通过加载H5页面展示的,还有一个模块中部分嵌套了H5页面。这些页面中,有三个页面使用 WkWebView 加载,剩下的使用的是 UIWebView 加载页面,发生卡顿的页面多是频繁初始化 UIWebView 加载H5时发生的。

这里我们的项目中使用UIWebViewWKWebView 的地方有很多,没有一个管理类去居中调控的话,后期维护起来会很耗时,而且很容易出现bug。下一步的优化就是要构建这样一种集构建,配置,分发,操控为一身的通用类。

参考资料

WebView性能、体验分析与优化

webView:decidePolicyForNavigationAction:decisionHandler:

WKWebView 那些坑

iOS WebView 中的 Cookie 处理业务场景“IP直连”方案说明

Developer wknavigationdelegate documentation

WKWebView and UIWebView Cookie

WKWebView 那些坑

From: https://zhuanlan.zhihu.com/p/24990222

本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:WKWebView 那些坑

导语

WKWebView 是苹果在 WWDC 2014 上推出的新一代 webView 组件,用以替代 UIKit 中笨重难用、内存泄漏的 UIWebView, 拥有60fps滚动刷新率、和 safari 相同的 JavaScript 引擎。简单的适配方法就不细说了,本文主要讲述适配 WKWebView 过程中填过的坑以及善待解决的技术难题。

1、WKWebView 白屏问题

WKWebView 自诩拥有更快的加载速度,更低的内存占用,但实际上 WKWebView 是一个多进程组件,Network Loading 以及 UI Rendering 在其它进程中执行。初次适配 WKWebView 的时候,我们也惊讶于打开 WKWebView 后,App 进程内存消耗反而大幅下降,但是仔细观察会发现,Other Process 的内存占用会增加。在一些用 webGL 渲染的复杂页面,使用 WKWebView 总体的内存占用(App Process Memory + Other Process Memory)不见得比 UIWebView 少很多。

在 UIWebView 上当内存占用太大的时候,App Process 会 crash;而在 WKWebView 上当总体的内存占用比较大的时候,WebContent Process 会 crash,从而出现白屏现象。在 WKWebView 中加载下面的测试链接可以稳定重现白屏现象:

<Memory test>

这个时候 WKWebView.URL 会变为 nil, 简单的 reload 刷新操作已经失效,对于一些长驻的H5页面影响比较大。

我们最后的解决方案是:

A、借助 WKNavigtionDelegate

iOS 9以后 WKNavigtionDelegate 新增了一个回调函数:

- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0));

当 WKWebView 总体内存占用过大,页面即将白屏的时候,系统会调用上面的回调函数,我们在该函数里执行[webView reload](这个时候 webView.URL 取值尚不为 nil)解决白屏问题。在一些高内存消耗的页面可能会频繁刷新当前页面,H5侧也要做相应的适配操作。

B、检测 webView.title 是否为空

并不是所有H5页面白屏的时候都会调用上面的回调函数,比如,最近遇到在一个高内存消耗的H5页面上 present 系统相机,拍照完毕后返回原来页面的时候出现白屏现象(拍照过程消耗了大量内存,导致内存紧张,WebContent Process 被系统挂起),但上面的回调函数并没有被调用。在WKWebView白屏的时候,另一种现象是 webView.titile 会被置空, 因此,可以在 viewWillAppear 的时候检测 webView.title 是否为空来 reload 页面。

综合以上两种方法可以解决绝大多数的白屏问题。

Cookie 问题是目前 WKWebView 的一大短板

2.1、WKWebView Cookie存储

业界普遍认为 WKWebView 拥有自己的私有存储,不会将 Cookie 存入到标准的 Cookie 容器 NSHTTPCookieStorage 中。

实践发现 WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,在iOS 8上,当页面跳转的时候,当前页面的 Cookie 会写入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中,FireFox 工程师曾建议通过 reset WKProcessPool 来触发 Cookie 同步到 NSHTTPCookieStorage 中,实践发现不起作用,并可能会引发当前页面 session cookie 丢失等问题。

WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie

比如,NSHTTPCookieStorage 中存储了一个 Cookie:

name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2019 23:38:25 GMT;

通过 UIWebView 发起请求http://y.qq.com/, 则请求头会自动带上 cookie: Nicholas=test;
而通过 WKWebView发起请求http://y.qq.com/, 请求头不会自动带上 cookie: Nicholas=test。

2.2、WKProcessPool

苹果开发者文档对 WKProcessPool 的定义是:A WKProcessPool object represents a pool of Web Content process. 通过让所有 WKWebView 共享同一个 WKProcessPool 实例,可以实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie)数据。不过 WKWebView WKProcessPool 实例在 app 杀进程重启后会被重置,导致 WKProcessPool 中的 Cookie、session Cookie 数据丢失,目前也无法实现 WKProcessPool 实例本地化保存。

2.3、Workaround

由于许多 H5 业务都依赖于 Cookie 作登录态校验,而 WKWebView 上请求不会自动携带 Cookie, 目前的主要解决方案是:

a、WKWebView loadRequest 前,在 request header 中设置 Cookie, 解决首个请求 Cookie 带不上的问题;(注: 如果页面直接读取浏览器缓存,其实并没有写入浏览器,也有可能时机不对, 我自己现有操作是后台java 读取请求中的cookies 并设置,然后代理跳转的时候也手动加上cookies)

WKWebView * webView = [WKWebView new]; 
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"手机统一登录"]]; 
[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"]; 
[webView loadRequest:request];

b、通过 document.cookie 设置 Cookie 解决后续页面(同域)Ajax、iframe 请求的 Cookie 问题;

注意:document.cookie()无法跨域设置 cookie

WKUserContentController* userContentController = [WKUserContentController new]; 
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie = 'skey=skeyValue';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; 
[userContentController addUserScript:cookieScript];

这种方案无法解决302请求的 Cookie 问题,比如,第一个请求是 [http://www.a.com][5],我们通过在 request header 里带上 Cookie 解决该请求的 Cookie 问题,接着页面302跳转到 http://www.b.com,这个时候 http://www.b.com 这个请求就可能因为没有携带 cookie 而无法访问。当然,由于每一次页面跳转前都会调用回调函数:

[5]: https://link.zhihu.com/?target=http%3A//www.a.com
  • (void)webView:(WKWebView )webView decidePolicyForNavigationAction:(WKNavigationAction )navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

可以在该回调函数里拦截302请求,copy request,在 request header 中带上 cookie 并重新 loadRequest。不过这种方法依然解决不了页面 iframe 跨域请求的 Cookie 问题,毕竟-[WKWebView loadRequest:]只适合加载 mainFrame 请求。

3、WKWebView NSURLProtocol问题

WKWebView 在独立于 app 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在 WKWebView 上直接使用 NSURLProtocol 无法拦截请求。苹果开源的 webKit2 源码暴露了[私有API][7]:

[7]: https://link.zhihu.com/?target=https%3A//github.com/WebKit/webkit/blob/master/Tools/TestWebKitAPI/cocoa/TestProtocol.mm%23L56-L63
  • [WKBrowsingContextController registerSchemeForCustomProtocol:]

通过注册 http(s) scheme 后 WKWebView 将可以使用 NSURLProtocol 拦截 http(s) 请求:

Class cls = NSClassFromString(@"WKBrowsingContextController”); 
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); 
if ([(id)cls respondsToSelector:sel]) { 
           // 注册http(s) scheme, 把 http和https请求交给 NSURLProtocol处理 
           [(id)cls performSelector:sel withObject:@"http"]; 
           [(id)cls performSelector:sel withObject:@"https"]; 
}

但是这种方案目前存在两个严重缺陷:

a、post 请求 body 数据被清空

由于 WKWebView 在独立进程里执行网络请求。一旦注册 http(s) scheme 后,网络请求将从 Network Process 发送到 App Process,这样 NSURLProtocol 才能拦截网络请求。在 webkit2 的设计里使用 MessageQueue 进行进程之间的通信,Network Process 会将请求 encode 成一个 Message,然后通过 IPC 发送给 App Process。出于性能的原因,encode 的时候 HTTPBody 和 HTTPBodyStream 这两个字段被丢弃掉了(参考苹果源码:
https://github.com/WebKit/webkit/blob/fe39539b83d28751e86077b173abd5b7872ce3f9/Source/WebKit2/Shared/mac/WebCoreArgumentCodersMac.mm#L61-L88 及bug report: <WKWebView does not fully support custom NSURLProtocol>)。

因此,如果通过 registerSchemeForCustomProtocol 注册了 http(s) scheme, 那么由 WKWebView 发起的所有 http(s)请求都会通过 IPC 传给主进程 NSURLProtocol 处理,导致 post 请求 body 被清空

b、对ATS支持不足

测试发现一旦打开ATS开关:Allow Arbitrary Loads 选项设置为NO,同时通过 registerSchemeForCustomProtocol 注册了 http(s) scheme,WKWebView 发起的所有 http 网络请求将被阻塞(即便将Allow Arbitrary Loads in Web Content 选项设置为YES);

WKWebView 可以注册 customScheme, 比如 dynamic://, 因此希望使用离线功能又不使用 post 方式的请求可以通过 customScheme 发起请求,比如 dynamic://http://www.dynamicalbumlocalimage.com/,然后在 app 进程 NSURLProtocol 拦截这个请求并加载离线数据。不足:使用 post 方式的请求该方案依然不适用,同时需要 H5 侧修改请求 scheme 以及 CSP 规则;

4、WKWebView loadRequest 问题

在 WKWebView 上通过 loadRequest 发起的 post 请求 body 数据会丢失:

//同样是由于进程间通信性能问题,HTTPBody字段被丢弃
[request setHTTPMethod:@"POST"];
[request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];
[wkwebview loadRequest: request];

workaround:

假如想通过-[WKWebView loadRequest:]加载 post 请求 request1: http://h5.qzone.qq.com/mqzone/index,可以通过以下步骤实现:

  1. 替换请求 scheme,生成新的 post 请求 request2: post://http://h5.qzone.qq.com/mqzone/index, 同时将 request1 的 body 字段复制到 request2 的 header 中(WebKit 不会丢弃 header 字段);
  2. 通过-[WKWebView loadRequest:]加载新的 post 请求 request2;
  3. 通过 +[WKBrowsingContextController registerSchemeForCustomProtocol:]注册 scheme: post://;
  4. 注册 NSURLProtocol 拦截请求post://http://h5.qzone.qq.com/mqzone/index ,替换请求 scheme, 生成新的请求 request3: http://h5.qzone.qq.com/mqzone/index,将 request2 header的body 字段复制到 request3 的 body 中,并使用 NSURLConnection 加载 request3,最后通过 NSURLProtocolClient 将加载结果返回 WKWebView;

5、WKWebView 页面样式问题

在 WKWebView 适配过程中,我们发现部分H5页面元素位置向下偏移被拉伸变形,追踪后发现主要是H5页面高度值异常导致:

a. 空间H5页面有透明导航、透明导航下拉刷新、全屏等需求,因此之前 webView 整个是从(0, 0)开始布局,通过调整webView.scrollView.contentInset 来适配特殊导航栏需求。而在 WKWebView 上对 contentInset 的调整会反馈到webView.scrollView.contentSize.height的变化上,比如设置 webView.scrollView.contentInset.top = a,那么contentSize.height的值会增加a,导致H5页面长度增加,页面元素位置向下偏移;

解决方案是:调整WKWebView布局方式,避免调整webView.scrollView.contentInset。实际上,即便在 UIWebView 上也不建议直接调整webView.scrollView.contentInset的值,这确实会带来一些奇怪的问题。如果某些特殊情况下非得调整 contentInset 不可的话,可以通过下面方式让H5页面恢复正常显示:

/**设置contentInset值后通过调整webView.frame让页面恢复正常显示 
 *参考:http://km.oa.com/articles/show/277372
 */ 
webView.scrollView.contentInset = UIEdgeInsetsMake(a, 0, 0, 0); 
webView.frame = CGRectMake(webView.frame.origin.x, webView.frame.origin.y, webView.frame.size.width, webView.frame.size.height - a);

b. 在接入 now 直播的时候,我们发现在 iOS 9 上 WKWebView 会出现页面被拉伸变形的情况,最后发现是window.innerHeight值不准确导致(在WKWebView上返回了一个非常大的值),而H5同学通过获取window.innerHeight来设置页面高度,导致页面整体被拉伸。通过查阅相关资料发现,这个bug只在 iOS 9 的几个系统版本上出现,苹果后来fix了这个bug。我们最后的解决方案是:延迟调用window.innerHeight

setTimeout(function(){height = window.innerHeight},0);

or

Use shrink-to-fit meta-tag 
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1, shrink-to-fit=no">

6、WKWebView 截屏问题

空间玩吧H5小游戏有截屏分享的功能,WKWebView 下通过 -[CALayer renderInContext:]实现截屏的方式失效,需要通过以下方式实现截屏功能:

@implementation UIView (ImageSnapshot) 
- (UIImage*)imageSnapshot { 
    UIGraphicsBeginImageContextWithOptions(self.bounds.size,YES,self.contentScaleFactor); 
    [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES]; 
    UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext(); 
    UIGraphicsEndImageContext(); 
    return newImage; 
} 
@end

然而这种方式依然解决不了 webGL 页面的截屏问题,笔者已经翻遍苹果文档,研究过 webKit2 源码里的截屏私有API,依然没有找到合适的解决方案,同时发现 Safari 以及 Chrome 这两个全量切换到 WKWebView 的浏览器也存在同样的问题:对webGL 页面的截屏结果不是空白就是纯黑图片。无奈之下,我们只能约定一个JS接口,让游戏开发商实现该接口,具体是通过 canvas getImageData()方法取得图片数据后返回 base64 格式的数据,客户端在需要截图的时候,调用这个JS接口获取 base64 String 并转换成 UIImage。

7、WKWebView crash问题

WKWebView 放量后,外网新增了一些 crash, 其中一类 crash 的主要堆栈如下:

... 
28 UIKit 0x0000000190513360 UIApplicationMain + 208 
29 Qzone 0x0000000101380570 main (main.m:181) 
30 libdyld.dylib 0x00000001895205b8 _dyld_process_info_notify_release + 36 
Completion handler passed to -[QZWebController webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:] was not called

主要是JS调用window.alert()函数引起的,从 crash 堆栈可以看出是 WKWebView 回调函数:

+ (void) presentAlertOnController:(nonnull UIViewController*)parentController title:(nullable NSString*)title message:(nullable NSString *)message handler:(nonnull void (^)())completionHandler;

completionHandler 没有被调用导致的。在适配 WKWebView 的时候,我们需要自己实现该回调函数,window.alert()才能调起 alert 框,我们最初的实现是这样的:

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler 
{ 
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; 
    [alertController addAction:[UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]]; 
    [self presentViewController:alertController animated:YES completion:^{}]; 
}

如果 WKWebView 退出的时候,JS刚好执行了window.alert(), alert 框可能弹不出来,completionHandler 最后没有被执行,导致 crash;另一种情况是在 WKWebView 一打开,JS就执行window.alert(),这个时候由于 WKWebView 所在的 UIViewController 出现(push或present)的动画尚未结束,alert 框可能弹不出来,completionHandler 最后没有被执行,导致 crash。我们最终的实现大致是这样的:

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler 
{ 
    if (/*UIViewController of WKWebView has finish push or present animation*/) { 
        completionHandler(); 
        return; 
    } 
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; 
    [alertController addAction:[UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]]; 
    if (/*UIViewController of WKWebView is visible*/) 
        [self presentViewController:alertController animated:YES completion:^{}]; 
    else 
        completionHandler(); 
}

确保上面两种情况下 completionHandler 都能被执行,消除了 WKWebView 下弹 alert 框的 crash,WKWebView 下弹 confirm 框的 crash 的原因与解决方式与 alert 类似。

另一个 crash 发生在 WKWebView 退出前调用:

-[WKWebView evaluateJavaScript: completionHandler:]

执行JS代码的情况下。WKWebView 退出并被释放后导致completionHandler变成野指针,而此时 javaScript Core 还在执行JS代码,待 javaScript Core 执行完毕后会调用completionHandler(),导致 crash。这个 crash 只发生在 iOS 8 系统上,参考Apple Open Source,在iOS9及以后系统苹果已经修复了这个bug,主要是对completionHandler block做了copy(refer: <[Changeset 179160][14]>);对于iOS 8系统,可以通过在 completionHandler 里 retain WKWebView 防止 completionHandler 被过早释放。我们最后用 methodSwizzle hook 了这个系统方法:

[14]: https://link.zhihu.com/?target=https%3A//trac.webkit.org/changeset/179160
  • (void) load
    {
    [self jr_swizzleMethod:NSSelectorFromString(@"evaluateJavaScript:completionHandler:") withMethod:@selector(altEvaluateJavaScript:completionHandler:) error:nil]; 
    
    }
    /*
    • fix: WKWebView crashes on deallocation if it has pending JavaScript evaluation
      */
      • (void)altEvaluateJavaScript:(NSString )javaScriptString completionHandler:(void (^)(id, NSError ))completionHandler
        {
        id strongSelf = self;
        [self altEvaluateJavaScript:javaScriptString completionHandler:^(id r, NSError *e) {
        [strongSelf title];
        if (completionHandler) {
        completionHandler(r, e); 
        
        }
        }];
        }

8、其它问题

8.1、视频自动播放

WKWebView 需要通过WKWebViewConfiguration.mediaPlaybackRequiresUserAction设置是否允许自动播放,但一定要在 WKWebView 初始化之前设置,在 WKWebView 初始化之后设置无效。

8.2、goBack API问题

WKWebView 上调用 -[WKWebView goBack], 回退到上一个页面后不会触发window.onload()函数、不会执行JS。

8.3、页面滚动速率

WKWebView 需要通过scrollView delegate调整滚动速率:

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
     scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
}

9、结语

本文总结了在 WKWebView 上踩过的一些坑。虽然 WKWebView 坑比较多,但是相对 UIWebView 在内存消耗、稳定性方面还是有很大的优势。尽管苹果对 WKWebView 的开发进度过于缓慢,但相信 WKWebView 才是未来。

更多精彩内容欢迎关注腾讯 Bugly的微信公众账号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!

CocoaPods 项目添加错误记录

From: https://www.jianshu.com/p/283584683b0b

上篇文章[Cocoapods]项目添加Cocoapods支持主要介绍了添加Cocoapods支持的大致过程, 当然文章看上去是一帆风顺的. 但是, 事实并不是这样. 上篇文章篇幅过长, 就把之间遇到的一些问题, 单独写了出来.

1. ERROR | spec: The specification defined in LZTool.podspec could not be loaded.
MacBook:PodTest Artron_LQQ$ pod lib lint LZTool.podspec

 -> LZTool.podspec
    - ERROR | spec: The specification defined in `LZTool.podspec` could not be loaded.


[!] Invalid `LZTool.podspec` file: syntax error, unexpected tIDENTIFIER, expecting keyword_end
...<-DESC 这是一个测试文档, 这里主要是描述类库...
...                               ^
LZTool.podspec:28: syntax error, unexpected '\n', expecting '='.

 #  from LZTool.podspec:27
 #  -------------------------------------------
 #    #   * Finally, don't worry about the indent, CocoaPods strips it!
 >    s.description  = <<-DESC 这是一个测试文档, 这里主要是描述类库的功能设计初衷介绍
 #                     DESC
 #  -------------------------------------------


[!] LZTool.podspec did not pass validation, due to 1 error.
[!] The validator for Swift projects uses Swift 3.0 by default, if you are using a different version of swift you can use a `.swift-version` file to set the version for your Pod. For example to use Swift 2.3, run: 
    `echo "2.3" > .swift-version`.
You can use the `--no-clean` option to inspect any issue.
MacBook:PodTest Artron_LQQ$

这个错误是使用指令pod lib lint LZTool.podspec 检查文件是否合法时发生的;

可以看出是在设置s.description 字段时发生的错误, 当时的写法是:

s.description  = <<-DESC LZTool 是一个用于保存一些常用工具类的工具 DESC

以为可以吧他们并到一行, 导致一直报这个错, 后来写成两行, 还是报错, 最后才试出这样写才对:

s.description  = <<-DESC 
                          LZTool 是一个用于保存一些常用工具类的工具
                   DESC
2. [iOS] file patterns: The source_files pattern did not match any file.
MacBook:PodTest Artron_LQQ$ pod lib lint LZTool.podspec

 -> LZTool (0.0.1)
    - WARN  | github_sources: Github repositories should end in `.git`.
    - ERROR | [iOS] file patterns: The `source_files` pattern did not match any file.

[!] LZTool did not pass validation, due to 1 error and 1 warning.
[!] The validator for Swift projects uses Swift 3.0 by default, if you are using a different version of swift you can use a `.swift-version` file to set the version for your Pod. For example to use Swift 2.3, run: 
    `echo "2.3" > .swift-version`.
You can use the `--no-clean` option to inspect any issue.
MacBook:PodTest Artron_LQQ$

这个错误也是使用指令pod lib lint LZTool.podspec 检查文件是否合法时发生的;

这个是在指定共享的类库时, 文件路径不对, 也就是设置s.source_files 字段时, 发生了错误, 这里的路径是相对于LZTool.podspec文件的, 如果是与LZTool.podspec同级的文件夹, 直接写文件夹名称即可, 如:

s.source_files = "LZTool"

如果有多级目录, 一定要逐级添加. 这里也可以这么写:

s.source_files = "LZTool/*.{h,m}"
3. fatal: Remote branch 0.0.1 not found in upstream origin
MacBook:PodTest Artron_LQQ$ pod spec lint

 -> LZTool (0.0.1)
    - ERROR | [iOS] unknown: Encountered an unknown error ([!] /usr/bin/git clone https://github.com/LQQZYY/PodTest.git /var/folders/14/95vmx0495_s5292ltvwpsc8h0000gn/T/d20170111-11240-1l3iq9n --template= --single-branch --depth 1 --branch 0.0.1

Cloning into '/var/folders/14/95vmx0495_s5292ltvwpsc8h0000gn/T/d20170111-11240-1l3iq9n'...
warning: Could not find remote branch 0.0.1 to clone.
fatal: Remote branch 0.0.1 not found in upstream origin
) during validation.

Analyzed 1 podspec.

[!] The spec did not pass validation, due to 1 error.
[!] The validator for Swift projects uses Swift 3.0 by default, if you are using a different version of swift you can use a `.swift-version` file to set the version for your Pod. For example to use Swift 2.3, run: 
    `echo "2.3" > .swift-version`.
MacBook:PodTest Artron_LQQ$

这个是使用指令pod spec lint来检查文件是否可用时发生的错误;

这是因为在你托管代码的库里(这里是指github)找不到这个分支, 也就是在编辑 LZTool.podspec 时, 里面的字段 s.verson, s.source 中的 taggithub创建的release版本号不一致导致, 修改为一样即可!

这里还有另外一个解决方法方法二

4. 加载xib问题

PS: 这个不是我个人遇到的问题, 是在[][3]文章中看到, 这里记录一下;
如果通过cocoapods下载的类库中含有Xib文件, 使用原来的方式初始化就不起作用了:

[3]: https://link.jianshu.com?t=http://www.cocoachina.com/ios/20160415/15939.html

[[[NSBundle mainBundle] loadNibNamed:@”xibName” owner:self options:nil] lastObject];
[self.collectionView registerNib:[UINib nibWithNibName:@”xibName” bundle:nil] forCellWithReuseIdentifier:@”ZLCollectionCell”];

应该使用下面这种方式初始化:

#define kZLPhotoBrowserBundle [NSBundle bundleForClass:[self class]]
[[kZLPhotoBrowserBundle loadNibNamed:@"ZLPhotoActionSheet" owner:self options:nil] lastObject];
[self.collectionView registerNib:[UINib nibWithNibName:@"ZLCollectionCell" bundle:kZLPhotoBrowserBundle] forCellWithReuseIdentifier:@"ZLCollectionCell"];

这样就能正常使用了;

5.加载图片资源问题

PS: 这个问题我尝试了一个demo, 直接设置了s.resources = “LZScaner/images/*.png” , 图片也能正常显示( 可参考我设置的podspec文件LZScaner.podspec )没有遇到这个问题, 但是还是把这个方式记录一下吧…

如果通过代码“[UIImage imageNamed:@”picName”]” 去设置图片的话,则图片资源有可能无法正常显示, 可通过以下方式解决:

  • 创建bundle资源目录

    command+N -> Resource -> Settings Bundle

删除bundle携带的无用文件,把图片资源添加到bundle资源内

  • 改变代码图片路径

    // 图片路径
    #define kZLPhotoBrowserSrcName(file) [@”ZLPhotoBrowser.bundle” stringByAppendingPathComponent:file]
    #define kZLPhotoBrowserFrameworkSrcName(file) [@”Frameworks/ZLPhotoBrowser.framework/ZLPhotoBrowser.bundle” stringByAppendingPathComponent:file]
    kZLPhotoBrowserSrcName(file) 为通过copy文件夹方式获取图片路径的宏
    kZLPhotoBrowserFrameworkSrcName(file) 为通过cocoapods下载安装获取图片路径的宏

  • 然后修改代码中设置图片的方式如下

    UIImage *img = [UIImage imageNamed:kZLPhotoBrowserSrcName(@”img.png”)]?:[UIImage imageNamed:kZLPhotoBrowserFrameworkSrcName(@”img.png”)];

podspec地址: ZLPhotoBrowser.podspec 可以参考学习!

6. trunk: getaddrinfo: nodename nor servname provided, or not known
MacBook:LZScaner Artron_LQQ$ pod trunk register 302934443@qq.com --description= 'LZScaner'
[!] There was an error registering with trunk: getaddrinfo: nodename nor servname provided, or not known
MacBook:LZScaner Artron_LQQ$

出现这个原因是, 我开了APN, 把APN关了, 重新启动一下网络就好了…

7. xcodebuild: Returned an unsuccessful exit code. You can use --verbose for more information
MacBook:LZSortTool Artron_LQQ$ pod spec lint

 -> LZSortTool (0.0.1)
    - WARN  | [iOS] license: Unable to find a license file
    - ERROR | [iOS] xcodebuild: Returned an unsuccessful exit code. You can use `--verbose` for more information.
    - NOTE  | [iOS] xcodebuild:  LZSortTool/LZSortToolDemo/LZSortToolDemo/LZSortClass/LZSortTool.m:14:9: fatal error: 'ChineseToPinyin.h' file not found

Analyzed 1 podspec.

[!] The spec did not pass validation, due to 1 error and 1 warning.
[!] The validator for Swift projects uses Swift 3.0 by default, if you are using a different version of swift you can use a `.swift-version` file to set the version for your Pod. For example to use Swift 2.3, run: 
    `echo "2.3" > .swift-version`.
MacBook:LZSortTool Artron_LQQ$

这个是执行 pod spec lint指令的时候产生的错误:

这个错误从- NOTE可以看出, 是项目中的这个文件ChineseToPinyin.h找不到, 回到项目, 编译一下也会报这个错. 是因为我修改了本地文件中的这个文件的路径, 项目中引用的还是原先的路径, 打开左侧文件列表, 可以发现这个文件是红色的, 删除引用, 重新添加, 然后到你的github上面, 重新添加一个release版本, 再重新执行指令即可验证通过.

8. 验证成功后搜索不到新加的支持库

如果在制作支持库的过程中没有错误, 或者最好添加库验证通过, 即出现下面这个界面:

上传成功

在使用

pod search LZTool

的时候, 搜索不到这个库, 或者在使用时找不到这个库, 可以使用下面的指令来清理一下缓存:

rm ~/Library/Caches/CocoaPods/search_index.json

然后再去使用, 基本就可以了.

CocoaPods 私有仓库的创建(超详细)

CocoaPods 私有仓库的创建(超详细)

From: https://www.jianshu.com/p/0c640821b36f

本文发布于 BY Blog简书 转载请保留链接

最近参照了网上一大堆 CocoaPods私有仓库 的教程,按教程操作得到的pod仓库里面是这样的~

代码和版本描述居然混在了一起,简直太糟糕~

虽然也能用,但是和 CocoaPods 本身的理念就不相符。

在上一篇《CocoaPods公有仓库的创建》中我们了解到,master 目录中只存放 代码库 的描述文件,而不是存放代码。就像这样

代码我们另外存放在代码仓库中

很多人不了解CocoaPods的工作原理就复制粘贴别人的教程来做教程~

吐槽结束,进入正文

创建版本库(repo)

首先,创建一个像 master 一样的存放版本描述文件的git仓库,因为是私人git仓库,我们选择 oschina 创建远程私有仓库(因为是免费的)或者也可以在GitHub上创建($7/month)。

下面以 oschina 为例

创建版本描述仓库

回到终端,将这个远程的私有版本仓库添加到本地,repo 就是 repository 储存库的缩写。

$ pod repo add MyRepo https://git.oschina.net/baiyingqiu/MyRepo.git

查看在 Finder 目录 ~/.cocoapods/repos, 可以发现增加了一个 MyRepo 的储存库

创建代码库

回到 oschina 创建私人代码库

创建时添加 MIT LicenseREADME

将仓库克隆到本地,添加你的代码文件仓库名.podspec 描述文件,还有.swift-version.

如下

.swift-version文件用来知道swift版本,用命令行创建

$ echo "3.0" > .swift-version

.podspec 文件是你这个代码库的pod描述文件,可以通过pod指令创建空白模板:

$ pod spec create MyAdditions

或者 强烈建议 直接拷贝下面的模板进行修改

Pod::Spec.new do |s|
  s.name         = "MyAdditions" # 项目名称
  s.version      = "0.0.1"        # 版本号 与 你仓库的 标签号 对应
  s.license      = "MIT"          # 开源证书
  s.summary      = "私人pod代码" # 项目简介

  s.homepage     = "https://git.oschina.net/baiyingqiu/MyAdditions" # 仓库的主页
  s.source       = { :git => "https://git.oschina.net/baiyingqiu/MyAdditions.git", :tag => "#{s.version}" }#你的仓库地址,不能用SSH地址
  s.source_files = "MyAdditions/*.{h,m}" # 你代码的位置, BYPhoneNumTF/*.{h,m} 表示 BYPhoneNumTF 文件夹下所有的.h和.m文件
  s.requires_arc = true # 是否启用ARC
  s.platform     = :ios, "7.0" #平台及支持的最低版本
  # s.frameworks   = "UIKit", "Foundation" #支持的框架
  # s.dependency   = "AFNetworking" # 依赖库

  # User
  s.author             = { "BY" => "qiubaiyingios@163.com" } # 作者信息
  s.social_media_url   = "http://qiubaiying.github.io" # 个人主页

end

这里我要说一下一个坑,用 [oschina][8] 创建私人仓库时, 在验证时可能会找不到 MIT LICENSE证书,将其中的

[8]: https://link.jianshu.com?t=http://git.oschina.net/

s.license = “MIT”
修改为,指定文件
s.license = { :type => “MIT”, :file => “LICENSE” }

然后开始验证我们的仓库配置是否正确,并按照要求进行修改

$ pod lib lint

一般出现错误警告,需要添加 --private 或者 --allow-warnings,就可以通过验证

$ pod lib lint --private

验证成功后出现

 -> MyAdditions (0.0.1)

MyAdditions passed validation.

将描述文件推送到版本库

将项目打上标签推到远程仓库,标签号 和 版本号对应 都是0.0.1

最后将我们的代码仓库的描述信息,push 到我们的版本仓库中

$ pod repo push MyRepo MyAdditions.podspec

这时会对远程仓库进行验证,成功的话就会在 ~/.cocoapods/repos/MyRep中发现新增的仓库描述信息了

若是出现错误信息

[!] The repo `MyRepo` at `../.cocoapods/repos/MyRepo` is not clean

更新下我们的版本库,

$ pod repo update MyRepo

再继续上传即可。

pod repo push MyRepo MyAdditions.podspec 的过程就是

  1. 验证 MyAdditions.podspec 文件
  • 拉取远程版本库 MyRepo
  • 添加 MyAdditions.podspec 到版本库中
  • push 到远程

添加完成后我们就可以在pod中搜索

$ pod search MyAdditions

-> MyAdditions (0.0.1)
   Some category of the framework and UIKit
   pod 'MyAdditions', '~> 0.0.1'
   - Homepage: https://git.oschina.net/baiyingqiu/MyAdditions
   - Source:   https://git.oschina.net/baiyingqiu/MyAdditions.git
   - Versions: 0.0.1 [MyRepo repo]
(END)

私人pod库的使用

使用私人pod库的需要在Podflie中添加这句话,指明你的版本库地址。

source ‘https://git.oschina.net/baiyingqiu/MyRepo.git’

注意是版本库的地址,而不是代码库的地址,很多教程都把我搞晕了~

若有还使用了公有的pod库,需要把公有库地址也带上

source 'https://github.com/CocoaPods/Specs.git'

最后的Podflie文件变成这个样子

source ‘https://github.com/CocoaPods/Specs.git’
source ‘https://git.oschina.net/baiyingqiu/MyRepo.git’

platform :ios, '8.0'

target ‘MyPodTest’ do
use_frameworks!

pod “BYPhoneNumTF” #公有库
pod ‘MyAdditions’ #我们的私有库
pod ‘BYAdditions’ #这是我又添加到版本库中的另一个代码库

end

测试:

$ pod install

加载完成可以看到代码已经整合到我们的项目中了

perfect!

回到Fender中 ~/.cocoapods/repos,会发现 repos 中增加了一个pod版本库。

执行 pod install 命令时

  • 会拉取远程 Podfliesource 标记 版本库 到本地的 repos 文件夹中

  • 在 版本库 中搜索我们pod ‘MyAdditions’MyAdditions.podspec 文件。

  • 根据 MyAdditions.podspec 文件中描述的源码地址下载并整合到项目中

通过 《CocoaPods私有仓库的创建》《CocoaPods公有仓库的创建》这两篇文章,相信大家对CocoaPods的工作原理都有了更深层次的了解。

在写博客和和创建的过程中,踩了不少的坑(😀前人教程留下的),很多的东西只有自己操作完才能真正的领会。

最后,如果本文有什么错误或者有什么不同的观点欢迎提出交流。😉


Objective-C Runtime 的一些基本使用

Objective-C Runtime 的一些基本使用
From: https://www.jianshu.com/p/ff114e69cc0a

在上一篇文章《Objective-C Runtime详解》中我们探讨了Runtime的基本原理,这篇文章我们将总结一下Runtime的一些基本使用

目录

  • 查询方法
  • 给分类添加属性
  • 更换代码的实现方法
  • 动态添加方法
  • 字典转属性

先创建两个类

ClassA.h

#import <Foundation/Foundation.h>

@interface ClassA : NSObject {
    // 公有变量
    NSString *_publicVar1;
    NSString *_publicVar2;

}
// 公有属性
@property(nonatomic,copy) NSString *publicProperty1;
@property(nonatomic,copy) NSString *publicProperty2;

/* 公有方法 */
-(void)methodAOfClassAWithArg:(NSString *)arg;

@end

ClassA.m

#import "ClassA.h"

@interface ClassA()
// 私有属性
@property(nonatomic,copy) NSString *privateProperty1;
@property(nonatomic,copy) NSString *privateProperty2;

@end

@implementation ClassA {
    // 私有变量
    NSString *_privateVar1;
    NSString *_privateVar2;
}

/* 公有方法 */
-(void)methodAOfClassAWithArg:(NSString *)arg {
    NSLog(@" methodAOfClassA arg = %@", arg);
}

/* 私有方法 */
-(void)MethodBOfClassAWithArg:(NSString *)arg {
    NSLog(@" methodBOfClassA arg = %@", arg);
}
@end

ClassB.h

#import <Foundation/Foundation.h>

@interface ClassB : NSObject

/* 公有方法 */
-(void)methodAOfClassBWithArg:(NSString *)arg;

@end

ClassB.m

#import "ClassB.h"

@implementation ClassB
- (void)methodAOfClassBWithArg:(NSString *)arg {
    NSLog(@" methodAOfClassB arg = %@", arg);
}

-(void)methodBOfClassBWithArg:(NSString *)arg {
    NSLog(@" methodBOfClassB arg = %@", arg);
}

@end

查询方法


在Objective-C Runtime下没有真正意义上的私有变量和方法,因为这些私有变量和方法都可以通过Runtime方法获取,这当然包括系统的私有API。接下来我们来一一介绍获取类中属性和方法的方法。当然不要忘了#import <objc/runtime.h>.

获取类的名称

方法:const char *object_getClassName(id obj),使用比较简单,传入对象即可得到对应分类名。

ClassA *classA = [[ClassA alloc] init];
const char *className = object_getClassName(classA);
NSLog(@"className = %@", [NSString stringWithUTF8String:className]);

//输出
className = ClassA
获取类中的方法

方法:Method *class_copyMethodList(Class cls, unsigned int *outCount)

上代码:

UInt32 count;
char dst;
Method *methods = class_copyMethodList([classA class], &count);//获取方法列表
for (int i = 0; i < count; i++) {
    Method method = methods[i];// 获取方法
    SEL methodName = method_getName(method);// 获取方法名
    method_getReturnType(method, &dst, sizeof(char));// 获取方法返回类型
    const char *methodType = method_getTypeEncoding(method);// 获取方法参数类型和返回类型
    NSLog(@"methodName = %@",NSStringFromSelector(methodName));
    NSLog(@"dst = %c", dst);
}

 // 输出
 methodName = methodAOfClassAWithArg:
 dst = v
 methodType = v24@0:8@16
 methodName = MethodBOfClassAWithArg:
 dst = v
 methodType = v24@0:8@16
 methodName = publicProperty1
 dst = @
 methodType = @16@0:8
 methodName = setPublicProperty1:
 dst = v
 methodType = v24@0:8@16
 methodName = publicProperty2
 dst = @
 methodType = @16@0:8
 methodName = setPublicProperty2:
 dst = v
 methodType = v24@0:8@16
 methodName = privateProperty1
 dst = @
 methodType = @16@0:8
 methodName = setPrivateProperty1:
 dst = v
 methodType = v24@0:8@16
 methodName = privateProperty2
 dst = @
 methodType = @16@0:8
 methodName = setPrivateProperty2:
 dst = v
 methodType = v24@0:8@16
 methodName = .cxx_destruct
 dst = v
 methodType = v16@0:8

class_copyMethodList([classA class], &count) 传入元类和计数器地址,返回方法列表。这里注意,返回的是Method结构体类型的C数组,Method类型我们在[上篇文章][2]中已经详细说明,

[2]: https://www.jianshu.com/p/a36bfc976b8e

typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
} 

但要区分Method *methodsMethod method的区别,这是比较基础C语言知识。还有Uint32是OC定义的unsigned int类型typedef unsigned int UInt32;

这里我们来看看 method_getReturnType(method, &dst, sizeof(char)) 方法简单输出返回值类型,输出为 v@ ,参考Apple文档可知道返回类型为 voidid

A void v
A method selector (SEL)  :
An object (whether statically typed or typed id) @ 

method_getTypeEncoding(method)方法可以输出返回值,参数类型以及接收器类型。我们看输出的v24@0:8@16,分析上面的说明就可以知道: v24返回类型为viod,@0接收器类型为id,@16参数类型为id

至于类型后面的值观察可以发现都是相差8,我认为是在method中的位置,分别以8bit存储不同类型的数据。

若有两个参数返回值为 v32@0:8@16@24 ,对比可以猜测,在method中各个成员的排列是这样的: 接收器|SEl标识|参数1|参数2|...|返回值,然后由 method_getTypeEncoding(method) 输出的顺序为: 返回值类型|接收器类型|SEL标识|参数1|参数2|... 此处为个人见解,如有错误或不同意见欢迎提出探讨。

最后发现了一个奇怪的方法 .cxx_destruct ,在中这篇文章中:

ARC actually creates a -.cxx_destruct method to handle freeing instance variables. This method was originally created for calling C++ destructors automatically when an object was destroyed.

和《Effective Objective-C 2.0》中提到的:

When the compiler saw that an object contained C++ objects, it would generate a method called .cxx_destruct. ARC piggybacks on this method and emits the required cleanup code within it.

可以了解到,.cxx_destruct 方法原本是为了C++对象析构的,ARC借用了这个方法插入代码实现了自动内存释放的工作

关于 .cxx_destruct 可以参考这篇文章:ARC下dealloc过程及.cxx_destruct的探究

获取类中的属性

在 [上篇文章][5] 的 Property 中我们也提到了获取类中的属性的方法,如下:

[5]: https://www.jianshu.com/p/a36bfc976b8e

id LenderClass = objc_getClass(“ClassA”);//获取classA 的元类,不同于[ClassA class]返回本身
unsigned int outCount;//属性数量
// 获取属性列表
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);

// 遍历
for (int i = 0; i < outCount; i++) {

    objc_property_t property = properties[i];

    const char *propertyName = property_getName(property);// 获取属性名
    const char *propertyAttributes = property_getAttributes(property);// 获取属性描述

    printf("propertyName:%s \n", propertyName);
    printf("propertyAttributes:%s\n--------\n", propertyAttributes);//属性名及描述
}


// 输出
propertyName:privateProperty1 
propertyAttributes:T@"NSString",C,N,V_privateProperty1
--------
propertyName:privateProperty2 
propertyAttributes:T@"NSString",C,N,V_privateProperty2
--------
propertyName:publicProperty1 
propertyAttributes:T@"NSString",C,N,V_publicProperty1
--------
propertyName:publicProperty2 
propertyAttributes:T@"NSString",C,N,V_publicProperty2
--------

发现会输出公有属性以及私有属性。

获取类中的成员变量

我们可以发现获取类中的方法,属性过程基本一致:通过元类获取方法列表或属性列表,然后在进行遍历。获取成员变量也一样:

id selfClass = [Class class];
unsigned int numIvars = 0;
Ivar *ivars = class_copyIvarList(selfClass, &numIvars);
for(int i = 0; i < numIvars; i++) {
    Ivar ivar = ivars[i];
    const char *ivarName = ivar_getName(ivar);
    const char *ivarType = ivar_getTypeEncoding(ivar);// 获取类型

    printf("ivarName:%s\n", ivarName);
    printf("ivarType:%s\n------\n", ivarType);
}


// 输出
ivarName:_publicVar1
ivarType:@"NSString"
------
ivarName:_publicVar2
ivarType:@"NSString"
------
ivarName:_privateVar1
ivarType:@"NSString"
------
ivarName:_privateVar2
ivarType:@"NSString"
------
ivarName:_publicProperty1
ivarType:@"NSString"
------
ivarName:_publicProperty2
ivarType:@"NSString"
------
ivarName:_privateProperty1
ivarType:@"NSString"
------
ivarName:_privateProperty2
ivarType:@

可以发现输出了所有的成员变量,包括属性声明的 _+属性名 变量。

给分类添加属性


众所周知,分类中是不能声明属性的。

我们创建一个 ClassA 的分类 ClassA+CategoryA ,在 ClassA+CategoryA 中添加一个属性 name

#import "ClassA.h"

@interface ClassA (CategoryA)

@property (nonatomic, strong) NSString *name;

@end

若在我们调用ClassA分类的name 将会crash,原因是分类中使用 @property 声明属性并不会生成settergetter方法,但是我们会想,我们可以自己实现呀,没错,看下面的代码

#import "ClassA+CategoryA.h"
#import <objc/runtime.h>

@implementation ClassA (CategoryA)

- (NSString *)name {
    return name;
}

- (void)setName:(NSString *)name {
    _name = name;
}

@end

这里会报编译错误,因为分类中使用 @property 声明属性也不会生成成员变量 _name,并且手动声明也不行

编译错误,提示实例变量无法添加到分类中,用正常的方法确实无法在分类中添加属性。

但是可以通过Runtim机制进行“添加”。其本质是给这个类添加属性关联,而非把这个属性添加到类中。

#import "ClassA+CategoryA.h"
#import <objc/runtime.h>


@implementation ClassA (CategoryA)

- (NSString *)name {
    // _cmd -> @selector(name)
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

调用:

classA.name = @"邱帅";
NSLog(@"%@",classA.name);

// 输出
2016-11-21 16:18:48.084 UseRuntime[4392:1325037] 邱帅

可以看出添加属性成功!

我们来看看关联属性的这几个方法:

OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);

OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);

OBJC_EXPORT void objc_removeAssociatedObjects(id object)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);

objc_setAssociatedObject() 方法为关联属性,参数如下:

  • object:属性关联的源对象,这里使用了self,代表关联本类的对象
  • key:区分属性的唯一标识,因为关联的属性可能不止一个,我们使用了- (NSString *)name方法的SEL @selector(name)作为唯一标示,当然也可以用下面的方法来生成Key :

    //利用静态变量地址唯一不变的特性
    1、static void *strKey = &strKey;

    2、static NSString *strKey = @”strKey”;

    3、static char strKey;

  • value:关联的属性值
  • policy:设置关联对象的copystorynonatomic等参数:

这些常量对应着引用关联值的政策,也就是 Objc 内存管理的引用计数机制。

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,              
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,                                                  
    OBJC_ASSOCIATION_RETAIN = 01401,       
    OBJC_ASSOCIATION_COPY = 01403                                               
};

objc_getAssociatedObject(id object, const void *key) 方法通过 objectKey 直接获取关联的属性值

上面代码中的第二个参数写的是 _cmd,等价于@selector(name)

Objective-C的编译器在编译后会在每个方法中加两个隐藏的参数:
一个是_cmd,当前方法的一个SEL指针。
另一个就是用的比较多的self,指向当前对象的一个指针。

objc_removeAssociatedObjects() 移除关联

我们使用上面的获取类中属性和成员变量的方法,发现输出:

// 有属性输出
propertyName:name 
propertyAttributes:T@"NSString",&,N

没有成员变量 _name,进一步说明分类中不能添加成员变量!其本质是添加属性与分类之间关联。

更换代码实现方法(Method Swizzling)


在[上篇][6]中详细介绍了Method Swizzling的原理,其本质是更换了 selectorIMP

[6]: https://www.jianshu.com/p/a36bfc976b8e

#import “ViewController.h”

#import <objc/runtime.h>
#import "ClassA.h"
#import "ClassB.h"

@interface ViewController ()

@end

@implementation ViewController

+ (void)load {
    Method classA_method = class_getInstanceMethod([ClassA class], @selector(methodAOfClassAWithArg:));
    Method classB_method = class_getInstanceMethod([ClassB class], @selector(methodAOfClassBWithArg:));
    method_exchangeImplementations(classA_method, classB_method);
}
- (void)viewDidLoad {
    [super viewDidLoad];

    [classA methodAOfClassAWithArg:@"classA 发出的 A方法"];
    [classB methodAOfClassBWithArg:@"classB 发出的 A方法"];
}    

// 输出

2016-11-22 13:07:15.151 UseRuntime[1015:533335]  methodAOfClassB arg = classA 发出的 A方法
2016-11-22 13:07:15.151 UseRuntime[1015:533335]  methodAOfClassA arg = classB 发出的 A方法

首先交换方法写在 +(void)load,在程序的一开始就调用执行,你将不会碰到并发问题。

我们可以发现两个方法的实现过程以及对换。

当然,平时使用我们并不会这么做,当我们要在系统提供的方法上再扩充功能时(不能重写系统方法),就可以使用Method Swizzling.

我们给NSArray添加一个分类AddLog,给 arrayByAddingObject:方法添加一个输出方法:

#import "NSArray+AddLog.h"
#import <objc/runtime.h>

@implementation NSArray (AddLog)

+ (void)load {

    SEL ori_selector = @selector(arrayByAddingObject:);
    SEL my_selector = @selector(my_arrayByAddingObject:);

    Method ori_method = class_getInstanceMethod([NSArray class], ori_selector);
    Method my_method  = class_getInstanceMethod([NSArray class], my_selector);

    if (([NSArray class], ori_selector, method_getImplementation(my_method), method_getTypeEncoding(my_method))) {

        class_replaceMethod([NSArray class], my_selector, method_getImplementation(ori_method), method_getTypeEncoding(ori_method));

    } else {
        method_exchangeImplementations(ori_method, my_method);
    }

}

- (NSArray *)my_arrayByAddingObject:(id)anObject {

    NSArray *array = [self my_arrayByAddingObject:anObject];
    NSLog(@"添加了一个元素 %@", anObject);
    return array;
}

@end

我们来看看这三个方法:

  • class_addMethod():给一个方法添加新的方法和实现
  • class_replaceMethod():取代了对于一个给定的类的实现方法
  • method_exchangeImplementations():交换两个类的实现方法

这里我们先使用 class_addMethod() 在类中添加方法,若返回Yes说明类中没有该方法,然后再使用 class_replaceMethod() 方法进行取代;若返回NO,说明类中有该方法,使用method_exchangeImplementations()直接交换两者的 IMP.

其实在这里直接使用method_exchangeImplementations()进行交换就可以了。因为类中必定有arrayByAddingObject:方法。

我给我们自己的方法命名为my_arrayByAddingObject:,在原来的方法名上加上前缀,既可以防止命名冲突,又方便阅读,在我们my_arrayByAddingObject:方法中调用本身

NSArray *array = [self my_arrayByAddingObject:anObject];

看似会陷入递归调用,其实则不会,因为我们已经在+ (void)load方法中更换了IMP,他会调用arrayByAddingObject:方法,然后在后面添加我们需要添加的功能。

arrayByAddingObject:方法的调用不变;

NSArray *arr1 = @[@"one", @"two"];
NSArray *arr2 = [arr1 arrayByAddingObject:@"three"];
NSLog(@"arr2 = %@", arr2);


// 输出
2016-11-22 13:57:00.021 UseRuntime[1147:743449] 添加了一个元素 three
2016-11-22 13:57:00.021 UseRuntime[1147:743449] arr2 = (
    one,
    two,
    three
)

动态添加方法

动态添加方法就是在消息转发前在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中使用class_addMethod() 添加方法。

下面我面添加一个名为resolveThisMethodDynamically的方法:

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
    printf("执行了dynamicMethodIMP!!!!");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    if (sel == @selector(resolveThisMethodDynamically)) {
        class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

调用:

performSelector:@selector(resolveThisMethodDynamically)];

// 输出
执行了dynamicMethodIMP!!!!

对于上面添加的的方法 resolveThisMethodDynamically ,使用 [self performSelector:@selector(resolveThisMethodDynamically)] 进行调用,不能使用[self resolveThisMethodDynamically],因为压根就没有声明 -(void)resolveThisMethodDynamically,会报编译错误。

整个过程就是,performSelector:调用resolveThisMethodDynamically方法,然后在列表中找不到(因为类中根本就没有注册该方法),然后跳入 + (BOOL)resolveInstanceMethod: 中,我们再为resolveThisMethodDynamically方法添加具体实现。

字典转属性

将字典转化为模型,是在我们iOS开发中最为常用的技能。iOS的模型框架如JSONModel,MJExtension,MJExtension等皆是利用了runtime,将字典转为模型,不过兼顾的细节更多。下面我们来实现一个简易的字典转模型框架。

先上代码:

#import "NSObject+BYModel.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation NSObject (BYModel)

- (void)by_modelSetDictionary:(NSDictionary *)dic {

    Class cls = [self class];

    // 遍历本类和父类的变量
    while (cls) {
        //获取所有成员变量
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(cls, &outCount);

        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];

            // 获取变量名
            NSMutableString *ivar_Name = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];

            [ivar_Name replaceCharactersInRange:NSMakeRange(0, 1) withString:@""];// _ivar -> ivar

            //
            NSString *key = [ivar_Name copy];
            if ([key isEqualToString:@"dece"]) {
                key = @"description";
            }
            if ([key isEqualToString:@"ID"]) {
                key = @"id";
            }

            id value = dic[key];
            if (!value) continue;

            // 拼接SEL    ivar -> setIvar:

            NSString *cap = [ivar_Name substringToIndex:1];
            cap = cap.uppercaseString; // a->A
            [ivar_Name replaceCharactersInRange:NSMakeRange(0, 1) withString:cap];
            [ivar_Name insertString:@"set" atIndex:0];
            [ivar_Name appendString:@":"];

            SEL selector = NSSelectorFromString(ivar_Name);

            // 判断类型并发送消息
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

            if ([type hasPrefix:@"@"]) { // 对象类型
                objc_msgSend(self, selector, value);
            } else { // 非对象类型
                if ([type isEqualToString:@"d"]) {
                    objc_msgSend(self, selector, [value doubleValue]);
                } else if ([type isEqualToString:@"f"]) {
                    objc_msgSend(self, selector, [value floatValue]);
                } else if ([type isEqualToString:@"i"]) {
                    objc_msgSend(self, selector, [value intValue]);
                } else {
                    objc_msgSend(self, selector, [value longLongValue]);
                }
            }


        }
        // 获取父类进行遍历变量
        cls = class_getSuperclass(cls);
    }

}

这个这个段代码可能出现编译错误:

解决办法很简单:

将项目 Project -> Build Settings -> Enable strct checking of objc_msgSend Calls 设置为 NO 即可

接下来我们创建一个模型类Student

#import <Foundation/Foundation.h>

@interface Student : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int idNumber;

@end

使用我们的转模型方法:

NSDictionary *dic = @{ @"name":@"邱帅", @"age": @(23), @"idNumber":@(1234567)};

Student *stu = [Student new];
[stu by_modelSetDictionary:dic];

NSLog(@"%@", [NSString stringWithFormat:@"%@, %d, %d", stu.name, stu.age, stu.idNumber]);

// 输出
2016-11-24 15:32:46.351 Demo_字典转模型(Runtime)[2131:884627] 邱帅, 23, 1234567

该方法先利用我们上面介绍的class_copyIvarList()获取类中的成员变量列表,然后进行遍历,拼接字符串setIvar:,最后调用objc_msgSend()直接发送设置变量的消息,完成属性的赋值。

while (cls) {

    //code..

 cls = class_getSuperclass(cls);
}

这个循环是则获取父类中的属性:当前类的属性遍历结束之后,指向父类,若父类存在则在继续遍历属性,否则就退出循环。

当然,这个方法只是介绍了利用runtime进行字典转模型的原理,实际中还有很多需要考虑的细节,项目中我还是推荐使用像YYModel这些比较成熟而且安全的模型框架。

关于快速字典转模型可以参考我写的一篇《快速完成JSON\字典转模型 For YYModel》

Objective-C Runtime详解

From: https://www.jianshu.com/p/a36bfc976b8e

最近在学习Runtime的知识,恰巧发现了这篇博客《Objective-C Runtime》,在此基础上,进行了些许补充说明,如有错误或其他想法,欢迎提出交流。

目录

  • 引言
  • 简介
  • 与Runtime交互
  • RunTime术语
  • 消息
  • 动态方法解析
  • 消息转发
  • 健壮的实例变量
  • 动态添加属性(Object-C Associated Objects)
  • 方法调剂(Method Swizzling)
  • 总结

引言

Objective-C的方法调用实则为“发送消息”,我们来看[dog eat]实际会被编译器转化为

objc_msgSend(dog, SEL)//SEL为eat方法的标识符@selector(@"eat")

若方法中函数参数,则为:

objc_msgSend(dog, SEL, arg1, arg2, ...)

如果消息的接收者能够找到对应的方法,那么就相当于直接执行了接收者这个对象的特定方法;否则,消息要么被转发,或是临时向接收者动态添加这个方法对应的实现内容,要么就干脆就crash掉。

现在可以看出[dog eat]真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送eat这条消息,而dog将要如何响应这条消息,那就要看运行时发生的情况来决定了。

Objective-C 的 Runtime 铸就了它动态语言的特性,这些深层次的知识虽然平时写代码用的少一些,但是却是每个 Objc 程序员需要了解的。

简介

因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。

Runtime其实有两个版本:“modern”和 “legacy”。我们现在用的 Objective-C 2.0 采用的是现行(Modern)版的Runtime系统,只能运行在 iOS 和 OS X 10.5 之后的64位程序中。而OS X较老的32位程序仍采用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。

Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。

与Runtime交互

Objc 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject类定义的方法,通过对 runtime 函数的直接调用。

Objective-C源代码

大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。
还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义,这些内容在后面会讲到。(比如objc_msgSend函数及其参数列表中的idSEL都是啥)

NSObject的方法

Cocoa 中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类,说白了就是领导把自己展现给大家风光无限,但是把活儿都交给幕后小弟去干。

有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

Runtime的函数

Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。

Runtime术语

还记得引言中的objc_msgSend:方法吧,它的真身是这样的

id objc_msgSend ( id self, SEL op, ... );

下面将会逐渐展开介绍一些术语,其实它们都对应着数据结构。

SEL

objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的标识,而这个标识的数据结构是SEL:

typedef struct objc_selector *SEL;

本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。这个查找过程我们将在下面讨论。

我们可以在运行时添加新的selector,也可以在运行时获取已存在的selector,我们可以通过下面三种方法来获取SEL:

  1. sel_registerName函数

  2. Objective-C编译器提供的@selector()

  3. NSSelectorFromString()方法

id

objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:

typedef struct objc_object *id;

objc_object又是啥呢:

struct objc_object { Class isa; };

objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。

PS:isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术,详见官方文档的这句段说明

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

Class

之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:

typedef struct objc_class *Class;

objc_class就是我们摸到的那个瓜,里面的东西多着呢:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议

PS:OBJC2_UNAVAILABLE之类的宏定义是苹果在 Objc 中对系统运行版本进行约束的黑魔法,为的是兼容非Objective-C 2.0的遗留逻辑,但我们仍能从中获得一些有价值的信息,有兴趣的可以查看源代码

Objective-C 2.0 的头文件虽然没暴露出objc_class结构体更详细的设计,我们依然可以从Objective-C 1.0 的定义中小窥端倪

objc_class结构体中:ivarsobjc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改*methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。而最新版的 Runtime 源码对这一块的描述已经有很大变化,可以参考下美团技术团队的深入理解Objective-C:Category.

PS:任性的话可以在Category中添加@dynamic的属性,并利用运行期动态提供存取方法或干脆动态转发;或者干脆使用关联度对象(AssociatedObject)

其中objc_ivar_listobjc_method_list分别是成员变量列表和方法列表:

struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}

如果你C语言不是特别好,可以理解为objc_ivar_list结构体存储着objc_ivar数组列表,而objc_ivar结构体存储了类的单个成员变量的信息;同理objc_method_list结构体存储着objc_method数组列表,而objc_method结构体存储了类的某个方法的信息。

最后要提到的还有一个objc_cache,顾名思义它是缓存,它在objc_class的作用很重要,在后面会讲到。

不知道你是否注意到了objc_class中也有一个isa对象,这是因为一个 ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。

实际上,类对象是元类对象的一个实例!!元类描述了 一个类对象,就像类对象描述了普通对象一样。不同的是元类的方法列表是类方法的集合,由类对象的选择器来响应。当向一个类发送消息时,objc_msgSend 会通过类对象的isa指针定位到元类,并检查元类的方法列表(包括父类)来决定调用哪个方法。元类代替了类对象描述了类方法,就像类对象代替了实例对象描述了实例化方法。
很显然,元类也是对象,也应该是其他类的实例,实际上元类是根元类(root class’s metaclass)的实例,而根元类是其自身的实例,即根元类的isa指针指向自身。
类的super_class指向其父类,而元类的super_class则指向父类的元类。元类的super class链与类的super class链平行,所以类方法的继承与实例方法的继承也是并行的。而根元类(root class’s metaclass)的super_class指向根类(root class),这样,整个指针链就链接起来了!!

记住,当一个消息发送给任何一个对象, 方法的检查 从对象的 isa 指针开始,然后是父类。实例方法在类中定义, 类方法 在元类和根类中定义。(根类的元类就是根类自己)。在一些计算机语言的原理中,一个类和元类层次结构可以更自由的组成,更深元类链和从单一的元类继承的更多的实例化的类。Objective-C 的类方法 是使用元类的根本原因,在其他方面试图在隐藏元类。例如[NSObject class] 完全相等于 [NSObject self],所以,在形式上他还是返回的 NSObject->isa 指向的元类。 Objective-C语言是一组实用的折中方案。

上图实线是 super_class 指针,虚线是isa指针。 有趣的是根元类的超类是NSObject,而isa指向了自己,而NSObject的超类为nil,也就是它没有超类

Method

Method是一种代表类中的某个方法的类型。

typedef struct objc_method *Method;

objc_method在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
  • 方法名 method_name 类型为 SEL, 相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
  • 方法类型method_types是个char指针,存储着方法的 参数类型 和 返回值 类型。
  • method_imp指向了方法的实现,本质上是一个函数指针,后面会详细讲到。

Ivar

Ivar是一种代表类中实例变量的类型。定义如下:

typedef struct objc_ivar *Ivar;

它是一个指向objc_ivar结构体的指针,结构体有如下定义:

struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE;
    char *ivar_type                                          OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}                                                            OBJC2_UNAVAILABLE;

这里我们注意第三个成员 ivar_offset。它表示基地址偏移字节。

在编译我们的类时,编译器生成了一个 ivar 布局,显示了在类中从哪可以访问我们的 ivars

我们对 ivar 的访问就可以通过 对象地址ivar偏移字节的方法。

但是当我们增加了父类的ivar,这个时候布局就出错了,我们就不得不重新编译子类来恢复兼容性。

而Objective-C Runtime中使用了Non Fragile ivars来避免这个问题

使用Non Fragile ivars时,Runtime会进行检测来调整类中新增的ivar的偏移量。 这样我们就可以通过 对象地址 + 基类大小 + ivar偏移字节的方法来计算出ivar相应的地址,并访问到相应的ivar

可以根据实例查找其在类中的名字,也就是“反射”:

-(NSString *)nameWithInstance:(id)instance {
    unsigned int numIvars = 0;
    NSString *key=nil;
    Ivar * ivars = class_copyIvarList([self class], &numIvars);
    for(int i = 0; i < numIvars; i++) {
        Ivar thisIvar = ivars[i];
        const char *type = ivar_getTypeEncoding(thisIvar);
        NSString *stringType =  [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
        if (![stringType hasPrefix:@"@"]) {
            continue;
        }
        if ((object_getIvar(self, thisIvar) == instance)) {//此处若 crash 不要慌!
            key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
            break;
        }
    }
    free(ivars);
    return key;
}

class_copyIvarList 函数获取的不仅有实例变量,还有属性。但会在原本的属性名前加上一个下划线。(属性的本质就是 _属性名+set+get方法)

IMP

IMPobjc.h中的定义是:

typedef id (*IMP)(id, SEL, ...);

它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。

我们再来看看objc_msgSend()的定义:id objc_msgSend(id self, SEL op, ...)

你会发现IMP指向的方法与objc_msgSend函数类型相同,参数都包含id和SEL类型。每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法实现肯定是唯一的,通过一组idSEL参数就能确定唯一的方法实现地址。

Cache

runtime.h中Cache的定义如下:

typedef struct objc_cache *Cache

还记得之前 objc_class 结构体中有一个 struct objc_cache *cache 吧,它到底是缓存啥的呢,先看看 objc_cache 的实现:

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

objc_cache 的定义看起来很简单,它包含了下面三个变量:

  • mask:可以认为是当前能达到的最大index(从0开始的),所以缓存的size(total)是mask+1
  • occupied:被占用的槽位,因为缓存是以散列表的形式存在的,所以会有空槽,而occupied表示当前被占用的数目
  • buckets:用数组表示的hash表,cache_entry类型,每一个cache_entry代表一个方法缓存

(buckets定义在objc_cache的最后,说明这是一个可变长度的数组)

Cache为方法调用的性能进行优化,下面我们来看看objc_msgSend具体又是如何分发的呢? 我们来看下runtime层objc_msgSend的源码。

objc-msg-arm.s中,objc_msgSend的代码如下:

ps:Apple为了高度优化objc_msgSend的性能,这个文件是汇编写成的,不过即使我们不懂汇编,详尽的注释也可以让我们一窥其真面目

ENTRY objc_msgSend
# check whether receiver is nil
teq     a1, #0
    beq     LMsgSendNilReceiver
# save registers and load receiver's class for CacheLookup
stmfd   sp!, {a4,v1}
ldr     v1, [a1, #ISA]
# receiver is non-nil: search the cache
CacheLookup a2, v1, LMsgSendCacheMiss
# cache hit (imp in ip) and CacheLookup returns with nonstret (eq) set, restore registers and call
ldmfd   sp!, {a4,v1}
bx      ip
# cache miss: go search the method lists
LMsgSendCacheMiss:
ldmfd sp!, {a4,v1}
b _objc_msgSend_uncached
LMsgSendNilReceiver:
    mov     a2, #0
    bx      lr
LMsgSendExit:
END_ENTRY objc_msgSend
STATIC_ENTRY objc_msgSend_uncached
# Push stack frame
stmfd sp!, {a1-a4,r7,lr}
add     r7, sp, #16
# Load class and selector
ldr a3, [a1, #ISA] /* class = receiver->isa  */
/* selector already in a2 */
/* receiver already in a1 */
# Do the lookup
MI_CALL_EXTERNAL(__class_lookupMethodAndLoadCache3)
MOVE    ip, a1
# Prep for forwarding, Pop stack frame and call imp
teq v1, v1 /* set nonstret (eq) */
ldmfd sp!, {a1-a4,r7,lr}
bx ip

如果向更深入了解 objc_cache ,可以看看这篇博文深入理解Objective-C:方法缓存

从上述代码中可以看到,objc_msgSend(就ARM平台而言)的消息分发分为以下几个步骤:

  1. 判断receiver是否为nil,也就是objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象
  2. 从缓存里寻找,找到了则分发,否则
  3. 利用objc-class.mm中_class_lookupMethodAndLoadCache3(为什么有个这么奇怪的方法。本文末尾会解释)方法去寻找selector
  4. 如果支持GC,忽略掉非GC环境的方法(retain等)
  5. 从本class的method list寻找selector,如果找到,填充到缓存中,并返回selector,否则
  6. 寻找父类的method list,并依次往上寻找,直到找到selector,填充到缓存中,并返回selector,否则
  7. 调用_class_resolveMethod,如果可以动态resolve为一个selector,不缓存,方法返回,否则
  8. 转发这个selector,否则
  9. 报错,抛出异常

从上面的分析中我们可以看到,当一个方法在比较“上层”的类中,用比较“下层”(继承关系上的上下层)对象去调用的时候,如果没有缓存,那么整个查找链是相当长的。就算方法是在这个类里面,当方法比较多的时候,每次都查找也是费事费力的一件事情。

当我们需要去调用一个方法数十万次甚至更多地时候,查找方法的消耗会变的非常显著。就算我们平常的非大规模调用,除非一个方法只会调用一次,否则缓存都是有用的。在运行时,那么多对象,那么多方法调用,节省下来的时间也是非常可观的。可见缓存的重要性。

方法缓存存在什么地方?

让我们再去去翻看 objc_class 的定义,

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

我们看到在类的定义里就有cache字段,没错,类的所有缓存都存在metaclass上,所以每个类都只有一份方法缓存,而不是每一个类的object都保存一份。

子类类即便是从父类取到的方法,也会存在类本身的方法缓存里。而当用一个父类对象去调用那个方法的时候,也会在父类的metaclass里缓存一份。

Property

@property标记了类中的属性,这个不必多说大家都很熟悉,它是一个指向objc_property结构体的指针:

typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用

现在在类中声明声明属性和成员变量:

@interface ViewController ()
{
    int age;
    NSString *name;
}
@property (nonatomic, strong) NSString *property1;
@property (nonatomic, strong) NSString *property2;
@property (nonatomic, assign) int age;//这里的age为属性,对应变量:_age
@property (nonatomic, assign) long ID;

@end

然后用下面的方法来获取类中属性列表:

id LenderClass = objc_getClass("ViewController");//获取calss
//id LenderClass = [MyViewController class];//同上
unsigned int outCount;//属性数量
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);//获取属性列表
for (int i = 0; i < outCount; i++) {// 遍历
    objc_property_t property = properties[i];
    const char *propertyName = property_getName(property);
    const char *propertyAttributes = property_getAttributes(property);
    printf("propertyName:%s \n", propertyName);
    printf("propertyAttributes:%s\n--------\n", propertyAttributes);//属性名及描述
}

控制台输出:

propertyName:property1 
propertyAttributes:T@"NSString",&,N,V_property1
--------
propertyName:property2 
propertyAttributes:T@"NSString",&,N,V_property2
--------
propertyName:age 
propertyAttributes:Ti,N,V_age
--------
propertyName:ID 
propertyAttributes:Tq,N,V_ID

我们再来来看看获取成员变量的方法:

id selfClass = [self class];
unsigned int numIvars = 0;
Ivar *ivars = class_copyIvarList(selfClass, &numIvars);
for(int i = 0; i < numIvars; i++) {
    Ivar ivar = ivars[i];
    const char *ivarType = ivar_getTypeEncoding(ivar);// 获取类型
    const char *ivarName = ivar_getName(ivar);
    printf("ivarName:%s\n", ivarName);
    printf("ivarType:%s\n------\n", ivarType);
}

控制台输出:

ivarName:age
ivarType:i
------
ivarName:name
ivarType:@"NSString"
------
ivarName:_age
ivarType:i
------
ivarName:_property1
ivarType:@"NSString"
------
ivarName:_property2
ivarType:@"NSString"
------
ivarName:_ID
ivarType:q

我们会发现与 class_copyIvarList 函数不同,使用 class_copyPropertyList 函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的,得到属性或者变量名后我们就可以使用KVC去修改访问类中的私有属性或变量。所以OC中没有真正意义上的私有变量,私有方法也是。

消息

前面做了这么多铺垫,现在终于说到了消息了。Objc 中发送消息是用中括号 [] 把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。

有关消息发送和消息转发机制的原理,可以查看这篇文章

objc_msgSend函数

在引言中已经对 objc_msgSend 进行了一点介绍,看起来像是 objc_msgSend 返回了数据,其实 objc_msgSend 从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:

  1. 检测这个 消息 是不是要忽略的。比如 Mac OS X 开发,在ARC中有了垃圾回收就不理会MRC的 retain, release 这些函数了。
  2. 检测这个 目标对象 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
  3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
  4. 如果 cache 找不到就找一下方法分发表。
  5. 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
  6. 如果还找不到就要开始进入动态方法解析了,后面会提到。

PS:这里说的分发表其实就是 Class 中的方法列表,它将方法选择器和方法实现地址联系起来。

其实编译器会根据情况在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法

PS:有木有发现这些函数的命名规律哦?带 “Super” 的是消息传递给超类;“stret”可分为“st”+“ret”两部分,分别代表 “struct”“return”“fpret”就是 “fp” + “ret”,分别代表“floating-point”“return”

方法中的隐藏参数

我们经常在方法中使用 self 关键字来引用实例本身,但从没有想过为什么 self 就能取到调用当前方法的对象吧。其实 self 的内容是在方法运行时被偷偷的动态传入的

objc_msgSend 找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:

  • 接收消息的对象(也就是self指向的内容)
  • 方法选择器(_cmd指向的内容)

之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。在下面的例子中,self引用了接收者对象,而_cmd引用了方法本身的选择器:

- strange
{
    id  target = getTheReceiver();
    SEL method = getTheMethod();

    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}

在这两个参数中,self 更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径

而当方法中的 super 关键字接收到消息时,编译器会创建一个 objc_super 结构体:

struct objc_super { id receiver; Class class; };

这个结构体指明了消息应该被传递给特定父类的定义。但receiver仍然是self本身,这点需要注意,因为当我们想通过[super class]获取超类时,编译器只是将指向selfid指针和classSEL传递给了objc_msgSendSuper函数,因为只有在NSObject类才能找到class方法,然后class方法调用object_getClass(),接着调用objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向selfid指针,与调用[self class]相同,所以我们得到的永远都是self的类型。

获取方法地址

IMP 那节提到过可以避开消息绑定而直接获取方法的地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。
NSObject类中有个methodForSelector:实例方法,你可以用它来获取某个方法选择器对应的 IMP ,举个栗子:

void (*imp)(id, SEL, BOOL);//定义一个函数指针
imp = (void (*)(id, SEL, BOOL))[self methodForSelector:@selector(setFilled:)];//获取setFilled:函数的IMP

动态方法解析

你可以动态地提供一个方法的实现。例如我们可以用 @dynamic 关键字在类的实现文件中修饰一个属性:

@dynamic propertyName;

这表明我们会为这个属性提供存取方法,也就是说编译器不会默认为我们生成 setPropertyName:prepertyName 方法,而需要我们自己提供动态方法。我们可以通过分别重载 resolveIntanceMethod:resolvrClassMethod: 方法分别添加实例方法实现和类方法实现。因为当 Runtime 系统在 Cache 和方法分发表中(包括父类)找不到要执行的方法时,Runtime会调用 resolveIntanceMethod:resolvrClassMethod: 来给我们一次动态添加实现的机会。我们需要 class_addMethod函数完成向特定类添加特定方法实现的操作:

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

调用必须使用performSelector:方法,

[self performSelector:@selector(resolveThisMethodDynamically)];

因为performSelector:方法在编译阶段不做检查,若使用[self resolveThisMethodDynamically]则编译无法通过,因为[receiver message]会在编译阶段检查resolveThisMethodDynamically方法是否存在。
上面的例子为resolveThisMethodDynamically方法添加了实现内容,也就是dynamicMethodIMP方法中的代码。其中 “v@:” 表示返回值和参数,这个符号涉及 Type Encoding

PS:动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector:instancesRespondToSelector: 方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的 IMP 的机会。如果你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod: 返回 NO

消息转发

重定向

在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector 方法替换消息的接受者为其他对象:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择 。如果此方法返回 nilself,则会进入消息转发机制(forwardInvocation:);否则将向返回的对象重新发送消息。

转发

当动态方法解析不作处理返回NO时,消息转发机制会被触发。在这时forwardInvocation:方法会被执行,我们可以重写这个方法来定义我们的转发逻辑:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

该消息的唯一参数是个NSInvocation类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。

当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过 forwardInvocation: 消息通知该对象。每个对象都从NSObject类中继承了 forwardInvocation: 方法。然而,NSObject中的方法实现只是简单地调用了 doesNotRecognizeSelector: 。通过实现我们自己的 forwardInvocation: 方法,我们可以在该方法实现中将消息转发给其它对象。

forwardInvocation: 方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。

注意: forwardInvocation: 方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将negotiate消息转发给其它对象,则这个对象不能有negotiate方法。否则,forwardInvocation:将不可能会被调用。

转发和多继承

转发和继承相似,可以用于为Objc编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另一个对象中的方法借过来或是“继承”过来一样。

这使得不同继承体系分支下的两个类可以“继承”对方的方法,在上图中 WarriorDiplomat 没有继承关系,但是 Warriornegotiate 消息转发给了 Diplomat 后,就好似 DiplomatWarrior 的超类一样。
消息转发弥补了 Objc 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,而且转发机制是透明的

替代者对象(Surrogate Objects)

转发不仅能模拟多继承,也能使轻量级对象代表重量级对象。弱小的女人背后是强大的男人,毕竟女人遇到难题都把它们转发给男人来做了。这里有一些适用案例,可以参看官方文档

转发于继承

尽管转发很像继承,但是NSObject类不会将两者混淆。像 respondsToSelector:isKindOfClass: 这类方法只会考虑继承体系,不会考虑转发链。比如上图中一个 Warrior 对象如果被问到是否能响应 negotiate 消息:

if ( [aWarrior respondsToSelector:@selector(negotiate)] )
    ...

结果是 NO ,尽管它能够接受 negotiate 消息而不报错,因为它靠转发消息给 Diplomat 类来响应消息。

如果你为了某些意图偏要“弄虚作假”让别人以为Warrior 继承到了 Diplomatnegotiate 方法,你得重新实现 respondsToSelector:isKindOfClass: 来加入你的转发算法:

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ( [super respondsToSelector:aSelector] )
        return YES;
    else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
    }
    return NO;
}

除了respondsToSelector:isKindOfClass:之外,instancesRespondToSelector:中也应该写一份转发算法。如果使用了协议,conformsToProtocol:同样也要加入到这一行列中。类似地,如果一个对象转发它接受的任何远程消息,它得给出一个methodSignatureForSelector:来返回准确的方法描述,这个方法会最终响应被转发的消息。比如一个对象能给它的替代者对象转发消息,它需要像下面这样实现methodSignatureForSelector::

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

健壮的实例变量(Non Fragile ivars)

在 Runtime 的现行版本中,最大的特点就是健壮的实例变量。当一个类被编译时,实例变量的布局也就形成了,它表明访问类的实例变量的位置。从对象头部地址开始,实例变量依次根据自己所占空间而产生位移:

再翻出Ivar的定义:

struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE;
    char *ivar_type                                          OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}                                                            OBJC2_UNAVAILABLE;

ivar 的访问可以通过 对象地址ivar偏移字节(ivar_offset)的方法。

当我们增加了父类的ivar,这个时候布局就出错了,我们就不得不重新编译子类来恢复兼容性。

在健壮的实例变量下编译器生成的实例变量布局跟以前一样,但是当 runtime 系统检测到与超类有部分重叠时它会调整你新添加的实例变量的位移,那样你在子类中新添加的成员就被保护起来了

需要注意的是在健壮的实例变量下,不要使用 sizeof(SomeClass),而是用 class_getInstanceSize([SomeClass class]) 代替;也不要使用 offsetof(SomeClass, SomeIvar) ,而要用 ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar")) 来代替。

/* 定义一个Student类 */
@interface Student : NSObject
{
@private
    int age;
}
@end

@implementation Student
// 重写%@输出方法
- (NSString *)description
{
    NSLog(@"current pointer = %p", self);
    NSLog(@"age pointer = %p", &age);
    return [NSString stringWithFormat:@"age = %d", age];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...

        Student *student = [[Student alloc] init];
        Ivar age_ivar = class_getInstanceVariable(object_getClass(student), "age");//获取"age"的ivar
        int *age_pointer = (int *)((__bridge void *)(student) + ivar_getOffset(age_ivar));//定义一个指向age_ivar的指针:指向地址为 student对象地址 + age_ivar的偏移量(ivar_offset)
        NSLog(@"age ivar offset = %td", ivar_getOffset(age_ivar));//输出offset偏移量
        *age_pointer = 10;//对指针age_pointer指向的变量(age_ivar)赋值
        NSLog(@"%@", student);//输出重写的description方法

    }
    return 0;
}

观察控制台输出:

2016-11-11 16:22:56.364 Ivar_offset[1501:928608] age ivar offset = 8
2016-11-11 16:22:56.365 Ivar_offset[1501:928608] current pointer = 0x100400170
2016-11-11 16:22:56.365 Ivar_offset[1501:928608] age pointer = 0x100400178
2016-11-11 16:22:56.366 Ivar_offset[1501:928608] age = 10

我们发现age pointer = current pointer + age ivar offset

Objective-C Associated Objects

在 OS X 10.6 之后,Runtime系统让Objc支持向对象动态添加变量。涉及到的函数有以下三个:

void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
id objc_getAssociatedObject ( id object, const void *key );
void objc_removeAssociatedObjects ( id object );

这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联政策是一组枚举常量:

enum {
   OBJC_ASSOCIATION_ASSIGN  = 0,
   OBJC_ASSOCIATION_RETAIN_NONATOMIC  = 1,
   OBJC_ASSOCIATION_COPY_NONATOMIC  = 3,
   OBJC_ASSOCIATION_RETAIN  = 01401,
   OBJC_ASSOCIATION_COPY  = 01403
};

这些常量对应着引用关联值的政策,也就是 Objc 内存管理的引用计数机制。

Method Swizzling

之前所说的消息转发虽然功能强大,但需要我们了解并且能更改对应类的源代码,因为我们需要实现自己的转发逻辑。当我们无法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,但是有时无法达到目的。这里介绍的是 Method Swizzling ,它通过重新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling 的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。

这里摘抄一个 NSHipster 的例子

#import <objc/runtime.h> 

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        // When swizzling a class method, use the following: 
        // Class class = object_getClass((id)self); 
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        BOOL didAddMethod =
            class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
#pragma mark - Method Swizzling 
- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}
@end

上面的代码通过添加一个 Tracking 类别到 UIViewController 类中,将 UIViewController 类的 viewWillAppear: 方法和 Tracking 类别中 xxx_viewWillAppear: 方法的实现相互调换。Swizzling 应该在 +load 方法中实现,因为 +load 是在一个类最开始加载时调用。dispatch_once 是GCD中的一次性方法,它保证了代码块只执行一次,并让其为一个原子操作,线程安全是很重要的。

先用 class_addMethodclass_replaceMethod 函数将两个方法的实现进行调换,如果类中已经有了 viewWillAppear: 方法的实现,那么就调用 method_exchangeImplementations 函数交换了两个方法的 IMP ,这是苹果提供给我们用于实现 Method Swizzling 的便捷方法。
最后 xxx_viewWillAppear: 方法的定义看似是递归调用引发死循环,其实不会的。因为 [self xxx_viewWillAppear:animated] 消息会动态找到 xxx_viewWillAppear: 方法的实现,而它的实现已经被我们与 viewWillAppear:方法实现进行了互换,所以这段代码不仅不会死循环,如果你把 [self xxx_viewWillAppear:animated] 换成 [self viewWillAppear:animated] 反而会引发死循环。
看到有人说 +load方法本身就是线程安全的,因为它在程序刚开始就被调用,很少会碰到并发问题,于是 stackoverflow 上也有大神给出了另一个 Method Swizzling 的实现:

- (void)replacementReceiveMessage:(const struct BInstantMessage *)arg1 {
    NSLog(@"arg1 is %@", arg1);
    [self replacementReceiveMessage:arg1];
}
+ (void)load {
    SEL originalSelector = @selector(ReceiveMessage:);
    SEL overrideSelector = @selector(replacementReceiveMessage:);
    Method originalMethod = class_getInstanceMethod(self, originalSelector);
    Method overrideMethod = class_getInstanceMethod(self, overrideSelector);
    if (class_addMethod(self, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
            class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
            method_exchangeImplementations(originalMethod, overrideMethod);
    }
}

其实也就是去掉了dispatch_once的部分罢了。

Method Swizzling 的确是一个值得深入研究的话题,Method Swizzling 的最佳实现是什么呢?小弟才疏学浅理解的不深刻,找了几篇不错的资源推荐给大家:

总结

我们之所以让自己的类继承 NSObject 不仅仅因为苹果帮我们完成了复杂的内存分配问题,更是因为这使得我们能够用上 Runtime 系统带来的便利。深入理解 Runtime 系统的细节更有利于我们利用消息机制写出功能更强大的代码,比如 Method Swizzling 等。

参考链接

最后,附上Runtime的基本使用:[《Objective-C Runtime 的基本使用》][23]

[23]:

NetworkExtension

From: https://www.jianshu.com/p/5072a8485ceb

前言: [iOS][1] 9 发布之后,推出NetworkExtension, 它可给系统WiFi列表列表里边的WiFi设置密码 、标签(副标题)。 还可获取整个WiFi列表。
首先你得向苹果申请一个权限,人家允许你使用了,你再在工程里面配置一下,这样你才可以使用.苹果会给你发个问卷调查,根据你自己的情况填写.这儿谢谢我初中学霸,专业的英语翻译果然6.
1-1.框架申请链接:https://developer.apple.com/contact/network-extension

问卷调查表

根据自己的实际情况填写.里面有个产品介绍,最好找个英文好的…

1-2 调查表填写完成后,大概过了2 ,3小时,苹果会回复给你一封邮件,并且返回给你一个fllowup.(注意,这并不是代表你已经申请成功了,邮件里面只是确认你填写的问卷信息!)

并不是成功的邮件

1-3 接下来,就只能等了.逛苹果论坛,据说要等三星期,可是,我TM等了5星期是什么鬼.所以当超过三星期的时候我也没闲着,打电话 :4006701855 , 虽然得到的回复还是等…
而且,值得一提的是:有人遇见过这种情况,一直没有收到苹果拒绝或者同意的邮件.但是可以使用这个类.所以当超过3星期以后,我是每天都登录到开发者账号 配置描述文件,如果发现这儿多了一个选项.如下图,这也说明你申请成功了


配置描述文件

1-5 所以呢,打了两次电话,重发了5 ,6 次邮件之后,大概苹果也觉得不好意思了吧,终于通过了.此时你会收到这也一封邮件:

恭喜您,通过了.但是不得不说,这只成功了一小半.后面你可能遇见更痛苦的事情.

2下面就来来说更痛苦的事情.
2-1 配置工程
a .新建一个App ID.

新建 App ID

b. 添加iCloud 和Wireless Accessory

c.新建iCloud Containers

新建iCloud Containers.png

d.打开刚刚创建的App ID 发现这个是黄色的,下面就要编辑这个App ID

编辑刚刚创建的AppID.png

e

f

g 配置App ID完成

2-2 配置描述文件 .

注意选择新建的App ID

注意这个值要加上.png

配置好这个之后,可以到苹果提供的检测环境检测一下配置文件的正确性
附上网址 :https://forums.developer.apple.com/message/75928#75928

这一步很重要:就是检测你的工程配置的描述文件和这个账号使用权限是否对等.后面有小伙伴遇到一个bug就是ruternType一直返回NO,获取不到wifi列表,然后通过上面验证发现,证书的权限和申请使用的权限不相同.
所以这儿有个建议,如果发现获取列表时返回值是NO,把测试证书删掉,重新创建.然后再走一遍上面的流程.(我遇到的BUG奇怪的很,对着英文文档走了好几遍,确定文件没什么问题,就是返回值一直都是NO,后来,在创建iCoud的时候,把那个id改成和App ID不一样,就是按照他下面的要求创建,就奇怪的好了.)

注意 :创建完描述文件别忘了安装到Xcode,直接下载,完了双击就ok了.

3.配置Xcode工程了;
3-1 配置plist文件 (允许后台运行)
通过xml添加

UIBackgroundModes


network-authentication

通过xml方式添加.png

添加完了会有这

3-2 targets->Capabilities->iCloud 和Wireless-Accessory-Configuration

,打开并配置icould

打开Wireless-Accessory-Configuration.png


配置完上面两个你就会发现工程左边会多了一个.entitlements结尾的文件.
然后还要向这个文件里面添加一个BOOL值为YES的字段 com.apple.developer.networking.HotspotHelper

配置.entitlements文件.png

3-3 Tagarts–>Build Settings -> code Signing

配置Build Settings

3-4 上代码 (也是蛮辛苦的)

Register a Hotspot Helper

+ (BOOL)registerWithOptions:(NSDictionary*)options queue:(dispatch_queue_t)queue handler:(NEHotspotHelperHandler)handler

@param options 

 kNEHotspotHelperOptionDisplayName :WIFI的注释tag字符串// 此处设置的内容会在WiFi列表中每个WiFi下边展示出来 

@param queue dispatch_queue_t 用来调用handle的block 

@param handler NEHotspotHelperHandler block 用于执行处理 helper commands.

 @return 注册成功YES, 否则NO. 

@discussion 一旦这个API调用成功,应用程序有资格在后台启动,并参与各种热点相关的功能。 当应用程序启动此方法应该调用一次。再次调用它不会产生影响,并返回NO。

这个方法是主要的.


+ (BOOL)logoff:(NEHotspotNetwork *)network

@param network 对应当前关联的WiFi网络NEHotspotNetwork

 @return 注销命令已成功进入队列YES, 否则NO. 

@discussion 调用此方法使kNEHotspotHelperCommandTypeLogoff型的NEHotspotHelperCommand向应用程序发出的“handler”模块 网络参数必须符合当前关联的WiFi网络,即它必须来自对NEHotspotHelperCommand网络属性或方法supportedInterfaces

+ (NSArray *)supportedNetworkInterfaces

@return 如果没有网络接口被管理,返回nil。否则,返回NEHotspotNetwork对象数组。 

@discussion 每个网络接口由NEHotspotNetwork对象表示。当前返回的数组包含一个NEHotspotNetwork对象代表Wi-Fi接口。

 这种方法的主要目的是当没有得到一个命令来处理它时,让一个热点助手偶尔提供在UI里其准确的状态。 此方法加上NEHotspotNetwork的isChosenHelper方法允许应用程序知道它是否是当前处理的网络。

//最后奉上我的实现代码,获取wifi列表,并给指定ssid做标记;

+(void)getWifiList{

    NSMutableDictionary* options = [[NSMutableDictionary alloc] init];
    [options setObject:@"🔑😀新网程-点我上网😀🔑" forKey:kNEHotspotHelperOptionDisplayName];

    dispatch_queue_t queue = dispatch_queue_create("com.pronetwayXY", NULL);
    BOOL returnType = [NEHotspotHelper registerWithOptions:options queue:queue handler: ^(NEHotspotHelperCommand * cmd) {
        NEHotspotNetwork* network;
        NSLog(@"COMMAND TYPE:   %ld", (long)cmd.commandType);
        [cmd createResponse:kNEHotspotHelperResultAuthenticationRequired];
        if (cmd.commandType == kNEHotspotHelperCommandTypeEvaluate || cmd.commandType ==kNEHotspotHelperCommandTypeFilterScanList) {
            NSLog(@"WIFILIST:   %@", cmd.networkList);
            for (network  in cmd.networkList) {
                // NSLog(@"COMMAND TYPE After:   %ld", (long)cmd.commandType);
                if ([network.SSID isEqualToString:@"ssid"]|| [network.SSID isEqualToString:@"proict_test"]) {

                    double signalStrength = network.signalStrength;
                    NSLog(@"Signal Strength: %f", signalStrength);
                    [network setConfidence:kNEHotspotHelperConfidenceHigh];
                    [network setPassword:@"password"];

                    NEHotspotHelperResponse *response = [cmd createResponse:kNEHotspotHelperResultSuccess];
                    NSLog(@"Response CMD %@", response);

                    [response setNetworkList:@[network]];
                    [response setNetwork:network];
                    [response deliver];
                }
            }
        }
    }];
    NSLog(@"result :%d", returnType);
    NSArray *array = [NEHotspotHelper supportedNetworkInterfaces];
    NSLog(@"wifiArray:%@", array);
    NEHotspotNetwork *connectedNetwork = [array lastObject];
    NSLog(@"supported Network Interface: %@", connectedNetwork);

}

注意 :运行一遍,然后需要打开系统设置连wifi界面,才能在控制台查看打印信息的.

下面附上两张效果图:

系统的wifi列表.png

修改系统wifi的标签

最后附上一个Network Extension 使用的难兄(福)难(利)弟群(群里很多都已经实现了这个功能)—-585640621

demo已上传到gitHub上 :https://github.com/chengkunlun/FirstRePository

参考文档:http://blog.csdn.net/qinxianjun163/article/details/51583057

ReactiveCocoa进阶

From: https://vinefiner.github.io/2017/01/06/ReactiveCocoa-%E8%BF%9B%E9%98%B6/

上篇文章中介绍了ReactiveCocoa的基础知识,接下来我们来深入介绍ReactiveCocoa及其在MVVM中的用法。

ReactiveCocoa进阶思维导图

操作须知

所有的信号(RACSignal)都可以进行操作处理,因为所有操作方法都定义在RACStream.h中,因此只要继承RACStream就有了操作处理方法。

操作思想

运用的是Hook(钩子)思想,Hook是一种用于改变API(应用程序编程接口:方法)执行结果的技术.

Hook用处:截获API调用的技术。

有关Hook的知识可以看我的这篇博客《Objective-C Runtime 的一些基本使用》中的 更换代码的实现方法 一节,

Hook原理:在每次调用一个API返回结果之前,先执行你自己的方法,改变结果的输出。

操作方法

bind(绑定)- ReactiveCocoa核心方法

ReactiveCocoa 操作的核心方法是 bind(绑定),而且也是RAC中核心开发方式。之前的开发方式是赋值,而用RAC开发,应该把重心放在绑定,也就是可以在创建一个对象的时候,就绑定好以后想要做的事情,而不是等赋值之后在去做事情。

列如,把数据展示到控件上,之前都是重写控件的 setModel 方法,用RAC就可以在一开始创建控件的时候,就绑定好数据。

  • 作用

RAC底层都是调用bind, 在开发中很少直接使用 bind 方法,bind属于RAC中的底层方法,我们只需要调用封装好的方法,bind用作了解即可.

  • bind方法使用步骤
    1. 传入一个返回值 RACStreamBindBlock 的 block。
    2. 描述一个 RACStreamBindBlock 类型的 bindBlock作为block的返回值。
    3. 描述一个返回结果的信号,作为 bindBlock 的返回值。

注意:在bindBlock中做信号结果的处理。

  • bind方法参数

RACStreamBindBlock: typedef RACStream * (^RACStreamBindBlock)(id value, BOOL *stop);

参数一(value):表示接收到信号的原始值,还没做处理

参数二(*stop):用来控制绑定Block,如果*stop = yes,那么就会结束绑定。

返回值:信号,做好处理,在通过这个信号返回出去,一般使用 RACReturnSignal,需要手动导入头文件RACReturnSignal.h

  • 使用

假设想监听文本框的内容,并且在每次输出结果的时候,都在文本框的内容拼接一段文字“输出:”

* 使用封装好的方法:在返回结果后,拼接。 

  [_textField.rac_textSignal subscribeNext:^(id x) {

      // 在返回结果后,拼接 输出:
      NSLog(@"输出:%@",x);

  }];


* 方式二:,使用RAC中 `bind` 方法做处理,在返回结果前,拼接。 

这里需要手动导入#import <ReactiveCocoa/RACReturnSignal.h>,才能使用RACReturnSignal

[[_textField.rac_textSignal bind:^RACStreamBindBlock{
   // 什么时候调用:
   // block作用:表示绑定了一个信号.

   return ^RACStream *(id value, BOOL *stop){

       // 什么时候调用block:当信号有新的值发出,就会来到这个block。

       // block作用:做返回值的处理

       // 做好处理,在返回结果前,拼接 输出:
       return [RACReturnSignal return:[NSString stringWithFormat:@"输出:%@",value]];
   };

}] subscribeNext:^(id x) {

   NSLog(@"%@",x);

}];
  • 底层实现
    1. 源信号调用bind,会重新创建一个绑定信号。
    2. 当绑定信号被订阅,就会调用绑定信号中的 didSubscribe ,生成一个 bindingBlock
    3. 当源信号有内容发出,就会把内容传递到 bindingBlock 处理,调用bindingBlock(value,stop)
    4. 调用bindingBlock(value,stop),会返回一个内容处理完成的信号RACReturnSignal
    5. 订阅RACReturnSignal,就会拿到绑定信号的订阅者,把处理完成的信号内容发送出来。

注意:不同订阅者,保存不同的nextBlock,看源码的时候,一定要看清楚订阅者是哪个。

映射

映射主要用这两个方法实现:flattenMap,Map,用于把源信号内容映射成新的内容。

flattenMap
  • 作用

把源信号的内容映射成一个新的信号,信号可以是任意类型

  • 使用步骤

    1. 传入一个block,block类型是返回值RACStream,参数value
    2. 参数value就是源信号的内容,拿到源信号的内容做处理
    3. 包装成RACReturnSignal信号,返回出去。
  • 使用

监听文本框的内容改变,把结构重新映射成一个新值.

[[_textField.rac_textSignal flattenMap:^RACStream *(id value) {

    // block调用时机:信号源发出的时候

    // block作用:改变信号的内容

    // 返回RACReturnSignal
    return [RACReturnSignal return:[NSString stringWithFormat:@"信号内容:%@", value]];

}] subscribeNext:^(id x) {

    NSLog(@"%@", x);
}];
  • 底层实现

    1. flattenMap内部调用 bind 方法实现的,flattenMap中block的返回值,会作为bind中bindBlock的返回值。
    2. 当订阅绑定信号,就会生成 bindBlock
    3. 当源信号发送内容,就会调用 bindBlock(value, *stop)
    4. 调用bindBlock,内部就会调用 flattenMap 的 bloc k,flattenMap 的block作用:就是把处理好的数据包装成信号。
    5. 返回的信号最终会作为 bindBlock 中的返回信号,当做 bindBlock 的返回信号。
    6. 订阅 bindBlock 的返回信号,就会拿到绑定信号的订阅者,把处理完成的信号内容发送出来。
Map
  • 作用

把源信号的值映射成一个新的值

  • 使用步骤
    1. 传入一个block,类型是返回对象,参数是 value
    2. value就是源信号的内容,直接拿到源信号的内容做处理
    3. 把处理好的内容,直接返回就好了,不用包装成信号,返回的值,就是映射的值。
  • 使用

监听文本框的内容改变,把结构重新映射成一个新值.

[[_textField.rac_textSignal map:^id(id value) {

   // 拼接完后,返回对象
    return [NSString stringWithFormat:@"信号内容: %@", value];

}] subscribeNext:^(id x) {

    NSLog(@"%@", x);
}];
  • 底层实现:
    1. Map底层其实是调用 flatternMap,Map 中block中的返回的值会作为 flatternMap 中block中的值
    2. 当订阅绑定信号,就会生成 bindBlock
    3. 当源信号发送内容,就会调用 bindBlock(value, *stop)
    4. 调用 bindBlock ,内部就会调用 flattenMap的block
    5. flattenMap的block 内部会调用 Map 中的block,把 Map 中的block返回的内容包装成返回的信号
    6. 返回的信号最终会作为 bindBlock 中的返回信号,当做 bindBlock 的返回信号
    7. 订阅 bindBlock 的返回信号,就会拿到绑定信号的订阅者,把处理完成的信号内容发送出来。
FlatternMap 和 Map 的区别
  • FlatternMap 中的Block 返回信号
    1. Map 中的Block 返回对象
    2. 开发中,如果信号发出的值 不是信号 ,映射一般使用 Map
    3. 如果信号发出的值 是信号,映射一般使用 FlatternMap
  • signalOfsignalsFlatternMap

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 创建信号中的信号
    RACSubject *signalOfsignals = [RACSubject subject];
    RACSubject *signal = [RACSubject subject];
    [[signalOfsignals flattenMap:^RACStream *(id value) {
    // 当signalOfsignals的signals发出信号才会调用
    return value;
    }] subscribeNext:^(id x) {
    // 只有signalOfsignals的signal发出信号才会调用,因为内部订阅了bindBlock中返回的信号,也就是flattenMap返回的信号。
    // 也就是flattenMap返回的信号发出内容,才会调用。
    NSLog(@"signalOfsignals:%@",x);
    }];
    // 信号的信号发送信号
    [signalOfsignals sendNext:signal];
    // 信号发送内容
    [signal sendNext:@"hi"];

组合

组合就是将多个信号按照某种规则进行拼接,合成新的信号。

  • 作用

顺序拼接信号,当多个信号发出的时候,有顺序的接收信号。

  • 底层实现
    1. 当拼接信号被订阅,就会调用拼接信号的didSubscribe
    2. didSubscribe中,会先订阅第一个源信号(signalA)
    3. 会执行第一个源信号(signalA)的didSubscribe
    4. 第一个源信号(signalA)didSubscribe中发送值,就会调用第一个源信号(signalA)订阅者的nextBlock,通过拼接信号的订阅者把值发送出来.
    5. 第一个源信号(signalA)didSubscribe中发送完成,就会调用第一个源信号(signalA)订阅者的completedBlock,订阅第二个源信号(signalB)这时候才激活(signalB)。
    6. 订阅第二个源信号(signalB),执行第二个源信号(signalB)的didSubscribe
    7. 第二个源信号(signalA)didSubscribe中发送值,就会通过拼接信号的订阅者把值发送出来.
  • 使用步骤

    1. 使用concat:拼接信号
    2. 订阅拼接信号,内部会自动按拼接顺序订阅信号
  • 使用

拼接信号 signalAsignalBsignalC

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

    [subscriber sendNext:@"Hello"];

    [subscriber sendCompleted];

    return nil;
}];

RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

    [subscriber sendNext:@"World"];

    [subscriber sendCompleted];

    return nil;
}];

RACSignal *signalC = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

    [subscriber sendNext:@"!"];

    [subscriber sendCompleted];

    return nil;
}];

// 拼接 A B, 把signalA拼接到signalB后,signalA发送完成,signalB才会被激活。
RACSignal *concatSignalAB = [signalA concat:signalB];

// A B + C
RACSignal *concatSignalABC = [concatSignalAB concat:signalC];


// 订阅拼接的信号, 内部会按顺序订阅 A->B->C
// 注意:第一个信号必须发送完成,第二个信号才会被激活...
[concatSignalABC subscribeNext:^(id x) {

    NSLog(@"%@", x);
}];
then
  • 作用

用于连接两个信号,当第一个信号完成,才会连接then返回的信号。

  • 底层实现

    1. 先过滤掉之前的信号发出的值
    2. 使用concat连接then返回的信号
  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@1];
    [subscriber sendCompleted];
    return nil;
    }] then:^RACSignal *{
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@2];
    return nil;
    }];
    }] subscribeNext:^(id x) {
    // 只能接收到第二个信号的值,也就是then返回信号的值
    NSLog(@"%@", x);
    }];
    ///
    输出:2
  • 注意

注意使用then,之前信号的值会被忽略掉.

merge
  • 作用

合并信号,任何一个信号发送数据,都能监听到.

  • 底层实现

    1. 合并信号被订阅的时候,就会遍历所有信号,并且发出这些信号。
    2. 每发出一个信号,这个信号就会被订阅
    3. 也就是合并信号一被订阅,就会订阅里面所有的信号。
    4. 只要有一个信号被发出就会被监听。
  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"A"];
    return nil;
    }];
    RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"B"];
    return nil;
    }];
    // 合并信号, 任何一个信号发送数据,都能监听到
    RACSignal *mergeSianl = [signalA merge:signalB];
    [mergeSianl subscribeNext:^(id x) {
    NSLog(@"%@", x);
    }];
    // 输出
    2017-01-03 13:29:08.013 ReactiveCocoa进阶[3627:718315] A
    2017-01-03 13:29:08.014 ReactiveCocoa进阶[3627:718315] B
zip
  • 作用

把两个信号压缩成一个信号,只有当两个信号 同时 发出信号内容时,并且把两个信号的内容合并成一个元组,才会触发压缩流的next事件。

  • 底层实现

    1. 定义压缩信号,内部就会自动订阅signalA,signalB
    2. 每当signalA或者signalB发出信号,就会判断signalA,signalB有没有发出个信号,有就会把每个信号 第一次 发出的值包装成元组发出
  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"A1"];
    [subscriber sendNext:@"A2"];
    return nil;
    }];
    RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"B1"];
    [subscriber sendNext:@"B2"];
    [subscriber sendNext:@"B3"];
    return nil;
    }];
    RACSignal *zipSignal = [signalA zipWith:signalB];
    [zipSignal subscribeNext:^(id x) {
    NSLog(@"%@", x);
    }];
    // 输出
    2017-01-03 13:48:09.234 ReactiveCocoa进阶[3997:789720] zipWith: <RACTuple: 0x600000004df0> (
    A1,
    B1
    )
    2017-01-03 13:48:09.234 ReactiveCocoa进阶[3997:789720] zipWith: <RACTuple: 0x608000003410> (
    A2,
    B2
    )
    ```
    ###### combineLatest
    * **作用**
    将多个信号合并起来,并且拿到各个信号最后一个值,必须每个合并的signal至少都有过一次sendNext,才会触发合并的信号。
    * **底层实现**
    1. 当组合信号被订阅,内部会自动订阅signalA,signalB,必须两个信号都发出内容,才会被触发。 2. 并且把两个信号的 最后一次 发送的值组合成元组发出。
    * **使用**

    RACSignal signalA = [RACSignal createSignal:^RACDisposable (id subscriber) {

    [subscriber sendNext:@"A1"];
    [subscriber sendNext:@"A2"];
    
    return nil;
    

    }];

    RACSignal signalB = [RACSignal createSignal:^RACDisposable (id subscriber) {

    [subscriber sendNext:@"B1"];
    [subscriber sendNext:@"B2"];
    [subscriber sendNext:@"B3"];
    
    return nil;
    

    }];

    RACSignal *combineSianal = [signalA combineLatestWith:signalB];

    [combineSianal subscribeNext:^(id x) {

    NSLog(@"combineLatest:%@", x);
    

    }];

    // 输出
    2017-01-03 13:48:09.235 ReactiveCocoa进阶[3997:789720] combineLatest: (
    A2,
    B1
    )
    2017-01-03 13:48:09.235 ReactiveCocoa进阶[3997:789720] combineLatest: (
    A2,
    B2
    )
    2017-01-03 13:48:09.236 ReactiveCocoa进阶[3997:789720] combineLatest: (
    A2,
    B3
    )

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    * **注意**
    **combineLatest**与**zip**用法相似,必须每个合并的signal至少都有过一次sendNext,才会触发合并的信号。
    区别看下图:
    ![][4]
    [4]: http://images.chenzhao.date/blogimages/ReactiveCocoa%20%E8%BF%9B%E9%98%B6/pic_002.jpg
    ###### reduce
    * **作用**
    把信号发出元组的值聚合成一个值
    * **底层实现**
    1. 订阅聚合信号, 2. 每次有内容发出,就会执行reduceblcok,把信号内容转换成reduceblcok返回的值。
    * **使用**
    常见的用法,(先组合在聚合)`combineLatest:(id<NSFastEnumeration>)signals reduce:(id (^)())reduceBlock`
    reduce中的block简介:
    reduceblcok中的参数,有多少信号组合,reduceblcok就有多少参数,每个参数就是之前信号发出的内容 reduceblcok的返回值:聚合信号之后的内容。
    RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"A1"];
    [subscriber sendNext:@"A2"];
    return nil;
    }];
    RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"B1"];
    [subscriber sendNext:@"B2"];
    [subscriber sendNext:@"B3"];
    return nil;
    }];
    RACSignal *reduceSignal = [RACSignal combineLatest:@[signalA, signalB] reduce:^id(NSString *str1, NSString *str2){
    return [NSString stringWithFormat:@"%@ %@", str1, str2];
    }];
    [reduceSignal subscribeNext:^(id x) {
    NSLog(@"%@", x);
    }];
    // 输出
    2017-01-03 15:42:41.803 ReactiveCocoa进阶[4248:1264674] A2 B1
    2017-01-03 15:42:41.803 ReactiveCocoa进阶[4248:1264674] A2 B2
    2017-01-03 15:42:41.803 ReactiveCocoa进阶[4248:1264674] A2 B3
    #### 过滤
    过滤就是过滤信号中的 特定值 ,或者过滤指定 发送次数 的信号。
    ###### filter
    * **作用**
    过滤信号,使用它可以获取满足条件的信号.
    block的返回值是Bool值,返回`NO`则过滤该信号
    * **使用**

    // 过滤:
    // 每次信号发出,会先执行过滤条件判断.
    [[_textField.rac_textSignal filter:^BOOL(NSString *value) {

    NSLog(@"原信号: %@", value);
    
    // 过滤 长度 <= 3 的信号
    return value.length > 3;
    

    }] subscribeNext:^(id x) {

    NSLog(@"长度大于3的信号:%@", x);
    

    }];

    // 在_textField中输出12345
    // 输出
    2017-01-03 16:36:54.938 ReactiveCocoa进阶[4714:1552910] 原信号: 1
    2017-01-03 16:36:55.383 ReactiveCocoa进阶[4714:1552910] 原信号: 12
    2017-01-03 16:36:55.706 ReactiveCocoa进阶[4714:1552910] 原信号: 123
    2017-01-03 16:36:56.842 ReactiveCocoa进阶[4714:1552910] 原信号: 1234
    2017-01-03 16:36:56.842 ReactiveCocoa进阶[4714:1552910] 长度大于3的信号:1234
    2017-01-03 16:36:58.350 ReactiveCocoa进阶[4714:1552910] 原信号: 12345
    2017-01-03 16:36:58.351 ReactiveCocoa进阶[4714:1552910] 长度大于3的信号:12345

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    ###### ignore
    * **作用**
    忽略某些信号.
    * **使用**
    * **作用**
    忽略某些值的信号.
    底层调用了 `filter` 与 过滤值进行比较,若相等返回则 `NO`
    * **使用**
    ``` // 内部调用filter过滤,忽略掉字符为 @“1”的值 [[_textField.rac_textSignal ignore:@”1”] subscribeNext:^(id x) {
    NSLog(@”%@”,x);
    }];
distinctUntilChanged
  • 作用

当上一次的值和当前的值有明显的变化就会发出信号,否则会被忽略掉。

  • 使用

    1
    2
    3
    4
    5
    [[_textField.rac_textSignal distinctUntilChanged] subscribeNext:^(id x) {
    NSLog(@"%@",x);
    }];
skip
  • 作用

跳过 第N次 的发送的信号.

  • 使用
1
2
3
4
5
// 表示输入第一次,不会被监听到,跳过第一次发出的信号
[[_textField.rac_textSignal skip:1] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
take
  • 作用

前N次 的发送的信号.

  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    RACSubject *subject = [RACSubject subject] ;
    // 取 前两次 发送的信号
    [[subject take:2] subscribeNext:^(id x) {
    NSLog(@"%@", x);
    }];
    [subject sendNext:@1];
    [subject sendNext:@2];
    [subject sendNext:@3];
    // 输出
    2017-01-03 17:35:54.566 ReactiveCocoa进阶[4969:1677908] 1
    2017-01-03 17:35:54.567 ReactiveCocoa进阶[4969:1677908] 2
takeLast
  • 作用

最后N次 的发送的信号

前提条件,订阅者必须调用完成 sendCompleted,因为只有完成,就知道总共有多少信号.

  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    RACSubject *subject = [RACSubject subject] ;
    // 取 后两次 发送的信号
    [[subject takeLast:2] subscribeNext:^(id x) {
    NSLog(@"%@", x);
    }];
    [subject sendNext:@1];
    [subject sendNext:@2];
    [subject sendNext:@3];
    // 必须 跳用完成
    [subject sendCompleted];
    ```
    ###### takeUntil
    * **作用**
    获取信号直到某个信号执行完成
    * **使用**
    ```
    // 监听文本框的改变直到当前对象被销毁
    [_textField.rac_textSignal takeUntil:self.rac_willDeallocSignal];
    ```
    ###### switchToLatest
    * **作用**
    用于signalOfSignals(信号的信号),有时候信号也会发出信号,会在signalOfSignals中,获取signalOfSignals发送的最新信号。
    * **注意**
    switchToLatest:只能用于信号中的信号
    * **使用**

    RACSubject signalOfSignals = [RACSubject subject];
    RACSubject
    signal = [RACSubject subject];

    // 获取信号中信号最近发出信号,订阅最近发出的信号。
    [signalOfSignals.switchToLatest subscribeNext:^(id x) {

    NSLog(@"%@", x);
    

    }];

    [signalOfSignals sendNext:signal];
    [signal sendNext:@1];

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    #### 秩序
    秩序包括 `doNext` 和 `doCompleted` 这两个方法,主要是在 执行`sendNext` 或者 `sendCompleted`之前,先执行这些方法中Block。
    ###### doNext
    执行`sendNext`之前,会先执行这个`doNext`的 Block
    ###### doCompleted
    执行`sendCompleted`之前,会先执行这`doCompleted`的`Block`
    [[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"hi"];
    [subscriber sendCompleted];
    return nil;
    }] doNext:^(id x) {
    // 执行 [subscriber sendNext:@"hi"] 之前会调用这个 Block
    NSLog(@"doNext");
    }] doCompleted:^{
    // 执行 [subscriber sendCompleted] 之前会调用这 Block
    NSLog(@"doCompleted");
    }] subscribeNext:^(id x) {
    NSLog(@"%@", x);
    }];
    #### 线程
    **ReactiveCocoa** 中的线程操作 包括 `deliverOn` 和 `subscribeOn`这两种,将 _传递的内容_ 或 创建信号时 _block中的代码_ 切换到指定的线程中执行。
    ###### deliverOn
    * **作用**
    内容传递切换到制定线程中,副作用在原来线程中,把在创建信号时block中的代码称之为副作用。
    * **使用**
  // 在子线程中执行
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

      [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

          NSLog(@"%@", [NSThread currentThread]);

          [subscriber sendNext:@123];

          [subscriber sendCompleted];

          return nil;
      }]
        deliverOn:[RACScheduler mainThreadScheduler]]

       subscribeNext:^(id x) {

           NSLog(@"%@", x);

           NSLog(@"%@", [NSThread currentThread]);
       }];
  });

  // 输出
2017-01-04 10:35:55.415 ReactiveCocoa进阶[1183:224535] <NSThread: 0x608000270f00>{number = 3, name = (null)}
2017-01-04 10:35:55.415 ReactiveCocoa进阶[1183:224482] 123
2017-01-04 10:35:55.415 ReactiveCocoa进阶[1183:224482] <NSThread: 0x600000079bc0>{number = 1, name = main}
1
2
3
4
5
6
7
8
9
10
可以看到`副作用`_子线程_ 中执行,而 `传递的内容`_主线程_ 中接收
###### subscribeOn
* **作用**
**subscribeOn**则是将 `内容传递``副作用` 都会切换到指定线程中
* **使用**
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

      [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

          NSLog(@"%@", [NSThread currentThread]);

          [subscriber sendNext:@123];

          [subscriber sendCompleted];

          return nil;
      }]
        subscribeOn:[RACScheduler mainThreadScheduler]] //传递的内容到主线程中
       subscribeNext:^(id x) {

           NSLog(@"%@", x);

           NSLog(@"%@", [NSThread currentThread]);
       }];
  });   
  //
2017-01-04 10:44:47.558 ReactiveCocoa进阶[1243:275126] <NSThread: 0x608000077640>{number = 1, name = main}
2017-01-04 10:44:47.558 ReactiveCocoa进阶[1243:275126] 123
2017-01-04 10:44:47.558 ReactiveCocoa进阶[1243:275126] <NSThread: 0x608000077640>{number = 1, name = main}

```

内容传递副作用 都切换到了 主线程 执行

时间

时间操作就会设置信号超时,定时和延时。

interval 定时
  • 作用

定时:每隔一段时间发出信号

// 每隔1秒发送信号,指定当前线程执行
[[RACSignal interval:1 onScheduler:[RACScheduler currentScheduler]] subscribeNext:^(id x) {

    NSLog(@"定时:%@", x);
}];

// 输出
2017-01-04 13:48:55.196 ReactiveCocoa进阶[1980:492724] 定时:2017-01-04 05:48:55 +0000
2017-01-04 13:48:56.195 ReactiveCocoa进阶[1980:492724] 定时:2017-01-04 05:48:56 +0000
2017-01-04 13:48:57.196 ReactiveCocoa进阶[1980:492724] 定时:2017-01-04 05:48:57 +0000
  • 作用

超时,可以让一个信号在一定的时间后,自动报错。

RACSignal *signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

    // 不发送信号,模拟超时状态
    // [subscriber sendNext:@"hello"];
    //[subscriber sendCompleted];

    return nil;
}] timeout:1 onScheduler:[RACScheduler currentScheduler]];// 设置1秒超时

[signal subscribeNext:^(id x) {

    NSLog(@"%@", x);
} error:^(NSError *error) {

    NSLog(@"%@", error);
}];

// 执行代码 1秒后 输出:
2017-01-04 13:48:55.195 ReactiveCocoa进阶[1980:492724] Error Domain=RACSignalErrorDomain Code=1 "(null)"
delay 延时
  • 作用

延时,延迟一段时间后发送信号

RACSignal *signal2 = [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

    [subscriber sendNext:@"延迟输出"];

    return nil;
}] delay:2] subscribeNext:^(id x) {

    NSLog(@"%@", x);
}];

// 执行代码 2秒后 输出
2017-01-04 13:55:23.751 ReactiveCocoa进阶[2030:525038] 延迟输出

重复

retry
  • 作用

重试:只要 发送错误 sendError:,就会 重新执行 创建信号的Block 直到成功

  __block int i = 0;

  [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

      if (i == 5) {

          [subscriber sendNext:@"Hello"];

      } else {

          // 发送错误
          NSLog(@"收到错误:%d", i);
          [subscriber sendError:nil];
      }

      i++;

      return nil;

  }] retry] subscribeNext:^(id x) {

      NSLog(@"%@", x);

  } error:^(NSError *error) {

      NSLog(@"%@", error);

  }];

  // 输出
2017-01-04 14:36:51.594 ReactiveCocoa进阶[2443:667226] 收到错误信息:0
2017-01-04 14:36:51.595 ReactiveCocoa进阶[2443:667226] 收到错误信息:1
2017-01-04 14:36:51.595 ReactiveCocoa进阶[2443:667226] 收到错误信息:2
2017-01-04 14:36:51.596 ReactiveCocoa进阶[2443:667226] 收到错误信息:3
2017-01-04 14:36:51.596 ReactiveCocoa进阶[2443:667226] 收到错误信息:4
2017-01-04 14:36:51.596 ReactiveCocoa进阶[2443:667226] Hello
replay
  • 作用

重放:当一个信号被多次订阅,反复播放内容

  RACSignal *signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

      [subscriber sendNext:@1];
      [subscriber sendNext:@2];

      return nil;
  }] replay];

  [signal subscribeNext:^(id x) {
      NSLog(@"%@", x);
  }];

  [signal subscribeNext:^(id x) {
      NSLog(@"%@", x);
  }];

  // 输出
2017-01-04 14:51:01.934 ReactiveCocoa进阶[2544:706740] 1
2017-01-04 14:51:01.934 ReactiveCocoa进阶[2544:706740] 2
2017-01-04 14:51:01.934 ReactiveCocoa进阶[2544:706740] 1
2017-01-04 14:51:01.935 ReactiveCocoa进阶[2544:706740] 2
throttle
  • 作用

节流:当某个信号发送比较频繁时,可以使用节流,在某一段时间不发送信号内容,过了一段时间获取信号的最新内容发出。

RACSubject *subject = [RACSubject subject];

// 节流1秒,1秒后接收最后一个发送的信号
[[subject throttle:1] subscribeNext:^(id x) {

    NSLog(@"%@", x);
}];

[subject sendNext:@1];
[subject sendNext:@2];
[subject sendNext:@3];

// 输出
2017-01-04 15:02:37.543 ReactiveCocoa进阶[2731:758193] 3

程序为什么要有架构?便于程序开发与维护.

常见的架构

  • MVC

M:模型 V:视图 C:控制器

  • MVVM

M:模型 V:视图+控制器 VM:视图模型

  • MVCS

M:模型 V:视图 C:控制器 C:服务类

V:视图 I:交互器 P:展示器 E:实体 R:路由

MVVM介绍

  • 模型(M):保存视图数据。

  • 视图+控制器(V):展示内容 + 如何展示

  • 视图模型(VM):处理展示的业务逻辑,包括按钮的点击,数据的请求和解析等等。

需求

  1. 监听两个文本框的内容
  2. 有内容登录按键才允许按钮点击
  3. 返回登录结果

分析

  1. 界面的所有业务逻辑都交给控制器做处理
  2. 在MVVM架构中把控制器的业务全部搬去VM模型,也就是每个控制器对应一个VM模型.

步骤

  1. 创建LoginViewModel类,处理登录界面业务逻辑.
  2. 这个类里面应该保存着账号的信息,创建一个账号Account模型
  3. LoginViewModel应该保存着账号信息Account模型。
  4. 需要时刻监听Account模型中的账号和密码的改变,怎么监听?
  5. 在非RAC开发中,都是习惯赋值,在RAC开发中,需要改变开发思维,由赋值转变为绑定,可以在一开始初始化的时候,就给Account模型中的属性绑定,并不需要重写set方法。
  6. 每次Account模型的值改变,就需要判断按钮能否点击,在VM模型中做处理,给外界提供一个能否点击按钮的信号.
  7. 这个登录信号需要判断Account中账号和密码是否有值,用KVO监听这两个值的改变,把他们聚合成登录信号.
  8. 监听按钮的点击,由VM处理,应该给VM声明一个RACCommand,专门处理登录业务逻辑.
  9. 执行命令,把数据包装成信号传递出去
  10. 监听命令中信号的数据传递
  11. 监听命令的执行时刻

运行效果

登录界面

代码

MyViewController.m

#import "MyViewController.h"
#import "LoginViewModel.h"

@interface MyViewController ()

@property (nonatomic, strong) LoginViewModel *loginViewModel;

@property (weak, nonatomic) IBOutlet UITextField *accountField;

@property (weak, nonatomic) IBOutlet UITextField *pwdField;

@property (weak, nonatomic) IBOutlet UIButton *loginBtn;

@end

@implementation MyViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self bindModel];

}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}



// 视图模型绑定
- (void)bindModel {

    // 给模型的属性绑定信号
    //
    RAC(self.loginViewModel.account, account) = _accountField.rac_textSignal;
    RAC(self.loginViewModel.account, pwd) = _pwdField.rac_textSignal;

    RAC(self.loginBtn, enabled) = self.loginViewModel.enableLoginSignal;

    // 监听登录点击
    [[_loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {

        [self.loginViewModel.LoginCommand execute:nil];
    }];

}
- (IBAction)btnTap:(id)sender {


}

#pragma mark - lazyLoad

- (LoginViewModel *)loginViewModel {

    if (nil == _loginViewModel) {
        _loginViewModel = [[LoginViewModel alloc] init];
    }

    return _loginViewModel;
}

LoginViewModel.h

#import <UIKit/UIKit.h>

@interface Account : NSObject

@property (nonatomic, strong) NSString *account;
@property (nonatomic, strong) NSString *pwd;

@end


@interface LoginViewModel : UIViewController

@property (nonatomic, strong) Account *account;

// 是否允许登录的信号
@property (nonatomic, strong, readonly) RACSignal *enableLoginSignal;

@property (nonatomic, strong, readonly) RACCommand *LoginCommand;

@end

LoginViewModel.m

#import "LoginViewModel.h"

@implementation Account

@end


@interface LoginViewModel ()

@end

@implementation LoginViewModel

- (instancetype)init {

    if (self = [super init]) {
        [self initialBind];
    }
    return self;
}

- (void)initialBind {

    // 监听账号属性改变, 把他们合成一个信号
    _enableLoginSignal = [RACSubject combineLatest:@[RACObserve(self.account, account), RACObserve(self.account, pwd)] reduce:^id(NSString *accout, NSString *pwd){

        return @(accout.length && pwd.length);
    }];

    // 处理业务逻辑
    _LoginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {

        NSLog(@"点击了登录");
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

            // 模仿网络延迟

            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

                // 返回登录成功 发送成功信号
                [subscriber sendNext:@"登录成功"];
            });

            return nil;
        }];
    }];


    // 监听登录产生的数据
    [_LoginCommand.executionSignals.switchToLatest subscribeNext:^(id x) {

        if ([x isEqualToString:@"登录成功"]) {
            NSLog(@"登录成功");
        }

    }];

    [[_LoginCommand.executing skip:1] subscribeNext:^(id x) {

        if ([x isEqualToNumber:@(YES)]) {

            NSLog(@"正在登陆...");
        } else {

        // 登录成功
        NSLog(@"登陆成功");

        }

    }];
}

#pragma mark - lazyLoad

- (Account *)account
{
    if (_account == nil) {
        _account = [[Account alloc] init];
    }
    return _account;
}

- (void)viewDidLoad {
    [super viewDidLoad];

}

@end

需求

  1. 请求一段网络数据,将请求到的数据在tableView上展示
  2. 该数据为豆瓣图书的搜索返回结果,URL:url:https://api.douban.com/v2/book/search?q=悟空传

分析

  1. 界面的所有业务逻辑都交给控制器做处理
  2. 网络请求交给MV模型处理

步骤

  1. 控制器提供一个视图模型(requesViewModel),处理界面的业务逻辑
  2. VM提供一个命令,处理请求业务逻辑
  3. 在创建命令的block中,会把请求包装成一个信号,等请求成功的时候,就会把数据传递出去。
  4. 请求数据成功,应该把字典转换成模型,保存到视图模型中,控制器想用就直接从视图模型中获取。

其他

网络请求与图片缓存用到了[AFNetworking][7] 和 SDWebImage,自行在Pods中导入。

[7]: https://github.com/AFNetworking/AFNetworking

platform :ios, ‘8.0’

target 'ReactiveCocoa进阶' do

use_frameworks!
pod 'ReactiveCocoa', '~> 2.5'
pod 'AFNetworking'
pod 'SDWebImage'
end

运行效果

代码

SearchViewController.m

#import "SearchViewController.h"
#import "RequestViewModel.h"

@interface SearchViewController ()<UITableViewDataSource>

@property (nonatomic, strong) UITableView *tableView;

@property (nonatomic, strong) RequestViewModel *requesViewModel;

@end

@implementation SearchViewController

- (RequestViewModel *)requesViewModel
{
    if (_requesViewModel == nil) {
        _requesViewModel = [[RequestViewModel alloc] init];
    }
    return _requesViewModel;
}

- (void)viewDidLoad {
    [super viewDidLoad];


    self.tableView = [[UITableView alloc] initWithFrame:self.view.frame];

    self.tableView.dataSource = self;

    [self.view addSubview:self.tableView];

    //
    RACSignal *requesSiganl = [self.requesViewModel.reuqesCommand execute:nil];

    [requesSiganl subscribeNext:^(NSArray *x) {

        self.requesViewModel.models = x;

        [self.tableView reloadData];
    }];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.requesViewModel.models.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *ID = @"cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
    if (cell == nil) {

        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
    }

    Book *book = self.requesViewModel.models[indexPath.row];
    cell.detailTextLabel.text = book.subtitle;
    cell.textLabel.text = book.title;

    [cell.imageView sd_setImageWithURL:[NSURL URLWithString:book.image] placeholderImage:[UIImage imageNamed:@"cellImage"]];


    return cell;
}
@end

RequestViewModel.h

#import <Foundation/Foundation.h>

@interface Book : NSObject

@property (nonatomic, copy) NSString *subtitle;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *image;

@end

@interface RequestViewModel : NSObject

// 请求命令
@property (nonatomic, strong, readonly) RACCommand *reuqesCommand;

//模型数组
@property (nonatomic, strong) NSArray *models;


@end

RequestViewModel.m

#import "RequestViewModel.h"

@implementation Book

- (instancetype)initWithValue:(NSDictionary *)value {

    if (self = [super init]) {

        self.title = value[@"title"];
        self.subtitle = value[@"subtitle"];
        self.image = value[@"image"];
    }
    return self;
}

+ (Book *)bookWithDict:(NSDictionary *)value {

    return [[self alloc] initWithValue:value];
}



@end

@implementation RequestViewModel

- (instancetype)init
{
    if (self = [super init]) {

        [self initialBind];
    }
    return self;
}


- (void)initialBind
{
    _reuqesCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {

      RACSignal *requestSiganl = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

          NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
          parameters[@"q"] = @"悟空传";

          //
          [[AFHTTPSessionManager manager] GET:@"https://api.douban.com/v2/book/search" parameters:parameters progress:^(NSProgress * _Nonnull downloadProgress) {

              NSLog(@"downloadProgress: %@", downloadProgress);
          } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

              // 数据请求成功就讲数据发送出去
              NSLog(@"responseObject:%@", responseObject);

              [subscriber sendNext:responseObject];

              [subscriber sendCompleted];

          } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

              NSLog(@"error: %@", error);
          }];


         return nil;
      }];

        // 在返回数据信号时,把数据中的字典映射成模型信号,传递出去
        return [requestSiganl map:^id(NSDictionary *value) {

            NSMutableArray *dictArr = value[@"books"];

            NSArray *modelArr = [[dictArr.rac_sequence map:^id(id value) {

                return [Book bookWithDict:value];

            }] array];

            return modelArr;

        }];

    }];
}


@end

最后附上GitHub:https://github.com/qiubaiying/ReactiveCocoa_Demo



ReactiveCocoa之基础篇

From: http://qiubaiying.top/2016/12/26/ReactiveCocoa-%E5%9F%BA%E7%A1%80/

本文修改自[最快让你上手ReactiveCocoa之基础篇][1]

[1]: http://www.jianshu.com/p/87ef6720a096

有关对 ReactiveCocoa 的看法可以看一下唐巧的这篇ReactiveCocoa 讨论会

ReactiveCocoa思维导图

ReactiveCocoa

ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS和OS开发的新框架,Cocoa是苹果整套框架的简称,因此很多苹果框架喜欢以Cocoa结尾。

在我们iOS开发过程中,当某些事件响应的时候,需要处理某些业务逻辑,这些事件都用不同的方式来处理。

比如按钮的点击使用action,ScrollView滚动使用delegate,属性值改变使用KVO等系统提供的方式。其实这些事件,都可以通过RAC处理

ReactiveCocoa为事件提供了很多处理方法,而且利用RAC处理事件很方便,可以把要处理的事情,和监听的事情的代码放在一起,这样非常方便我们管理,就不需要跳到对应的方法里。

非常符合我们开发中高聚合,低耦合的思想。

在开发中我们也不能太依赖于某个框架,否则这个框架不更新了,导致项目后期没办法维护,比如之前Facebook提供的 Three20 框架,在当时也是神器,但是后来不更新了,也就没什么人用了。因此我感觉学习一个框架,还是有必要了解它的编程思想。

先简单介绍下目前咱们已知的编程思想:

响应式编程思想

响应式编程思想:不需要考虑调用顺序,只需要知道考虑结果,类似于蝴蝶效应,产生一个事件,会影响很多东西,这些事件像流一样的传播出去,然后影响结果,借用面向对象的一句话,万物皆是流。

代表:KVO

链式编程思想

链式编程 是将多个操作(多行代码)通过点号(.)链接在一起成为一句代码,使代码可读性好。如:

make.add(1).add(2).sub(5).muilt(-4).divide(4);

特点:方法的返回值是block,block必须有返回值(本身对象),block参数(需要操作的值)

代表:masonry框架。

实现:模仿masonry,写一个加法计算器,练习链式编程思想。

NSObject+Caculator.h

# import <Foundation/Foundation.h>

@class CaculatorMaker;

@interface NSObject (Caculator)

// 计算
+ (int)makeCaculators:(void (^)(CaculatorMaker *))block;

@end

NSObject+Caculator.m

@implementation NSObject (Caculator)

+ (int)makeCaculators:(void (^)(CaculatorMaker *))block {

    CaculatorMaker *mgr = [[CaculatorMaker alloc] init];

    block(mgr);

    return (mgr.result);
}

@end

CaculatorMaker.h

# import <Foundation/Foundation.h>

@class CaculatorMaker;

typedef CaculatorMaker *(^CasulatorBlock)(int);

@interface CaculatorMaker : NSObject

@property (nonatomic, assign) int result;

// 算数方法
- (CaculatorMaker *(^)(int))add;
- (CasulatorBlock)sub;
- (CasulatorBlock)muilt;
- (CasulatorBlock)divide;


@end

CaculatorMaker.m

# import "CaculatorMaker.h"

@implementation CaculatorMaker

- (CaculatorMaker *(^)(int))add {

    return ^CaculatorMaker *(int value) {

        _result += value;

        return self;
    };
}

- (CasulatorBlock)sub {

    return ^CaculatorMaker *(int value) {

        _result -= value;

        return self;
    };
}

- (CasulatorBlock)muilt {

    return ^CaculatorMaker *(int value) {

        _result *= value;

        return self;
    };
}

- (CasulatorBlock)divide {

    return ^CaculatorMaker *(int value) {

        _result /= value;

        return self;
    };
}

@end

使用:

int result = [NSObject makeCaculators:^(CaculatorMaker *make) {

        // ( 1 + 2 - 5 ) * (-4) / 4
        make.add(1).add(2).sub(5).muilt(-4).divide(4);

    }];

    NSLog(@"%d", result);

函数式编程思想

函数式编程思想:是把操作尽量写成一系列嵌套的函数或者方法调用。

特点:每个方法必须有返回值(本身对象),把函数或者Block当做参数,block参数(需要操作的值)block返回值(操作结果)

代表ReactiveCocoa

实现:用函数式编程实现,写一个加法计算器,并且加法计算器自带判断是否等于某个值.

Calculator *caculator = [[Calculator alloc] init];

BOOL isqule = [[[caculator caculator:^int(int result) {

    result += 2;
    result *= 5;
    return result;

}] equle:^BOOL(int result) {

    return result == 10;

}] isEqule];

NSLog(@"%d", isqule);

Calculator.h

#import <Foundation/Foundation.h>

@interface Calculator : NSObject

@property (nonatomic, assign) BOOL isEqule;

@property (nonatomic, assign) int result;

- (Calculator *)caculator:(int (^)(int result))caculator;

- (Calculator *)equle:(BOOL (^)(int result))operation;

@end

Calculator.m

#import "Calculator.h"

@implementation Calculator

- (Calculator *)caculator:(int (^)(int))caculator {

    _result = caculator(_result);

    return self;

}


- (Calculator *)equle:(BOOL (^)(int))operation {

    _isEqule = operation(_result);

    return self;
}

@end

ReactiveCocoa 结合了这两种种编程风格:

  • 函数式编程(Functional Programming)

  • 响应式编程(Reactive Programming)

所以,你可能听说过 ReactiveCocoa 被描述为函数响应式编程(FRP)框架。

以后使用RAC解决问题,就不需要考虑调用顺序,直接考虑结果,把每一次操作都写成一系列嵌套的方法中,使代码高聚合,方便管理。


ReactiveCocoa的GitHub地址

Objective-C

ReactiveCocoa 2.5版本以后改用了Swift,所以Objective-C项目需要导入2.5版本

CocoaPods集成:

platform :ios, '8.0'

target 'YouProjectName' do

use_frameworks!
pod 'ReactiveCocoa', '~> 2.5'

end

PS:新版本的CocoaPods需要加入

target 'YouProjectName' do 
... 
end

这句话来限定项目,否则导入失败。

Swift

Swift项目导入2.5后的版本

platform :ios, '8.0'

target 'YouProjectName' do

use_frameworks!
pod 'ReactiveCocoa'

end

使用时在全局头文件导入头文件即可

PrefixHeader.pch

#ifndef PrefixHeader_pch
#define PrefixHeader_pch

#import <ReactiveCocoa/ReactiveCocoa.h>

#endif

RACSiganl 信号类

信号类,一般表示将来有数据传递,只要有数据改变,信号内部接收到数据,就会马上发出数据。

注意:

  • 信号类(RACSiganl),只是表示当数据改变时,信号内部会发出数据,它本身不具备发送信号的能力,而是交给内部一个订阅者去发出。
  • 默认一个信号都是冷信号,也就是值改变了,也不会触发,只有订阅了这个信号,这个信号才会变为热信号,值改变了才会触发。
  • 如何订阅信号:调用信号RACSignal的subscribeNext就能订阅
  • 订阅多次, 发送的闭包会多次执行,可以用RACMulticastConnection 避免

使用:

// RACSignal使用步骤:
    // 1.创建信号 + (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
    // 2.订阅信号,才会激活信号. - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
    // 3.发送信号 - (void)sendNext:(id)value


    // RACSignal底层实现:
    // 1.创建信号,首先把didSubscribe保存到信号中,还不会触发。
    // 2.当信号被订阅,也就是调用signal的subscribeNext:nextBlock
    // 2.2 subscribeNext内部会创建订阅者subscriber,并且把nextBlock保存到subscriber中。
    // 2.1 subscribeNext内部会调用siganl的didSubscribe
    // 3.siganl的didSubscribe中调用[subscriber sendNext:@1];
    // 3.1 sendNext底层其实就是执行subscriber的nextBlock

    // 1.创建信号
    RACSignal *siganl = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

        // block调用时刻:每当有订阅者订阅信号,就会调用block。

        // 2.发送信号
        [subscriber sendNext:@1];

        // 如果不在发送数据,最好发送信号完成,内部会自动调用[RACDisposable disposable]取消订阅信号。
        [subscriber sendCompleted];

        return [RACDisposable disposableWithBlock:^{

            // block调用时刻:当信号发送完成或者发送错误,就会自动执行这个block,取消订阅信号。

            // 执行完Block后,当前信号就不在被订阅了。

            NSLog(@"信号被销毁");

        }];
    }];

    // 3.订阅信号,才会激活信号.
    [siganl subscribeNext:^(id x) {
        // block调用时刻:每当有信号发出数据,就会调用block.
        NSLog(@"接收到数据:%@",x);
    }];

RACSubscriber

表示订阅者的意思,用于发送信号,这是一个协议,不是一个类,只要遵守这个协议,并且实现方法才能成为订阅者。通过create创建的信号,都有一个订阅者,帮助他发送数据。

RACDisposable

用于取消订阅或者清理资源,当信号发送完成或者发送错误的时候,就会自动触发它。

使用场景:不想监听某个信号时,可以通过它主动取消订阅信号。

RACSubject

RACSubject:信号提供者,自己可以充当信号,又能发送信号。

使用场景:通常用来代替代理,有了它,就不必要定义代理了。

RACReplaySubject

重复提供信号类,RACSubject的子类。

RACReplaySubjectRACSubject区别:

RACReplaySubject可以先发送信号,在订阅信号,RACSubject就不可以。

使用场景一:如果一个信号每被订阅一次,就需要把之前的值重复发送一遍,使用重复提供信号类。

使用场景二:可以设置capacity数量来限制缓存的value的数量,即只缓充最新的几个值。

ACSubjectRACReplaySubject 简单使用:

ACSubject

 // RACSubject使用步骤
// 1.创建信号 [RACSubject subject],跟RACSiganl不一样,创建信号时没有block。
// 2.订阅信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
// 3.发送信号 sendNext:(id)value

// RACSubject:底层实现和RACSignal不一样。
// 1.调用subscribeNext订阅信号,只是把订阅者保存起来,并且订阅者的nextBlock已经赋值了。
// 2.调用sendNext发送信号,遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock。


// 1. 创建信号
RACSubject *subject = [RACSubject subject];

// 2.订阅信号
[subject subscribeNext:^(id x) {

    // block调用时机:当信号发出新值,就会调用
    NSLog(@"收到信号");

}];

// 3.发送信号
NSLog(@"发送信号");
[subject sendNext:@"1"];


 // RACReplaySubject使用步骤:
// 1.创建信号 [RACSubject subject],跟RACSiganl不一样,创建信号时没有block。
// 2.可以先订阅信号,也可以先发送信号。
// 2.1 订阅信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
// 2.2 发送信号 sendNext:(id)value

// RACReplaySubject:底层实现和RACSubject不一样。
// 1.调用sendNext发送信号,把值保存起来,然后遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock。
// 2.调用subscribeNext订阅信号,遍历保存的所有值,一个一个调用订阅者的nextBlock

// 如果想当一个信号被订阅,就重复播放之前所有值,需要先发送信号,在订阅信号。
// 也就是先保存值,在订阅值。


// 1.创建信号
RACReplaySubject *replaySubject = [RACReplaySubject subject];



// 3.先订阅信号
[replaySubject subscribeNext:^(id x) {

    NSLog(@"第一个订阅者接受到的数据%@", x);
}];

// 2.发送信号
[replaySubject sendNext:@1];
[replaySubject sendNext:@2];

// 后订阅信号
[replaySubject subscribeNext:^(id x) {

    NSLog(@"第二个订阅者接收到的数据%@",x);
}];

RACSubject替换代理(与block类似)

// 需求:
    // 1.给当前控制器添加一个按钮,modal到另一个控制器界面
    // 2.另一个控制器view中有个按钮,点击按钮,通知当前控制器

步骤一:在第二个控制器.h,添加一个RACSubject代替代理。
@interface TwoViewController : UIViewController

@property (nonatomic, strong) RACSubject *delegateSignal;

@end

步骤二:监听第二个控制器按钮点击
@implementation TwoViewController
- (IBAction)notice:(id)sender {
    // 通知第一个控制器,告诉它,按钮被点了

     // 通知代理
     // 判断代理信号是否有值
    if (self.delegateSignal) {
        // 有值,才需要通知
        [self.delegateSignal sendNext:nil];
    }
}
@end

步骤三:在第一个控制器中,监听跳转按钮,给第二个控制器的代理信号赋值,并且监听.
@implementation OneViewController 
- (IBAction)btnClick:(id)sender {

    // 创建第二个控制器
    TwoViewController *twoVc = [[TwoViewController alloc] init];

    // 设置代理信号
    twoVc.delegateSignal = [RACSubject subject];

    // 订阅代理信号
    [twoVc.delegateSignal subscribeNext:^(id x) {

        NSLog(@"点击了通知按钮 %@", x);
    }];

    // 跳转到第二个控制器
    [self presentViewController:twoVc animated:YES completion:@"hi"];

}
@end

RACTuple

元组类,类似NSArray,用来包装值.(@[key, value])

RACSequence

RAC中的集合类,用于代替NSArray,NSDictionary,可以使用它来快速遍历数组和字典。

使用场景:字典转模型

// 1.遍历数组
NSArray *numbers = @[@1,@2,@3,@4];

// 这里其实是三步
// 第一步: 把数组转换成集合RACSequence numbers.rac_sequence
// 第二步: 把集合RACSequence转换RACSignal信号类,numbers.rac_sequence.signal
// 第三步: 订阅信号,激活信号,会自动把集合中的所有值,遍历出来。

[numbers.rac_sequence.signal subscribeNext:^(id x) {

    NSLog(@"%@", x);
}];



// 2.遍历字典,遍历出来的键值对 都会包装成 RACTuple(元组对象) @[key, value]
NSDictionary *dic = @{@"name": @"BYqiu", @"age": @18};

[dic.rac_sequence.signal subscribeNext:^(RACTuple *x) {

    // 解元组包,会把元组的值,按顺序给参数里的变量赋值
    // 写法相当与
    // NSString *key = x[0];
    // NSString *value = x[1];
    RACTupleUnpack(NSString *key, NSString *value) = x;

    NSLog(@"key:%@, value:%@", key, value);

}];

// 3.字典转模型

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"flags.plist" ofType:nil];

NSArray *dicArray = [NSArray arrayWithContentsOfFile:filePath];

NSMutableArray *items = [NSMutableArray array];

// OC写法
for (NSDictionary *dic in dicArray) {

    //FlagItem *item = [FlagItem flagWithDict:dict];
    //[items addObject:item];
}


// RAC写法
[dicArray.rac_sequence.signal subscribeNext:^(id x) {
    // 利用RAC遍历, x:字典

    //FlagItem *item = [FlagItem flagWithDict:x];
    //[items addObject:item];
}];

// RAC高级用法(函数式编程)
NSArray *flags = [[dicArray.rac_sequence map:^id(id value) {

    return  [FlagItem flagWithDict:value];

}] array];

RACCommand

RAC中用于处理事件的类,可以把事件如何处理,事件中的数据如何传递,包装到这个类中,他可以很方便的监控事件的执行过程。

一、RACCommand使用步骤:

  1. 创建命令 initWithSignalBlock:(RACSignal * (^)(id input))signalBlock
  2. 在signalBlock中,创建RACSignal,并且作为signalBlock的返回值
  3. 执行命令 - (RACSignal *)execute:(id)input

二、RACCommand使用注意:

  1. signalBlock必须要返回一个信号,不能传nil.
  2. 如果不想要传递信号,直接创建空的信号[RACSignal empty];
  3. RACCommand中信号如果数据传递完,必须调用[subscriber sendCompleted],这时命令才会执行完毕,否则永远处于执行中。
  4. RACCommand需要被强引用,否则接收不到RACCommand中的信号,因此RACCommand中的信号是延迟发送的。

三、RACCommand设计思想:

内部signalBlock为什么要返回一个信号,这个信号有什么用。

  1. 在RAC开发中,通常会把网络请求封装到RACCommand,直接执行某个RACCommand就能发送请求。
  2. 当RACCommand内部请求到数据的时候,需要把请求的数据传递给外界,这时候就需要通过signalBlock返回的信号传递了。

四、如何拿到RACCommand中返回信号发出的数据。

  1. RACCommand有个执行信号源executionSignals,这个是signal of signals(信号的信号),意思是信号发出的数据是信号,不是普通的类型。
  2. 订阅executionSignals就能拿到RACCommand中返回的信号,然后订阅signalBlock返回的信号,就能获取发出的值。

五、监听当前命令是否正在执行executing

六、使用场景,监听按钮点击,网络请求

使用:

// 1.创建命令
    RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
        NSLog(@"执行命令");

        // 返回空信号
        //return [RACSignal empty];

        // 2.创建信号 传递数据
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

            [subscriber sendNext:@"请求数据"];

            // 注意:数据传递完,最好调用sendCompleted,这时命令才执行完毕
            [subscriber sendCompleted];

            return nil;
        }];
    }];

    // 强引用命令,不要被销毁,否则接收不到数据
    _command = command;

    // 3.订阅RACCommand的信号
    [command.executionSignals subscribeNext:^(id x) {
        [x subscribeNext:^(id x) {

            NSLog(@"订阅RACCommand的信号: %@", x);
        }];
    }];

    // RAC高级用法
    // switchToLatest:用于signal of signals,获取signal of signals发出的最新信号,也就是可以直接拿到RACCommand中的信号
    [command.executionSignals.switchToLatest subscribeNext:^(id x) {

        NSLog(@"RAC高级用法: %@", x);
    }];

    // 4.监听命令是否执行完毕,默认会来一次,可以直接跳过,skip表示跳过第一次信号。
    [[command.executing skip:1] subscribeNext:^(id x) {

        if ([x boolValue] == YES) {

            // 正在执行
            NSLog(@"正在执行");

        } else {

            // 执行完毕
            NSLog(@"执行完成");
        }
    }];

    // 5.执行命名
    [self.command execute:@1];

RACMulticastConnection

用于当一个信号,被多次订阅时,为了保证创建信号时,避免多次调用创建信号中的block,造成副作用,可以使用这个类处理。

注意:RACMulticastConnection通过RACSignal的-publish或者-muticast:方法创建.

RACMulticastConnection使用步骤:

  1. 创建信号 + (RACSignal )createSignal:(RACDisposable (^)(id subscriber))didSubscribe
  2. 创建连接 RACMulticastConnection *connect = [signal publish];
  3. 订阅信号,注意:订阅的不在是之前的信号,而是连接的信号。 [connect.signal subscribeNext:nextBlock]
  4. 连接 [connect connect]

RACMulticastConnection底层原理:

  1. 创建connect,connect.sourceSignal -> RACSignal(原始信号) connect.signal -> RACSubject
  2. 订阅connect.signal,会调用RACSubject的subscribeNext,创建订阅者,而且把订阅者保存起来,不会执行block。
  3. [connect connect]内部会订阅RACSignal(原始信号),并且订阅者是RACSubject
    1. 订阅原始信号,就会调用原始信号中的didSubscribe
    2. didSubscribe,拿到订阅者调用sendNext,其实是调用RACSubject的sendNext
  4. RACSubject的sendNext,会遍历RACSubject所有订阅者发送信号。
    • 因为刚刚第二步,都是在订阅RACSubject,因此会拿到第二步所有的订阅者,调用他们的nextBlock

需求:假设在一个信号中发送请求,每次订阅一次都会发送请求,这样就会导致多次请求。

解决:使用RACMulticastConnection就能解决.

问题:每次订阅一次都会发送请求

// 创建请求信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

    NSLog(@"发送请求");
    [subscriber sendNext:@1];

    return nil;
}];

// 订阅信号
[signal subscribeNext:^(id x) {

    NSLog(@"接受数据: %@", x);
}];

// 再次订阅信号,会再次执行发送请求,也就是每次订阅都会发送一次请求
[signal subscribeNext:^(id x) {

    NSLog(@"接受数据: %@", x);
}];

输出:

2016-12-28 11:37:04.397 ReactiveCacoa[1505:340573] 发送请求
2016-12-28 11:37:04.398 ReactiveCacoa[1505:340573] 接受数据: 1
2016-12-28 11:37:04.398 ReactiveCacoa[1505:340573] 发送请求
2016-12-28 11:37:04.398 ReactiveCacoa[1505:340573] 接受数据: 1

可以发现每次订阅都会重新发送请求.

下面我们使用RACMulticastConnection:

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

    NSLog(@"发送请求");
    [subscriber sendNext:@1];

    return nil;
}];

// 创建连接
RACMulticastConnection *connect = [signal publish];

// 订阅信号
// 注意:订阅信号,也不能激活信号,只是保存订阅者到数组,必须通过连接,当调用连接,就会一次性调用所有订阅者的SendNext
[connect.signal subscribeNext:^(id x) {

    NSLog(@"订阅者1信号: %@", x);
}];

[connect.signal subscribeNext:^(id x) {

    NSLog(@"订阅者2信号: %@", x);
}];

// 连接、激活信号
[connect connect];

输出:

2016-12-28 11:37:04.399 ReactiveCacoa[1505:340573] 发送请求
2016-12-28 11:37:04.399 ReactiveCacoa[1505:340573] 订阅者1信号: 1
2016-12-28 11:37:04.399 ReactiveCacoa[1505:340573] 订阅者2信号: 1

RACScheduler

RAC中的队列,用GCD封装的。

RACUnit

表⽰stream不包含有意义的值,也就是看到这个,可以直接理解为nil.

RACEven

把数据包装成信号事件(signal event)。它主要通过RACSignal的-materialize来使用,然并卵。

  1. 替换代理
  2. 替换KVO
  3. 监听事件
  4. 替换通知
  5. 监听文本框文字改变
  6. 统一处理多个网络请求

替换代理:

rac_signalForSelector:

rac_signalForSelector: 直接监听 Selector 事件的调用

应用场景:监听 RedViewController 中按钮的点击事件 btnTap:

跳转到RedViewController前,先使用rac_signalForSelector订阅rvc中的 btnTap: 点击事件

// 使用segue跳转
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
- 
    if ([segue.identifier isEqualToString:@"goRedVC"]) {

        RedViewController *rvc = segue.destinationViewController;

        // 订阅rvc中的 btnTap: 点击事件
        [[rvc rac_signalForSelector:@selector(btnTap:)] subscribeNext:^(id x) {

            NSLog(@"RedVC btnTap!");
        }];
    }
}

RedViewController.m 中的按钮事件

- (IBAction)btnTap:(id)sender {

    NSLog(@"!");
}

替换KVO

rac_valuesForKeyPath:

// KVO
// 监听 slider 的 value 变化
[[self.slider rac_valuesForKeyPath:@"value" observer:nil] subscribeNext:^(id x) {

    NSLog(@"slider value Change:%@", x);
}];

替换通知

rac_addObserverForName

// 原生的订阅通知
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(userDidChange:)
                                             name:kTTCurrentUserLoggedOffNotification
                                           object:nil];

// 使用RAC订阅通知 ,takeUntil限定信号的声明周期                                  
[[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil]
  takeUntil:[self rac_willDeallocSignal]]
  subscribeNext:^(id x) {
     NSLog(@"Notification received");
}];

监听事件

rac_signalForControlEvents:

// 监听 btn 的 UIControlEventTouchUpInside 点击事件
[[self.btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {

    NSLog(@"btnTap");
}];

监听 textField 文字变化

rac_textSignal

[[self.textField rac_textSignal] subscribeNext:^(id x) {

        NSLog(@"textField change: %@", x);
}];

统一处理多个网络请求

rac_liftSelector:

- (void)viewDidLoad {
    [super viewDidLoad];

        // 处理多个请求都返回结果的时候,统一处理
    // 如同时进行多个网络请求,每个请求都正确返回时,再去刷新页面

    RACSignal *signalOne = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

        // 网络请求1
        // ...

        // 返回成功
        [subscriber sendNext:@"网络请求1 data"];

        return nil;
    }];

    RACSignal *signalTwo = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

        // 网络请求2
        // ...

        // 返回成功
        [subscriber sendNext:@"网络请求2 data"];

        return nil;
    }];

    [self rac_liftSelector:@selector(updateWithR1:R2:) withSignalsFromArray:@[signalOne, signalTwo]];

}

// 更新界面
- (void)updateWithR1:(id)r1 R2:(id)r2 {

    NSLog(@"R1:%@, R2:%@ 完成!", r1, r2);

}

注意

  • 替换KVO监听文本框文字改变 方法在创建监听方法时就会执行一次。

    2016-12-28 16:53:50.746 ReactiveCacoa[4956:1246592] slider value Change:0.5
    2016-12-28 16:53:50.748 ReactiveCacoa[4956:1246592] textField change:

  • 使用rac_liftSelector@selector(updateWithR1:R2:) 中的方 参数个数 要与 signal个数 相同,否则会被断言Crash!