可插拔自定义视图 Nibs (Nib-in-a-Nib):内存泄漏 - 为啥?

Posted

技术标签:

【中文标题】可插拔自定义视图 Nibs (Nib-in-a-Nib):内存泄漏 - 为啥?【英文标题】:Pluggable custom-view Nibs (Nib-in-a-Nib): Memory leak – why?可插拔自定义视图 Nibs (Nib-in-a-Nib):内存泄漏 - 为什么? 【发布时间】:2011-06-15 12:17:31 【问题描述】:

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

    在 Nib 中构建自定义视图。 在视图控制器中,以编程方式加载 Nib,从加载的对象数组中获取自定义视图(我们在 UIView 类别方法 +loadInstanceFromNib 中执行此操作)。 将自定义视图添加为子视图,设置其框架。

我们实际上想要的是“嵌入”自定义视图 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: 并直接转到标记为“步骤 2”的 if 分支),设置其框架,并将其保留计数设置为 1。

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

结果,总结:

好的:它工作!我们在 Interface Builder 中有“可插入”的自定义视图! 坏处: 一个丑陋的静态变量……:-/ 丑陋的: 自定义视图的一个实例泄露了! 这就是我希望你帮助的地方——我不明白为什么。有什么想法吗?

【问题讨论】:

【参考方案1】:

我们最终找到了一种更好的方法,其中包括在我们的自定义视图中覆盖 -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;

【讨论】:

在 ARC 下进行这项工作的后续措施:blog.yangmeyer.de/blog/2012/07/09/…【参考方案2】:

Yang 的回答很好……但仍然会出现“发送到已释放实例的消息”。我通过使用“自我”分配解决了这个问题。

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

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

另外,客户端 nib 的视图控制器应该使用 strong 属性来保存 awakeFromNib 返回的实例。在我的示例代码中,customView 是这样引用的:


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


我终于使用在我的 UIView+Util 类别中定义的 copyUIPropertiesTo:loadNibNamed 对属性处理和 nib 加载进行了一些其他改进。

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

【讨论】:

我喜欢将属性复制提取到方法的想法(这使得它可以被子类覆盖)。请注意,该技术确实在 ARC 下工作,正如我终于在后续帖子中描述的那样:blog.yangmeyer.de/blog/2012/07/09/… @Yang 太好了,我会尽快试一试 :-)【参考方案3】:

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

假设您在Interface Builder 中使用View1,然后您创建另一个名为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];

有了这个,您可以在需要将自定义视图放在Interface Builder 中的地方使用View1,从而使View1 可以在Interface Builder 中重复使用,而无需编写更多代码。

【讨论】:

以上是关于可插拔自定义视图 Nibs (Nib-in-a-Nib):内存泄漏 - 为啥?的主要内容,如果未能解决你的问题,请参考以下文章

子类化 Flask 可插拔视图以实现可扩展功能的最佳方式

可以在 VHDL 中完成动态可插拔模块吗?

Django-settings可插拔实现

可插拔应用程序的 Django 默认设置约定?

# "可插拔式"组件设计,领略组件开发的奥秘

sentinel限流二开(2)—可插拔的分布式存储