如何清除填充表情符号字符的字体缓存?

Posted

技术标签:

【中文标题】如何清除填充表情符号字符的字体缓存?【英文标题】:How to clear font cache filled with emoji characters? 【发布时间】:2015-09-18 00:04:06 【问题描述】:

我正在为 iPhone 开发键盘扩展。有一个类似于 Apple 自己的表情符号键盘的表情符号屏幕,在 UICollectionView 中显示大约 800 个表情符号字符。

当滚动此表情符号UIScrollView 时,内存使用量会增加并且不会下降。我正确地重用了单元格,并且在使用显示 800 次的单个表情符号字符进行测试时,滚动期间内存不会增加。

使用工具,我发现我的代码中没有内存泄漏,但表情符号字形似乎已被缓存,并且可能需要大约 10-30MB 的内存,具体取决于字体大小(研究表明它们实际上是 PNG)。键盘扩展在被杀死之前可以使用很少的内存。有没有办法清除字体缓存?


编辑

添加代码示例重现问题:

let data = Array("????????☺️????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????✨????????????????????????????????????????????????????????????????????✊✌️????✋????????????????????????????☝️????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????⭐️☀️⛅️☁️⚡️☔️❄️⛄️????????????????☕️????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????❤️????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????️????????????????⚽️⚾️????????????????⛳️????????????????????????????????????????").map String($0)

class CollectionViewTestController: UICollectionViewController 
    override func viewDidLoad() 
        collectionView?.registerClass(Cell.self, forCellWithReuseIdentifier: cellId)
    

    override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
        return data.count
    

    override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell 
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellId, forIndexPath: indexPath) as! Cell
        if cell.label.superview == nil 
            cell.label.frame = cell.contentView.bounds
            cell.contentView.addSubview(cell.label)
            cell.label.font = UIFont.systemFontOfSize(34)
        
        cell.label.text = data[indexPath.item]
        return cell
    

    override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int 
        return 1
    


class Cell: UICollectionViewCell 
    private let label = UILabel()

运行并滚动UICollectionView 后,我得到如下内存使用图:

【问题讨论】:

【参考方案1】:

我遇到了同样的问题,并通过从 /System/Library/Fonts/Apple Color Emoji.ttf 转储 .png 并使用 UIImage(contentsOfFile: String) 而不是字符串来修复它。

我使用https://github.com/github/gemoji 提取.png 文件,将文件重命名为@3x 后缀。

func emojiToHex(emoji: String) -> String 
    let data = emoji.dataUsingEncoding(NSUTF32LittleEndianStringEncoding)
    var unicode: UInt32 = 0
    data!.getBytes(&unicode, length:sizeof(UInt32))
    return NSString(format: "%x", unicode) as! String


let path = NSBundle.mainBundle().pathForResource(emojiToHex(char) + "@3x", ofType: "png")
UIImage(contentsOfFile: path!)

UIImage(contentsOfFile: path!) 已正确释放,因此内存应保持在较低水平。到目前为止,我的键盘扩展还没有崩溃。

如果 UIScrollView 包含大量表情符号,请考虑使用 UICollectionView,它在缓存中仅保留 3 或 4 个页面,并释放其他看不见的页面。

【讨论】:

是的,这是我的备用计划。它有一些缺点:那些 .PNG 将使应用程序下载大小增加约 15 MB,我的项目中将有大约 800 个额外文件,一些额外的工作我认为不应该是必要的...... 通过适当的 png 压缩,我认为您可以将额外的大小减少到 4MB 左右。我同意这不是必需的,不幸的是,除非 Apple 解决了这个问题,否则我看不到任何其他选项...... gemoji 对我来说不是很好,所以我创建了自己的工具来制作这些 .png 文件。但是他们有 24 MB!你能建议如何减少这个额外的尺寸吗?我对 .png 压缩一无所知... 我使用 Adob​​e Fireworks 批处理作业来压缩我的图像,但我想任何其他图像软件都可以做到这一点。您可以将图像限制为 256 色以减小尺寸。我个人认为与原始表情符号没有区别。 由于法律问题,Apple 刚刚拒绝了我的一个使用表情符号 PNG 的键盘应用程序。因此,我不相信使用 Apple 表情符号的 PNG 会起作用。 :(【参考方案2】:

我遇到了同样的问题并尝试了很多方法来释放内存,但没有运气。我只是根据 Matthew 的建议更改了代码。它工作正常,对我来说不再有内存问题,包括 iPhone 6 Plus。

代码更改很少。在下面的 UILabel 子类中找到更改。如果你问我,挑战是获取表情符号图像。我不知道 gemoji (https://github.com/github/gemoji) 是如何工作的。

    //self.text = title //what it used to be
    let hex = emojiToHex(title)  // this is not the one Matthew provides. That one return strange values starting with "/" for some emojis. 
    let bundlePath = NSBundle.mainBundle().pathForResource(hex, ofType: "png")

    // if you don't happened to have the image
    if bundlePath == nil
    
        self.text = title
        return
    
    // if you do have the image 
    else
    
        var image = UIImage(contentsOfFile: bundlePath!)

        //(In my case source images 64 x 64 px) showing it with scale 2 is pretty much same as showing the emoji with font size 32.
        var cgImage = image!.CGImage
        image = UIImage( CGImage : cgImage, scale : 2, orientation: UIImageOrientation.Up  )!
        let imageV = UIImageView(image : image)

        //center
        let x = (self.bounds.width - imageV.bounds.width) / 2
        let y = (self.bounds.height - imageV.bounds.height) / 2
        imageV.frame = CGRectMake( x, y, imageV.bounds.width, imageV.bounds.height)
        self.addSubview(imageV)
    

Matthew 提供的 emojiToHex() 方法会为某些表情符号返回以“/”开头的奇怪值。到目前为止,给定链接上的解决方案没有问题。 Convert emoji to hex value using Swift

func emojiToHex(emoji: String) -> String

    let uni = emoji.unicodeScalars // Unicode scalar values of the string
    let unicode = uni[uni.startIndex].value // First element as an UInt32

    return String(unicode, radix: 16, uppercase: true)

--------- 一段时间后----

事实证明,这个 emojiToHex 方法并不适用于每个表情符号。所以我最终通过 gemoji 下载所有表情符号,并将每个表情符号图像文件(文件名如 1.png、2.png 等)与表情符号本身映射到字典对象中。现在改用以下方法。

func getImageFileNo(s: String) -> Int

        if Array(emo.keys).contains(s)
        
             return emo[s]!
           
        return -1

【讨论】:

这样的flag怎么样? ??,我看到 emojiToHex 返回缺少的十六进制值。这个标志必须是 1f1fe-1f1ea ,但它只返回 1f1fe? 后来我遇到了与 emojiToHex 类似的问题。然后我碰巧弄清楚了表情符号下载表情符号并手动将每个图像文件映射到表情符号。现在更新帖子。【参考方案3】:

我猜你正在使用[UIImage imageNamed:] 加载图像,或者从它派生的东西。这会将图像缓存在系统缓存中。

您需要使用 [UIImage imageWithContentsOfFile:] 来加载它们。这将绕过缓存。

(如果这不是问题,那么您需要在问题中包含一些代码,以便我们可以看到发生了什么。)

【讨论】:

我没有直接在我的代码中加载任何图像。我只是向UICollectionViewCells 添加标签,这些标签的文本是表情符号的 unicode 字符,如下所示:??☺️。在内部,它们可能是 PNG,但我无法影响它们的加载方式。如果UIKit 在内部使用[UIImage imageNamed:] 来加载它们,我无法更改它。请查看我的编辑。 啊,好的,我明白了。我认为你是对的,有一个字形缓存妨碍了你的某处。看起来使用 NSTextStorage / NSLayoutManager 渲染可能是一种选择,这样您就可以自己控制存储,但我以前从未这样做过(我只是在猜测,查看文档)。 我尝试了您的建议,但没有成功。我现在直接用NSLayoutManager画emoji,字符代码存储在NSTextStorage。即使我确保所有用于渲染的类都被释放,内存也不会被释放。我只能猜测字形被缓存得更深,可能在NSGlyphGenerator 中,但该类是 ios 上的私有 API。【参考方案4】:

我也曾在这方面四处走动,经过多次测试,我得出以下结论:

虽然字体缓存确实会影响您的扩展程序的内存占用以及 Xcode 调试导航器和内存报告中的总使用量,但它的处理方式与您预算的其余部分并不完全相同。

有些人引用 50 MB 作为扩展限制,而在 Apple 文档中,我想我见过 30 或 32 MB 被引用。我们在 30 到 40 MB 之间的不同点看到内存警告,这太不一致了,无法满足任何特定值,但看起来确实很具体的一件事是发生在 53 MB 的内存异常,该异常被注销Xcode 正是那个数字。如果我使用空白键盘扩展并用 40 MB 的图像视图填充它,这是一回事,但如果我使用 30 MB 和 20 MB 的字体字形使用,我发现我的键盘没有关闭。

根据我的观察,字体缓存似乎已被清理干净,但没有您认为必要的频率(尤其是当无用的组合内存值超过 30 或 32 MB 时您变得紧张时)。

如果您将自己的内存使用量预算为 30 MB,那么您应该是安全的,前提是您不会引入一次性全部需要 23 MB(即 53-30)字体字形的场景。这将受到表情符号网格的密集程度的影响,甚至可能受到使用的字体大小的影响。这里的共识是,如果您从表情符号集合视图的一端滚动到另一端,您将通过超过 23 MB 的字体字形,但如果您的其余内存占用是合理的(即 30 MB或以下),字体缓存应该有机会清理。

在我的测试中,我尝试使用更多字体字形自动轰炸扩展程序,我认为我能够击败字体缓存清理过程,从而导致崩溃。

因此,考虑到 UICollectionView 的用例以及它的滚动速度,如果您真的推动了 30 MB 的内存预算并且滚动速度非常快,那么应用程序可能会崩溃。您可以让自己达到这个 53 MB 的硬限制。

鉴于以上所有因素 - 使用成熟的键盘扩展程序,只要我保留大约 30 MB 自己的(非字体字形)占用空间,即使在快速更改表情符号类别时我也没有遇到崩溃并快速滚动。 不过,我确实会以这种方式遇到系统内存警告,这让我再次产生怀疑。

与使用UIImage(contentsOfFile) 相比,这种方法的另一个问题是,除了字体缓存正在做什么之外,使用内存报告的整体内存占用来检查您的应用程序更加困难。也许有办法将它们分开,但我不知道。

【讨论】:

我相信这是正确的答案。根据我的测试,我观察到了同样的事情。使用 Instruments 进行检查还发现内存消耗位于 glyp 的 malloc 中。最后一点:Apple 不喜欢人们使用他们的表情符号,因此将它们作为图像嵌入肯定会被拒绝。【参考方案5】:

许多表情符号由包含多个 unicode 标量的 sequences 表示。 Matthew's answer 适用于基本表情符号,但它只返回来自国家标志等表情符号序列的第一个标量。

下面的代码将获取完整的序列并创建一个与gemoji 导出的文件名匹配的字符串。

一些简单的笑脸表情也有fe0f 选择器。但是 gemoji 不会在导出时将这个选择器添加到文件名中,因此应该将其删除。

func emojiToHex(_ emoji: String) -> String

    var name = ""

    for item in emoji.unicodeScalars 
        name += String(item.value, radix: 16, uppercase: false)

        if item != emoji.unicodeScalars.last 
            name += "-"
        
    

    name = name.replacingOccurrences(of: "-fe0f", with: "")
    return name

【讨论】:

【参考方案6】:

就我而言,简单的CATextLayer 有助于减少我的应用程序的内存使用量。当我使用UILabel 渲染表情符号时,键盘扩展内存从~16MB 增加到~76MB。将UILabel 替换为CATextLayer 后,键盘扩展内存从~16MB 增加到仅~26MB。

以前的UICollectionViewCell 子类设置:

- (instancetype)initWithFrame:(CGRect)frame 
    self = [super initWithFrame:frame];

    // somewhere in the header file 
    // @property (nonatomic, strong, readonly) UILabel *textLabel;
    _textLabel = [UILabel new];
    self.textLabel.font = [UIFont fontWithName:@"HelveticaNeue" size:28];
    self.textLabel.textAlignment = NSTextAlignmentCenter;
    [self addSubview:self.textLabel];
    // some auto layout logic using Masonry
    [self.textLabel mas_makeConstraints:^(MASConstraintMaker *make) 
        make.size.equalTo(self);
        make.center.equalTo(self);
    ];

    return self;

我的UICollectionViewCell 子类设置与CATextLayer

- (instancetype)initWithFrame:(CGRect)frame 
    self = [super initWithFrame:frame];

    // somewhere in the header file 
    // @property (nonatomic, strong, readonly) CATextLayer *textLayer;
    _textLayer = [CATextLayer new];
    self.textLayer.frame = CGRectMake(0, 0, 33, 33);
    self.textLayer.font = CFBridgingRetain([UIFont fontWithName:@"HelveticaNeue" size:28].fontName);
    self.textLayer.fontSize = 28;
    self.textLayer.alignmentMode = kCAAlignmentCenter;
    [self.layer addSublayer:self.textLayer];

    return self;

更新

抱歉,伙计们忘记添加self.textLayer.contentsScale = [[UIScreen mainScreen] scale]; 以获得清晰的文本。不幸的是,内存使用量从 ~16MB 增加到 ~44MB,但仍然比 UILabel 解决方案好。

使用CATextLayer 的最终UICollectionViewCell 子类设置:

- (instancetype)initWithFrame:(CGRect)frame 
    self = [super initWithFrame:frame];

    [self.layer setRasterizationScale:[[UIScreen mainScreen] scale]];

    // somewhere in the header file 
    // @property (nonatomic, strong, readonly) CATextLayer *textLayer;
    _textLayer = [CATextLayer new];
    self.textLayer.frame = CGRectMake(0, 0, 33, 33);
    self.textLayer.font = CFBridgingRetain([UIFont fontWithName:@"HelveticaNeue" size:28].fontName);
    self.textLayer.fontSize = 28;
    self.textLayer.alignmentMode = kCAAlignmentCenter;
    NSDictionary *newActions = @
        @"onOrderIn": [NSNull null],
        @"onOrderOut": [NSNull null],
        @"sublayers": [NSNull null],
        @"contents": [NSNull null],
        @"bounds": [NSNull null]
    ;
    self.textLayer.actions = newActions;
    [self.layer addSublayer:self.textLayer];

    [self.layer setShouldRasterize:YES];

    return self;

【讨论】:

使用CATextLayer好像一点效果都没有。 @Niels 你为什么这么肯定?你验证了吗?就我而言,该应用受益于使用CATextLayer

以上是关于如何清除填充表情符号字符的字体缓存?的主要内容,如果未能解决你的问题,请参考以下文章

CSS 对手机表情符号字体的引用?

如何在网站上使用表情符号字体?

java如何实现微信表情及特殊字符存入数据库

如何在 django 中使用 xhtml2pdf 生成的 PDF 中显示表情符号字符?

如何使用python在字符串中查找和计算表情符号?

如何从字符串中删除表情符号字符?