【问题标题】:Accessing NSObject's (superclass) version of allocWithZone while bypassing overriding (subclass) version of allocWithZone在绕过 allocWithZone 的覆盖(子类)版本时访问 NSObject 的(超类)版本的 allocWithZone
【发布时间】:2013-10-20 01:02:16
【问题描述】:

在 Big Nerd Ranch 的 iOS 编程书(第 3 版)中,他们在 pg.194 上说 ..知识渊博的程序员仍然可以通过 allocWithZone: 创建 BNRItemStore 的实例,这将绕过我们偷偷摸摸的 alloc 陷阱。为防止这种可能性,请在 BNRItemStore.m 中覆盖 allocWithZone: 以返回单个 BNRItemStore 实例。

+(id) allocWithZone:(NSZone *)zone
{
    return [self sharedStore];
}

这句话让我感到困惑。下面的代码不是在某种程度上证明这是错误的吗-

#import <Foundation/Foundation.h>

@interface BNRItemStore : NSObject
+(BNRItemStore *)sharedStore;
+(id)retrieveObject;
@end
@implementation BNRItemStore
+(BNRItemStore *)sharedStore{
    static BNRItemStore *sharedStore=nil;
    if (!sharedStore){
        NSLog(@"Test2");
        sharedStore= [[super allocWithZone:nil] init];
    }
    NSLog(@"sharedStore-> %@",sharedStore);
    return sharedStore;
}
+(id)allocWithZone:(NSZone *)zone{
    NSLog(@"Test1");
    return [self sharedStore];
}
+(id)alloc{
    NSLog(@"Retrieving super object");
    NSLog(@"%@", [super allocWithZone:nil]);//Bypassing the subclass version of    allocWithZone.
    return [super allocWithZone:nil];
}
@end

int main(){
    [[BNRItemStore alloc] init]; //the alloc message triggers a call to the subclass  (overriding) version of +(id)alloc method
}

输出是:

  • 2013-10-18 18:24:40.132 BNRItemStore[381:707] 检索超级对象
  • 2013-10-18 18:24:40.134 BNRItemStore[381:707] BNRItemStore:0x7f8c72c091e0

如果子类 'alloc' 方法内的调用 [super allocWithZone:nil] 会触发对子类 allocWithZone 的调用,则控制台将记录“Test1”和“Test2”,最后会导致分配静态指针。但这并没有发生。 这意味着如果我们直接调用 [NSObject allocWithZone:nil] 或 [super allocWithZone:nil],消息将不会重定向到 allocWithZone 的覆盖版本(子类版本),但会直接访问执行实际操作的 NSAllocateObject() 函数分配。 NSObject中+(id)allocWithZone的代码一定是这样的-

+(id)allocWithZone:(NSZone *)zone{
    return NSAllocateObject();
}

如果这个实现(NSObject 的 allocWithZone:) 包含类似 [self allocWithZone] 的东西,那么消息分发机制将包含 allocWithZone 的子类版本,这将使我们通过涉及调用 sharedStore 方法的“偷偷摸摸”的陷阱。以下是我正在谈论的情况。现在,如果是这种情况,代码肯定会无限循环。显然情况并非如此。

+(id)allocWithZone:(NSZone *)zone{
    if([self allocWithZone:zone])      //this would trigger a call to subclass ver. which would call sharedStore method which would then have [super allocWithZone:nil].Infinite Loop
    return NSAllocateObject();
}

所以有人可以澄清这个关于这个所谓的“偷偷摸摸”陷阱的问题。陷阱是否意味着阻止任何人单独实例化。即不能使用 NSObject 的 allocWithZone ,除非在 sharedStore 方法内部?请澄清..

【问题讨论】:

    标签: objective-c


    【解决方案1】:

    这里的第一个也是最重要的教训是,您不应该覆盖+allocWithZone:。我知道 BNR 书描述了它(BNR 书一般都很好)。你不应该这样做。我知道 Apple 包含一些示例代码。你不应该这样做。 (Apple 在解释中指出很少需要这个。)应该使用 dispatch_once pattern 创建单例。

    您没有给出初始代码,但我怀疑他们的示例代码会覆盖alloc,但不会覆盖allocWithZone:。他们只是说如果调用者使用allocWithZone:,它不会通过alloc,所以他们还覆盖了alloc 来捕捉它。 (当然正确的答案是覆盖allocWithZone: alloc。但无论如何你都不应该覆盖这些方法。)


    编辑:

    我相信你误解了这里的“我们偷偷摸摸的分配陷阱”是什么意思。作者在文中此时假设如下代码:

    @interface BNRItemStore : NSObject
    +(BNRItemStore *)sharedStore;
    @end
    
    @implementation BNRItemStore   
    +(BNRItemStore *)sharedStore{
        static BNRItemStore *sharedStore=nil;
        if (!sharedStore){
            sharedStore = [[super allocWithZone:nil] init];
        }
        return sharedStore;
    }
    @end
    

    就是这样;根本没有 +alloc 覆盖。然后它指出“要强制执行单例状态……您必须确保不能分配 BNRItemStore 的另一个实例。” (*)

    作者继续建议我们可以通过覆盖+alloc 来强制执行单例状态,但立即指出这是不够的,因为调用者可以使用+allocWithZone: 代替。由于记录了[NSObject alloc] 调用[self allocWithZone:],因此覆盖+allocWithZone: 是必要且充分的,而覆盖+alloc 是不必要且不足的。

    您在代码中所做的是证明您可以修改BNRItemStore 以在+alloc 中调用[super allocWithZone:]。那不是重点。如果您可以修改BNRItemStore,您也可以将其设为非单例。关键是外部调用者(在您的情况下为main())是否可以绕过单例实例化,而她不能。 (**)

    (*) 在这一点上它没有说明的一点,而且可能应该说明的是,当调用者要求您分配一个新的目的。如果您需要强制执行单例状态,IMO 最好使用init 中的断言来执行此操作,因为第二次分配的请求表示编程错误。也就是说,有时出于性能原因,不可变对象的“透明”单例可能很有用,例如特殊的单例NSNumber 提供了某些常见的整数,这种技术在这些情况下是合适的。 (通过“透明”,我的意思是单例是调用者永远不应该担心的实现细节。这至少假定对象是不可变的。)

    (**) 如果她下定决心,其实她可以。她总是可以自己拨打NSAllocateObject(),完全绕过+alloc,然后拨打-init。这当然是疯了,没有理由这样做来“保护”她免受自己的伤害。 SDK 的工作不是保护自己免受调用者的影响。保护调用者免受可能的错误只是 SDK 的工作。调用者永远不是敌人。

    【讨论】:

    • 确实如此。重写这些方法是不明智的。但既然我在上面的代码中,你能告诉我关于allocWithZone的绕过子类版本我是对还是错。我对这本书的代码深信不疑,并认为您无法逃脱陷阱。但事实证明你可以。是真是假,如果是的话,上面的代码是这样做的吗?
    • " 关键是外部调用者(在你的例子中是 main() )是否可以绕过单例实例化,而她不能。(**)" 你的意思是说没有干预使用预先存在的代码(包含 allocWithZone 实现的子类版本的代码),让任何类型的外部函数调用通过从 allocWithZone(subclass ver) 开始并在 sharedStore 方法结束的“陷阱”就足够了(它返回单例)。
    • 好吧,忘记我代码中的 alloc 实现(注释掉),然后在 main 方法 [[BNRItemStore alloc] init] 中通过陷阱运行。正如我们所料。现在,再一次,如果我们将其更改为 [[NSObject alloc] init],那么它就不会通过陷阱。在 main 方法中类似的事情也是如此 - NSAllocateObject([BNRItemStore class], 10, nil)。那么这不是来自外部方法(如 main)的外部利用吗?因为这次 NSObject 版本的 alloc 调用了 NSObject 的 allocWithZone 版本。所以这不是与本书的观点相反吗??
    • 你的意思是如果我打电话给BNRItemStore *x = [[NSObject alloc] init]?如果你这样做,那么x 的类型将是NSObject 而不是BNRItemStore。自己试试看,然后看[x class]的值。我在第二个脚注中解释了NSAllocateObject() 案例。是的,任何调用者都可以生成任何类的对象,如果他们愿意直接参与运行时。他们还可以使用适当的isa 指针手动构建objc_object 结构,或者绕过通常的消息调度系统。我不认为这些高级案例会破坏所提出的基本观点。
    • 这些都不应该被视为“外部漏洞利用”。他们都生活在同一个程序中。不能说程序“利用”自身。
    【解决方案2】:

    我不确定这是否完全回答了您的问题,但“allocWithZone:”在当天被用来对分配的内存进行分区。从那以后,苹果已经放弃了这个概念,并希望所有东西都分配在同一个堆空间中。 “allocWithZone:” 甚至不再像以前那样运行,而且苹果明确表示不要使用它。

    【讨论】:

    • 是的,这绝对是真的,但由于我现在正处于学习阶段,并且逐章遵循 BNR 书籍,所以我将继续前进。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-06
    • 1970-01-01
    • 1970-01-01
    • 2015-08-24
    相关资源
    最近更新 更多