iOS长图生成的pdf性能优化记录

背景

某日产品拿来了一个由30多页高清长图生成的pdf,在应用中运行出现了崩溃。

排查

经过调试发现加载长图生成的pdf时,运行内存会出现缓慢增长,直至崩溃。经过代码定位发现时pdf转成image对象的过程中由于是长图生成的pdf,这一页的pdf的size相当于正常pdfsize的30多页,转换的过程中context的fill的size也是正常pdf的30多倍。经过调研,尝试,发现对于同一页的pdf,可以通过调整context的fill的size来只把pdf中的部分内容转换成image对象,内存正常也不大。

方案

原来的方案是每页pdf生成一个image对象,通过一个collectonViewCell来显示。调整后的方案为:根据屏幕大小来决定一个pdf页面生成多少个image对象,有多少个image对象,一个section里就有多少个cell。方案如下:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        if pdfSize.height/pdfSize.width >= 3.0 { //长图生成的pdf 备注:pdfSize 即一页pdf的大小
            let visible_width = pdfSize.width
            let visible_height = visible_width * Board_height/Board_width
            let visibleSize = CGSize(width: visible_width, height: visible_height)
            let count:Int = Int(ceil(pdfSize.height/visible_height))// 计算需要分割的次数
            let index:Int
            let itemSize:CGSize
            if indexPath.item < count {
                index = indexPath.item
            } else {
                index = count - 1
            }
            if CGFloat(index) * visibleSize.height + visibleSize.height < pdfSize.height {
                itemSize = visibleSize
            } else {
             let tailHeight = pdfSize.height - CGFloat(index) * visibleSize.height
                 itemSize = CGSize(width: visibleSize.width, height: tailHeight)
            }
            return itemSize
        }
        return pdfSize
    }

在讲pdf转成image对象的过程中,方案调整核心代码如下:

//index就是pdf中的从上往下被分割后的image的索引值
 private func subImageFromLongPDF(ori_page:PDFPage, pdfSize:CGSize, index:Int) -> UIImage {
        guard let page = ori_page.pageRef else {
            return UIImage()
        }
        var dic = [PDFAnnotation:CGRect]()
        let originalPageRect = ori_page.originalPageRect
        let elaborate: CGFloat = 1.0
        let scale_W = pdfSize.width / originalPageRect.size.width * elaborate
        let scale_H = pdfSize.height / originalPageRect.size.height * elaborate
        let width = originalPageRect.size.width * scale_W
        let height = width * Board_height/Board_width // Board_height屏幕高度,Board_width屏幕宽度
        let visibleSize = CGSize(width: width, height: height)

        let rotation = ori_page.rotation
        ori_page.rotation = 0

        let scaledOrigin = CGPoint(x: originalPageRect.origin.x * scale_W, y: originalPageRect.origin.y * scale_H)

        let scaledPageSize = CGSize(width: originalPageRect.size.width * scale_W, height: originalPageRect.size.height * scale_H)
        let scaledPageRect:CGRect
        var tailHeight = 0.0
        if CGFloat(index) * visibleSize.height + height < scaledPageSize.height {
            scaledPageRect = CGRect(origin: CGPoint(x: 0, y: CGFloat(index) * visibleSize.height), size: visibleSize)
        } else {
            tailHeight = scaledPageSize.height - CGFloat(index) * visibleSize.height
            scaledPageRect = CGRect(origin: CGPoint(x: 0, y: CGFloat(index) * visibleSize.height), size: CGSize(width: visibleSize.width, height: tailHeight))
        }

        var img:UIImage?
        autoreleasepool {
            let renderer = UIGraphicsImageRenderer(size: scaledPageRect.size)
            var tmpImg:UIImage? = renderer.image {
                ctx in
                UIColor.white.set()
                //这个核心代码,scaledPageRect就是计算好的rect,
                ctx.fill(scaledPageRect)
                let rotationAngle: CGFloat
                switch page.rotationAngle {//保持和安卓一致,强制为0了
                case 90:
                    rotationAngle = 270
                    //平移 以用户空间为单位,指定上下文的坐标空间 x 轴的位移量。
                    ctx.cgContext.translateBy(x: -scaledPageRect.origin.x, y: 0)
                case 180:
                    rotationAngle = 180
                    //平移
                    ctx.cgContext.translateBy(x: scaledPageRect.width,y: 0)

                case 270:
                    rotationAngle = 90
                    //平移
                    ctx.cgContext.translateBy(x: scaledPageRect.origin.x, y: scaledPageRect.size.height - scaledPageRect.origin.y)
                default:
                    rotationAngle = 0
                    //平移 以用户空间为单位,指定上下文的坐标空间 x 轴的位移量。
                    //指定上下文的坐标空间 y 轴的位移量(以用户空间为单位)。
                    if rotation == 180 {
                        if tailHeight > 0 {//尾部不足一屏的特殊处理逻辑
                            ctx.cgContext.translateBy(x: 0 - scaledOrigin.x, y: 0 + scaledOrigin.y + CGFloat(index+1) * visibleSize.height - (visibleSize.height - tailHeight))//翻转180度正常
                        } else {
                            ctx.cgContext.translateBy(x: 0 - scaledOrigin.x, y: 0 + scaledOrigin.y + CGFloat(index+1) * visibleSize.height)//翻转180度正常
                        }
                        
                    } else {
                        ctx.cgContext.translateBy(x: 0 - scaledOrigin.x, y: scaledPageSize.height + scaledOrigin.y - scaledPageRect.origin.y)
                    }

                }
                //Rotate是以原点为圆心旋转,Quartz创建的图形上下文旋转圆心为左下角,角度值正数为逆时针旋转,负数为顺时针旋转
                //UIKit创建的图像上下文旋转圆心为左上角,角度值正数为顺时针旋转,负数为逆时针旋转。

                // Flip the context vertically because the Core Graphics coordinate system starts from the bottom.
                ctx.cgContext.scaleBy(x:1.0, y: -1.0)//垂直翻转上下文,因为核心图形坐标系从底部开始
                //旋转 正值逆时针旋转,负值顺时针旋转
                ctx.cgContext.rotate(by: rotationAngle.degreesToRadians)
                ctx.cgContext.scaleBy(x: scale_W, y: scale_H)//缩放
                // Draw the PDF page.
                // 此处仍然是正常的绘制pdf,因为前面设置了context的fillSize,因此pdf绘制的时候只在前面指定的rect才会生效。
                ctx.cgContext.drawPDFPage(page)
                for annotation in ori_page.annotations {
                    let origin = annotation.bounds.origin
                    dic[annotation] = annotation.bounds
                    let annotation_fill_bounds = CGRect(x: origin.x + originalPageRect.origin.x, y: origin.y + originalPageRect.origin.y, width: annotation.bounds.size.width, height: annotation.bounds.size.height)
                    annotation.bounds = annotation_fill_bounds
                    annotation.draw(with: .cropBox, in: ctx.cgContext)
                }
            }

            if rotation%360 != 0 {
                let scale:Float =  Float(rotation) / Float(180)
                tmpImg = tmpImg?.rotate(radians: Float.pi * scale) ?? UIImage.init()
            }
            
            img = tmpImg
            tmpImg = nil
            
        }
        //将对pdfpage的修改进行还原
        ori_page.rotation = rotation
        for (annotation,bounds) in dic {
           annotation.bounds = bounds
        }
        return img ?? UIImage()
    }

extension PDFPage {
    
    var originalPageRect: CGRect {
        switch rotation {
        case 90, 270:
            let originalRect = bounds(for: PDFDisplayBox.cropBox)
            let rotatedSize = CGSize(width: originalRect.height, height: originalRect.width)
            return CGRect(origin: originalRect.origin, size: rotatedSize)
        default:
            return bounds(for: PDFDisplayBox.cropBox)
        }
    }
}

extension UIImage {
    func rotate(radians: Float) -> UIImage? {
        var newSize = CGRect(origin: CGPoint.zero, size: self.size).applying(CGAffineTransform(rotationAngle: CGFloat(radians))).size
        // Trim off the extremely small float value to prevent core graphics from rounding it up
        newSize.width = floor(newSize.width)
        newSize.height = floor(newSize.height)

        UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
        guard let context = UIGraphicsGetCurrentContext() else {
            return nil
        }

        // Move origin to middle
        context.translateBy(x: newSize.width/2, y: newSize.height/2)
        // Rotate around middle
        context.rotate(by: CGFloat(radians))
        // Draw the image at its center
        self.draw(in: CGRect(x: -self.size.width/2, y: -self.size.height/2, width: self.size.width, height: self.size.height))

        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return newImage
    }
}


extension FloatingPoint {
    var degreesToRadians: Self { return self * .pi / 180 }
    var radiansToDegrees: Self { return self * 180 / .pi }
}

备注:这段代码结合了自己的项目实际业务,大家作为参考。多看下核心代码和注释

相关推荐
EterNity_TiMe_1 小时前
【机器学习】智驭未来:探索机器学习在食品生产中的革新之路
人工智能·python·机器学习·性能优化·学习方法
belldeep7 小时前
python:reportlab 将多个图片合并成一个PDF文件
python·pdf·reportlab
韩楚风12 小时前
【linux 多进程并发】linux进程状态与生命周期各阶段转换,进程状态查看分析,助力高性能优化
linux·服务器·性能优化·架构·gnu
墨染辉12 小时前
pdf处理2
pdf
missmisslulu19 小时前
电容笔值得买吗?2024精选盘点推荐五大惊艳平替电容笔!
学习·ios·电脑·平板
GEEKVIP20 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
GEEKVIP20 小时前
如何在 Windows 10 上恢复未保存/删除的 Word 文档
macos·ios·智能手机·电脑·word·笔记本电脑·iphone
奇客软件21 小时前
iPhone使用技巧:如何恢复变砖的 iPhone 或 iPad
数码相机·macos·ios·电脑·笔记本电脑·iphone·ipad
长天一色21 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
墨染辉1 天前
10.2 如何解决从复杂 PDF 文件中提取数据的问题?
pdf