滚动TableView时自定义CGFont无法释放的问题排查
TableView中自定义CGFont无法释放的内存问题
问题现象
在CollectionView中使用自定义字体管理器加载字体时,内存中留存了全部400个已加载字体的CGFont对象,但使用系统字体UIFont.systemFont(ofSize: 16.0)时,仅留存当前屏幕显示的约33个CGFont对象。字体显示正常,但内存占用远超预期。
代码实现
CollectionView数据源代码
extension FontPickerPageViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { fontItems.count } func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell( withReuseIdentifier: FontPickerCollectionViewCell.reuseIdentifier, for: IndexPath(item: indexPath.item, section: 0) ) as! FontPickerCollectionViewCell let fontItem = fontItems[indexPath.item] let name = fontItem.family cell.label.text = name if let font = try? fontManager.getFont(forName: name) { cell.label.font = font } else if let fontFile = fontItem.files.first(where: { $0.variant == .regular }), let url = URL(string: fontFile.urlString) { // Download font Task { do { let data = try await NetworkManager.downloadData(URLRequest(url: url)) try await fontManager.registerFont(for: data, name: name) } catch { print(error) } } } return cell } }
字体管理器代码
final class FontManager { enum FontManager2Error: LocalizedError { case registerProviderError case postScriptNameError case fontNotFound var errorDescription: String? { switch self { case .registerProviderError: return "Failed to register font with provider" case .postScriptNameError: return "Failed to get the post script name" case .fontNotFound: return "Failed to find font" } } } // MARK: - Properties static let shared = FontManager() private var storage = [String: String]() //fontName : postScriptName private let lockQueue = DispatchQueue(label: "lock", attributes: .concurrent) // MARK: - Initializers private init() {} // MARK: - Font Management private func registerFont(for data: Data, name: String, completion: @escaping (Result<(), Error>) -> Void) { guard let provider = CGDataProvider(data: data as CFData), let cgFont = CGFont(provider) else { return completion(.failure(FontManager2Error.registerProviderError)) } guard let postScriptName = cgFont.postScriptName as? String else { return completion(.failure(FontManager2Error.postScriptNameError)) } CTFontManagerRegisterGraphicsFont(cgFont, nil) lockQueue.async(flags: .barrier) { [weak self] in self?.storage[name] = postScriptName completion(.success(())) } } func registerFont(for data: Data, name: String) async throws { try await withCheckedThrowingContinuation { continuation in registerFont(for: data, name: name) { result in switch result { case .success: continuation.resume() case .failure(let error): continuation.resume(throwing: error) } } } } func isFontRegistered(name: String) -> Bool { lockQueue.sync { storage[name] != nil } } func getFont(forName name: String) throws -> UIFont { try lockQueue.sync { guard let postScriptName = storage[name], let uiFont = UIFont(name: postScriptName, size: Config.defaultFontSize) else { throw FontManager2Error.fontNotFound } return uiFont } } }
原因分析
问题核心在于字体注册的机制差异:
- 调用
CTFontManagerRegisterGraphicsFont注册的自定义字体,会被Core Text框架全局持有,不会随对应UIFont对象的释放而自动卸载。 - 系统字体由iOS系统统一管理,内置了缓存回收机制,会自动释放不在屏幕显示范围内的字体资源,因此内存占用仅对应当前显示的单元格数量。
- 你的代码中,字体一旦被下载注册,就会永久留在内存中,不管CollectionViewCell是否被复用或销毁。
解决方案
方案1:按需注册+主动卸载字体
如果不需要长期保留所有字体,可以在字体不再被使用时主动卸载:
- 在FontManager中添加卸载方法:
func unregisterFont(forName name: String) throws { try lockQueue.sync(flags: .barrier) { guard let postScriptName = storage.removeValue(forKey: name), let cgFont = CGFont(postScriptName as CFString) else { throw FontManager2Error.fontNotFound } CTFontManagerUnregisterGraphicsFont(cgFont, nil) } }
- 在CollectionView的
didEndDisplayingCell方法中,判断字体是否不再被引用,调用卸载方法。注意:需确保无其他地方使用该字体,避免显示异常。
方案2:使用NSCache缓存字体(推荐)
放弃全局注册,直接通过CGFont创建UIFont,用NSCache管理缓存,内存不足时自动回收:
修改FontManager核心逻辑:
final class FontManager { // 保留原有错误定义和单例 private var fontCache = NSCache<NSString, UIFont>() private var fontDataCache = NSCache<NSString, Data>() private let lockQueue = DispatchQueue(label: "lock", attributes: .concurrent) private init() { // 设置缓存上限,控制内存占用 fontCache.countLimit = 50 } func getFont(forName name: String, size: CGFloat = Config.defaultFontSize) async throws -> UIFont { // 先查字体缓存 if let cachedFont = fontCache.object(forKey: name as NSString) { return cachedFont } // 查字体数据缓存 if let cachedData = fontDataCache.object(forKey: name as NSString) { let font = try createFont(from: cachedData, size: size) fontCache.setObject(font, forKey: name as NSString) return font } // 下载并创建字体 guard let fontFile = fontItems.first(where: { $0.family == name })?.files.first(where: { $0.variant == .regular }), let url = URL(string: fontFile.urlString) else { throw FontManager2Error.fontNotFound } let data = try await NetworkManager.downloadData(URLRequest(url: url)) fontDataCache.setObject(data, forKey: name as NSString) let font = try createFont(from: data, size: size) fontCache.setObject(font, forKey: name as NSString) return font } private func createFont(from data: Data, size: CGFloat) throws -> UIFont { guard let provider = CGDataProvider(data: data as CFData), let cgFont = CGFont(provider) else { throw FontManager2Error.registerProviderError } var error: Unmanaged<CFError>? guard let ctFont = CTFontCreateWithGraphicsFont(cgFont, size, nil, &error) else { throw error!.takeRetainedValue() as Error } return UIFont(ctFont: ctFont) } }
这种方式下,NSCache会自动回收不常用的字体对象,内存占用仅保留近期使用的字体,符合CollectionView复用场景的需求。
方案3:页面退出时批量清理
如果必须全局注册字体,可在用户退出字体选择页面时批量卸载所有自定义字体:
func unregisterAllCustomFonts() { lockQueue.sync(flags: .barrier) { for postScriptName in storage.values { if let cgFont = CGFont(postScriptName as CFString) { CTFontManagerUnregisterGraphicsFont(cgFont, nil) } } storage.removeAll() } }
在页面的viewWillDisappear或deinit方法中调用该方法,清理全局注册的字体。
内容的提问来源于stack exchange,提问作者Kex
相关产品推荐
相关产品推荐

