Проблемы с памятью UIBezierPath

Я создаю приложение, которое отображает большие чертежи в мозаичном представлении с различными аннотациями (фигурами), наложенными на изображение. Я рисую эти фигуры (в основном эллипсы, линии и многоугольники) с помощью UIBezierPaths, но при этом используется значительный объем памяти. Это мое первое приложение, поэтому буду признателен за любой совет.

Это пример из подкласса UIView, который рисует эллипсы. Я знаю, что использование представления изображения для отображения пути не обязательно, но, похоже, оно работает лучше, чем альтернатива. Я надеялся, что когда у меня будет UIImage, я смогу избавиться от памяти, используемой путем, но, как вы можете видеть на моем снимке экрана ниже, это на самом деле не работает.

CGSize size = CGSizeMake(self.frame.size.width+self.lineWeight,
                          self.frame.size.height+self.lineWeight);

UIGraphicsBeginImageContext(size);

UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
[self.fillColor setFill];
[path closePath];
[path fill];


CGRect strokeRect = CGRectMake(self.lineWeight/2.0, self.lineWeight/2.0,
                               size.width-self.lineWeight*2.0, size.height-self.lineWeight*2.0);
UIBezierPath *strokePath = [UIBezierPath bezierPathWithOvalInRect:strokeRect];
strokePath.lineWidth = self.lineWeight;
[self.lineColor setStroke];
[strokePath stroke];
[strokePath closePath];

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
[self addSubview:imageView];

Вот скриншот из инструмента, показывающий использование памяти при отображении изображения с 26 фигурами, в основном эллипсами.

введите здесь описание изображения

Некоторые чертежи, которые я показываю, могут иметь до 500 форм, поэтому мне действительно не помешал бы совет, как лучше этого добиться. Спасибо!


person seth    schedule 03.06.2014    source источник


Ответы (2)


Проблема с памятью здесь на самом деле заключается не в объектах UIBezierPath, а скорее в объектах UIImage (каждый из которых будет занимать четыре байта на пиксель для каждой формы).

Если это подкласс UIView, будет гораздо эффективнее просто поместить этот код рисования в ваш метод drawRect и рисовать там, а не рисовать изображение. Или используйте этот UIBezierPath для создания CAShapeLayer, а затем добавьте этот слой и подслой слоя вашего представления.

Но не создавайте UIImage объектов без крайней необходимости, так как это займет много памяти.


Я проверил это, рассмотрев три подхода, сравнив его с довольно экстремальным примером с 5000 перекрывающихся овалов, каждый размером от 20x20 до 40x40 пикселей. Понятно, что это абсурдный пример, когда все они накладываются друг на друга, но я просто пытался провести стресс-тестирование потребления памяти в экстремальном сценарии. Кроме того, в своих примерах я визуализирую только овалы, но вы, очевидно, можете адаптировать это для любых форм, которые хотите нарисовать.

Во всяком случае, мои результаты были следующими:

  1. Создание отдельных UIImage объектов, по одному для каждого нарисованного овала. Ясно, что это будет довольно агрессивно использовать память, требуя 4 байта на пиксель для каждого изображения. В моем примере это потребляло 92 МБ. Код подпрограммы «добавить овал» выглядел так:

    UIGraphicsBeginImageContextWithOptions(self.frame.size, NO, 0);
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    CGRect strokeRect = CGRectMake(self.lineWeight/2.0, self.lineWeight/2.0,
                                   self.frame.size.width-self.lineWeight*2.0, self.frame.size.height-self.lineWeight*2.0);
    
    UIBezierPath *strokePath = [UIBezierPath bezierPathWithOvalInRect:strokeRect];
    CGContextAddPath(context, strokePath.CGPath);
    [self.lineColor setStroke];
    [self.fillColor setFill];
    CGContextSetLineWidth(context, self.lineWeight);
    CGContextDrawPath(context, kCGPathEOFillStroke);
    
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.frame];
    imageView.image = image;
    [self addSubview:imageView];
    
  2. Создание отдельных экземпляров подкласса UIView для каждой фигуры, каждый подкласс UIView со своим собственным методом drawRect. Хотя я ожидал увидеть здесь значительную экономию, она была скромной, потребляя 94,5% памяти, всего 87 МБ:

    - (void)drawRect:(CGRect)rect
    {
        CGRect strokeRect = CGRectMake(self.lineWeight/2.0, self.lineWeight/2.0,
                                       rect.size.width-self.lineWeight*2.0, rect.size.height-self.lineWeight*2.0);
    
        UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:strokeRect];
    
        [self.lineColor setStroke];
        [self.fillColor setFill];
        path.lineWidth = self.lineWeight;
    
        [path fill];
        [path stroke];
    }
    

    Похоже, что iOS должна поддерживать растровое представление отдельных экземпляров подкласса UIView (даже если shouldRasterize было отключено).

  3. Создание структуры модели для отрисовываемых объектов, а затем их рендеринг в одном методе drawRect в суперпредставлении, которое показывает все эти овалы. Это было намного более эффективным с точки зрения использования памяти, потребляя 3,5 МБ при рендеринге этих 5000 овалов, всего на 3,8% больше памяти, чем исходный подход с отдельными UIImage объектами для каждой формы.

    - (void)drawRect:(CGRect)rect
    {
        [self.lineColor setStroke];
        [self.fillColor setFill];
    
        for (NSValue *rectValue in self.ovals) {
            CGRect frame = [rectValue CGRectValue];
            CGRect strokeRect = CGRectMake(frame.origin.x + self.lineWeight/2.0, frame.origin.y + self.lineWeight/2.0,
                                           frame.size.width - self.lineWeight*2.0, frame.size.height - self.lineWeight*2.0);
            UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:strokeRect];
            path.lineWidth = self.lineWeight;
            [path fill];
            [path stroke];
        }
    }
    

Я подозреваю, что вы могли бы сделать первые подходы более эффективными, сделав снимок, а затем удалив отдельные экземпляры, но третий подход кажется более логичным.

person Rob    schedule 03.06.2014
comment
Спасибо за предложение. Я попытался изменить представление эллипса на использование drawRect вместо UIImage, но теперь использование памяти фактическим классом эллипса значительно увеличилось, в результате чего общая используемая память по-прежнему составляет около 150 ГБ. - person seth; 03.06.2014
comment
@seth У меня наконец-то появилось несколько минут, чтобы взглянуть на это. Я заметил, что когда я использовал отдельные UIImage объекты, по одному для каждой формы, я наблюдал значительный расход памяти. Когда я использовал подкласс UIView для каждой формы, я ожидал некоторой экономии, но она была скромной. Но когда у меня был superview с пользовательским drawRect, который рисовал все отдельные фигуры, была огромная экономия памяти. Смотрите пересмотренный ответ. - person Rob; 04.06.2014
comment
Спасибо, что приложили столько усилий к этому ответу! Я посмотрю на это сегодня и отчитаюсь. - person seth; 04.06.2014

Есть хорошее решение, и я не могу дать вам полный конкретный ответ в виде кода, но я могу дать вам абстрактную отправную точку. Вы должны подумать об использовании Programming Design Patterns, и для вашей ситуации вы должны принять

Flyweight pattern

Когда бы вы использовали паттерн наилегчайшего веса?

Вы, естественно, подумали бы об его использовании, когда верно все следующее:

  1. Ваше приложение использует много объектов.
  2. Хранение объектов в памяти может повлиять на производительность памяти.
  3. Большая часть уникального состояния объекта (внешнее состояние) может быть экстернализована и упрощена.
  4. ....

текст выше взят из книги: Pro Objective-C Design Patterns для iOS

читайте и получайте удовольствие от записи...

person Goppinath    schedule 03.06.2014