從高斯模糊到Category方法加載

1. UIVisualEffectView

iOS 8系統爲咱們提供了UIVisualEffectView。咱們能夠利用這個類來完成高斯模糊的效果。函數

@interface ViewController ()
@property (nonatomic, strong) UIView *boxView;
@property (nonatomic, strong) UIVisualEffectView *blurView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
    imageView.contentMode = UIViewContentModeScaleAspectFill;
    imageView.image = [UIImage imageNamed:@"sao"];
    [self.view addSubview:imageView];
    
    self.boxView = [[UIView alloc] initWithFrame:CGRectMake(0, self.view.bounds.size.height - 83.0, self.view.bounds.size.width, 83.0)];
    [self.view addSubview:self.boxView];
    
    UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
    self.blurView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
    self.blurView.frame = self.boxView.bounds;
    [self.boxView addSubview:self.blurView];
    
    CGFloat width = self.boxView.bounds.size.width / 4;
    for (int i = 0; i < 4; i++) {
        UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
        button.frame = CGRectMake(width * i, 0, width, self.boxView.bounds.size.height);
        [button setTitleColor:UIColor.blackColor forState:UIControlStateNormal];
        [button setTitle:[NSString stringWithFormat:@"Btn%d", i] forState:UIControlStateNormal];
        [self.boxView addSubview:button];
    }
}
複製代碼

2. snapshot的問題

咱們可能在作轉場動畫的時候須要對這個視圖進行"截圖",或者說可能利用的某個三方庫的實現就是截圖,那麼咱們會遇到一個bug。flex

在利用截圖視圖進行動畫的時候,咱們發現高斯模糊效果沒有了,顯示的是一個半透明的背景。動畫

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    // 截圖
    UIView *snapView = [self.boxView snapshotViewAfterScreenUpdates:NO];
    snapView.frame = self.boxView.frame;
    [self.view addSubview:snapView];
    self.boxView.hidden = YES;
    [UIView animateWithDuration:0.8 animations:^{
        if (self.boxView.transform.ty > 0) {
            snapView.transform = CGAffineTransformMakeTranslation(0, -self.boxView.frame.size.height);
        } else {
            snapView.transform = CGAffineTransformMakeTranslation(0, self.boxView.frame.size.height);
        }
    } completion:^(BOOL finished) {
        if (self.boxView.transform.ty > 0) {
            self.boxView.transform = CGAffineTransformIdentity;
        } else {
            self.boxView.transform = CGAffineTransformMakeTranslation(0, self.boxView.frame.size.height);
        }
        self.boxView.hidden = NO;
        [snapView removeFromSuperview];
    }];
}
複製代碼

這裏的截圖嘗試了3種寫法:ui

// 1
UIView *snapView = [self.boxView snapshotViewAfterScreenUpdates:NO];
// 2
UIView *snapView = [self.boxView snapshotViewAfterScreenUpdates:YES];
// 3
@implementation UIView (Snapshot)
- (UIImageView *)yc_snapshotImageView
{
    UIImage *image;
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    if (context) {
        [self.layer renderInContext:context];
        image = UIGraphicsGetImageFromCurrentImageContext();
    }
    UIGraphicsEndImageContext();
    if (image) {
        return [[UIImageView alloc] initWithImage:image];
    }
    return nil;
}
@end
UIImageView *snapView = [self.boxView yc_snapshotImageView];
複製代碼

嘗試以後發現均不能解決這個「透明」問題。可是若是咱們直接用高斯模糊視圖進行動畫,發現是有效果的。atom

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    // 不使用截圖
    [UIView animateWithDuration:0.8 animations:^{
        if (self.boxView.transform.ty > 0) {
            self.boxView.transform = CGAffineTransformIdentity;
        } else {
            self.boxView.transform = CGAffineTransformMakeTranslation(0, self.boxView.frame.size.height);
        }
    }];
}
複製代碼

下面咱們來思考一下,爲何截圖不行呢?spa

  • 高斯模糊的效果是怎麼來的?3d

    咱們能夠看到,高斯模糊的效果實際上是對後面圖像的一個「濾鏡」效果。也就是說,若是背後沒有圖像,那麼高斯模糊是沒有效果的。code

  • 截圖時發生了什麼?orm

    截圖的時候,咱們是直接獲取視圖圖像的。不管是使用 snapshotViewAfterScreenUpdates: 仍是利用 renderInContext:,其實都 只能拿到該視圖的圖像,沒法獲取它和後面圖層的混合效果 。因此最後只獲到一個半透明的白色,這個是由UIBlurEffectStyleLight提供的一個半透明的圖像。cdn

3. 解決snapshot的效果問題

如今底層的實現爲 UIView *snapView = [self.boxView yc_snapshotImageView]; ,如何解決存在半透明視圖的問題呢?

咱們知道 使用高斯模糊視圖作動畫是沒問題的 ,那咱們的思路能夠是先隱藏原有的高斯模糊視圖,而後進行截圖,最後在它的下面添加一個實際的用高斯模糊視圖。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    // 容器
    UIView *snapBoxView = [[UIView alloc] initWithFrame:self.boxView.frame];
    [self.view addSubview:snapBoxView];
    // 實際高斯模糊視圖
    UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
    UIVisualEffectView *blurView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
    blurView.frame = snapBoxView.bounds;
    [snapBoxView addSubview:blurView];
    // 只渲染非高斯模糊部分的視圖
    self.blurView.hidden = YES;
    UIImageView *snapView = [self.boxView yc_snapshotImageView];
    self.blurView.hidden = NO;
    [snapBoxView addSubview:snapView];
    // 動畫
    self.boxView.hidden = YES;
    [UIView animateWithDuration:0.8 animations:^{
        if (self.boxView.transform.ty > 0) {
            snapBoxView.transform = CGAffineTransformMakeTranslation(0, -self.boxView.frame.size.height);
        } else {
            snapBoxView.transform = CGAffineTransformMakeTranslation(0, self.boxView.frame.size.height);
        }
    } completion:^(BOOL finished) {
        if (self.boxView.transform.ty > 0) {
            self.boxView.transform = CGAffineTransformIdentity;
        } else {
            self.boxView.transform = CGAffineTransformMakeTranslation(0, self.boxView.frame.size.height);
        }
        self.boxView.hidden = NO;
        [snapBoxView removeFromSuperview];
    }];
}
複製代碼

4. Category

在實際狀況中,咱們的視圖可能被封裝了起來,像下面這樣:

@interface BottomView : UIView
@property (nonatomic, strong) UIVisualEffectView *blurView;
@end

@implementation BottomView
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.blurView = [self innerBlurView];
        [self addSubview:self.blurView];
        CGFloat width = self.bounds.size.width / 4;
        for (int i = 0; i < 4; i++) {
            UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
            button.frame = CGRectMake(width * i, 0, width, self.bounds.size.height);
            [button setTitleColor:UIColor.blackColor forState:UIControlStateNormal];
            [button setTitle:[NSString stringWithFormat:@"Btn%d", i] forState:UIControlStateNormal];
            [self addSubview:button];
        }
    }
    return self;
}

- (UIVisualEffectView *)innerBlurView
{
    UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
    UIVisualEffectView *blurView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
    blurView.frame = self.bounds;
    return blurView;
}

@end

@implementation BottomView (Snapshot)

- (nullable UIImageView *)yc_snapshotImageView
{
    // 容器
    UIImageView *snapBoxView = [[UIImageView alloc] initWithFrame:self.bounds];
    // 實際高斯模糊視圖
    UIVisualEffectView *blurView = [self innerBlurView];
    [snapBoxView addSubview:blurView];
    // 只渲染非高斯模糊部分的視圖
    self.blurView.hidden = YES;
    UIImageView *snapView = nil;
    UIImage *image;
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    if (context) {
        [self.layer renderInContext:context];
        image = UIGraphicsGetImageFromCurrentImageContext();
    }
    UIGraphicsEndImageContext();
    if (image) {
        snapView = [[UIImageView alloc] initWithImage:image];
        [snapBoxView addSubview:snapView];
    }
    self.blurView.hidden = NO;
    return snapBoxView;
}
@end
複製代碼

4.1 考點問題

那麼如今問題來了:

@implementation UIView (Snapshot)
- (nullable UIImageView *)yc_snapshotImageView
{
    ...
}
@end

@implementation BottomView (Snapshot)
- (nullable UIImageView *)yc_snapshotImageView
{
    ...
}
@end
複製代碼

咱們有兩個Category,均實現了同一個方法,那麼執行哪個呢?

4.2 底層實現

這裏不帶你們挨着走dyld的加載流程了,咱們只說重點部分attachCategoriesattachLists 函數:

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }
    
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    auto rw = cls->data();
    
    // 遍歷分類
    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];
        // 獲取每一個分類的方法
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
                rw->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        // 獲取每一個分類的屬性
        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rw->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }
        // 獲取每一個分類的協議
        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rw->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }
    
    // 遍歷完還有方法、屬性、分類,再掃個尾
    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
        rw->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) flushCaches(cls);
    }

    rw->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    rw->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;

    if (hasArray()) {
        // many lists -> many lists
        // 要擴充多列表
        uint32_t oldCount = array()->count;
        uint32_t newCount = oldCount + addedCount;
        // 從新分配內存空間
        setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
        // 更新總數
        array()->count = newCount;
        // 把老的列表放在後移,放在addedCount以後
        memmove(array()->lists + addedCount, array()->lists, 
                oldCount * sizeof(array()->lists[0]));
        // 新列表放在頭部
        memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
    }
    else if (!list  &&  addedCount == 1) {
        // 0 lists -> 1 list
        // 只有一個列表直接賦值
        list = addedLists[0];
    } 
    else {
        // 1 list -> many lists
        // 只有1個列表,要擴充爲多列表
        List* oldList = list;
        uint32_t oldCount = oldList ? 1 : 0;
        uint32_t newCount = oldCount + addedCount;
        // 分配內存空間
        setArray((array_t *)malloc(array_t::byteSize(newCount)));
        // 更新總數
        array()->count = newCount;
        // 老列表接在尾部
        if (oldList) array()->lists[addedCount] = oldList;
        // 新列表放在頭部
        memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
    }
}
複製代碼

從上面的代碼咱們能夠看出:

  • 分類在加載方法的時候,分類的方法是放在方法列表的頭部的。

  • 同一個類的方法,根據編譯順序,越後面的的方法放在方法列表的越前面。

4.3 回到問題

@implementation UIView (Snapshot)
- (nullable UIImageView *)yc_snapshotImageView
{
    ...
}
@end

@implementation BottomView (Snapshot)
- (nullable UIImageView *)yc_snapshotImageView
{
    ...
}
@end
複製代碼

對於不一樣繼承類的同一個Category方法,這兩個方法是分別在UIViewBottomView的類對象中的。

  • 不存在前後順序問題。
    • 不一樣的前後編譯順序,不影響最終的響應方法。
  • 在實際消息發送方法的查找過程當中,實例對象先查找自身類的方法列表,若是沒有才會向上在父類進行方法列表的查找。
    • BottomView會在查找本身類的方法列表時找到 yc_snapshotImageView 方法,用本身的實現而非父類的實現。

下面咱們驗證一下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    // 容器
    UIImageView *snapBoxView = [self.boxView yc_snapshotImageView];
    snapBoxView.frame = self.boxView.frame;
    [self.view addSubview:snapBoxView];
    // 動畫
    self.boxView.hidden = YES;
    [UIView animateWithDuration:0.8 animations:^{
        if (self.boxView.transform.ty > 0) {
            snapBoxView.transform = CGAffineTransformMakeTranslation(0, -self.boxView.frame.size.height);
        } else {
            snapBoxView.transform = CGAffineTransformMakeTranslation(0, self.boxView.frame.size.height);
        }
    } completion:^(BOOL finished) {
        if (self.boxView.transform.ty > 0) {
            self.boxView.transform = CGAffineTransformIdentity;
        } else {
            self.boxView.transform = CGAffineTransformMakeTranslation(0, self.boxView.frame.size.height);
        }
        self.boxView.hidden = NO;
        [snapBoxView removeFromSuperview];
    }];
}
複製代碼

咱們再試試直接調用父類的 yc_snapshotImageView 方法。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    // 容器
    struct objc_super superClass = {
        self.boxView,
        class_getSuperclass([self.boxView class])
    };
    UIImageView *snapBoxView = objc_msgSendSuper(&superClass, @selector(yc_snapshotImageView));
    snapBoxView.frame = self.boxView.frame;
    [self.view addSubview:snapBoxView];
    // 動畫
    self.boxView.hidden = YES;
    [UIView animateWithDuration:0.8 animations:^{
        if (self.boxView.transform.ty > 0) {
            snapBoxView.transform = CGAffineTransformMakeTranslation(0, -self.boxView.frame.size.height);
        } else {
            snapBoxView.transform = CGAffineTransformMakeTranslation(0, self.boxView.frame.size.height);
        }
    } completion:^(BOOL finished) {
        if (self.boxView.transform.ty > 0) {
            self.boxView.transform = CGAffineTransformIdentity;
        } else {
            self.boxView.transform = CGAffineTransformMakeTranslation(0, self.boxView.frame.size.height);
        }
        self.boxView.hidden = NO;
        [snapBoxView removeFromSuperview];
    }];
}
複製代碼

重要

使用objc_msgSendSuper時,可能編譯器會報錯:

Too many arguments to function call, expected 0, have 3

解決辦法:在Build Setting修改Enable Strict Checking of objc_msgSend CallsNo

經過實際的運行結果也驗證了咱們的理論。


若是以爲本文對你有所幫助,給我點個贊吧~ 👍🏻

相關文章
相關標籤/搜索