谈谈代理优化

协议(protocol)

协议是OC的一项特性,与Java的“接口”类似。我们都知道OC不支持多继承,所以我们可以把某个类要实现的一系列方法定义在协议里面。而协议最常见的用途是实现委托模式。

委托模式

委托模式是一种编程设计模式,iOS中用它来实现对象间的通信。委托模式定义一套接口,某个对象若想接受另一个对象的委托,则需遵从此接口,以便成为其“委托对象”。另一个则可以给其委托对象回传一些信息,也可以发生相关事件时通知委托对象。

委托模式可以将数据与业务逻辑解耦。在整个Cocoa系统框架中,一般都是用协议这项特性来实现此模式。简单举个例子。

我们都知道iOS8之后推出了WKWebView来替代UIWebView。WKWebView相比UIWebView有更优越的性能,可是对于要兼容iOS7的项目来说,却无能为力。但是我们却可以自定义一个webview,iOS8以上用WKWebView,iOS8以下用UIWebView。利用运行时来判断。

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
if(NSClassFromString(@"WKWebView")) {
self.wkWebView = [[WKWebView alloc] init];
} else {
self.uiWebView = [[UIWebView alloc] init];
}
self.backgroundColor = [UIColor redColor];
if(self.wkWebView) {
[self.wkWebView setFrame:frame];
[self.wkWebView setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];
[self.wkWebView setNavigationDelegate:self];
[self.wkWebView setUIDelegate:self];
[self.wkWebView setMultipleTouchEnabled:YES];
[self.wkWebView setAutoresizesSubviews:YES];
[self.wkWebView.scrollView setAlwaysBounceVertical:YES];
[self addSubview:self.wkWebView];
[self.wkWebView addObserver:self forKeyPath:NSStringFromSelector(@selector(estimatedProgress)) options:0 context:FTDIntegrationWebBrowserContext];
[self.wkWebView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:NULL];
} else {
[self.uiWebView setFrame:frame];
[self.uiWebView setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];
[self.uiWebView setDelegate:self];
[self.uiWebView setMultipleTouchEnabled:YES];
[self.uiWebView setAutoresizesSubviews:YES];
[self.uiWebView setScalesPageToFit:YES];
[self.uiWebView.scrollView setAlwaysBounceVertical:YES];
[self addSubview:self.uiWebView];
}

有了webview,但是由于WKWebView和UIWebView有不同的代理方法,这时我们就可以用协议来解决这个问题。

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
@protocol FTDIntegrationWebViewDelegate <NSObject>
@optional
/**
* webview内容的标题
*/
- (void)FTD_WebView:(FTDIntegrationWebView *)webview title:(NSString *)title;
/**
* webview监听
*/
- (void)FTD_WebView:(FTDIntegrationWebView *)webview shouldStartLoadWithURL:(NSURL *)URL;
/**
* webview开始加载
*/
- (void)FTD_WebViewDidStartLoad:(FTDIntegrationWebView *)webview;
/**
* webview加载完成
*/
- (void)FTD_WebView:(FTDIntegrationWebView *)webview didFinishLoadingURL:(NSURL *)URL;
/**
* webview加载失败
*/
- (void)FTD_WebView:(FTDIntegrationWebView *)webview didFailToLoadURL:(NSURL *)URL error:(NSError *)error;
@end

我们定义一个协议,让WKWebView和UIWebView相应的代理方法里面实现上面相应的协议方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
例如:
#pragma mark - UIWebViewDelegate
- (void)webViewDidStartLoad:(UIWebView *)webView {
if(webView == self.uiWebView) {
if ([self.delegate respondsToSelector:@selector(FTD_WebViewDidStartLoad:)]) {
[self.delegate FTD_WebViewDidStartLoad:self];
}
}
}
...
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
if(webView == self.wkWebView) {
if ([self.delegate respondsToSelector:@selector(FTD_WebViewDidStartLoad:)]) {
[self.delegate FTD_WebViewDidStartLoad:self];
}
}
}
...

在上面我们用”respondsToSelector:”来判断委托对象是否实现了相关方法。如果实现了,就调用,如果没实现,就不执行任何操作。这样的话,delegate对象就可以完全按照其需要来实现协议中的方法。即时没有设置代理,程序也能照常运行,因为给nil发送消息将使if语句的值成为false。这样子看起来很完美,但是到目前都没有涉及到标题说的代理优化。慢慢来。

上面的代码很容易判断出每个代理对象是否响应特定的选择子,如果需要频繁的调用这些方法,除了第一次检测的结果有用之外,后面的可能都是多余的。我们就可以想办法把代理对象能否响应某个协议方法这一信息缓存起来,以优化程序效率。

将方法响应能力缓存起来的最佳途径是使用”位字段”数据类型。我们可以把结构体中的某个字段所占用的二进制位个数设为特定的值。

1
2
3
4
5
6
7
struct {
unsigned int testA :8;
unsigned int testB :4;
unsigned int testC :2;
unsigned int testD :1;
} _flags;

在结构体中,testA位字段占用8个二进制位,testB位字段占用4个二进制位,testC位字段占用2个二进制位,testD位字段占用1个二进制位。于是,testA可以表示0到255之间的值,而testD则表示0或1这两个值。所以,我们可以像testD这样,把代理对象是否实现了协议中的相关方法这一信息缓存起来。如果创建的结构体中只有大小为1的位段,那么就可以把许多BooL值塞入这一块数据里面。那么,上面的协议方法则可以定义这几个位字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface FTDIntegrationWebView (){
struct {
unsigned int didTitle :1;
unsigned int didshouldStartLoad :1;
unsigned int didStartLoad :1;
unsigned int didFinishLoad :1;
unsigned int didFailToLoad :1;
} _delegateFlags;
}

我们如果要获取或设置结构体中的位字段的话,只能用点语法,

1
2
3
4
5
6
7
set:
_delegateFlags. didTitle = 1;
get:
if(_delegateFlags. didTitle){
}

这样我们就可以用这个结构体来换存代理对象是否能响应特定的选择子。实现缓存的方法则在delegate属性的设置方法里面:

1
2
3
4
5
6
7
8
9
10
11
- (void)setDelegate:(id<FTDIntegrationWebViewDelegate>)delegate
{
_delegate = delegate;
_delegateFlags.didTitle = [delegate respondsToSelector:@selector(FTD_WebView:title:)];
_delegateFlags.didshouldStartLoad = [delegate respondsToSelector:@selector(FTD_WebView:shouldStartLoadWithURL:)];
_delegateFlags.didStartLoad = [delegate respondsToSelector:@selector(FTD_WebViewDidStartLoad:)];
_delegateFlags.didFinishLoad = [delegate respondsToSelector:@selector(FTD_WebView:didFinishLoadingURL:)];
_delegateFlags.didFailToLoad = [delegate respondsToSelector:@selector(FTD_WebView:didFailToLoadURL:error:)];
}

我们需要调用的时候,检测是否响应了协议方法就可以直接查询结构体里的标志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#pragma mark - UIWebViewDelegate
- (void)webViewDidStartLoad:(UIWebView *)webView {
if(webView == self.uiWebView) {
if (_delegateFlags.didStartLoad) {
[self.delegate FTD_WebViewDidStartLoad:self];
}
}
}
...
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
if(webView == self.wkWebView) {
if (_delegateFlags.didStartLoad) {
[self.delegate FTD_WebViewDidStartLoad:self];
}
}
}
...

这样就完成了对代理的优化,其实是否需要优化都是根据具体情景来的,脱离了具体情景一切优化都是空谈。这种优化适合需要频繁的通过协议方法获取多份相互独立的数据。

本文讲到的例子可以从这里获得。