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

时间: 2024-07-31 01:19:03

iOS 超大高清图展示策略 TileLayer 及 levelsOfDetailBias 分析的相关文章

Qt ,mac osx ios x11 高清屏,视网膜的支持

Qt 5.0中添加了对于retina显示的基本支持.即将到来的Qt 5.1中提供了新的API和缺陷修复,对于这一问题进行了改进.Qt 4.8也获得了良好的支持,我们反向移植了一些Qt 5的补丁. 尽管这些实现的努力和Mac以及iOS程序员最为相关,但是来看一看其它平台是如何处理高DPI显示这一问题,也是很有趣的.这里主要有两种方式: 基于DPI缩放--Win32 GDI和KDE.在这种方式中,应用程序在全物理设备分辨率下工作,使用系统提供的一个DPI设定或者缩放因子,用于缩放布局.字体通常会被操

Motorola C118 PCB原理高清图

有丝分裂高清图

https://zh.wikipedia.org/wiki/%E6%9C%89%E7%B5%B2%E5%88%86%E8%A3%82#/media/File:Animal_cell_cycle_zh-hans.svg

基于Flask实现后台权限管理系统 - 高清图

主要描述如何基于Python的Flask WEB框架实现后台权限管理系统,内容包含:用户管理.角色管理.资源管理和机构管理.

Vim 键盘指令高清图

个人感觉挺好用的 推荐大家使用windows版的vim,个人用着感觉不错,在linux上用惯了vim的朋友可以试试这个.

西安OpenParty11月29日活动高清图文回顾——新增西安APEC蓝美图!

本次活动由西安OpenParty负责线下活动组织运营,线上由InfoQ-QClub.OSChina协办. OSChina活动召集帖:运维为王--应用系统.DevOps与Docker(11月29日) Info-QClub活动召集帖:QClub西安:运维为王--应用系统.DevOps与Docker(11月29日) 报名人数统计: 总报名121人,过滤掉无效数据实际报名115人 报名来源通过:InfoQ网站宣传.金数据报名链接.OSChina报名页面三种途径. 报名参与人数比较集中的西安公司 (参加本

图像超分辨率项目帮你「拍」出高清照片

相机不够算法凑,拥有超级拍照能力的手机也离不开算法的加持.本文介绍的图像超分辨率项目可以帮你补齐相机镜头的短板. 华为 P30 发布会上展示的埃菲尔铁塔高清远距离照片 今天,一位 Reddit 网友贴出了自己基于 Keras 的图像超分辨率项目,可以让照片放大后依然清晰.先来看一下效果. 放大数倍后,照片中的蝴蝶(蛾子?)依然没有失真,背上的绒毛清晰可见 作者表示,该项目旨在改善低分辨率图像的质量,使其焕然一新.使用该工具可以对图像进行超级放缩,还能很容易地在 RDN 和 GAN上进行实验. 该

利用python爬虫关键词批量下载高清大图

前言 在上一篇写文章没高质量配图?python爬虫绕过限制一键搜索下载图虫创意图片!中,我们在未登录的情况下实现了图虫创意无水印高清小图的批量下载.虽然小图能够在一些移动端可能展示的还行,但是放到pc端展示图片太小效果真的是很一般!建议阅读本文查看上一篇文章,在具体实现不做太多介绍,只讲个分析思路. 当然,本文可能技术要求不是特别高,但可以当作一个下图工具使用. 环境:python3+pycharm+requests+re+BeatifulSoup+json 在这里插入图片描述这个确实也属实有一

css图片高清适配

同一张图片,在普通屏显示正常,但高清屏出现模糊.原因是原来一个像素的点分成的四个像素的点进行了显示. 解决方案:在高清屏中把图片变成二倍图,前提是二倍的高清图已经存在. .icon{ background: url(bg.png) no-repeat; /* 宽200px; */ } @media screen and (-webkit-min-device-pixel-ratio:1.5){ .icon{ background-image: url(bgx2.png); /* 宽400px;