1. 程式人生 > >利用OC物件的訊息重定向forwardingTargetForSelector方法構建高擴充套件性的濾鏡功能

利用OC物件的訊息重定向forwardingTargetForSelector方法構建高擴充套件性的濾鏡功能

在OC中,當像一個物件傳送訊息,而物件找到訊息後,從它的類方法列表,父類方法列表,一直找到根類方法列表都沒有找到與這個選擇子對應的函式指標。那麼這個物件就會觸發訊息轉發機制。

OC物件的繼承鏈和isa指標鏈如圖:

 

訊息轉發流程如下: 1.先呼叫例項方法resolveInstanceMethod 如果作者在這裡使用runtime動態新增對應的方法,並且返回yes。就萬事大吉。物件找到了處理的方法, 並且將這個新增的方法新增到類的方法快取列表 2.如果上面的方法返回NO的話,物件會呼叫forwardingTargetForSelector方法 允許作者選擇其他的物件,處理這個訊息。 這個方法,也是待會我們要做文章的地方。畫重點。 3.如果上面兩個方法都沒有做處理,那麼物件會執行最後一個方法methodSignatureForSelector,提供一個有效的方法簽名,若提供了有效的方法簽名,程式將會通過forwardInvocation方法執行簽名。若沒有提供方法簽名就會觸發doesNotRecognizeSelector方法,觸發崩潰。

整個呼叫流程圖如下:

 整個程式碼呼叫順序如下:

//1
+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"1---%@",NSStringFromSelector(sel));
    NSLog(@"1---%@",NSStringFromSelector(_cmd));
    return NO;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"1---%@",NSStringFromSelector(sel));
    NSLog(@"1---%@",NSStringFromSelector(_cmd));
    
return NO; } //2 - (id)forwardingTargetForSelector:(SEL)aSelector { NSLog(@"2---%@",NSStringFromSelector(aSelector)); NSLog(@"2---%@",NSStringFromSelector(_cmd)); return nil; } //3.最後一步,返回方法簽名 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ NSLog(@"3---%@",NSStringFromSelector(aSelector)); NSLog(
@"3---%@",NSStringFromSelector(_cmd)); if ([NSStringFromSelector(aSelector) isEqualToString:@"gogogo"]) { return [[UnknownModel2 new] methodSignatureForSelector:aSelector]; } return [super methodSignatureForSelector:aSelector]; } //3.1處理返回的方法簽名 -(void)forwardInvocation:(NSInvocation *)anInvocation{ NSLog(@"4---%@",NSStringFromSelector(_cmd)); NSLog(@"4-最後一步--%@",anInvocation); if ([NSStringFromSelector(anInvocation.selector) isEqualToString:@"gogogo"]) { [anInvocation invokeWithTarget:[UnknownModel2 new]]; }else{ [super forwardInvocation:anInvocation]; } } //觸發崩潰 - (void)doesNotRecognizeSelector:(SEL)aSelector { }

列印結果如下:

2018-12-27 00:14:00.469445+0800 iOS_KnowledgeStructure[7940:110114] 1---gogogo
 2018-12-27 00:14:00.469613+0800 iOS_KnowledgeStructure[7940:110114] 1---resolveInstanceMethod:
 2018-12-27 00:14:00.469765+0800 iOS_KnowledgeStructure[7940:110114] 2---gogogo
 2018-12-27 00:14:00.469873+0800 iOS_KnowledgeStructure[7940:110114] 2---forwardingTargetForSelector:
 2018-12-27 00:14:00.469978+0800 iOS_KnowledgeStructure[7940:110114] 3---gogogo
 2018-12-27 00:14:00.470097+0800 iOS_KnowledgeStructure[7940:110114] 3---methodSignatureForSelector:
 2018-12-27 00:14:00.470247+0800 iOS_KnowledgeStructure[7940:110114] 1---_forwardStackInvocation:
 2018-12-27 00:14:00.470355+0800 iOS_KnowledgeStructure[7940:110114] 1---resolveInstanceMethod:
 2018-12-27 00:14:00.470765+0800 iOS_KnowledgeStructure[7940:110114] 4---forwardInvocation:
 2018-12-27 00:14:00.471367+0800 iOS_KnowledgeStructure[7940:110114] 4-最後一步--<NSInvocation: 0x600002442000>
 2018-12-27 00:14:00.471969+0800 iOS_KnowledgeStructure[7940:110114] lalalalala---gogogo
  OC訊息轉發的應用   當訊息轉發走到第二步時forwardingTargetForSelector,會讓物件提供一個第三者來處理這個訊息。 那麼可以得出結論:只要對物件傳送沒有實現的訊息,物件最後就會尋找一個第三者來接收這個訊息。

下面就利用訊息轉發機制,構建裝飾器,來實現影象濾鏡功能。 

 科普一下裝飾器模式。

裝飾器模式概念: 裝飾器模式是向物件新增東西(行為),而不破壞原有物件內容結構的一種設計模式。舉個例子,物件如同照片,裝飾器如同相框。而一張照片可以放到多種相框內產生多種賞心悅目的效果,而又不會對照片產生改變。   裝飾器模式UML圖:

 

說明如下: 1.Component為抽象父類,它為元件聲明瞭一些操作。 ConcreteComponent為例項元件類,相當於影象濾鏡中的原材料“圖片”。 2.Decorator為從Component父類實現而來的子抽象類,它是裝飾器的抽象父類。 它裡面包含了元件“圖片”(圖中的屬性:component)的引用。 3.Component父類,Decorator父類都包含了operation介面。 4.下面的“由裝飾器擴充套件功能”的標示部分,展示了用裝飾器為元件“圖片”新增功能的實際使用。

 

影象濾鏡的UML類圖為: 

影象濾鏡的uml類圖同裝飾器類圖的uml結構一致。 ImageComponent抽象父類定義介面,UIImage作為例項元件。 ImageFilter作為濾鏡父類介面,擴充類apply方法。並且對元件(component)新增引用。   重點 重點 重點: 在 forwardingTargetForSelector中先呼叫自己的apply方法,然後返回它所引用的component. 1.因為ImageFilter裝飾器中沒有draw:方法,所以向Image物件傳送[self setNeedDisplay]訊息時,ImageFilter物件會呼叫自己的forwardingTargetForSelector方法,這方法內包含了當前裝飾器的功能擴充套件,會執行擴充套件功能。 2.方法的最後有return component; 這一句是進行訊息轉發,讓component物件進行處理這次繪製。

 主要程式碼實現如下:

mageComponent抽象父類介面設計如下:

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol ZHFImageComponent <NSObject>
- (void)drawAtPoint:(CGPoint)point;
- (void)drawAtPoint:(CGPoint)point blendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
- (void)drawInRect:(CGRect)rect;
- (void)drawInRect:(CGRect)rect blendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
- (void)drawAsPatternInRect:(CGRect)rect;
@end
NS_ASSUME_NONNULL_END
  Image例項元件程式碼如下: 只是聲明瞭遵守ImageComponent的協議。
#import <UIKit/UIKit.h>
#import "ZHFImageComponent.h"
NS_ASSUME_NONNULL_BEGIN
@interface UIImage (ZHFImageComponent) <ZHFImageComponent>
@end
NS_ASSUME_NONNULL_END

裝飾器介面程式碼如下:

 .h檔案

#import <Foundation/Foundation.h>
#import "ZHFImageComponent.h"
NS_ASSUME_NONNULL_BEGIN
@interface ZHFImageFilter : NSObject <ZHFImageComponent>
{
    @private
    id<ZHFImageComponent> component_;
}
@property (nonatomic, strong) id<ZHFImageComponent> component;
- (instancetype)initWithImageComponent:(id<ZHFImageComponent>)component;
- (void)apply;
- (id)forwardingTargetForSelector:(SEL)aSelector;
@end
NS_ASSUME_NONNULL_END

 .m檔案

#import "ZHFImageFilter.h"
@implementation ZHFImageFilter
@synthesize component = component_;
- (instancetype)initWithImageComponent:(id<ZHFImageComponent>)component {
    if (self = [super init]) {
        self.component = component;
    }
    return self;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) hasPrefix:@"draw"]) {
        [self apply];
    }
    //使用訊息轉發給另一個物件處理,來實現任務處理鏈條,非常巧妙!!!
    return component_;
}
@end

forwardingTargetForSelector方法的實現是整個裝飾器的靈魂,子類其實只是呼叫父類的這個方法而已。

形變裝飾器程式碼如下:

#import "ZHFImageFilter.h"
NS_ASSUME_NONNULL_BEGIN
@interface ZHFImageTransformFilter : ZHFImageFilter
{
    @private
    CGAffineTransform transform_;
}
@property (nonatomic, assign) CGAffineTransform transform;
- (instancetype)initWithImageComponent:(id<ZHFImageComponent>)component
                             transform:(CGAffineTransform)transform;
@end
NS_ASSUME_NONNULL_END

#import "ZHFImageTransformFilter.h"
@implementation ZHFImageTransformFilter
@synthesize transform = transform_;
- (instancetype)initWithImageComponent:(id<ZHFImageComponent>)component
                             transform:(CGAffineTransform)transform {
    if (self = [super initWithImageComponent:component]) {
        transform_ = transform;
    }
    return self;
}
- (void)apply {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextConcatCTM(context, transform_);
}
@end
可以看到,形變裝飾器只是實現了apply方法,並沒有對forwardingTargetForSelector方法做任何處理。   呼叫流程如下: 1.向ImageTransformFilter傳送 drawInRect訊息 2.ImageTransformFilter因為沒有drawInRect方法,而呼叫父類的forwardingTargetForSelector方法 3.在父類的forwardingTargetForSelector方法中 包含 [selfapply]; 4.當在父類中呼叫[selfapply];程式碼時,會執行ImageTransformFilter的apply方法。(方法的泛型) 5.最後呼叫returncomponent_;,將訊息傳給下一個影象濾鏡元件。 6.重複1-5的過程。完成了訊息的轉發過程,形成任務處理鏈條。

 

完整的demo實現: https://github.com/zhfei/Objective-C_Design_Patterns