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

时间:2015-07-01 01:11:24

标签: ios caching memory-management fonts emoji

我正在为iPhone开发键盘扩展程序。苹果自己的表情符号键盘有一个表情符号屏幕,在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后,我得到了内存使用情况图: enter image description here

6 个答案:

答案 0 :(得分: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页,并释放其他看不见的页面。

答案 1 :(得分: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)
    }

emojiToHex()方法Matthew为某些emojis提供了以“/”开头的奇怪值。到目前为止,给定链接的解决方案没有问题。 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
}

答案 2 :(得分:1)

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

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

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

答案 3 :(得分:0)

许多表情符号由sequences表示,其中包含多个unicode标量。 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
}

答案 4 :(得分:0)

我也在这附近待过,经过大量测试,我得出以下结论:

虽然字体高速缓存确实会占用扩展程序的内存空间,并在Xcode Debug Navigator和Memory Report中占用总使用量,但与其余预算的处理方式却不尽相同。

有人引用50 MB作为扩展限制,在Apple文档中,我认为我看到的是30 MB或32 MB。我们在30到40 MB之间的各个位置看到内存警告,这太不一致了,无法满足于任何特定的值,但是似乎很具体的一件事是发生在53 MB的内存异常,该内存异常被注销Xcode就是那个数字。如果我使用空白的键盘扩展名并在其中填充了40 MB的图像视图,那是一回事,但是如果我使用30 MB然后使用20 MB的字形,则发现我的键盘没有关闭。 / p>

根据我的观察,字体缓存看起来似乎已经清理干净了,但并没有达到您认为必要的频率(特别是如果无用的组合内存值超过30或32 MB时您变得紧张)。

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

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

因此,考虑到UICollectionView的用例及其滚动速度,如果您确实推动了30 MB的内存预算并且滚动速度非常快,则有可能使应用程序崩溃。您可能会允许自己达到这个53 MB的硬限制。

鉴于以上所有内容-具有完整的键盘扩展名,只要我保持自己的大约30 MB(非字体字形)占用空间,即使快速更改表情符号类别,我也不会崩溃并快速滚动。 但是,我确实会以这种方式遇到系统内存警告,这使我重新产生了怀疑。

与使用UIImage(contentsOfFile)相比,此方法的另一个问题是,除了字体缓存正在执行的操作之外,很难使用内存报告的整体内存占用量来检查您的应用程序。也许有一种方法可以将它们分开,但是我不知道。

答案 5 :(得分:-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;
}