最近项目需求,需要将用户写的本地内容写成生成PDF,然后导出功能.经过多方资料的查阅.了解了一些相关知识,特此记录一下开发中遇到的问题和解决思路.
用户写的内容包含:图片(最多12张),内容(分页),标题(固定一行)
一:生成PDF的思路.
第一种是自己画,但是布局,文字支持很繁琐.然后该用本人采用的是第二种方法.第二种网上说爆内存,但是后面也解决啦.
第二种:利用View布局,然后将View生成PDF.直观方便.(目前我使用也是这个方法).
- 1.第一步就是固定好pdf的尺寸,和页眉页脚的高度,和各个控件之间的高度间隔:
CGFloat A4Width = 595.f; // PDF页面的宽
CGFloat A4Height = 842.f; // PDF页面的高
CGFloat topSpace = 40.f; // 页眉和页脚的高度
CGFloat bottomSpace = 50.f; // 页眉和页脚的高度 // 下边距需要留出来一定间距,不然会很挤
CGFloat leftRightSpace = 20.f; // 左右间距的宽度
CGFloat contentHeight = A4Height-topSpace-bottomSpace; // 除去页眉页脚之后的内容高度
CGFloat contentWidth = A4Width-leftRightSpace * 2; // 内容宽度
NSInteger imagesNumberInLine = 3; // 每行显示图片的个数
CGFloat imageInsertSpace = 20.f; // 左右间距的宽度
CGFloat contentWidthHeightForImage = (A4Width - leftRightSpace *2 - imageInsertSpace*2)/imagesNumberInLine; // 图片宽度和高度
CGFloat targetSpace = 10.f; // 每个词条View的间距
- 2.然后就是根据每篇文章内容生成view,图片的九宫格布局,文本的换行,和pdf的分页.
- pdf页码
allTargetHeigh:总高度 子控件高度+控件之见的间隔 (不包含页眉页脚)
contentHeight:出去页眉页脚之后的高度
NSInteger allPageCount = allTargetHeigh / contentHeight > 0 ? (allTargetHeigh / contentHeight + 1) : allTargetHeigh / contentHeight;
-
- 文本分页,开发过程中最头疼就是这个文本写到下一页的问题.一种方法时直接计算,根据pdf宽度和字号大小,计算出每行大概多少字,然后计算多少行,然后对内容进行截取.但是文本里面有自定义表情,和emoji表情,而且截取操作,容易发生奔溃,就果断舍弃.后来查资料发现可以通过lable指定顶宽度获取当前行数,还可以获取每行的文本.这就有解决了这个头疼的问题. 获取文本行数(ps:浏览器编辑器总是莫名的加星号-_-||)
- (NSArray *)getLinesArrayOfStringWidth:(CGFloat)width {
NSString *text = [**self** text];
UIFont *font = [**self** font];
**if** (text == **nil**) {
**return** **nil**;
}
CTFontRef myFont = CTFontCreateWithName(( CFStringRef)([font fontName]), [font pointSize], **NULL**);
NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:text];
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
[attStr addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, attStr.length)];
[attStr addAttribute:(NSString *)kCTFontAttributeName
value:( **__bridge** **id**)myFont
range:NSMakeRange(0, attStr.length)];
CFRelease(myFont);
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString(( CFAttributedStringRef)attStr);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, **NULL**, CGRectMake(0,0,width,100000));
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, **NULL**);
NSArray *lines = ( NSArray *)CTFrameGetLines(frame);
NSMutableArray *linesArray = [[NSMutableArray alloc]init];
**for** (**id** line **in** lines) {
CTLineRef lineRef = ( **__bridge** CTLineRef )line;
CFRange lineRange = CTLineGetStringRange(lineRef);
NSRange range = NSMakeRange(lineRange.location, lineRange.length);
NSString *lineString = [text substringWithRange:range];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attStr,
lineRange,
kCTKernAttributeName,
(CFTypeRef)([NSNumber numberWithFloat:0.0]));
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attStr,
lineRange,
kCTKernAttributeName,
(CFTypeRef)([NSNumber numberWithInt:0.0]));
[linesArray addObject:lineString];
}
CGPathRelease(path);
CFRelease(frame);
CFRelease(frameSetter);
**return** (NSArray *)linesArray;
}
然后就直接把每行文本都用一个Label 来显示,这样直接布局lable即可,没有显示的label 显示到下一页上去.完美解决了文本分页的问题.
- 4.就是最后一步生成PDF.点击开始,内存直接爆掉,文件数量2篇.生成->导出->完美.当内容增加到100篇的时候开始内存暴增,而且导出的PDf很大.这..........
开始发现不对劲.排查问题有两点:
1).for循环产生的大量label ,view,image没有及时释放.
2).所有产生的要生成PDF的每一页的View都在数组里面放着,只有PDF生成代码结束才释放.
这不爆内存真是见鬼了-_-||.
第一次解决:
1>.撤掉数组,每次布局好一页View,马上生成pdf放到本地文件夹,然后把当前View 释放掉. (所有页面生成结束,统一合成PDF,然后删除临时文件)
2>.添加autoreleasepool,保证临时变量使用后及时释放.
3>.压缩图片.
for (NSString *str in lines) {
@autoreleasepool {
CGFloat height = 22;
UILabel *targetV = [UILabel new];
targetV.numberOfLines = 0;
targetV.text = str;
targetV.textColor = UIColor.blackColor;
targetV.font = [UIFont systemFontOfSize:18];
targetV.lineBreakMode = NSLineBreakByWordWrapping;
[targetArr addObject:contontLayer];
[targetHeightArr addObject:@(height + targetSpace)];
allTargetHeigh = allTargetHeigh + height + targetSpace;
}
但是发现内容增加到500篇时,还是爆掉.
第二次解决:把所有控件换成Calayer.这次内存直接没超过50M.还是很可以的,而且pdf文件导出也只有不到10M.
总体上已经可以接受啦.
PDF生成代码
- (**void**)createPDFWithViewArr:(CALayer*)viewArr PDFName:(NSString *)pdfName progress:(**nullable** **void**(^)(NSString *progress))PDFCreateProgressBlock {
// if (viewArr.count == 0 || pdfName.length == 0) return;
// 文档信息 可设置为nil
**self**.pdfData = [NSMutableData data];
**@autoreleasepool** {
CFMutableDictionaryRef myDictionary = CFDictionaryCreateMutable(**nil**, 0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(myDictionary, kCGPDFContextTitle, CFSTR("PDF Content Title"));
CFDictionarySetValue(myDictionary, kCGPDFContextCreator, CFSTR("PDF Author"));
\
// 设置PDF文件每页的尺寸
CGRect pageRect = CGRectMake(0, 0, A4Width, A4Height);
// PDF绘制尺寸,设置为CGRectZero则使用默认值612*912
UIGraphicsBeginPDFContextToData(**self**.pdfData, pageRect, **nil**);
// PDF文档是分页的,开启一页文档开始绘制
UIGraphicsBeginPDFPage();
// 获取当前的上下文
CGContextRef pdfContext = UIGraphicsGetCurrentContext();
[viewArr renderInContext:pdfContext];
UIGraphicsEndPDFContext();
viewArr = **nil**;
}
NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, **YES**);
NSString *documentDirectory = [documentDirectories objectAtIndex:0];
NSString *documentDirectoryFilename = [documentDirectory stringByAppendingPathComponent:pdfName];
[**self**.pdfData writeToFile:documentDirectoryFilename atomically:**YES**];
NSLog(@"documentDirectoryFileName: %@",documentDirectoryFilename);
[**self**.viewArr removeAllObjects];
[**self**.pathArr addObject:documentDirectoryFilename];
}
好啦,今天先到这吧.如有问题欢迎指出.