iOS 超大高清圖展示策略 TileLayer 及 levelsOfDetailBias 分析


本次分析針對當下流行的中國地圖圖片處理,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的核心工作原則就是圍繞這三個屬性開展, 官方解釋如下(不喜可先略過此處解釋):

 levelsOfDetail

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划分視圖區域最大尺寸

 

下面的文章分析很透徹:

CATiledLayer (Part 2)

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,直接看一下tileSize

tileSize

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關系 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM