可插拔的自定义视图笔尖(Nib-in-a-Nib):内存泄漏 - 为什么?

时间:2011-06-15 12:17:31

标签: uiview memory-leaks custom-controls nib nscoding

我们当前的best-practice for custom views是:

  1. 在笔尖中构建自定义视图。
  2. 在视图控制器中,以编程方式加载Nib,从加载对象数组中获取自定义视图(我们在UIView类别方法+loadInstanceFromNib中执行此操作)。
  3. 将自定义视图添加为子视图,设置其框架。
  4. 我们实际想要的是在查看控制器Nib中“嵌入”自定义视图Nib。如果不这样做,至少我们想要添加和将自定义视图实例放在视图控制器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:,它将从Nib加载视图(将调用-initWithCoder:并右转到标记为“Step 2”的if分支) ,设置其框架,并将其保留计数设置为1。

    但是,如果我们在视图控制器Nib中实例化自定义视图,那么(肯定是相当丑陋的)静态loadNormally变量最初是NO:我们从“步骤1”开始,我们在其中加载在确保我们将立即使用-initWithCoder:的“正常”if-branch之后,返回从其Nib加载的实例。从自定义视图Nib加载意味着我们回到-initWithCoder:,这次使用loadNormally==YES,即我们让Nib加载机制完成其工作并返回自定义视图实例。

    结果,总结:

    • 好处: IT工作!!!我们在Interface Builder中有“可插拔”的自定义视图!
    • 糟糕:丑陋的静态变量......: - /
    • 丑陋的: 自定义视图的一个实例被泄露了!这是我爱你的帮助 - 我不明白为什么。有什么想法吗?

3 个答案:

答案 0 :(得分:3)

我们最终采用了一种更好的方法,包括在我们的自定义视图中覆盖-awakeAfterUsingCoder:,将从视图控制器Nib加载的对象替换为从“嵌入式”Nib(CustomView.xib)加载的对象。

我在一篇博文中写了how we embed custom-view Nibs inside other Nibs

代码如下:

// 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;
}

答案 1 :(得分:1)

杨的回答很棒......但是“发送到解除分配的实例的消息”仍然可能发生。我通过使用'self'赋值解决了这个问题。

因此,如果您使用ARC,则必须允许此“自我”分配。 (阅读https://blog.compeople.eu/apps/?p=142了解更多信息)

要在ARC项目中实现此目的,请在文件中添加“-fno-objc-arc”标志编译器设置。 然后在这个文件中进行NO-ARC编码(比如dealloc设置nils,调用super dealloc等等。)

此外,客户端nib的viewcontroller应该使用strong属性来保存awakeFromNib返回的实例。对于我的示例代码,customView的引用如下:


@property(强大,非原子)IBOutlet CustomView * customView;


我最后使用 UIView + Util 类别中定义的 copyUIPropertiesTo: loadNibNamed 在属性处理和nib加载方面添加了一些其他改进。< / p>

所以 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];
        }
    }    
}

答案 2 :(得分:0)

还有另一种方法可以做到这一点:

说你在View1中使用了Interface Builder,然后你创建了另一个名为View2的视图,View2有一个相应的View2.xib文件,你已经链接了View2.mView2.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,而无需再编写代码