天天看點

【原】Github系列之一:一起做仿天氣類應用中的實時模糊效果LiveBlur

從本文開始,我将專門開辟一個Github Code系列,開源自己寫的一部分有意思而且實用的demo,共同學習。以前都釋出在git OSChina上,後面有空會陸陸續續整理到Github上。OSChina最大的優點是可以免費托管私有項目,伺服器在國内速度快,這些是Github所比不了的。不過Github優勢在于開源氛圍濃烈,有利于向各位開源大牛學習交流。長話短說,just do it!

本文隻要實作随滾動實時模糊(或稱為動态模糊)的效果(請見文章末尾處),這種效果被廣泛應用于各大天氣類APP中,如墨迹天氣、黃曆天氣、雅虎天氣等。随着scrollView向上滾動,背景逐漸模糊,二者是互相關聯的,實作起來很簡單。在文章的後半部分我們嘗試用兩種方法實作這種效果。不過我要強調的一點是,我的一些文章的方法思路雖簡單,但我更注重這個過程中對一些“坑”的處理,這是個融彙貫通的過程。比如預告一下,本文提到了幾個坑:KVO陷阱、autoRelease坑、drawRect坑...

首先,我們利用KVO将scrollView的contentOffset與Image的模糊度進行綁定,這樣我們就能實時檢測到scrollview(本文為tableview)的滾動偏移量,進而改變image的模糊度。

在此之前,我們從UIImageView派生出子類WZLLiveBlurImageView來實作圖像的模糊改變,先看頭檔案不管實作:

1 #import <UIKit/UIKit.h>
 2 //default initial blur level
 3 #define kImageBlurLevelDefault          0.9f
 4 @interface WZLLiveBlurImageView : UIImageView
 5 
 6 /**
 7  *  set blur level
 8  *
 9  *  @param level blur level
10  */
11 - (void)setBlurLevel:(CGFloat)level;
12 
13 @end      

setBlurLevel:是唯一的方法接口給調用者調用。接着我們在一個ViewController中把這個WZLLiveBlurImageView顯示出來。在ViewController中還應該有一個tableView用于模拟天氣類應用的使用場景。那WZLLiveBlurImageView要放在哪裡比較合适呢?tableView有一個subView叫做backgroundView,就選它了。以下是初始化代碼,放在ViewController的viewDidLoad方法中:

1    //generate item content for tableView
 2     _items = [self items];
 3     self.tableView.dataSource = self;
 4     self.tableView.delegate = self;
 5     self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
 6     self.tableView.separatorColor = [UIColor clearColor];
 7     _blurImgView = [[WZLLiveBlurImageView alloc] initWithImage:[UIImage imageNamed:@"bg.jpg"]];
 8     _blurImgView.frame = self.tableView.frame;
 9     self.tableView.backgroundView = _blurImgView;
10     self.tableView.backgroundColor = [UIColor clearColor];
11     self.tableView.contentInset = UIEdgeInsetsMake(CGRectGetHeight(self.tableView.bounds) - 100, 0, 0, 0);      

那我們怎麼知道tableview到底滾動了多少呢?答案是KVO。KVO的使用當中是有坑的,寫出一個健壯穩定的KVO需要注意很多細節,比如要小心崩潰問題,要防止KVO鍊斷開等...對KVO不熟的同學請移步我的另一篇博文:《KVO使用中的陷阱》。 緊接着上面的代碼,我們繼續配置KVO,"contentOffset"字元串必須與tableview的屬性值完全一緻才有效:

1     //setup kvo on tableview`s contentoffset
2     [self.tableView addObserver:self forKeyPath:@"contentOffset"
3                         options:NSKeyValueObservingOptionNew
4                         context:(__bridge void *)(kWZLLiveBlurImageViewContext)];//kWZLLiveBlurImageViewContext是一個全局的字元串      

注冊與登出要成對出現,盡管是ARC,但在dealloc也要清理現場:

1 - (void)dealloc
2 {
3     [self.tableView removeObserver:self forKeyPath:@"contentOffset"
4                            context:(__bridge void *)kWZLLiveBlurImageViewContext];
5 }      

添加KVO的預設回調函數,目前類的所有KVO都走的這裡,建議KVO都寫成如下這樣,注意對super以及context的處理:

1 #pragma mark - KVO configuration
 2 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
 3 {
 4     if (context == (__bridge void *)(kWZLLiveBlurImageViewContext)) {
 5         CGFloat blurLevel = (self.tableView.contentInset.top + self.tableView.contentOffset.y) / CGRectGetHeight(self.tableView.bounds);
 6         [_blurImgView setBlurLevel:blurLevel];
 7     } else {
 8         [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
 9     }
10 }      

以上代碼基本就形成了實時模糊的整體架構了,現在還差WZLLiveBlurImageView中對模糊圖像的實作。對UIImage進行模糊處理的代碼網上有很多,基本都是同一個版本,這裡我們就站在巨人的肩膀上,用一下這個算法,使用 - (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius; 方法對image進行模糊處理:

【原】Github系列之一:一起做仿天氣類應用中的實時模糊效果LiveBlur
【原】Github系列之一:一起做仿天氣類應用中的實時模糊效果LiveBlur
1 //
 2 //  UIImage+Blur.h
 3 //  WZLLiveBlurImageView
 4 //
 5 //  Created by zilin_weng on 15/3/23.
 6 //  Copyright (c) 2015年 Weng-Zilin. All rights reserved.
 7 //
 8 
 9 #import <UIKit/UIKit.h>
10 
11 @interface UIImage (Blur)
12 
13 - (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius;
14 
15 @end      

UIImage+Blur.h

【原】Github系列之一:一起做仿天氣類應用中的實時模糊效果LiveBlur
【原】Github系列之一:一起做仿天氣類應用中的實時模糊效果LiveBlur
1 //
 2 //  UIImage+Blur.m
 3 //  WZLLiveBlurImageView
 4 //
 5 //  Created by zilin_weng on 15/3/23.
 6 //  Copyright (c) 2015年 Weng-Zilin. All rights reserved.
 7 //
 8 
 9 #import "UIImage+Blur.h"
10 #import <Accelerate/Accelerate.h>
11 
12 @implementation UIImage (Blur)
13 
14 - (UIImage *)applyBlurWithRadius:(CGFloat)blur
15 {
16     if (blur < 0.f || blur > 1.f) {
17         blur = 0.5f;
18     }
19     int boxSize = (int)(blur * 40);
20     boxSize = boxSize - (boxSize % 2) + 1;
21     
22     CGImageRef img = self.CGImage;
23     vImage_Buffer inBuffer, outBuffer;
24     vImage_Error error;
25     void *pixelBuffer;
26     
27     //create vImage_Buffer with data from CGImageRef
28     CGDataProviderRef inProvider = CGImageGetDataProvider(img);
29     CFDataRef inBitmapData = CGDataProviderCopyData(inProvider);
30     
31     inBuffer.width = CGImageGetWidth(img);
32     inBuffer.height = CGImageGetHeight(img);
33     inBuffer.rowBytes = CGImageGetBytesPerRow(img);
34     
35     inBuffer.data = (void*)CFDataGetBytePtr(inBitmapData);
36     
37     //create vImage_Buffer for output
38     pixelBuffer = malloc(CGImageGetBytesPerRow(img) * CGImageGetHeight(img));
39     
40     if(pixelBuffer == NULL)
41         NSLog(@"No pixelbuffer");
42     
43     outBuffer.data = pixelBuffer;
44     outBuffer.width = CGImageGetWidth(img);
45     outBuffer.height = CGImageGetHeight(img);
46     outBuffer.rowBytes = CGImageGetBytesPerRow(img);
47     
48     // Create a third buffer for intermediate processing
49     /*void *pixelBuffer2 = malloc(CGImageGetBytesPerRow(img) * CGImageGetHeight(img));
50      vImage_Buffer outBuffer2;
51      outBuffer2.data = pixelBuffer2;
52      outBuffer2.width = CGImageGetWidth(img);
53      outBuffer2.height = CGImageGetHeight(img);
54      outBuffer2.rowBytes = CGImageGetBytesPerRow(img);*/
55     //perform convolution
56     error = vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, NULL, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend)
57     ?: vImageBoxConvolve_ARGB8888(&outBuffer, &inBuffer, NULL, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend)
58     ?: vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, NULL, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
59     
60     if (error) {
61         NSLog(@"error from convolution %ld", error);
62     }
63     
64     CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
65     CGContextRef ctx = CGBitmapContextCreate(outBuffer.data,
66                                              outBuffer.width,
67                                              outBuffer.height,
68                                              8,
69                                              outBuffer.rowBytes,
70                                              colorSpace,
71                                              (CGBitmapInfo)kCGImageAlphaNoneSkipLast);
72     CGImageRef imageRef = CGBitmapContextCreateImage (ctx);
73     UIImage *returnImage = [UIImage imageWithCGImage:imageRef];
74     //clean up
75     CGContextRelease(ctx);
76     CGColorSpaceRelease(colorSpace);
77     free(pixelBuffer);
78     //free(pixelBuffer2);
79     CFRelease(inBitmapData);
80     CGImageRelease(imageRef);
81     return returnImage;
82 }
83 
84 
85 
86 @end      

UIImage+Blur.m

接下來有兩種思路可供參考:(1)直接法;(2)間接法

(1)直接法

從字面上看直接法可以了解為直接改變圖檔的模糊度,也就是在KVO響應函數中根據tableView偏移量算出一個模糊目标值,每次目标值更新了,就做一次圖像模糊處理算法。這種方法簡單粗暴,思路直接。由于滾動過程中對模糊處理調用非常頻繁,這個時候要特别注意性能問題。在這裡影響性能問題主要有兩個因素:記憶體跟繪圖。如果記憶體在短時間内不能及時釋放,則在小記憶體裝置上将顯得很脆弱!在記憶體問題上,注意到上述圖像模糊算法傳回的是一個autoRelease類型的對象,但是autoRelease對象并不會在作用域之外就自動釋放的(what?如果你覺得驚訝,說明你對autoRelease認識還不夠,建議你看看我的另一篇文章:《你真的懂autoRelease嗎?》)。解決辦法就是使用autoReleasePool手動幹預對象的釋放時機。記憶體問題解決了,接着是繪圖問題。很多文章都說使用比UIKIt更底層的CGGraphy繪圖可以達到更高的效率,于是我們寫出如下代碼:(函數内的單元檢測是必須的)

1 #import "WZLLiveBlurImageView.h"
 2 #import "UIImage+Blur.h"
 3 
 4 @interface WZLLiveBlurImageView ()
 5 {
 6     UIImage *_originImage;
 7     CGFloat _blurLevel;
 8 }
 9 @end
11 @implementation WZLLiveBlurImageView
18 - (void)setBlurLevel:(CGFloat)level
19 {
20     if (!self.image) {
21         NSLog(@"image is empty!");
22         return;
23     }
24     _blurLevel = level;
25     [self setNeedsDisplay];
26 }
27 
28 #pragma mark - private apis
29 - (void)drawRect:(CGRect)rect
30 {
31     @autoreleasepool {
32         if (_originImage) {
33             UIImage *blurImg = [_originImage applyBlurWithRadius:_blurLevel];
34             [blurImg drawInRect:self.bounds];
35         }
36     }
37 }
38 @end      

是不是覺得很完美,該考慮的都考慮到了。運作一下發現,圖像模糊度沒發生改變。為啥?因為drawRect函數根本沒調用到!按command+shift+0打開APPLE文檔檢視UIImageVIew發現以下的描述:

【原】Github系列之一:一起做仿天氣類應用中的實時模糊效果LiveBlur

蘋果說的很清楚了,意思是UIImageView類已經做了優化,如果你子類化UIImageView是不會調用drawRect的!隻有從UIView派生的類才允許走drawRect。雖然遇到了坑,不過我們可以放心地直接使用 self.image = xxxBlurImage; 這樣的方法來更新模糊圖像了。同時,為了避免 self.image = [_originImage applyBlurWithRadius:level]; 方法太耗時進而阻塞UI,我們使用GCD去處理模糊操作,處理結束後回到主線程更新image。使用GCD過程中要防止循環引用(點我點我),下面就是完整的代碼:

1 #import "WZLLiveBlurImageView.h"
 2 #import "UIImage+Blur.h"
 3 
 4 @interface WZLLiveBlurImageView ()
 5 @property (nonatomic, strong) UIImage *originImage;
 6 @end
 7 
 8 @implementation WZLLiveBlurImageView
 9 /**
10  *  set blur level
11  *
12  *  @param level blur level
13  */
14 - (void)setBlurLevel:(CGFloat)level
15 {
16     if (!self.image) {
17         NSLog(@"image is empty!");
18         return;
19     }
20     level = (level > 1 ? 1 : (level < 0 ? 0 : level));
21     NSLog(@"level:%@", @(level));
22     //self.realBlurImageView.alpha = level;
23     @autoreleasepool {
24         if (_originImage) {
25             __weak typeof(WZLLiveBlurImageView*) weakSelf = self;
26             __block UIImage *blurImage = nil;
27             dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
28                 blurImage = [weakSelf.originImage applyBlurWithRadius:level];
29                 //get back to main thread to update UI
30                 dispatch_async(dispatch_get_main_queue(), ^{
31                     weakSelf.image = blurImage;
32                 });
33             });
34         }
35     }
36 }      

缺點是:在低性能裝置上雖然可以實作滾動流暢,但由于CPU性能捉急,導緻tableView已經滾動了,image才慢慢模糊,這種延時給人很差的體驗。如果去掉GCD,則延時消失,但滾動卡頓嚴重。在寫這篇文章時最新裝置是iphone6p,iphone4基本淘汰。用iphone4s真機調試時,直接法hold不住。當然,模拟器無壓力妥妥的,不過那又怎樣!附直接法在模拟器的效果,錄制過程失真了導緻模糊效果看起來不自然:

【原】Github系列之一:一起做仿天氣類應用中的實時模糊效果LiveBlur

========================

(2)間接法

既然直接法暫時做不了,那我們可以考慮曲線救國。我的政策是在WZLLiveBlurImageView上添加一個UIImageVIew,稱為 realBlurImageView ,用 realBlurImageView 來實作真的模糊,但是隻模糊一次。在tableview滾動過程中隻改變 realBlurImageView 的透明度alpha。此法可謂移花接木。

1 #import "WZLLiveBlurImageView.h"
 2 #import "UIImage+Blur.h"
 3 
 4 @interface WZLLiveBlurImageView ()
 5 @property (nonatomic, strong) UIImage *originImage;
 6 @property (nonatomic, strong) UIImageView *realBlurImageView;
 7 @end
 8 
 9 @implementation WZLLiveBlurImageView
10 
11 - (void)setBlurLevel:(CGFloat)level
12 {
13     if (!self.image || !self.realBlurImageView) {
14         NSLog(@"image is empty!");
15         return;
16     }
17     level = (level > 1 ? 1 : (level < 0 ? 0 : level));
18     NSLog(@"level:%@", @(level));
19     self.realBlurImageView.alpha = level;
20 }
21 
22 #pragma mark - private apis
23 
24 - (void)setImage:(UIImage *)image
25 {
26     [super setImage:image];
27     if (_originImage == nil && image) {
28         _originImage = image;
29     }
30     if (!self.realBlurImageView) {
31         UIImage *blurImage = [image applyBlurWithRadius:kImageBlurLevelDefault];
32         self.realBlurImageView = [[UIImageView alloc] initWithImage:blurImage];
33         self.realBlurImageView.backgroundColor = [UIColor clearColor];
34         self.realBlurImageView.frame = self.bounds;
35         self.realBlurImageView.alpha = 0;
36         [self addSubview:self.realBlurImageView];
37     }
38 }      

間接法雖然引入了新的對象,但換來了性能上的完美折中,我認為是值得的。不管是模拟器還是低性能真機,調試都OK,實時性很好:

【原】Github系列之一:一起做仿天氣類應用中的實時模糊效果LiveBlur

我們抛出的問題都完美解決了,也順帶解決了一些坑。方法雖簡單,但過程還是比較有意義的。最後附上整個demo的github位址.

=======================================

原創文章,轉載請注明 程式設計小翁@部落格園,郵件[email protected],微信Jilon,歡迎各位與我在C/C++/Objective-C/機器視覺等領域展開交流!

 ======================================

上一篇: Linux 關機
下一篇: 無語