本次分析針對當下流行的中國地圖圖片處理,1億像素,就是下麵這張: 原圖尺寸:11935x8554 文件大小:22.1MB 原始載入方式 首先,我們嘗試一下直接載入的方式,看看效果會有多恐怖 首先,我們嘗試一下直接載入的方式,看看效果會有多恐怖 效果請看下麵的Gif動畫展示: 直接載入原圖記憶體占用 可 ...
本次分析針對當下流行的中國地圖圖片處理,1億像素,就是下麵這張:
原圖尺寸:11935x8554 文件大小:22.1MB
原始載入方式
首先,我們嘗試一下直接載入的方式,看看效果會有多恐怖 效果請看下麵的Gif動畫展示:
直接載入原圖記憶體占用
可以看到載入時記憶體的瞬間峰值達到了481M,毫無疑問,記憶體暴增,直接崩潰(視機型而定,好一點的機器比如iphone7可以正常展示~) 代碼很簡單,一個UIImageView,直接設置image:dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ self.largeImage = [UIImage imageWithContentsOfFile:_path]; dispatch_async(dispatch_get_main_queue(), ^{ self.imageView.image = self.largeImage; }); });
分塊載入方式
好,主角上場~CATiledLayer,先看一下使用之後的效果吧,Gif動畫展示如下分塊載入效果
CATiledLayer是為載入大圖造成的性能問題提供的一個解決方案,他將需要繪製的內容分割成許多小塊,然後在許多線程里按需非同步繪製相應的小塊,具體如何劃分小塊和縮放時的載入策略,與CATiledLayer三個重要屬性有關, 一句話描述:tile layer設置一個縮放區域的集合和重繪閾值,讓scroll view在縮放時,繪製層根據這些區域和縮放閾值去重新繪製當前顯示的區域 CATiledLayer的核心工作原則就是圍繞這三個屬性開展, 官方解釋如下(不喜可先略過此處解釋):The number of levels of detail maintained by this layer. Defaults to one. Each LOD is half the resolution of the previous level. If too many levels are specified for the current size of the layer, then the number of levels is clamped to the maximum value (the bottom most LOD must contain at least a single pixel in each dimension).
簡單來說就是從UIScrollView的縮小級別重繪設置,每一個層級的解析度是上一層級的½
levelsOfDetailBias The number of magnified levels of detail for this layer. Defaults t zero. Each previous level of detail is twice the resolution of the later. E.g. specifying 'levelsOfDetailBias' of two means that the layer devotes two of its specified levels of detail to magnification, i.e. 2x and 4x.
簡單來說就是,layer的放大級別重繪設置,每一個層級解析度是後面層級的2倍 不急,以上兩個屬性,我在後面會有詳細的描述
tileSize The maximum size of each tile used to create the layer's content. Defaults to (256, 256). Note that there is a maximum tile size, and requests for tiles larger than that limit will cause a suitable value to be substituted.
這個就簡單了,是layer劃分視圖區域最大尺寸
下麵的文章分析很透徹:
iOS開發 - CocoaChina CocoaChina_讓移動開發更簡單
但是我測試的結果和文章中的結論有比較大的出入,但是方向是一致的:(稍後我會做一些實驗展示tileSize和levelsOfDetailBias的關係)
分塊載入實現
代碼實現的核心,也是針對上面所提到的3個屬性和scrollview的zoomscale設置,當然,這裡面也走過一些彎路,下麵簡要介紹實現方式和中途遇到的坑 首先出場的是ViewController根頁面,沒什麼特別的:點擊按鈕,載入圖片- (void)btnTapped:(id)btnTapped { [self.scrollView removeFromSuperview]; self.largeImage = [UIImage imageWithContentsOfFile:_path]; self.scrollView = [[ImageScrollViewNoSlip alloc] initWithFrame:self.view.bounds image:self.largeImage]; [self.view addSubview:self.scrollView]; }
看到上面的代碼,就知道重點肯定在這個ImageScrollViewNoSlip了,其實這個view只是一個UIscrollview,負責圖片視圖的縮放和位置更新,與我們通常使用圖片縮放的手法無異,只是對縮放繫數的設置做了簡單計算,如果對縮放繫數要求不是很細緻,可以直接忽略計算過程,初始化內容如下:
-(id)initWithFrame:(CGRect)frame image:(UIImage*)img { if((self = [super initWithFrame:frame])) { // Set up the UIScrollView self.showsVerticalScrollIndicator = NO; self.showsHorizontalScrollIndicator = NO; self.bouncesZoom = YES; self.decelerationRate = UIScrollViewDecelerationRateFast; self.delegate = self; self.backgroundColor = [UIColor colorWithRed:0.4f green:0.2f blue:0.2f alpha:1.0f]; // 根據圖片實際尺寸和屏幕尺寸計算圖片視圖的尺寸 self.image = img; CGRect imageRect = CGRectMake( 0.0f, 0.0f, CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage)); imageScale = self.frame.size.width/imageRect.size.width; NSLog(@"imageScale: %f",imageScale); imageRect.size = CGSizeMake( imageRect.size.width*imageScale, imageRect.size.height*imageScale); //根據圖片的縮放計算scrollview的縮放級別 // 圖片相對於視圖放大了1/imageScale倍,所以用log2(1/imageScale)得出縮放次數, // 然後通過pow得出縮放倍數,至於為什麼要加1, // 是希望圖片在放大到原圖比例時,還可以繼續放大一次(即2倍),可以看的更清晰 int level = ceil(log2(1/imageScale))+1; CGFloat zoomOutLevels = 1; CGFloat zoomInLevels = pow(2, level); self.maximumZoomScale =zoomInLevels; self.minimumZoomScale = zoomOutLevels; frontTiledView = [[TiledImageViewNoSlip alloc] initWithFrame:imageRect image:image scale:imageScale]; [self addSubview:frontTiledView]; } return self; }其實在上面計算scrollview的放大倍數時,我有個疑問,如果放大的目的是讓屏幕可以顯示圖片正常的像素,那麼按照retina屏的特點,難道不應該讓這個倍數再除以屏幕的scale嗎?可是這樣一來圖片是達不到看到實際大小的效果的,這一點希望路過的大神不吝賜教~~ 然後在scrollview的代理方法,提供縮放視圖:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { return frontTiledView; }
最後要出場的就是我們的tile layer了,也就是上面scrollview中創建的TiledImageViewNoSlip,其實它也只是個普通的UIView,只是重新指定了Layer為CATiledLayer,然後在tileLayer執行分塊繪製時(調用drawrect),根據給定的區域(rect)對應到超大圖的區域進行繪製,這個view的關鍵實現如下:
+ (Class)layerClass { return [CATiledLayer class]; } -(id)initWithFrame:(CGRect)_frame image:(UIImage*)img scale:(CGFloat)scale { if ((self = [super initWithFrame:_frame])) { self.image = img; imageRect = CGRectMake(0.0f, 0.0f, CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage)); imageScale = scale; CATiledLayer *tiledLayer = (CATiledLayer *)[self layer]; //根據圖片的縮放計算scrollview的縮放次數 // 圖片相對於視圖放大了1/imageScale倍,所以用log2(1/imageScale)得出縮放次數, // 然後通過pow得出縮放倍數,至於為什麼要加1, // 是希望圖片在放大到原圖比例時,還可以繼續放大一次(即2倍),可以看的更清晰 int lev = ceil(log2(1/scale))+1; tiledLayer.levelsOfDetail = 1; tiledLayer.levelsOfDetailBias = lev; // tiledLayer.tileSize 此處tilesize使用預設的256x256即可 } return self; } -(void)drawRect:(CGRect)rect { //將視圖frame映射到實際圖片的frame CGRect rec = CGRectMake( rect.origin.x / imageScale, rect.origin.y / imageScale, rect.size.width / imageScale, rect.size.height / imageScale ); //截取指定圖片區域,重繪 CGImageRef cropImg = CGImageCreateWithImageInRect(self.image.CGImage, rec); UIImage *tileImg = [UIImage imageWithCGImage:cropImg]; [tileImg drawInRect:rect]; }
ok, 到這裡就完成了一個簡單的超大圖展示功能,下麵繼續分析一下實現原理
實現原理分析
從簡單的開始,先不管LOD和LODB,使用預設值1, 0,直接看一下tileSizetileSize
tile layer在繪製視圖層時,將View分割成最大不超過tileSize指定的尺寸,然後調用drawrect方法逐個區域繪製,註意這個“逐個”很重要,下麵會說為什麼 比如:視圖尺寸是 {375, 268}(也就是中國地圖縮放展示後的視圖尺寸),如果我們設置的尺寸超過375x268, 那麼整張圖會同時載入(註意這裡沒有設置LODB,如果設置的話結果就很樂觀了,LODB我們後面再討論),這樣和不使用tile layer的效果差不多,記憶體的峰值達到440M左右,如下圖:
使用預設的tile size256x256, 則視圖會劃分成4塊逐個載入,經過多次測試,記憶體峰值有所下降,為310M左右,畢竟這個尺寸相當於一次性繪製大部分的區域,如下圖:
設置tile size為{100, 100}, 多次測試記憶體峰值在110M左右
所以逐個載入的意義,就是分散同時繪製整個視圖的記憶體壓力,至此,tile size告一段落!
註:以上預設設置LOD和LODB,進行scrollview縮放時,layer不會進行重繪
接下來繼續探索~
levelsOfDetail
這個值表示layer在繪製時縮小層級設置,就是在縮小視圖時可以達到的最大縮小級數,可以和下麵的levelsOfDetailBias值對應起來,本次重點討論超大圖展示,主要涉及到放大,所以可以參考下麵的放大設置分析。
這個值是負責設定視圖縮小時的重繪節點, 通過實驗確實如此,在LODB預設值為0時, 無論設置LOD的值是多少,在放大圖片時,都不會進行重繪操作,視圖也就會越來越模糊,而對視圖進行縮小操作時,根據LOD的值不同,zoom scale達到觸發重繪的值會相應變化,LOD越大,zoom scale的觸發值就越小
既然LOD不影響放大的重繪結果,那就先置之不理吧,繼續看LODB~
levelsOfDetailBias
這個值表示layer在放大時,觸發重新繪製的最大層級設置。
簡單來說,就是從最小視圖需要放大多少次,才能達到我們需要的清晰度效果,註意是多少次,一次就是2倍,這個和scrollview的zoomScale不同,zoomscale表示的是放大多少倍~
數值越大,視圖在放大時可以重新渲染原圖的粒度就越細,在失真前視圖能呈現的紋理就越清晰,直到顯示到像素級別,這時tileSize已經無限接近0了,也就是每個tile負責繪製一個像素;
以上也說明瞭,繪製視圖時會分成多少個tile,除了與我們設置的tileSize有關,還與縮放級別、最大放大層級(也就是levelsOfDetailBias的值)有關。
tile的最大size為設置的tileSize尺寸,預設為256x256, 最小可以無限接近0,至於繪製時最小可以達到多少,就是levelsOfDetailBias說了算了,可以參考下圖數據表
說了這麼多,有點抽象,下表是我統計的 levelsOfDetailBias值不同時,隨著scrollview放大倍數的增加,tile可以達到的最小尺寸,省略了很多中間重繪的記錄,只寫出了每個tileSize最小時,縮放的倍數:
說明:
- 375x268是圖片視圖的frame size
- 中國地圖尺寸為:11935x8554
- levelsOfDetail = 1(預設值,不影響放大)
- tileSize = 256x256 預設值(為了不考慮tileSize對初始化和縮放的影響,此處也可以設置為375x268)
- 圖中放大倍數值為 *, 表示縮放不再影響效果
- 放大倍數的值為手動縮放的模糊值,不代表科學計算的實際值,只作為一個參考邊界
- 隨著LODB的值變大,相同的tile size時,視圖能繪製的最大尺寸會更大
- 值設置10以後,繼續放大圖片已經失真嚴重了,所以用灰色表示
- 重點觀察tileSize的最小值變化
levelsOfDetailBias與最小tileSize、zoomScale關係