启用ARC时出现奇怪的内存问题

时间:2012-03-16 17:18:15

标签: objective-c cocoa memory-management delegates automatic-ref-counting

我有一个非常非常奇怪的错误,可能与内存管理有关(即使我正在使用ARC)。

我有一个AppDelegate,Foo和SubFoo(它是Foo的子类)。

foo.h中

@protocol FooDelegate <NSObject>

- (void)didReceiveDownloadRequest:(NSURLRequest *)downloadRequest;

@end

@interface Foo : NSObject {
    __weak id <FooDelegate> delegate;
}

- (void)performRequest;

@property (nonatomic, weak) id <FooDelegate> delegate;
@property (nonatomic, retain) NSString *fileIdentifier;

Foo.m

@implementation Foo

@synthesize delegate, fileIdentifier;

- (id)init {
    if ((self = [super init])) {
        self.delegate = nil; // I tried leaving this line out, same result.
        NSLog(@"I am %p.", self);
    }

    return self;
}

- (void)performRequest {
    // Bah.
}

@end

SubFoo.h

@interface SubFoo : Foo {
    WebView *aWebView;
}

SubFoo.m

- (void)performRequest {
    if (self.fileIdentifier) {
        aWebView = [[WebView alloc] init];
        [aWebView setFrameLoadDelegate:self];
        [[aWebView mainFrame] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"theURL"]];
    }
}

- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame {
    NSLog(@"Finished loading.");

    // ...

    NSLog(@"Class Name: %@", NSStringFromClass([self class]));
    NSLog(@"Memory Location of delegate: %p", self.delegate);

    // ...
}

有时,webView上的类名:didFinishLoadForFrame:返回一个完全不同的类(而不是SubFoo,它返回随机类,如NSSet,NSArray,它甚至有时返回CFXPreferencesSearchListSource),有时它只是在那里用EXC_BAD_ACCESS崩溃,当它在类名上返回一个随机类时:它返回[randomClassName delegate]是一个无法识别的选择器。

编辑:当self被设置为另一个东西时,它在webView上设置为右:didFinishLoadForFrame:,并且在performRequest上它总是SubFoo。

任何帮助都将不胜感激。

5 个答案:

答案 0 :(得分:3)

首先,即使您在项目中使用ARC归零弱引用(@property (weak)),其他项目和框架可能也不会(并且可能不是)使用归零弱引用。

换句话说,假设框架中的所有代理都是__unsafe_unretained,除非:

  1. 委托属性在标题
  2. 中声明为weak
  3. 文档/标题明确说明

  4. 那就是说,让我们谈谈你的例子。您的对象所有权图表如下所示:

    Object ownership chart

    (注意:我不完全确定你的项目中哪个类使用SubFoo。基于通常的做法,我假设你有一个强烈引用SubFoo的类,并且该类也被设置为一个SubFooDelegate)

    最终,您的SubFoo实例正在失去其最后一个强引用并且正在取消分配。在一个完美的启用ARC的世界中,WebView指向SubFoo的指针此时将无法使用。但是,它还不是一个完美的世界,WebView的frameLoadDelegate是__unsafe_unretained。由于运行循环交互,WebView的寿命超过了SubFoo。 Web请求完成,并且取消引用死指针。

    要解决此问题,您需要在SubFoo的dealloc方法中调用[aWebView setFrameLoadDelegate:nil];。您还需要在重新分配aWebView时调用它,因为您正在丢失旧的aWebView:

    SubFoo.m

    @implementation SubFoo
    
    - (void)dealloc {
        [aWebView setFrameLoadDelegate:nil];
        // Also nil out any other unsafe-unretained references
    }
    
    - (void)performRequest {
        if (self.fileIdentifier) {
            [aWebView setFrameLoadDelegate:nil]; // Protects us if performRequest is called twice.  Is a no-op if aWebView is nil
            aWebView = [[WebView alloc] init];
            [aWebView setFrameLoadDelegate:self];
            [[aWebView mainFrame] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"theURL"]];
        }
    }
    
    - (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame {
        // ...
    }
    

答案 1 :(得分:1)

暂时忘记self.delegate错误,如果[self class]产生了错误的结果,那就是红色鲱鱼!你的结果表明你在某种程度上是在瞎扯self

webView:didFinishLoadForFrame:上的断点检查self值并逐步完成。

评论跟进

对于实例方法的第一个语句中的自我错误,让我们说,不寻常(但并非不可能)。

当一个对象被设置为另一个委托时,确保委托对象的生命周期至少与它作为委托者的生命周期一样长,这一点很重要。引入ARC可以使以前工作的代码失败,因为它可能比MRC下的代码更早地释放代理。发生这种情况时,对代理人的调用通常会失败。

但是,对代理人的调用,您的错误不会失败;通话开始 - 您最终进入webView:didFinishLoadForFrame: - 然后您发现self无效。要实际调用实例方法通常需要self的有效值,因为它用于确定要调用的方法实现。因此,self通常在方法开始时有效!

但请注意“通常”......

因此,尽管您已成功覆盖了您的方法,但您的错误可能是由于没有对SubFoo实例的强引用,您将其作为代理传递给aWebView,并且时间{ {1}}被称为webView:didFinishLoadForFrame:已经消失。

确保您对SubFoo保持强烈反对。如果您只是想测试(这是推荐的通用解决方案!)如果这是您的问题,您可以将其分配给本地静态(SubFoo instance声明在{{1}内声明在static SubFoo *holdMe中,它将至少在下次调用performRequest之前保留一个强引用。如果这确实是问题所在,那么您需要找到一种很好的方法来维护适合您设计的参考。

答案 2 :(得分:1)

这是真正的问题:您正在方法的上下文中创建SubFoo对象。因此,在方法完成后,SubFoo正在发布(在WebView有时间加载之前)。

要解决此问题,您需要将正在创建的SubFoo对象分配给持久性对象,例如您正在创建它的类的实例变量。这样,对象将持续超出其创建方法的范围,并且所有对象都将按预期工作。

答案 3 :(得分:0)

正如CRD所提到的,我会说一个不正确的对象/错误的访问返回是一个对象被释放的标志。有时候它会被另一个对象取代,有时它并不是因为你得到了糟糕的访问异常。关于如何在self发生这种情况,我会想到这是一个并发奇怪的情况(对象在另一个线程上被释放)。

确认这一点的最佳方法是在Instrument的NSZombie模板中运行您的代码,它会在您访问一个释放的对象时立即显示。它还显示何时保留/释放,因此您无需猜测。

答案 4 :(得分:0)

关于您的上述评论。

SubFoo *theClass = [[SubFoo alloc] init];

您必须将该类存储在

@property (strong)  SubFoo *mySubFoo;

如果你这样声明:

{
    SubFoo *theClass = [[SubFoo alloc] init];
}

它在结束时被释放。当变量超出范围时,ARC的这一部分就会被释放。如果你想让它漂浮在以太中你可以使用

{
    __weak SubFoo *theClass = [[SubFoo alloc] init];
}

并且它不会被释放,但这会导致内存泄漏,除非您仔细管理所有弱引用。如果它没有在-performRequest中发布,我假设请求看起来像这样:

{
    SubFoo *theClass = [[SubFoo alloc] init];
    [theClass performRequest];
}

wheras -webView:didFinishLoadForFrame:将来会在一些不分青红皂白的时间被调用。