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

25

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

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

使用Instruments,我发现我的代码中没有内存泄漏,但似乎表情符号被缓存并且根据字体大小可以占用大约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后,我得到了这样的内存使用图表: enter image description here

6个回答

10

我遇到了同样的问题,通过从 /System/Library/Fonts/Apple Color Emoji.ttf 中转储 .png 文件并使用 UIImage(contentsOfFile: String) 而不是 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页并释放其他未看到的页面。


2
是的,这是我的备选方案。不过它也有一些缺点:那些.PNG文件将会增加应用程序下载大小约15MB,我将在项目中多出大约800个额外的文件,还需要额外的工作,我认为这些工作并不必要... - Rasto
2
通过适当的 PNG 压缩,我认为你可以将额外的大小减少到约 4MB。我同意这不应该是必要的,但不幸的是除非 Apple 修复这个问题,否则我看不到其他选择... - Matthew
gemoji 对我来说效果不是很好,所以我创建了自己的工具来制作那些.png文件。但是它们有24兆字节大小!你能建议如何减小这个额外的体积吗?我对.png压缩一窍不通…… - Rasto
2
我使用Adobe Fireworks批处理作业来压缩我的图像,但我想任何其他图像软件都可以做到同样的事情。您可以将图像限制为256种颜色以减小大小。我个人认为与原始表情符号没有区别。 - Matthew
1
苹果刚刚因为法律问题拒绝了我使用表情符号PNG的键盘应用。因此,我不认为使用苹果表情符号的PNG会起作用。 :( - justColbs
显示剩余3条评论

3

我曾经遇到同样的问题,尝试了许多释放内存的方法,但是都没有成功。最终,我根据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()方法对于一些表情符号会返回以"/"开头的奇怪值。在给定链接中提供的解决方案目前没有任何问题。使用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
}

这个标记怎么样?我看到 emojiToHex 返回了缺失的十六进制值。这个标记应该是 1f1fe-1f1ea,但它只返回了 1f1fe? - TomSawyer
我后来也遇到了与 emojiToHex 相似的问题。然后我偶然发现可以下载表情符号并手动将每个图像文件映射到相应的表情符号上。现在正在更新帖子。 - Ahmet Akkök

1

我也做了许多尝试,经过多次测试,得出以下结论:

虽然字体缓存对您的扩展程序的内存占用和在 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处的内存消耗。最后需要注意的是:苹果不喜欢人们使用他们的表情符号,因此将它们嵌入为图像是被拒绝的确定方法。 - Warpzit

1
我猜测您正在使用[UIImage imageNamed:]或其衍生物来加载图像。这将在系统缓存中缓存图像。
相反,您需要使用[UIImage imageWithContentsOfFile:]来加载它们。这将绕过缓存。
(如果这不是问题的原因,则您需要在问题中包含一些代码,以便我们可以看到发生了什么。)

我在代码中没有直接加载任何图像。我只是向UICollectionViewCell添加标签,这些标签的文本是表情符号Unicode字符,例如:☺️。在内部,它们可能是PNG格式,但我无法影响它们的加载方式。如果UIKit在内部使用[UIImage imageNamed:]来加载它们,我无法更改它。请查看我的编辑。 - Rasto
1
啊,好的,我明白了。我认为你是对的,某个字形缓存正在阻挡你。看起来使用NSTextStorage / NSLayoutManager进行渲染可能是一个选择,这样你就可以自己控制存储,但我以前从未这样做过(我只是在猜测,看文档)。 - Ewan Mellor
我尝试了你的建议,但没有成功。现在我正在使用NSLayoutManager直接绘制表情符号,字符代码存储在NSTextStorage中。即使我确保所有用于渲染的类都被释放,内存也没有被释放。我只能猜测字形可能被更深层次地缓存,也许是在NSGlyphGenerator中,但这个类在iOS上是私有API。 - Rasto

0

许多表情符号由包含多个Unicode标量的序列表示。Matthew的答案适用于基本表情符号,但它仅返回序列中的第一个标量,例如国旗等表情符号。

下面的代码将获取完整的序列,并创建一个与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
}

-1
在我的情况下,简单的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解决方案好。

最终UICollectionViewCell子类设置与CATextLayer

- (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
@Niels 你为什么这么确定?你验证过了吗?在我的方面,应用程序受益于使用CATextLayer - Shyngys Kassymov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接