如何实现NSAttributedString仅在字母外侧描边且无性能问题?
嘿,这个需求我太熟悉了——很多开发者都会被NSStrokeWidthAttributeName的默认行为坑到,默认描边会同时处理文字的内外轮廓,想要只保留外侧描边还不拖垮性能,得用对方法。
先澄清一个常见误区:其实NSStrokeWidthAttributeName本身就支持外侧描边,只是属性组合要选对,但如果你的场景是大量文字(比如列表、长文本滚动),直接用UILabel设置属性可能会有性能瓶颈,这时候就得换更高效的绘制方式。
下面分两种场景给你最优方案:
一、简单场景(少量静态文字):正确的属性组合
如果只是少量静态文字展示,直接用NSAttributedString的正确属性就能实现,完全没有性能问题:
let attributedString = NSMutableAttributedString(string: "Your Target Text") let strokeWidth: CGFloat = 2.0 // 根据字体大小调整,正数代表向外扩展描边 attributedString.addAttributes([ .strokeColor: UIColor.black, .strokeWidth: strokeWidth, .foregroundColor: UIColor.clear // 关键:将填充色设为透明,只保留描边轮廓 ], range: NSRange(location: 0, length: attributedString.length))
注意:
strokeWidth是相对于字体点大小的比例,比如16pt字体设置2.0,描边会向外扩展16pt。如果你的描边看起来仍有内侧痕迹,大概率是strokeWidth设置过大,或者字体本身的轮廓设计导致——换个标准字体(比如HelveticaNeue)试试就能验证。
二、高性能场景(大量文字/滚动视图):用Core Text或CATextLayer绘制
如果文字要在滚动视图里展示,或是有大量重复绘制的场景,直接用UILabel可能会因为系统自动渲染逻辑导致卡顿,这时候用Core Text直接绘制或者CATextLayer会高效很多。
方案1:CATextLayer实现
CATextLayer基于Core Animation渲染,性能远优于UILabel,尤其适合滚动场景:
let textLayer = CATextLayer() textLayer.string = attributedString // 使用上面配置好属性的富文本 textLayer.font = CTFontCreateWithName("HelveticaNeue" as CFString, 16, nil) textLayer.fontSize = 16 textLayer.strokeColor = UIColor.black.cgColor textLayer.strokeWidth = 2.0 textLayer.foregroundColor = UIColor.clear.cgColor textLayer.frame = CGRect(x: 0, y: 0, width: 200, height: 30) textLayer.contentsScale = UIScreen.main.scale // 关键:解决Retina屏幕下的模糊问题 view.layer.addSublayer(textLayer)
方案2:Core Text手动绘制
如果需要更精细的排版控制,直接在draw(_ rect: CGRect)方法里用Core Text绘制,性能是最优的:
override func draw(_ rect: CGRect) { super.draw(rect) guard let context = UIGraphicsGetCurrentContext() else { return } let attributedString = NSMutableAttributedString(string: "High Performance Text") attributedString.addAttributes([ .strokeColor: UIColor.black, .strokeWidth: 2.0, .foregroundColor: UIColor.clear, .font: UIFont.systemFont(ofSize: 16) ], range: NSRange(location: 0, length: attributedString.length)) // Core Text绘制的坐标转换 context.textMatrix = .identity context.translateBy(x: 0, y: bounds.size.height) context.scaleBy(x: 1.0, y: -1.0) let path = CGMutablePath() path.addRect(bounds) let framesetter = CTFramesetterCreateWithAttributedString(attributedString as CFAttributedString) let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedString.length), path, nil) CTFrameDraw(frame, context) }
为什么之前的方法有性能问题?
很多开发者会用“双层绘制”:先画一层粗描边的文字,再在上面叠一层正常填充的文字。但这种方式会触发两次文本渲染,在大量文字或滚动场景下会导致CPU占用过高,而上面的方案都是单次渲染,性能提升非常明显。
内容的提问来源于stack exchange,提问作者stone

