Сменные наконечники с настраиваемым углом обзора (Nib-in-a-Nib): утечка памяти - почему?

Наша текущая передовая практика для настраиваемых представлений:

  1. Создайте индивидуальный вид в Nib.
  2. В контроллере представления программно загрузите перо, получите настраиваемое представление из массива загруженных объектов (мы делаем это в методе категории UIView +loadInstanceFromNib).
  3. Добавьте собственное представление в качестве подпредставления, установите его рамку.

Что мы на самом деле хотим, так это «встроить» перо настраиваемого представления в наконечник контроллера представления. В противном случае мы, по крайней мере, хотели бы добавить и расположить настраиваемое перо. -view внутри контроллера представления Nib (не видя его содержимого).

Мы очень близко подошли к следующему решению:

@implementation CustomView

static BOOL loadNormally;

- (id) initWithCoder:(NSCoder*)aDecoder {
    id returnValue = nil;
    if (loadNormally) { // Step 2
        returnValue = [super initWithCoder:aDecoder];
        loadNormally = !loadNormally;
    } else {            // Step 1
        loadNormally = !loadNormally;
        returnValue = [CustomView loadInstanceFromNib];
    }
    return returnValue;
}

- (id) initWithFrame:(CGRect)frame {
    loadNormally = YES;
    self = (id) [[CustomView loadInstanceFromNib] retain];
    self.frame = frame;
    return self;
}
// ...
@end

Если мы создаем экземпляр пользовательского представления программно, мы используем -initWithFrame:, который загрузит представление из пера (который вызовет -initWithCoder: и перейдет прямо к ветви if с меткой «Шаг 2»), установит его фрейм и установит его счетчик сохранения на 1.

Однако, если мы создаем экземпляр пользовательского представления внутри наконечника контроллера представления, статическая переменная loadNormally (по общему признанию довольно уродливая) изначально будет NO: мы начинаем с «Шага 1», где мы загружаем и возвращаем экземпляр, загруженный из его наконечника, предварительно убедившись, что что мы немедленно будем использовать "нормальную" if-ветвь -initWithCoder:. Загрузка из Nib с настраиваемым представлением означает, что мы возвращаемся в -initWithCoder:, на этот раз с loadNormally==YES, т.е. мы позволяем механизму загрузки Nib выполнять свою работу и возвращать экземпляр с настраиваемым представлением.

Итоги, вкратце:

  • Хорошо: ЭТО РАБОТАЕТ !!! У нас есть «подключаемые» пользовательские представления в Интерфейсном Разработчике!
  • Плохо: уродливая статическая переменная…: - /
  • Уродливое: Произошла утечка экземпляра настраиваемого представления! Мне нужна ваша помощь - я не понимаю почему. Любые идеи?

person Yang Meyer    schedule 15.06.2011    source источник


Ответы (3)


В итоге мы пришли к более приятному способу, который включает в себя переопределение -awakeAfterUsingCoder: в нашем настраиваемом представлении, замену объекта, загруженного из пера контроллера представления, на объект, загруженный из «встроенного» пера (CustomView.xib).

Я написал о том, как мы встраиваем перья с настраиваемым изображением в другие перья, в обширном сообщении в блоге. .

Код выглядит примерно так:

// CustomView.m
- (id) awakeAfterUsingCoder:(NSCoder*)aDecoder {
    BOOL theThingThatGotLoadedWasJustAPlaceholder = ([[self subviews] count] == 0);
    if (theThingThatGotLoadedWasJustAPlaceholder) {
        // load the embedded view from its Nib
        CustomView* theRealThing = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([CustomView class]) owner:nil options:nil] objectAtIndex:0];

        // pass properties through
        theRealThing.frame = self.frame;
        theRealThing.autoresizingMask = self.autoresizingMask;

        [self release];
        self = [theRealThing retain];
    }
    return self;
}
person Yang Meyer    schedule 20.07.2011
comment
Дополнительная информация, которая делает эту работу под ARC: blog.yangmeyer.de/blog/2012/07/09/ - person Yang Meyer; 10.07.2012

Янг ответил великолепно ... но "сообщения, отправленные в освобожденный экземпляр" все еще могут возникать. Я решил эту проблему, используя присвоение «себе».

Поэтому, если вы используете ARC, вам придется разрешить это «самостоятельное» назначение. (прочтите https://blog.compeople.eu/apps/?p=142 для получения дополнительной информации)

Чтобы добиться этого в проекте ARC, добавьте параметр компилятора флага -fno-objc-arc в свой файл. Затем выполните кодирование NO-ARC в этом файле (например, при установке функции dealloc на ноль, вызове super dealloc и т. Д.)

Кроме того, контроллер просмотра кончика клиента должен использовать свойство strong для хранения экземпляра, возвращаемого awakeFromNib. В случае моего примера кода на customView ссылаются следующим образом:


@property (сильный, неатомарный) IBOutlet CustomView * customView;


Наконец, я добавил некоторые другие улучшения в обработку свойств и загрузку пера с помощью copyUIPropertiesTo: и loadNibNamed, определенных в моей категории UIView + Util.

Итак, код awakeAfterUsingCoder: теперь

#import "UIView+Util.h"
...
- (id) awakeAfterUsingCoder:(NSCoder*)aDecoder
{
    // are we loading an empty “placeholder” or the real thing?
    BOOL theThingThatGotLoadedWasJustAPlaceholder = ([[self subviews] count] == 0);

    if (theThingThatGotLoadedWasJustAPlaceholder)
    {
        CustomView* customView = (id) [CustomView loadInstanceFromNib];
        // copy all UI properties from self to new view!
        // if not, property that were set using Interface buider are lost!
        [self copyUIPropertiesTo:customView];

        [self release];
        // need retain to avoid deallocation
        self = [customView retain];
    }
    return self;
}

Код категории UIView + Util:

@interface UIView (Util)
   +(UIView*) loadInstanceFromNib;
   -(void) copyUIPropertiesTo:(UIView *)view;
@end

вместе с его реализацией

#import "UIView+Util.h"
#import "Log.h"

@implementation UIView (Util)

+(UIView*) loadInstanceFromNib
{ 
    UIView *result = nil; 
    NSArray* elements = [[NSBundle mainBundle] loadNibNamed: NSStringFromClass([self class]) owner: nil options: nil];
    for (id anObject in elements)
    { 
        if ([anObject isKindOfClass:[self class]])
        { 
            result = anObject;
            break; 
        } 
    }
    return result; 
}

-(void) copyUIPropertiesTo:(UIView *)view
{
    // reflection did not work to get those lists, so I hardcoded them
    // any suggestions are welcome here

    NSArray *properties =
    [NSArray arrayWithObjects: @"frame",@"bounds", @"center", @"transform", @"contentScaleFactor", @"multipleTouchEnabled", @"exclusiveTouch", @"autoresizesSubviews", @"autoresizingMask", @"clipsToBounds", @"backgroundColor", @"alpha", @"opaque", @"clearsContextBeforeDrawing", @"hidden", @"contentMode", @"contentStretch", nil];

    // some getters have 'is' prefix
    NSArray *getters =
    [NSArray arrayWithObjects: @"frame", @"bounds", @"center", @"transform", @"contentScaleFactor", @"isMultipleTouchEnabled", @"isExclusiveTouch", @"autoresizesSubviews", @"autoresizingMask", @"clipsToBounds", @"backgroundColor", @"alpha", @"isOpaque", @"clearsContextBeforeDrawing", @"isHidden", @"contentMode", @"contentStretch", nil];

    for (int i=0; i<[properties count]; i++)
    {
        NSString * propertyName = [properties objectAtIndex:i];
        NSString * getter = [getters objectAtIndex:i];

        SEL getPropertySelector = NSSelectorFromString(getter);

        NSString *setterSelectorName =
            [propertyName stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[propertyName substringToIndex:1] capitalizedString]];

        setterSelectorName = [NSString stringWithFormat:@"set%@:", setterSelectorName];

        SEL setPropertySelector = NSSelectorFromString(setterSelectorName);

        if ([self respondsToSelector:getPropertySelector] && [view respondsToSelector:setPropertySelector])
        {
            NSObject * propertyValue = [self valueForKey:propertyName];

            [view setValue:propertyValue forKey:propertyName];
        }
    }    
}
person Pascal    schedule 07.06.2012
comment
Мне нравится идея извлечения копирования свойств в метод (что делает его переопределяемым подклассами). Обратите внимание, что метод действительно работает под ARC, как я, наконец, добрался до описания в следующем посте: blog.yangmeyer.de/blog/2012/07/09/ - person Yang Meyer; 10.07.2012
comment
@Yang отлично, я попробую как можно скорее :-) - person Pascal; 10.07.2012

Есть альтернативный способ сделать это:

скажем, вы используете View1 в своем Interface Builder, затем вы создаете другое представление с именем View2, View2 имеет соответствующий файл View2.xib, вы связали выходы в View2.m и View2.xib.

Затем в View1.m напишите следующее:

-(void)awakeFromNib
{
    NSArray *topObjects = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:nil options:nil];
    self.subContentView = topObjects.firstObject]
    [self addSubview:self.subContentView];
}

Таким образом, вы можете использовать View1 в тех местах, где вам нужно поместить свое собственное представление в Interface Builder, таким образом сделав View1 повторно используемым в Interface Builder без написания дополнительного кода.

person CarmeloS    schedule 02.09.2014