IOS 解决问题:一万像素超大地图分割显示

原创:问题解决型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 问题
  • 解决方案
  • 方案一:使用UIScrollView
  • 方案二:使用UITableView
  • 方案三:WKWebview
  • 方案四:CATiledLayer
  • Demo
  • 参考文献

问题

生成了一张大长图,用UIImageView直接设置image展示不了,因为内存占用很大,可能导致崩溃问题。


解决方案

比较笨的方案一:使用UIScrollView

方案步骤:
步骤一:通过循环体把大长图裁剪成多个等份,等份划分可以用长图的高度对屏幕高度进行相除,份数为除数+1,这样每一个等份就是当前屏幕大小的一张小图。
步骤二:渲染出每一个等份为一张小图,将其放置在scrollView当前屏幕显示的部分中。
步骤三:需要考虑两件事情,当前正显示在屏幕中的小图的上一张图,需要将其内存释放掉,而对于其下一张图,需要预先获取将其渲染出来,防止滑动不流畅的效果。

方案缺点:各种计算尺寸,想想都不寒而栗。

在方案一基础上改进后的方案二:使用UITableView

针对方案一中,需要考虑到当前正显示在屏幕中的小图的上一张图将其内存释放掉,对于其下一张图预先获取将其渲染,这个步骤,有没有感觉到很熟悉,灵光乍现这不就是我们的cell的重用机制吗?以前学习重用机制的时候,仿系统自己写了一个索引的Demo,印象比较深刻,不由得联想到了。

优势:

  • 和重用机制的共同点是都只初始化当前可见部分的对象的内存,而其余的对象始终重用这部分内存,只是绘制的内容改变了,这样就解决了我们长图造成的内存峰值崩溃问题。
  • 通过UITableView,系统帮我们自动实现了重用机制,不用我们再去考虑上一张图的内存释放掉,下一张图的预先渲染问题,大大简化了问题的复杂度。

方案步骤:
步骤一:通过循环体把大长图裁剪成多个等份,小图个数 = 长图高度 / Row行高 + 1
步骤二:分割图片保存到沙盒
步骤三:从沙盒中获取到缓存后的图片进行展示

缺点:1、因为该方案是加载多个小图片段,那么便不能实现类似缩略图那种放大缩小的功能了。2、存在布局计算、裁剪、缓存等操作,耗时。

相对较完美的方案三:WKWebview

跳出IOS编程的惯有思维,从IOS原生到借助H5来实现,复杂的问题丢给它解决,既解决了问题又简化了代码,就像数学题,简便的方法很难想到,容易想到的方法实现起来却很复杂。

其基本思路是将image包装为HTML代码,再使用WKWebView加载HTML代码。

不用我们操心的方案四:后端

按照常理来说,后端不应该返回这么占用大内存的图片给我们前端,他们应该控制下图片的大小再返回。我们前端遇到这种大长图需要自己解决的场景更多的是屏幕截长图后再分享出去,类似微信中的聊天记录一次性截取成一张超长图再分享出去。


方案一:使用UIScrollView

最初使用scrollView,生成用于长图滚动的scrollView和用于显示长图的imageView,配置二者属性后将其添加到之前创建的scrollViewimageViewscrollView的高度根据图片的高度来定,所以分别写了以下代码:

- (UIScrollView *)scrollView {
    if (!_scrollView) {
        _scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, ScreenHeight)];
        // 设置滚动视图的大小
        _scrollView.contentSize = CGSizeMake(0, ScreenHeight * 6);
        // 不反弹
        _scrollView.bounces = NO;
        // 隐藏水平滚动条
        _scrollView.showsHorizontalScrollIndicator = NO;
        // 隐藏垂直滚动条
        _scrollView.showsVerticalScrollIndicator = NO;
    }
    return _scrollView;
}

- (UIImageView *)imageView {
    if (!_imageView) {
        // 长图高度
        CGFloat imageHeight = [self getLongPictureHeight];
        _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, imageHeight)];

        NSString *path = [[NSBundle mainBundle] pathForResource:@"longPicture" ofType:@"jpg"];
        // 设置imageView显示的长图
        _imageView.image = [UIImage imageWithContentsOfFile:path];
        // 设置imageView的绘制模式
        _imageView.contentMode = UIViewContentModeScaleAspectFit;
        // 把imageView添加到当前的滚动视图中
        [self.scrollView addSubview:_imageView];
    }
    return _imageView;
}

// 获取长图的高度
- (CGFloat)getLongPictureHeight {
    UIImage *image = [self getLongImage];
    CGFloat imageHeight = image.size.height;
    return imageHeight;
}
最初效果

上面只是简单地把长图放了上去,现在需要考虑如何切分渲染和清除以及计算尺寸,这方案实现起来比较困难,而且也没必要,我写了一部分就直接全删掉进入了方案二,两个方案有些重合的部分也可作方案一的参考。


方案二:使用UITableView

有些代码其实已经写好了,但是为了讲清楚为什么会这样做的思路,选择先删除掉然后再在解释其作用之后将其添加上。

a、创建TableView,这里要注意因为我们只是展示长图,不是列表滑动,所以要去掉右侧滑块。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self createSubViews];
    [self createSubViewsConstraints];
}

// 添加子视图
- (void)createSubViews {
    self.view.backgroundColor = [UIColor whiteColor];
     // 建表
    self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
    self.tableView.bounces = NO;
    self.tableView.delegate = self;
    self.tableView.dataSource = self;
    self.tableView.showsVerticalScrollIndicator = NO;// 不显示右侧滑块
    self.tableView.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:self.tableView];
}

// 添加约束
- (void)createSubViewsConstraints {
    [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view);
    }];
}

b、为了实现TableViewUITableViewDataSource协议,我们需要获取将长图裁剪后等份小图后的数量,而数量的计算方式在TableView是有别是scrollView的,因为TableView每个cell有行高这个属性,我们要将每张小图绘制在一个cell上,而每张小图的高度即是cell的行高,所以计算公式改变为:小图个数 = 长图高度 / Row行高 + 1,为什么要+1,因为存在除不尽即不满一张图的情况,但是我们是还是需要把它绘制出来的,不然就丢失了。所以这里我们首先需要获取长图:

// 获取长图
- (UIImage *)getLongImage {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"longPicture" ofType:@"jpg"];
    UIImage *image = [UIImage imageWithContentsOfFile:path];
    return image;
}

再获取到长图的高度,但是为了裁剪时候能够按照图片本身的宽度来裁剪,不丢失或留空白,所以最好修改为直接获取长图的尺寸:

// 获取长图的尺寸
- (CGSize)getLongImageSize {
    UIImage *image = [self getLongImage];
    return image.size;
}

接下来计算小图个数,这里以行高50为例,一般工程中行高也是这个数值:

#define ScreenWidth  [UIScreen mainScreen].bounds.size.width
#define ScreenHeight [UIScreen mainScreen].bounds.size.height
#define RowHeight 50

// 行高
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return RowHeight;
}

// 计算小图个数
CGFloat longImageHeight = [self getLongImageSize].height;
int count = longImageHeight / RowHeight + 1;

c、进入重要的一步,裁剪长图为等份小图。实现方案有好几种,这里使用最简便的CGImage方式,需要注意以下几点关于裁剪区域的计算,每张小图的X坐标始终为0,Y坐标从0、50、100...变化,防止丢失或留白使用长图本身宽度,按照行高来作为小图高度。

UIImage *longImage = [self getLongImage];
CGFloat longImageWidth = [self getLongImageSize].width;

// 长图裁剪成一张张小图
for (int i = 0; i < count; i++) {
    // 裁剪区域:每张小图的X坐标始终为0,Y坐标从0、50、100...变化,防止丢失或留白使用长图本身宽度,按照行高来作为小图高度
    CGRect cropRect = CGRectMake(0, RowHeight * i, longImageWidth, RowHeight);
    // 获取小图片
    CGImageRef smallImageRef = CGImageCreateWithImageInRect([longImage CGImage], cropRect);
    // 将图片转为UIImage
    UIImage *newImage = [UIImage imageWithCGImage:smallImageRef];
    // 释放
    CGImageRelease(smallImageRef);
}

d、裁剪完的这些小图保存在什么地方呢?当然可以直接保存在NSMutableArray中,之后numberOfRows直接调用数组数量即可,但是像超长图这种内存达到足以crash的对象,我们应该下意识地想到要缓存,这里通过将其保存到沙盒中再从沙盒中取出即可,首先保存到沙盒中,需要注意通过字典的方式存储到沙盒中的时候,value不能直接使用UIImage,需要转化为NSData,否则无法保存,本地沙盒的Document目录下为空。

// 直接存储在沙盒中
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
NSString *filePath = [path stringByAppendingPathComponent:@"newImages.plist"];

// 长图裁剪成一张张小图并保存到沙盒
for (int i = 0; i < count; i++) {
    if (newImage) {
        // 在本地资源的情况下,优先使用 PNG格式文件,如果资源来源于网络,最好采用JPEG 格式文件
        NSData *imageData = UIImagePNGRepresentation(newImage);
        
        // 通过字典的方式存储到沙盒,注意到value不能直接使用UIImage,需要转化为NSData,否则无法保存
        NSString *key = [NSString stringWithFormat:@"newImage%d",i+1];
        [plistDict setObject:imageData forKey:key];
    }
}
// 其中atomically表示是否需要先写入一个辅助文件,再把辅助文件拷贝到目标文件地址。
// 这是更安全的写入文件方法,一般都写YES。
[plistDict writeToFile:filePath atomically:YES];

检验一下看是否顺利存储到了沙盒中,我们在Document目录下顺利找到了newImages.plist,其结构如下:

存储在沙盒

将上述的代码封装成一个裁剪方法cutLongImage,其功能为分割图片保存到沙盒,在viewDidLoad中调用,需要注意的是一般是按照懒加载的模式来调用耗时操作,但是这里该方法就是我们的数据来源,是一进来就需要显示的,所以在此调用:

// 分割图片保存到沙盒
- (void)cutLongImage {
    // 计算小图个数
    UIImage *longImage = [self getLongImage];
    CGFloat longImageWidth = [self getLongImageSize].width;
    CGFloat longImageHeight = [self getLongImageSize].height;
    int count = longImageHeight / RowHeight + 1;
    
    // 直接存储在沙盒中
    NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
    NSString *filePath = [path stringByAppendingPathComponent:@"newImages.plist"];
    
    NSMutableDictionary *plistDict = [NSMutableDictionary dictionary];
    // 长图裁剪成一张张小图并保存到沙盒
    for (int i = 0; i < count; i++) {
        
        // 裁剪区域:每张小图的X坐标始终为0,Y坐标从0、50、100...变化,防止丢失或留白使用长图本身宽度,按照行高来作为小图高度
        CGRect cropRect = CGRectMake(0, RowHeight * i, longImageWidth, RowHeight);
        // 获取小图片
        CGImageRef smallImageRef = CGImageCreateWithImageInRect([longImage CGImage], cropRect);
        // 将图片转为UIImage
        UIImage *newImage = [UIImage imageWithCGImage:smallImageRef];
        // 释放
        CGImageRelease(smallImageRef);
        
        
        if (newImage) {
            // 在本地资源的情况下,优先使用 PNG格式文件,如果资源来源于网络,最好采用JPEG 格式文件
            NSData *imageData = UIImagePNGRepresentation(newImage);
            
            // 通过字典的方式存储到沙盒,注意到value不能直接使用UIImage,需要转化为NSData,否则无法保存
            NSString *key = [NSString stringWithFormat:@"newImage%d",i+1];
            [plistDict setObject:imageData forKey:key];
        }
    }
    // 其中atomically表示是否需要先写入一个辅助文件,再把辅助文件拷贝到目标文件地址。
    // 这是更安全的写入文件方法,一般都写YES。
    [plistDict writeToFile:filePath atomically:YES];
}

e、接下来从沙盒中获取到缓存后的图片:

// 从沙盒中取出图片数组
- (NSArray *)getNewImages {
    NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
    NSString *filePath = [path stringByAppendingPathComponent:@"newImages.plist"];
    NSDictionary *plistDict = [[NSDictionary alloc] initWithContentsOfFile:filePath];
    
    CGFloat longImageHeight = [self getLongImageSize].height;
    int count = longImageHeight / RowHeight + 1;
    
    NSMutableArray *images = [[NSMutableArray alloc] init];
    for (int i = 0; i < count; i++) {
        NSString *key = [NSString stringWithFormat:@"newImage%d",i+1];
        NSData *newImageData = [plistDict objectForKey:key];
        UIImage *newImage = [UIImage imageWithData:newImageData];
        if (newImage) {
            [images addObject:newImage];
        }
    }
    return images;
}

f、这样就可以试下UITableViewDataSource协议了:

//节数
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

//行数
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self getNewImages].count;
}

关于cellForRow:,我们可以直接使用UITableViewCell自带的cell.imageView,但是这里为了标准示范,也因为实际工程中一个cell上肯定不可能只放一张图片,会有其他很多自定义内容,所以新建了一个Cell文件:

#import 

NS_ASSUME_NONNULL_BEGIN

@interface CutLongImageTableViewCell : UITableViewCell

@property (nonatomic, strong, readonly) UIImageView *cutImageView;

@end

NS_ASSUME_NONNULL_END


#import "CutLongImageTableViewCell.h"
#import 

@interface CutLongImageTableViewCell ()

@property (nonatomic, strong, readwrite) UIImageView *cutImageView;

@end

@implementation CutLongImageTableViewCell

#pragma mark - Life cycle

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        [self createSubViews];
        [self createSubViewsConstraints];
    }
    return self;
}

+ (BOOL)requiresConstraintBasedLayout {
    return YES;
}

- (void)dealloc {
    NSLog(@"%@ - dealloc", NSStringFromClass([self class]));
}

#pragma mark - Private Methods

// 添加子视图
- (void)createSubViews {
    [self.contentView addSubview:self.cutImageView];
}

// 添加约束
- (void)createSubViewsConstraints {
    [self.cutImageView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.contentView);
    }];
}

#pragma mark - Getters and Setters

- (UIImageView *)cutImageView {
    if (!_cutImageView) {
        _cutImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
        // 设置imageView的绘制模式
        _cutImageView.contentMode = UIViewContentModeScaleAspectFit;
        _cutImageView.backgroundColor = [UIColor blueColor];
    }
    return _cutImageView;
}

@end

使用该自定义的该Cell来实现cellForRow:方法:

//设置每行对应的cell(展示的内容)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    CutLongImageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[CutLongImageTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    NSArray *images = [self getNewImages];
    cell.cutImageView.image = images[indexPath.row];
    return cell;
}

可以运行了,看下效果:

存在留白

注意到前面我将_cutImageView.backgroundColor = [UIColor blueColor];设置为了蓝色,发现出现了留白部分,包括表头和表尾,cell之间,优化如下:

self.tableView.showsVerticalScrollIndicator = NO;//不显示右侧滑块
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;//去掉分割线
self.tableView.sectionHeaderHeight = 0.1;//表头表尾不留白
self.tableView.sectionFooterHeight = 0.1;

// 去掉留白部分
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    return 0.1;//设置为0不起作用
}

再将imageView的绘制模式从UIViewContentModeScaleAspectFit设置为填充满:

// 设置imageView的绘制模式
_cutImageView.contentMode = UIViewContentModeScaleToFill;
解决留白

g、微博的长图显示是占满整个屏幕的,所以考虑要隐藏掉导航栏和状态栏,两种实现方式。
方式一:通过frame

// 获取导航栏和状态栏的高度
- (CGFloat)getBarHeight {
    // 状态栏的高度
    CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame];
    // 导航栏的高度
    CGRect navigationBarFrame = self.navigationController.navigationBar.frame;
    
    // 返回二者之和
    CGFloat barHeight = statusBarFrame.size.height + navigationBarFrame.size.height;
    return barHeight;
}

方式二:通过系统方法,更加简单,这里使用这种

// 隐藏导航栏
[self.navigationController setNavigationBarHidden:YES animated:YES];

隐藏状态栏的时候遇到了一点问题,在我们将View controller-based status bar appearance设置为NO后,因为只是单一页面隐藏状态栏,所以通过这种方式实现:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    // 隐藏状态栏
    [UIApplication sharedApplication].statusBarHidden = YES;
}

-(void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
    
    // 恢复状态栏
    [UIApplication sharedApplication].statusBarHidden = NO;
}

但是运行结果很搞笑的是只隐藏了文字,并没有完全隐藏状态栏:

状态栏只隐藏了文字

原因是在iOS11中可能是为了适配iPhoneX的缘故,tableview就会给状态栏空出20像素,寻找解决方案后发现ScrollView新增了一个属性contentInsetAdjustmentBehavior,如果想要让tableview充满屏幕,只要将这个属性设置为不要自动调整即可:

// 不要自动调整
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
大功告成

方案三:WKWebview

上面那种实现方案,有两个缺点:1、由于是加载多个小图片段,那么便不能实现类似缩略图那种放大缩小的功能了。2、存在布局计算、裁剪、缓存等操作,耗时。

跳出IOS编程的惯有思维,从IOS原生到借助H5来实现,复杂的问题丢给它解决,既解决了问题又简化了代码,就像数学题,简便的方法很难想到,容易想到的方法实现起来却很复杂。其基本思路是将image包装为HTML代码,再使用WKWebView加载HTML代码。

@property(nonatomic, strong) WKWebView *webView;// 用于直接显示长图

#pragma mark - Life Circle

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor whiteColor];
    
    // 使用 WKWebView 加载 HTML 代码
    self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, ScreenHeight)];
    NSString *imgHtml = [self htmlForJPGImage:[self getLongImage]];
    [self.webView loadHTMLString:imgHtml baseURL:nil];
    
    [self.view addSubview:self.webView];
}

#pragma mark - Private Methods

// 获取长图
- (UIImage *)getLongImage {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"longPicture" ofType:@"jpg"];
    UIImage *image = [UIImage imageWithContentsOfFile:path];
    return image;
}

// 将 image 包装为 HTML 代码
- (NSString *)htmlForJPGImage:(UIImage *)image {
    NSData *imageData = UIImageJPEGRepresentation(image,1.f);
    // 图片编码->通过字符串接收,每64个字符插入\r或\n
    NSString *imageBase64 = [imageData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
    return [NSString stringWithFormat:@"
",imageBase64]; }
WKWebView

基本实现了需求,优化以后有时间再补充。


方案四:CATiledLayer

探索过程

在比较大的frame内绘图时内存爆炸,超过50M的图会直接Crash,这个问题也很好解决,就是将它分块显示,做一个循环,计算分块区域,分别显示图片相应的区域,就是我之前用TableView做的方式。还有个问题就是在放大重绘时会卡顿,一点都不流畅,而且内存也会暴增,在尝试过将重绘代码放进@autoreleasepool后,仍然不太流畅,不过内存倒是减少了(尝试过添加多线程,但是不能实时更新视图)。经过分析后,第二个问题主要原因是无论放到多大,重绘仍会将图片全部绘制出来,但是我们在屏幕上是只看到图片的部分区域,如下图

只看到图片的部分区域

所以只需要显示图片在屏幕的区域就可以了,在循环中加上判断绘制区域是否在屏幕上,是的话就绘制,不是的话就不绘制。


CATiledLayer简介

CATiledLayer是为载入大图造成的性能问题提供的一个解决方案,他将需要绘制的内容分割成许多小块,然后在许多线程里按需异步绘制相应的小块,具体如何划分小块和缩放时的加载策略,所以逐个加载的意义,就是分散同时绘制整个视图的内存压力,这与CATiledLayer三个重要属性有关。

一句话描述:tile layer设置一个缩放区域的集合和重绘阈值,让scroll view在缩放时,绘制层根据这些区域和缩放阈值去重新绘制当前显示的区域。

CATiledLayer类似瓦片视图,可以将绘制分区域进行,常用于一张大的图片的分部绘制,如图:

瓦片视图

使用这个layer的好处

  • 不需要你自己计算分块显示的区域,它直接提供,你只需要根据这个区域计算图片相应区域,然后画图就可以了。
  • 它是在其他线程画图,不会因为阻塞主线程而导致卡顿。
  • 它自己实现了只在屏幕区域显示图片,屏幕区域外不会显示,而且当移动图片时,它会自动绘制之前未绘制的区域,当你缩放时它也会自动重绘。

使用方法

示例中,我们将会从一个11935 × 8554分辨率的中国地图入手。这张图花了我2元钱购得,珍惜啊~

中国地图

a、直接加载,看下效果

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            
            NSString *path = [[NSBundle mainBundle] pathForResource:[@"地图.jpg" stringByDeletingPathExtension] ofType:[@"地图.jpg" pathExtension]];
            UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageWithContentsOfFile:path]];
            imageView.frame = self.view.frame;
            
            [self.view addSubview:imageView];
        });
    });
}

Gif动画展示如下:

直接加载原图内存占用达400MB

可以看到加载时内存的瞬间峰值达到了481M,毫无疑问,内存暴增,可能导致直接崩溃。

这是前辈的GIF图,我自己试验了一下,当图片内存达到20MB以上的时候,直接加载是不反应的,既不会显示图片,也不会导致内存暴涨,被系统无视掉了。

当图片内存达到20MB以上的时候,直接加载是不反应的

但是我又试过用13MB的图片,发现是可以正常显示的,只是加载稍微缓慢,但并没有出现暴涨的情况。所以我也不知道这个问题是否有意义,我在这上面耗费了一些时间。

用13MB的图片,发现是可以正常显示的

b、主角上场~CATiledLayer,即分块加载方式。先创建用于显示地图的按钮:

创建用于显示地图的按钮

按钮中的文本tileCount的含义有的模糊,我本来以为是切片数量,结果发现实验结果并不是, tiledSize的设置主要是影响CATiledLayer的切片数量,其计算公式如下:

CATiledLayer *tileLayer = (CATiledLayer *)self.layer;
NSInteger tileSizeScale = sqrt(self.tileCount)/2;//通过`sqrt`平方根计算后再除以2 当tileCount = 16时这里tileSizeScale = 2
// tiledSize的设置主要是影响CATiledLayer的切片数量
CGSize tileSize = self.bounds.size;// (CGSize) tileSize = (width = 414, height = 276)
tileSize.width /= tileSizeScale;
tileSize.height/= tileSizeScale;
tileLayer.tileSize = tileSize;// (CGSize) tileSize = (width = 207, height = 138)

我用的tileCount = 16来测试,得到这里tileSizeScale = 2。当tileCount不同的时候,tileSizeScale也就不同,虽然self.bounds.size不变,都是地图图片按照手机屏幕宽高按照比例缩小后的大小,但是因为tileSizeScale变了,导致tileLayer.tileSize发生了改变,所以系统切片的数量发生了改变。但是tileCount的值并不代表切片数量,正如你所看到的需要经过一系列的计算,实际切片数量会更多。比如tileCount = 16,16*138(tile height)并不等于图片实际高度4912,所以其实切片数量更多。

创建用于显示地图的按钮:

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self createSubviews];
}

#pragma mark - 创建视图

- (void)createSubviews
{
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"重新选择" style:UIBarButtonItemStylePlain target:self action:@selector(clearButtonAction:)];
    
    self.button0 = [self createButton:100 title:@"默认" andAction:@selector(buttonAction:) andtileCount:0];
    self.button4 = [self createButton:220 title:@"tileCount = 4" andAction:@selector(buttonAction:) andtileCount:4];
    self.button16 = [self createButton:340 title:@"tileCount = 16" andAction:@selector(buttonAction:) andtileCount:16];
    self.button36 = [self createButton:460 title:@"tileCount = 36" andAction:@selector(buttonAction:) andtileCount:36];
    self.button64 = [self createButton:580 title:@"tileCount = 64" andAction:@selector(buttonAction:) andtileCount:64];
    self.button100 = [self createButton:700 title:@"tileCount = 100" andAction:@selector(buttonAction:) andtileCount:100];
    [self.view addSubview:self.button0];
    [self.view addSubview:self.button4];
    [self.view addSubview:self.button16];
    [self.view addSubview:self.button36];
    [self.view addSubview:self.button64];
    [self.view addSubview:self.button100];
}

- (UIButton *)createButton:(CGFloat)y title:(NSString*)title andAction:(SEL)action andtileCount:(NSInteger)tileCount
{
    UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(150, y, 150, 100)];
    button.tag = tileCount;// 根据tag可以获取到tileCount的值
    button.backgroundColor = [UIColor blackColor];
    [button setTitle:title forState:UIControlStateNormal];
    [button addTarget:self action:action forControlEvents:UIControlEventTouchUpInside];
    return button;
}

// 按钮点击的事件
- (void)buttonAction:(UIButton *)button
{
    // 移除按钮
    [self.button0 removeFromSuperview];
    [self.button4 removeFromSuperview];
    [self.button16 removeFromSuperview];
    [self.button36 removeFromSuperview];
    [self.button64 removeFromSuperview];
    [self.button100 removeFromSuperview];
    
    // 根据tag可以获取到tileCount的值
    NSInteger tileCount = button.tag;
    // 创建地图图片
    self.largeImageView = [[LargeImageView alloc] initWithImageName:@"地图.jpg" andTileCount:tileCount];// frame = (0 309.85; 414 276.3) layer = 
    // 移动到中心位置
    self.largeImageView.center = self.view.center;// frame = (0 309.85; 414 276.3) layer = 
    // 显示地图图片
    [self.view addSubview:self.largeImageView];
}

c、为了方便测试不同tileCount的效果,在导航栏加了重置按钮,其点击事件如下:

// 重新选择tileCount
-(void)clearButtonAction:(UIButton *)button
{
    [self.largeImageView removeFromSuperview];
    self.largeImageView = nil;
    
    [self.view addSubview:self.button0];
    [self.view addSubview:self.button4];
    [self.view addSubview:self.button16];
    [self.view addSubview:self.button36];
    [self.view addSubview:self.button64];
    [self.view addSubview:self.button100];
}

d、创建地图视图

@interface LargeImageView : UIView

// 创建地图图片
-(UIView *)initWithImageName:(NSString*)imageName andTileCount:(NSInteger)tileCount;

@end


#import "LargeImageView.h"

@interface LargeImageView ()

@property (strong, nonatomic) NSString *imageName;// 要被切片的大内存图
@property (assign, nonatomic) NSInteger tileCount;// 会影响切片数量
@property (strong, nonatomic) UIImage *originImage;// 原始图
@property (assign, nonatomic) CGRect imageRect;// 图片大小
@property (assign, nonatomic) CGFloat imageScale;// 图片缩放比例

@end

@implementation LargeImageView

- (id)initWithImageName:(NSString*)imageName andTileCount:(NSInteger)tileCount
{
    self = [super init];
    if(self)
    {
        self.imageName = imageName;
        self.tileCount = tileCount;
        
        [self createView];
    }
    return self;
}

其中,createView内是关键部分,比较繁琐和复杂,里面标识的尺寸是当tileCount = 16时候得到的,不同设备不同,不用太在意。其中第一部分作用是设置地图视图frame为原图按照比例缩小后的尺寸:

- (void)createView
{
// 第一部分:缩小
    // 屏幕尺寸
    CGRect bounds = [[UIScreen mainScreen] bounds];// (CGRect) bounds = (origin = (x = 0, y = 0), size = (width = 414, height = 896))
    CGSize screenSize = bounds.size;// (CGSize) screenSize = (width = 414, height = 896)

    // 获取地图原图及其尺寸大小
    NSString *path = [[NSBundle mainBundle] pathForResource:[_imageName stringByDeletingPathExtension] ofType:[_imageName pathExtension]];
    self.originImage = [UIImage imageWithContentsOfFile:path];
    CGSize originImageSize = self.originImage.size;// (CGSize) originImageSize = (width = 7360, height = 4912)
    
    // 如果原图宽度>高度,宽度改为屏幕宽度,同时按照比例缩小高度
    CGSize viewSize = CGSizeZero;
    if (originImageSize.width > originImageSize.height)// YES
    {
        viewSize.width = screenSize.width;// width = 414
        viewSize.height = screenSize.width/originImageSize.width * originImageSize.height;// (414/7360)* 4912 = 276.3
    }
    // 设置地图视图frame为原图按照比例缩小后的尺寸
    self.frame = CGRectMake(0, 0, viewSize.width, viewSize.height);

第一部分达到了这样的效果:

设置地图视图frame为原图按照比例缩小后的尺寸

第二部分切片是精华所在,也是本Demo之所以存在的理由。上一部分进行了缩放,这部分首先获取缩放比例:

// 第二部分:切片
    // 缩放比例
    // imageRect 等于 原图尺寸大小
    self.imageRect = CGRectMake(0.0f, 0.0f, CGImageGetWidth(self.originImage.CGImage), CGImageGetHeight(self.originImage.CGImage));// _imageRect = (origin = (x = 0, y = 0), size = (width = 7360, height = 4912))
    // imageScale 是按照等比例缩小后的地图和原图的宽度作比得到的缩小系数,相当于比例尺吧
    self.imageScale = self.frame.size.width/self.imageRect.size.width;// 414/7360 = 0.056

接着创建CATiledLayer图层,并设置属性信息:

// 创建图层,并设置属性信息
CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];

// ceil向上取整 lev = 6
int lev = ceil(log2(1/self.imageScale)) + 1;

// 从最小视图需要放大多少次,才能达到我们需要的清晰度效果,注意是多少次,一次就是2倍
// 指的是该图层缓存的放大LOD数目,默认为0,即不会额外缓存放大层次,每进一级会对前一级两倍分辨率进行缓存
tiledLayer.levelsOfDetailBias = lev;

// 产生模糊的根源是图层的细节层次
// 缩小视图是,最大可以达到的缩小级数:指的是该图层缓存的放大LOD数目,默认为0,即不会额外缓存放大层次,每进一级会对前一级两倍分辨率进行缓存
tiledLayer.levelsOfDetail = 1;

levelsOfDetailBias这个值表示layer在放大时,触发重新绘制的最大层级设置。简单来说,就是从最小视图需要放大多少次,才能达到我们需要的清晰度效果,注意是多少次,一次就是2倍,这个和scrollviewzoomScale不同,zoomscale表示的是放大多少倍~

数值越大,视图在放大时可以重新渲染原图的粒度就越细,在失真前视图能呈现的纹理就越清晰,直到显示到像素级别,这时tileSize已经无限接近0了,也就是每个tile负责绘制一个像素;

以上也说明了,绘制视图时会分成多少个tile,除了与我们设置的tileSize有关,还与缩放级别、最大放大层级(也就是levelsOfDetailBias的值)有关。

tile的最大size为设置的tileSize尺寸,默认为256x256,最小可以无限接近0,至于绘制时最小可以达到多少,就是levelsOfDetailBias说了算了。

  • 375x268是图片视图的frame size
  • 中国地图尺寸为:11935x8554
  • levelsOfDetail = 1(默认值,不影响放大)
  • tileSize = 256x256 默认值(为了不考虑tileSize对初始化和缩放的影响)
  • 图中放大倍数值为 *, 表示缩放不再影响效果
  • 放大倍数的值为手动缩放的模糊值,不代表科学计算的实际值,只作为一个参考边界
  • 随着LODB的值变大,相同的tile size时,视图能绘制的最大尺寸会更大
  • 值设置10以后,继续放大图片已经失真严重了,所以用灰色表示
levelsOfDetailBias 放大部数 最小tileSize
0 * 256x256
1 * 128x128
2 * 64x64
3 2.3 32x32
4 5.42 16x16
5 7.47 8x8
6 15.1 4x4
7 28.8 2x2
8 57.9 1x1
9 115 0.5x0.5
10 230 0.25x0.25

最后,就是代入切片的计算公式,来计算tileSize

if(self.tileCount > 0)// 16 > 0
{
    NSInteger tileSizeScale = sqrt(self.tileCount)/2;// sqrt平方根计算 tileSizeScale = 2
    // 用于创建层内容的每个平铺的最大大小,默认为256*256
    // 如果tileSize设置太小就会把每块瓷砖图片展示的很小
    CGSize tileSize = self.bounds.size;// tileSize = (width = 414, height = 276.3)
    tileSize.width /= tileSizeScale;
    tileSize.height /= tileSizeScale;
    tiledLayer.tileSize = tileSize;// tileSize = CGSize (207 138.15)
}

e、如果现在直接运行的话是会直接崩溃的,会报错如下:

[CALayer setLevelsOfDetailBias:]: unrecognized selector sent to instance

因为tiledLayer图层只是强制转化了一下:

// 创建图层,并设置属性信息
CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];

还需要我们重写layerClass方法:

// 会调用到,返回CATiledLayer的class
+ (Class)layerClass
{
    return [CATiledLayer class];
}

然后运行结果一片黑暗:

运行结果一片黑暗

f、黑暗的原因是因为我们还么有对切片进行绘制,会反复调用到drawRect方法直到所有切片绘制完成。该方法中首先需要获取手机屏幕上切片区域大小,再计算出每张小图的裁切区域映射到原图中的位置和尺寸大小:

// 会反复调用到,直到切片数目绘制完成
-(void)drawRect:(CGRect)rect
{
    // _originImage: {7360, 4912} 原图尺寸
    // (CGFloat) _imageScale = 0.0562 缩放比例尺
    
    // (CGRect) rect = (origin = (x = 207, y = 115), size = (width = 34.5, height = 23))
    // rect代表在手机屏幕上裁剪区域,其x、y会一直变化,但是size不变,有多少个切片数量就变化多少次
    
    // 计算出每张小图的裁切区域映射到原图中的位置和尺寸大小
    CGFloat imageCutX = rect.origin.x / self.imageScale;
    CGFloat imageCutY = rect.origin.y / self.imageScale;
    CGFloat imageCutWidth = rect.size.width / self.imageScale;
    CGFloat imageCutHeight = rect.size.height / self.imageScale;
    
    // (CGRect) imageCutRect = (origin = (x = 3680, y = 2044.4), size = (width = 613.3, height = 408.8))
    // 第一行:x = 3066 x = 3680 x = 4293... y = 2453 不变
    // 第二行:x = 3066 x = 4293... y = 2862 不变
    // 第三行:x = 4293... y = 2044.4不变
    // ...何时结束,取决于有多少个切片
    CGRect imageCutRect = CGRectMake(imageCutX, imageCutY, imageCutWidth, imageCutHeight);

接着截取原图中指定裁剪区域,进行重绘:

// 截取原图中指定裁剪区域,重绘
@autoreleasepool
{
    CGImageRef imageRef = CGImageCreateWithImageInRect(self.originImage.CGImage, imageCutRect);
    UIImage *tileImage = [UIImage imageWithCGImage:imageRef];
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    UIGraphicsPushContext(context);
    [tileImage drawInRect:rect];
    UIGraphicsPopContext();
}

作为测试,看看总共绘制了多少个切片:

static NSInteger drawCount = 1;
drawCount++;
NSLog(@"正在切第%ld片,其rect为:(x:%f, y:%f, width:%f, height:%f )",drawCount,(float)imageCutX,(float)imageCutY,(float)imageCutWidth, (float)imageCutHeight);

tileCount = 4时,输出结果为:

2020-07-30 15:48:46.348275+0800 Demo[91913:24633001] 正在切第2片,其rect为:(x:3680.000000, y:2453.333252, width:1226.666626, height:817.777771 )
2020-07-30 15:48:46.367439+0800 Demo[91913:24633001] 正在切第3片,其rect为:(x:3680.000000, y:3271.111084, width:1226.666626, height:817.777771 )
2020-07-30 15:48:46.385109+0800 Demo[91913:24633001] 正在切第4片,其rect为:(x:4906.666504, y:2453.333252, width:1226.666626, height:817.777771 )
......
2020-07-30 15:48:46.975027+0800 Demo[91913:24633001] 正在切第36片,其rect为:(x:1226.666626, y:0.000000, width:1226.666626, height:817.777771 )
2020-07-30 15:48:46.992828+0800 Demo[91913:24633001] 正在切第37片,其rect为:(x:0.000000, y:0.000000, width:1226.666626, height:817.777771 )

再看看默认(tileCount = 0)和tileCount分别为4、16、36、64、100是的切片尺寸大小:

NSLog(@"切片宽度:%f,切片高度:%f",(float)tiledLayer.tileSize.width,(float)tiledLayer.tileSize.height);

输出结果为:

2020-07-30 16:13:11.506160+0800 Demo[92018:24652849] 切片宽度:256.000000,切片高度:256.000000
2020-07-30 16:13:15.668403+0800 Demo[92018:24652849] 切片宽度:414.000000,切片高度:296.720245
2020-07-30 16:13:28.741029+0800 Demo[92018:24652849] 切片宽度:207.000000,切片高度:148.360123
2020-07-30 16:13:45.177261+0800 Demo[92018:24652849] 切片宽度:138.000000,切片高度:98.906746
2020-07-30 16:13:50.903651+0800 Demo[92018:24652849] 切片宽度:103.500000,切片高度:74.180061
2020-07-30 16:13:55.921279+0800 Demo[92018:24652849] 切片宽度:82.800003,切片高度:59.344048

现在运行效果为:

运行效果

g、平移手势,里面有些坑点,最好自己亲自动手实验一下。

// 平移手势
static CGPoint originCenter;
-(void)panGestureAction:(UIPanGestureRecognizer*)gesture
{
    // 拖拽的距离(距离是一个累加)
    CGPoint trans = [gesture translationInView:self.view];
    NSLog(@"拖拽的距离(距离是一个累加): %@",NSStringFromCGPoint(trans));
    
    // 设置图片移动,center会平移过程中会一直变化
    CGPoint center = self.largeImageView.center;
    center.x += trans.x;
    center.y += trans.y;
    self.largeImageView.center = center;
    NSLog(@"图片移动后:%@",NSStringFromCGRect(self.largeImageView.frame));
    
    // 清除累加的距离,否则拖动的距离会在上次拖动数值基础上增加,造成视图移动飞快
    [gesture setTranslation:CGPointZero inView:self.view];
    
    // largeImageView的frame发生了改变,会调用setFrame方法进行重绘
    if (gesture.state == UIGestureRecognizerStateBegan)// 平移开始时
    {
        // 保存最初出发点
        originCenter = self.largeImageView.center;
    }
    else if(gesture.state == UIGestureRecognizerStateEnded)// 平移结束时
    {
        // 移动差距 = 最终结束点 - 最初出发点
        CGPoint move = CGPointMake(center.x - originCenter.x, center.y - originCenter.y);
        // 重新移动到最初出发点
        self.largeImageView.center = originCenter;
        // 变化最初出发点的frame,改变x,y为最终位置
        CGRect frame = self.largeImageView.frame;
        frame.origin.x += move.x;
        frame.origin.y += move.y;
        // largeImageView的frame发生了改变,会调用setFrame方法进行重绘
        self.largeImageView.frame = frame;
    }
}

这里更新self.largeImageView.frame的目的是想要调用setFrame方法进行重绘,即重新切片。如果不这么做移动视图就会绘制原图,造成之前的内存暴涨。

// 会调用到,每次缩放、平移都会重绘,作用是只会绘制屏幕区域内的图片
- (void)setFrame:(CGRect)frame
{
    [super setFrame:frame];
    
    // 更新frame后的缩放系数,即比例尺
    self.imageScale = self.frame.size.width/self.imageRect.size.width;// 414/7360 = 0.0562
    if(self.tileCount > 0)// 16
    {
        CATiledLayer *tileLayer = (CATiledLayer *)self.layer;
        NSInteger tileSizeScale = sqrt(self.tileCount)/2;// 2
        
        // tiledSize的设置主要是影响CATiledLayer的切片数量
        CGSize tileSize = self.bounds.size;// (CGSize) tileSize = (width = 414, height = 276)
        tileSize.width /= tileSizeScale;
        tileSize.height/= tileSizeScale;
        tileLayer.tileSize = tileSize;// (CGSize) tileSize = (width = 207, height = 138)
    }

    // 调用drawRect进行重新绘制
    [self setNeedsDisplay];
}

输出结果为:

2020-07-30 18:01:26.170180+0800 Demo[92550:24729773] 拖拽的距离(距离是一个累加): {-34.5, 33.333335876464844}
2020-07-30 18:01:26.170300+0800 Demo[92550:24729773] 图片移动后:{{-68.666694641113281, 540.97321857441204}, {414, 296.7202346041056}}
2020-07-30 18:01:26.193586+0800 Demo[92550:24729773] 拖拽的距离(距离是一个累加): {0, 0}
2020-07-30 18:01:26.193699+0800 Demo[92550:24729773] 图片移动后:{{-68.666694641113281, 540.97321857441204}, {414, 296.7202346041056}}

前辈们的话是这样说,但是我实践结果显示平移即使不重绘,也不会发生内存暴涨现象,内存平稳得像条马路。

平移即使不重绘,也不会发生内存暴涨现象

如果重绘制了,反而显得有点乱七八糟。

如果重绘制了,反而显得有点乱七八糟

h、缩放手势,理解了平移的坑点,缩放这里就简单明了。

// 缩放手势
-(void)pinchGestureAction:(UIPinchGestureRecognizer*)gesture
{
    self.largeImageView.transform = CGAffineTransformScale(self.largeImageView.transform, gesture.scale, gesture.scale);
    // frame在缩放时候一直在变化
    NSLog(@"缩放手势: %@",NSStringFromCGRect(self.largeImageView.frame));
    
    // 清除累加的距离,否则会在上次缩放数值基础上增加,造成视图缩放飞快
    gesture.scale = 1;
    
    if(gesture.state == UIGestureRecognizerStateEnded)// 缩放结束时
    {
        // 最终frame的值
        CGRect newFrame = self.largeImageView.frame;
        // 重置:当我们改变过一个view.transform属性或者view.layer.transform的时候需要恢复默认状态的话
        self.largeImageView.transform = CGAffineTransformIdentity;
        // 更新为最终frame的值,注意只有这里是我们手动调用了frame的Setter方法才会触发重新绘制
        self.largeImageView.frame = newFrame;
    }
}

输出结果如下:

2020-07-30 17:53:17.372028+0800 Demo[92472:24718295] 缩放手势: {{90.456072185725972, 359.49332628307428}, {234.42119913440717, 168.01331691627325}}
2020-07-30 17:53:17.439436+0800 Demo[92472:24718295] 缩放手势: {{91.370854046955941, 360.148964661035}, {232.59163541194724, 166.70204016035183}}
2020-07-30 17:53:17.462019+0800 Demo[92472:24718295] 缩放手势: {{91.620490911458845, 360.32788328181067}, {232.09236168294149, 166.34420291880048}}
2020-07-30 17:53:17.620193+0800 Demo[92472:24718295] 缩放手势: {{91.620490911458845, 360.32788328181067}, {232.09236168294149, 166.34420291880048}}

如果省略设置frame的语句,那么视图虽然能缩放,但是缩放不重绘的话就会造成内存峰值,如下:

缩放不重绘的话就会造成内存峰值

缩放重绘的话,效果如下,成功减少了内存暴涨问题。

缩放重绘的话则成功减少了内存暴涨问题
另外一种使用方法

如果你在运行时读入整个图片并裁切,那CATiledLayer这些所有的性能优点就损失殆尽了。理想情况下来说,最好能够逐个步骤来实现。Mac OS命令行程序,它用CATiledLayer将一个图片裁剪成小图并存储到不同的文件中。这个前辈很讨厌,它只列出了程序清单,但是没有把终端怎么传入参数的方式说出来,我也在网上搜索不到相关语句,找到一句这个:

open -n ./AppName.app --args -AppCommandLineArg

但是命令行程序不是一个APP,所以行不通。想直接在XCode中传入图片,但是发现NSBundle获取不到资源,pathnil,没搜索到解决方案。所以这种方式我只是罗列出来,没法实际验证,浪费了我些时间,所以就讨厌那些话说一半,写文章一点不仔细的人,留下的坑需要后人来填补。

#import 
#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (argc < 2)
        {
            NSLog(@"TileCutter arguments: inputfile");
            return 0;
        }
        
         //从入参中读取大图路径
        NSString *inputFile = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];
        
        // 设置图片最大宽高为 256 像素
        CGFloat tileSize = 256;

        // 指定输出路径,与入参图片路径同级
        NSString *outputPath = [inputFile stringByDeletingPathExtension];
        
        //加载图片
        NSImage *image = [[NSImage alloc] initWithContentsOfFile:inputFile];
        NSSize size = [image size];
        NSArray *representations = [image representations];
        if ([representations count])
        {
            NSBitmapImageRep *representation = representations[0];
            size.width = [representation pixelsWide];
            size.height = [representation pixelsHigh];
        }
        NSRect rect = NSMakeRect(0.0, 0.0, size.width, size.height);
        CGImageRef imageRef = [image CGImageForProposedRect:&rect context:NULL hints:nil];
        
        //计算 rows 和 columns
        NSInteger rows = ceil(size.height / tileSize);
        NSInteger cols = ceil(size.width / tileSize);
        for (int y = 0; y < rows; ++y)
        {
            for (int x = 0; x < cols; ++x)
            {
                // 提取图片
                CGRect tileRect = CGRectMake(x*tileSize, y*tileSize, tileSize, tileSize);
                CGImageRef tileImage = CGImageCreateWithImageInRect(imageRef, tileRect);
                
                // 转换为jpg图片
                NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:tileImage];
                NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypeJPEG properties:@{}];
                CGImageRelease(tileImage);
                
                // 保存文件至输出目录
                NSString *path = [outputPath stringByAppendingFormat: @"_%02i_%02i.jpg", x, y];
                [data writeToFile:path atomically:NO];
            }
            
        }
    }
    return 0;
}

这个程序将2048x2048分辨率的雪人图案裁剪成了64个不同的256x256的小图。(256x256是CATiledLayer的默认小图大小,默认大小可以通过tileSize属性更改)。程序接受一个图片路径作为命令行的第一个参数。我们可以在编译的scheme将路径参数硬编码然后就可以在Xcode中运行了,但是以后作用在另一个图片上就不方便了。所以,我们编译了这个程序并把它保存到敏感的地方,然后从终端调用,如下面所示:

> path/to/TileCutterApp path/to/Snowman.jpg

这个程序相当基础,但是能够轻易地扩展支持额外的参数比如小图大小,或者导出格式等等。运行结果是64个新图的序列,如下面命名:

Snowman_00_00.jpg
Snowman_00_01.jpg
Snowman_00_02.jpg
...
Snowman_07_07.jpg

既然我们有了裁切后的小图,我们就要让iOS程序用到他们。CATiledLayer很好地和UIScrollView集成在一起。除了设置图层和滑动视图边界以适配整个图片大小,我们真正要做的就是实现-drawLayer:inContext:方法,当需要载入新的小图时,CATiledLayer就会调用到这个方法。

滚动CATiledLayer实现:


@interface ViewController ()
// 构建一个容器来展示大图,保证其contenSize与图片尺寸大小一致
@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建图层,并设置属性信息
    CATiledLayer *tileLayer = [CATiledLayer layer];
    
    CGFloat scale = UIScreen.mainScreen.scale;  // 确保scale比例一致
    tileLayer.frame = CGRectMake(0, 0, 3972/scale,15718/scale);// 图片像素
    tileLayer.delegate = self;
    tileLayer.tileSize = CGSizeMake(256/scale, 256/scale);  // 每个瓷砖块的大小
    tileLayer.delegate = self;
    [self.scrollView.layer addSublayer:tileLayer];
    
    self.scrollView.contentSize = tileLayer.frame.size;
    
    // 刷新当前屏幕Rect
    [tileLayer setNeedsDisplay];
}

// 当滑动到不同区域时会调用此方法
- (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx
{
    // 确定坐标信息
    CGRect bounds = CGContextGetClipBoundingBox(ctx);
    NSInteger x = floor(bounds.origin.x / layer.tileSize.width);
    NSInteger y = floor(bounds.origin.y / layer.tileSize.height);
    
    // 加载小图
    NSString *imageName = [NSString stringWithFormat: @"zz_%02i_%02i", x, y];
    NSString *imagePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"jpg"];
    UIImage *tileImage = [UIImage imageWithContentsOfFile:imagePath];
    
    // 在TiledLayer上绘制图片
    UIGraphicsPushContext(ctx);
    [tileImage drawInRect:bounds];
    UIGraphicsPopContext();
}

当你滑动这个图片,你会发现当CATiledLayer载入小图的时候,他们会淡入到界面中。这是CATiledLayer的默认行为。你可以用fadeDuration属性改变淡入时长或直接禁用掉。CATiledLayer(不同于大部分的UIKitCore Animation方法)支持多线程绘制,-drawLayer:inContext:方法可以在多个线程中同时地并发调用,所以请小心谨慎地确保你在这个方法中实现的绘制代码是线程安全的。

你也许已经注意到了这些小图并不是以Retina的分辨率显示的。为了以屏幕的原生分辨率来渲染CATiledLayer,我们需要设置图层的contentsScale来匹配UIScreenscale属性:

tileLayer.contentsScale = [UIScreen mainScreen].scale;

Demo在我的Github上,欢迎下载。
SolveProblemsDemo

参考文献

你可能感兴趣的:(IOS 解决问题:一万像素超大地图分割显示)