需求:根據(jù)App中的數(shù)據(jù)來(lái)生成PDF文件竞膳,盡量越小越好航瞭,不要爆內(nèi)存。
找了很多資料坦辟,先用第一種方式實(shí)現(xiàn)了刊侯,然后發(fā)現(xiàn)生成的PDF文件過大,又找了新的方案實(shí)現(xiàn)锉走,在此記錄一下滨彻。
兩種實(shí)現(xiàn)方式說明:
一、自己創(chuàng)建View挪蹭,按照OC的方式畫頁(yè)面亭饵,畫完之后將一頁(yè)頁(yè)View繪制到PDF文件中
優(yōu)點(diǎn):在View中畫簡(jiǎn)單易懂,轉(zhuǎn)成PDF的方式也簡(jiǎn)單
缺點(diǎn):由于是將每一頁(yè)的整個(gè)View給當(dāng)成圖片繪制到PDF中梁厉,保存的PDF內(nèi)都是圖片辜羊,無(wú)法修改文字,且词顾!PDF文檔非常的大八秃!
二、從頭到尾都使用手繪的方式去生成PDF肉盹,將各個(gè)控件自己畫出來(lái)
優(yōu)點(diǎn):速度很快昔驱,生成的PDF文件足夠的小,例:測(cè)試我的五萬(wàn)條數(shù)據(jù)上忍,基本都是純文本的骤肛,共計(jì)3000多頁(yè),需要15s左右窍蓝,文件大小只有4M萌衬,如果使用第一種方式,五千條數(shù)據(jù)都300M了它抱,差距有點(diǎn)大秕豫。
缺點(diǎn):整個(gè)繪制過程比較麻煩,如果是統(tǒng)一樣式的列表,還可以用for循環(huán)混移,如果特殊樣式太多祠墅,全都要自己寫。例:一行文本數(shù)據(jù)顯示一行歌径,最后超出的部分省略號(hào)表示毁嗦,這個(gè)省略號(hào)都要自己寫,并且要定義樣式與前邊的文字相同回铛!
要非常非常注意狗准,在繪制PDF過程中,你創(chuàng)建的對(duì)象茵肃,都要釋放掉腔长。不然幾百頁(yè)的PDF在for循環(huán)的過程中會(huì)產(chǎn)生非常大的內(nèi)存占用,點(diǎn)幾次生成PDF之后验残,App直接就因?yàn)楸瑑?nèi)存崩掉了捞附。
具體如何釋放,請(qǐng)看第二種方式中的代碼提示
實(shí)現(xiàn)過程:
// 首先定義了頁(yè)面的一些常用數(shù)據(jù)
static const CGFloat A4Width = 595.f; // PDF頁(yè)面的寬
static const CGFloat A4Height = 842.f; // PDF頁(yè)面的高
static const CGFloat topSpace = 40.f; // 頁(yè)眉和頁(yè)腳的高度
static const CGFloat bottomSpace = 50.f; // 頁(yè)眉和頁(yè)腳的高度 // 下邊距需要留出來(lái)一定間距您没,不然會(huì)很擠
static const CGFloat leftRightSpace = 20.f; // 左右間距的寬度
static const CGFloat contentHeight = A4Height – topSpace – bottomSpace; // 除去頁(yè)眉頁(yè)腳之后的內(nèi)容高度
static const CGFloat contentWidth = A4Width – leftRightSpace * 2; // 內(nèi)容寬度
static const CGFloat targetSpace = 10.f; // 每個(gè)詞條View的間距
static const CGFloat targetHeight = 14.f; // 詞條信息每一行的高度
static const CGFloat favoritesHeight = 80.f; // 收藏夾的高度鸟召,也是收藏夾圖片的高度
第一種實(shí)現(xiàn)方式:
/**
通過在View上畫好頁(yè)面,然后繪制到PDF頁(yè)面中實(shí)現(xiàn)轉(zhuǎn)PDF, 生成的PDF文件因?yàn)閮?nèi)部全是圖片氨鹏,文件非常大
dataInfo:MOJi數(shù)據(jù)
pdfName: 保存的PDF名稱欧募,需要注意帶上.pdf后綴!
*/
+ (void)createPDFViewWithDataInfo:(MOJiPDFDataInfo *)dataInfo PDFName:(NSString *)pdfName {
NSMutableArray *viewArr = [[NSMutableArray alloc] init]; // 存放PDF的頁(yè)面的數(shù)組
// 存放所有詞條信息View的數(shù)組
NSMutableArray *targetViewArr = [[NSMutableArray alloc] init];
NSMutableArray *targetHeightArr = [[NSMutableArray alloc] init]; // 存放每一個(gè)詞條的所占高度
CGFloat allTargetHeight = headerView.height + targetSpace;
for (int i = 0; i < dataInfo.targetArr.count; i++) {
MOJiPDFTarget *targetInfo = [dataInfo.targetArr objectAtIndex:i];
UIView *targetView = [[UIView alloc] initWithFrame:CGRectZero];
CGFloat height = 100.f; // 這個(gè)高度需要自己計(jì)算仆抵,此處只是示例
targetView.frame = CGRectMake(0, 0, contentWidth, height);
[targetViewArr addObject:targetView];
[targetHeightArr addObject:@(height + targetSpace)];
allTargetHeight = allTargetHeight + height + targetSpace;
}
// 補(bǔ)充說明跟继,其實(shí)這里的頁(yè)碼計(jì)算方式是不太正確的,你需要根據(jù)自己的需求來(lái)計(jì)算
// 計(jì)算總共需要多少頁(yè)P(yáng)DF
NSInteger allPageCount = ((int)allTargetHeight % (int)contentHeight) > 0 ? (allTargetHeight / contentHeight + 1) : (allTargetHeight / contentHeight);
int t = 0; // targetViewArr的計(jì)數(shù)放這里是為了不在PDF頁(yè)碼循環(huán)時(shí)重置
for (int i = 0; i < allPageCount; i++) {
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, A4Width, A4Height)];
// 頁(yè)眉標(biāo)題
// 頁(yè)碼
// 頁(yè)腳
CGFloat topFrom = topSpace;
for (; t < targetViewArr.count; t++) {
if (t == targetArr.count) break;
// 剩余距離不夠的情況下肢础,翻頁(yè)
CGFloat th = [[targetHeightArr objectAtIndex:t] floatValue];
if ((topFrom + th - targetSpace) > (A4Height - bottomSpace)) break;
UIView *targetView = [targetViewArr objectAtIndex:t];
CGFloat targetH = targetView.height;
targetView.top = topFrom;
targetView.left = leftRightSpace;
[view addSubview:targetView];
topFrom = topFrom + targetH + targetSpace;
}
[viewArr addObject:view];
}
// 用生成的頁(yè)面生成PDF
[MOJiPDF createPDFWithViewArr:[viewArr copy] PDFName:pdfName progress:PDFCreateProgressBlock];
}
+ (void)createPDFWithViewArr:(NSArray <UIView *>*)viewArr PDFName:(NSString *)pdfName progress:(nullable void(^)(NSString *progress))PDFCreateProgressBlock {
if (viewArr.count == 0 || pdfName.length == 0) return;
NSMutableData *pdfData = [NSMutableData data];
// 文檔信息 可設(shè)置為nil
CFMutableDictionaryRef myDictionary = CFDictionaryCreateMutable(nil, 0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(myDictionary, kCGPDFContextTitle, CFSTR("PDF Content Title"));
CFDictionarySetValue(myDictionary, kCGPDFContextCreator, CFSTR("PDF Author"));
// 設(shè)置PDF文件每頁(yè)的尺寸
CGRect pageRect = CGRectMake(0, 0, A4Width, A4Height);
// PDF繪制尺寸,設(shè)置為CGRectZero則使用默認(rèn)值612*912
UIGraphicsBeginPDFContextToData(pdfData, pageRect, nil);
for (int i = 0; i < viewArr.count; i++) {
UIView *pageView = [viewArr objectAtIndex:i];
// PDF文檔是分頁(yè)的碌廓,開啟一頁(yè)文檔開始繪制
UIGraphicsBeginPDFPage();
// 獲取當(dāng)前的上下文
CGContextRef pdfContext = UIGraphicsGetCurrentContext();
[pageView.layer renderInContext:pdfContext];
}
UIGraphicsEndPDFContext();
NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = [documentDirectories objectAtIndex:0];
NSString *documentDirectoryFilename = [documentDirectory stringByAppendingPathComponent:pdfName];
[pdfData writeToFile:documentDirectoryFilename atomically:YES];
NSLog(@"documentDirectoryFileName: %@",documentDirectoryFilename);
}
第二種實(shí)現(xiàn)方式:
/// 完全手動(dòng)的畫出PDF
/// @param dataInfo 需要傳入的dataInfo
/// @param pdfName PDF名字传轰,且需要帶.pdf的后綴
+ (void)toDrawPDFWithDataInfo:(MOJiPDFDataInfo *)dataInfo pdfName:(nullable NSString *)pdfName {
NSArray *targetArr = dataInfo.targetArr;
NSMutableArray *targetHeightArr = [[NSMutableArray alloc] init]; // 存放每一個(gè)詞條的所占高度
NSInteger allPageCount = 1;
for (int i = 0; i < targetArr.count; i++) {
// 在這里寫代碼,計(jì)算出總共需要的頁(yè)碼數(shù)谷婆,以及每一個(gè)詞條的高度放入targetHeightArr數(shù)組中
}
// 1.創(chuàng)建media box
CGFloat myPageWidth = A4Width;
CGFloat myPageHeight = A4Height;
CGRect mediaBox = CGRectMake (0, 0, myPageWidth, myPageHeight);
// 2.設(shè)置pdf文檔存儲(chǔ)的路徑
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = paths[0];
filePath = [documentsDirectory stringByAppendingFormat:@"/%@", pdfName];
const char *cfilePath = [filePath UTF8String];
CFStringRef pathRef = CFStringCreateWithCString(NULL, cfilePath, kCFStringEncodingUTF8);
// NSLog(@"filePath = %@", filePath);
// 3.設(shè)置當(dāng)前pdf頁(yè)面的屬性
CFStringRef myKeys[3];
CFTypeRef myValues[3];
myKeys[0] = kCGPDFContextMediaBox;
myValues[0] = (CFTypeRef) CFDataCreate(NULL,(const UInt8 *)&mediaBox, sizeof (CGRect));
myKeys[1] = kCGPDFContextTitle;
myValues[1] = CFSTR("我的PDF");
myKeys[2] = kCGPDFContextCreator;
myValues[2] = CFSTR("PDF作者");
// 4.獲取pdf繪圖上下文
CGContextRef myPDFContext = MyPDFContextCreate (&mediaBox, pathRef);
// ————特別注意慨蛙,字體樣式大小和顏色要這樣設(shè)置,不然無(wú)法釋放——————
// 設(shè)置字體樣式
CTFontRef ctFontTitleMedium = CTFontCreateWithName(CFSTR("PingFangSC-Medium"), 12.0, NULL);
// 設(shè)置字體顏色
CGFloat cmykValue[] = {0.239, 0.270, 0.298, 1};
CGColorRef ctColorBlack = CGColorCreate(CGColorSpaceCreateDeviceRGB(), cmykValue);
int t = 0; // target的計(jì)數(shù)放這里是為了不在PDF頁(yè)碼循環(huán)時(shí)重置
for (int i = 0; i < allPageCount; i++) {
if (t == targetArr.count) break;
// 5.開始描繪每一頁(yè)的頁(yè)面
CFDictionaryRef pageDictionary = CFDictionaryCreate(NULL, (const void **) myKeys, (const void **) myValues, 3,
&kCFTypeDictionaryKeyCallBacks, & kCFTypeDictionaryValueCallBacks);
CGPDFContextBeginPage(myPDFContext, pageDictionary);
// 默認(rèn)的原點(diǎn)在左下角纪挎,每一頁(yè)都需要轉(zhuǎn)換坐標(biāo)系的操作!!!!!
/* 添加頁(yè)腳 */
CGFloat widthFotter = [MOJiPDF getStringWidthWithFontSize:[UIFont systemFontOfSize:10.f] height:14.f string:@"這是頁(yè)腳"];
CGRect rectFooter = CGRectMake(A4Width - 10.f - widthFotter, 10.f, widthFotter, targetHeight);
[MOJiPDF drawTextWithText:@"這是頁(yè)腳" color:ctColorBlack font:ctFontTargetRegular alignMent:kCTTextAlignmentRight rect:rectFooter maxWidth:contentWidth contextRef:myPDFContext];
CGFloat topFrom = topSpace;
for (; t < targetArr.count; t++) {
// 剩余距離不夠的情況下期贫,翻頁(yè)
CGFloat th = [[targetHeightArr objectAtIndex:t] floatValue];
if ((topFrom + th - targetSpace) > (A4Height - bottomSpace)) break;
MOJiPDFTarget *targetInfo = [targetArr objectAtIndex:t];
if (i == 0) {
topFrom = topSpace + favoritesHeight + targetSpace;
UIImage *iconImg = [MOJiPDF roundCorners:dataInfo.coverImg size:CGSizeMake(favoritesHeight, favoritesHeight) radius:8.f];
CGRect iconRect = [MOJiPDF getFinallyRectWithOriginalRect:CGRectMake(leftRightSpace, 40, favoritesHeight, favoritesHeight)];
CGContextDrawImage(myPDFContext, iconRect, iconImg.CGImage);
iconImg = nil;
}
CGFloat widthTargetTitle = [MOJiPDF getStringWidthWithFontSize:[UIFont systemFontOfSize:10.f] height:14.f string:targetInfo.title];
CGRect rectTargetTitle = [MOJiPDF getFinallyRectWithOriginalRect:CGRectMake(leftRightSpace, topFrom, widthTargetTitle, targetHeight)];
[MOJiPDF drawTextWithText:targetInfo.title color:ctColorBlack font:ctFontTargetMedium alignMent:kCTTextAlignmentLeft rect:rectTargetTitle maxWidth:contentWidth contextRef:myPDFContext];
topFrom = topFrom + targetHeight;
}
CGPDFContextEndPage(myPDFContext);
CFRelease(pageDictionary);
}
// 6.釋放創(chuàng)建的對(duì)象
CFRelease(ctColorBlack);
CFRelease(ctFontTitleMedium);
CGContextRelease(myPDFContext);
CFRelease(myValues[0]);
CFRelease(myValues[1]);
CFRelease(myValues[2]);
CFRelease(myKeys[0]);
CFRelease(myKeys[1]);
CFRelease(myKeys[2]);
CFRelease(pathRef);
}
以上是主要的代碼,以下是需要用到的幾個(gè)函數(shù)
/*
* 獲取pdf繪圖上下文
* inMediaBox指定pdf頁(yè)面大小
* path指定pdf文件保存的路徑
*/
CGContextRef MyPDFContextCreate (const CGRect *inMediaBox, CFStringRef path)
{
CGContextRef myOutContext = NULL;
CFURLRef url;
CGDataConsumerRef dataConsumer;
url = CFURLCreateWithFileSystemPath (NULL, path, kCFURLPOSIXPathStyle, false);
if (url != NULL)
{
dataConsumer = CGDataConsumerCreateWithURL(url);
if (dataConsumer != NULL)
{
myOutContext = CGPDFContextCreate (dataConsumer, inMediaBox, NULL);
CGDataConsumerRelease (dataConsumer);
}
CFRelease(url);
}
return myOutContext;
}
/**
繪制文字的方式
text: 需要繪制的文字
color:文字顏色
font:文字字體及大小
alignment:文字對(duì)齊方式 (注:這個(gè)參數(shù)在原先的寫法中沒有生效异袄,不知道為什么通砍,暫時(shí)不用管它)
rect:文字所在范圍
maxWidth:最大顯示寬度,大于此,先截取然后顯示省略
contextRef:上下文
*/
+ (void)drawTextWithText:(NSString *)text color:(CGColorRef)color font:(CTFontRef)font alignMent:(CTTextAlignment)alignment rect:(CGRect)rect maxWidth:(CGFloat)maxWidth contextRef:(CGContextRef)contextRef {
CFStringRef keys[] = {kCTFontAttributeName, kCTForegroundColorAttributeName};
CFTypeRef values[] = {font, color};
CFDictionaryRef attr = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFAttributedStringRef attrString = CFAttributedStringCreate(NULL, (__bridge CFStringRef)text, attr);
CTLineRef line = CTLineCreateWithAttributedString(attrString);
NSString *dotString = @"\u2026";
CFAttributedStringRef dotStringRef = CFAttributedStringCreate(NULL, (__bridge CFStringRef)dotString, attr);
CTLineRef token = CTLineCreateWithAttributedString(dotStringRef);
/** 將現(xiàn)有 CTLineRef 截?cái)嗖⒎祷匾粋€(gè)新的對(duì)象
* width 截?cái)鄬挾龋喝绻袑挻笥诮財(cái)鄬挾确馑铮瑒t該行將被截?cái)? * truncationType 截?cái)囝愋? * truncationToken 截?cái)嘤玫奶畛浞?hào)迹冤,通常是省略號(hào) ... ,為Null時(shí)則只截?cái)嗷⒓桑蛔鎏畛? * 該填充符號(hào)的寬度必須小于截?cái)鄬挾扰葆悖駝t該函數(shù)返回 NULL;
*/
CTLineRef newline = CTLineCreateTruncatedLine(line, maxWidth, kCTLineTruncationEnd, token);
CGContextSetTextMatrix(contextRef, CGAffineTransformIdentity);
CGContextSetTextPosition(contextRef, rect.origin.x, rect.origin.y);
CTLineDraw(newline, contextRef);
CFRelease(newline);
CFRelease(token);
CFRelease(line);
CFRelease(dotStringRef);
CFRelease(attrString);
CFRelease(attr);
CFRelease(keys[0]);
CFRelease(keys[1]);
}
// 獲取字符串寬度
+ (CGFloat)getStringWidthWithFontSize:(UIFont *)sizeFont height:(CGFloat)height string:(NSString *)string {
CGRect rect = [string boundingRectWithSize:CGSizeMake(MAXFLOAT, height) options:NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesFontLeading | NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:sizeFont} context:nil];
return rect.size.width;
}
// 根據(jù)正確的坐標(biāo)系 轉(zhuǎn)換為在PDF畫布上的坐標(biāo)系
+ (CGRect)getFinallyRectWithOriginalRect:(CGRect)originalRect {
CGFloat y = A4Height - originalRect.origin.y - originalRect.size.height;
return CGRectMake(originalRect.origin.x, y, originalRect.size.width, originalRect.size.height);
}
/**
給UIImage添加圓角
img: 需要處理的UIImage
size:UIImage真實(shí)顯示時(shí)候的size
radius:UIImage真實(shí)顯示時(shí)候的圓角大小
*/
+ (UIImage *)roundCorners:(UIImage*)img size:(CGSize)size radius:(CGFloat)radius {
int w = img.size.width;
int h = img.size.height;
CGFloat modulus = w / size.width; // 本身畫圖膜蠢,是根據(jù)img的原始尺寸來(lái)的堪藐,跟要展示的尺寸會(huì)不同,需要自己計(jì)算在原尺寸上的圓角大小
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
/**
CGContextRef CGBitmapContextCreate (
void *data, 指向要渲染的繪制內(nèi)存的地址挑围。這個(gè)內(nèi)存塊的大小至少是(bytesPerRow*height)個(gè)字節(jié)
size_t width, bitmap的寬度,單位為像素
size_t height, bitmap的高度,單位為像素
size_t bitsPerComponent, 內(nèi)存中像素的每個(gè)組件的位數(shù).例如礁竞,對(duì)于32位像素格式和RGB 顏色空間,你應(yīng)該將這個(gè)值設(shè)為8.
size_t bytesPerRow, bitmap的每一行在內(nèi)存所占的比特?cái)?shù)
CGColorSpaceRef colorspace, bitmap上下文使用的顏色空間贪惹。
CGBitmapInfo bitmapInfo 指定bitmap是否包含alpha通道苏章,像素中alpha通道的相對(duì)位置,像素組件是整形還是浮點(diǎn)型等信息的字符串奏瞬。
); */
CGContextRef context = CGBitmapContextCreate(NULL, w, h, 8, 8 * w, colorSpace, kCGImageAlphaPremultipliedFirst);
CGContextBeginPath(context);
addRoundedRectToPath(context, CGRectMake(0, 0, w, h), radius * modulus, radius * modulus);
CGContextClosePath(context);
CGContextClip(context);
CGContextDrawImage(context, CGRectMake(0, 0, w, h), img.CGImage);
CGImageRef imageMasked = CGBitmapContextCreateImage(context);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
UIImage * image = [UIImage imageWithCGImage:imageMasked];
CGImageRelease(imageMasked);
return image;
}
//這是被調(diào)用的靜態(tài)方法枫绅,繪制圓角用
static void addRoundedRectToPath(CGContextRef context, CGRect rect,
float ovalWidth,float ovalHeight)
{
float fw, fh;
if (ovalWidth == 0 || ovalHeight == 0) {
CGContextAddRect(context, rect);
return;
}
CGContextSaveGState(context);
CGContextTranslateCTM (context, CGRectGetMinX(rect), CGRectGetMinY(rect));
CGContextScaleCTM (context, ovalWidth, ovalHeight);
fw = CGRectGetWidth (rect) / ovalWidth;
fh = CGRectGetHeight (rect) / ovalHeight;
CGContextMoveToPoint(context, fw, fh/2);
CGContextAddArcToPoint(context, fw, fh, fw/2, fh, 1);
CGContextAddArcToPoint(context, 0, fh, 0, fh/2, 1);
CGContextAddArcToPoint(context, 0, 0, fw/2, 0, 1);
CGContextAddArcToPoint(context, fw, 0, fw, fh/2, 1);
CGContextClosePath(context);
CGContextRestoreGState(context);
}
上邊的代碼中可以看到,幾乎所有的CFXXXRef和一些CG類型的數(shù)據(jù)硼端,你創(chuàng)建或者持有的并淋,就必須要釋放掉!不然在大量數(shù)據(jù)的情況下珍昨,內(nèi)存占用非常的嚴(yán)重县耽。