宏在块中捕获Self

7
我有一个问题,关于下面这个宏,我用它来记录各种信息。
#define JELogVerbose(fmt, ...)  
DDLogVerbose((@"%@ %@ - " fmt), NSStringFromClass([self class]),
                                NSStringFromSelector(_cmd), ##__VA_ARGS__)

当这个最终宏在内使用时,它显然会强烈捕获self,这可能会有问题。
以下是解决方案的一些要求:
1. 它可以是一个多行宏,其中定义了weakSelf,但这并不能解决问题,因为你可以得到你创建的__weak指针的重新定义。 2. 使用__FILE____PRETTY_FUNCTION__,因为它们捕获的是超类而不是子类。因此,在用于创建许多实例的抽象类的情况下,日志记录不区分每个实例。捕获当前类是绝对必要的。 3. 解决方案仅需要修改宏或其他全局配置选项即可修复此问题,无需添加额外的广泛库。
5个回答

2

更新:

现在我知道问题所在了。这个宏应该可以正常工作:

#define LOG_CLASS_NAME(obj) typedef typeof(*(obj)) SelfType; \
                            NSLog(@"typeof self is %@", [SelfType class]);

LOG_CLASS_NAME(self) // typeof self is JEViewController

因为 typeof(*self) 在编译时被解析,所以编译器不需要保留 self 实例。这意味着可以在块内安全使用此宏。
第一个答案:
__PRETTY_FUNCTION__ 怎么样?它会打印类名和选择器。
NSLog("func: %s", __PRETTY_FUNCTION__); // func: [UIViewController viewDidAppear:]

不幸的是,这总是捕获它所在的文件,因此如果超类有一个使用此函数但向子类发送消息的函数,则会将其打印为来自超类,这不是我要寻找的。 - Ben Chester
不幸的是,这将捕获超类而不是子类。 - Ben Chester
@Marek Rogosz,我很钦佩您的智慧!这是一个如此具有挑战性的问题和如此有趣的解决方案! - user2260054
1
你可能想要将你的解决方案包装在 ({...}) 中,以避免在同一作用域内多次使用时重新声明 SelfType,并确保整体的健康性 :) - Sash Zats

1

也许可以使用这个链接: https://github.com/jspahrsummers/libextobjc

#import "EXTScope.h"

/* some code */

@weakify(self);
[SomeClass runOnBackgroundCode:^{    @strongify(self);    /* do something */}];

/* some code */

我使用这个解决方案已经有一段时间了 - 不需要添加weekSelf或其他任何内容。

有趣的方法,这确实让我可以使用相同的宏并解决问题。我一直在寻找不需要任何外部配置的东西,但这可能就是答案。如果没有直接解决宏的方法出现,我可能会接受这个方案。 - Ben Chester

0

我发现您的问题非常有趣,所以我决定花几个小时寻找一种替代方法来打印类名和方法在您的日志中。

我创建了一个小型的XCode项目,在其中测试了您最初发布的MACRO在以下场景下:

  1. 从我的AppDelegate类调用JELogVerbose
  2. 从超类(SOAAbstract)及其子类(SOAChild1和SOAChild1)调用JELogVerbose。 a)调用没有块的方法。 b)调用不会导致保留循环的块方法。 c)调用导致保留循环的块方法,因为self对该块具有强引用。
  3. 先将子类转换为超类,然后从子类调用JELogVerbose
我发现在情况1、2.a、2.b和3中,你的解决方案完美地运行。因此,在2.b情况下不需要使用libextobjc中的@weakify(self)/@strongify(self)。
但正如你已经指出的那样,在类似于2.c的情况中调用JELogVerbose会产生警告:“在此块中强烈捕获“self”可能导致保留周期。”

最终,我基于类通常在单独的文件中定义的事实得出了这个替代方案。
#define JELogVerbose(fmt, ...) DDLogVerbose((@"%@ %@ - " fmt), [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] stringByDeletingLastPathComponent], NSStringFromSelector(_cmd) ,##__VA_ARGS__)


这里的优势在于,当你需要从块内部调用JELogVerbose时,你不需要担心所有情况下的保留循环。此外,如果项目中的所有类都定义在单独的文件中,你可以从__FILE__信息中推导出类的名称。
如果您想提供有关记录消息的确切点的信息,还可以使用__LINE__

我知道这个宏的这个版本有一个缺点,即在你的类在同一个文件中的情况下,无法完全映射到类。但是我认为你只应该担心那些在你的代码中添加宏会导致在此块中强烈捕获'self'可能会导致保留循环的情况。对于这些情况,你可以使用这个第二个备选版本的宏来避免改变你的代码添加@weakify(self) / @strongify(self)。



除此之外,我在苹果文档中找到了一个链接([reference])1,其中他们推荐使用一些预处理器标准宏来为日志添加上下文信息。其中我发现了我提到的那个以及其他一些宏/表达式,其中一些你可能已经知道:

  1. __func__
  2. __LINE__
  3. __FILE__
  4. __PRETTY_FUNCTION__
  5. NSStringFromSelector(_cmd)
  6. NSStringFromClass([self class])
  7. [[NSString stringWithUTF8String:__FILE__] lastPathComponent]
  8. [NSThread callStackSymbols]

谢谢你的回答,但正如你所说,“不完全可映射到类的缺点在于,例如你的类在同一个文件中时,我不能使用这个解决方案。”这意味着我不能使用这个解决方案。 - Ben Chester

0

嗯,这确实是一个难题。

我并没有看到直接的解决方案。让我们仔细思考一下:

现在,您可能有一个大型代码库,在其中使用此日志记录宏。这很好,但由于在块中引用了self而创建了保留周期,因此它会创建一些内存泄漏。(或者,如果它没有创建内存泄漏,那么它正在威胁着。)

所以,您只需要确保对self的引用(概念上)是弱引用即可。听起来很合理。

让我们看看您的要求:

第一个要求

可以是多行宏,其中定义了对self的__weak引用。您建议这个棘手的部分可能是它可能重新定义弱引用(可能通过在同一段代码中多次使用)。这可以相对容易地通过将宏包装在do { something(); } while(0)结构中来解决。

#define JELogVerbose(fmt, ...)
do
{
    __weak __typeof(self) weakSelf = self;  
    DDLogVerbose((@"%@ %@ - " fmt), NSStringFromClass([weakSelf class]),
                                    NSStringFromSelector(_cmd), ##__VA_ARGS__)
} while (0)

这是一种非常常见的宏模式。如果您连续两次调用该宏,weakSelf变量仍不会被重新定义。但是,您可能会注意到,这实际上对解决问题没有任何帮助,因为您必须引用self才能获得其弱引用。实际上,您必须在创建块之前拥有weakSelf引用,然后在块中仅引用弱引用。事实上,通常情况下,您需要再创建一个强引用,以便它不会在您使用时被移除。像这样:

__weak typeof(self) weakSelf = self;
[someObj block:^{
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    MyLog(strongSelf);
}];

为了解决这个问题,你必须已经拥有一个对自身的弱引用,或者有一个跨越块创建的宏。(那将是相当棘手的。)
第二个要求
我们不能使用这些有用的预处理器标准宏,因为它们不会告诉你在运行时发生了什么,而是在编译时(更准确地说是预处理时间)。你还正确地拒绝了另一个答案,因为它提供了来自编译时的信息(更接近了,但仍然不够)而不是运行时。
不幸的是,我认为你无法在没有实际引用它的情况下获得你正在寻找的self的运行时信息。我们只是不知道在编译时对象的类实际上会是什么。所以所有那些PRETTY_FUNCTION的技巧都被排除了。当然,正如人们所期望的那样,所有的运行时函数都需要一个对你想要查找类的对象的引用。
第三个要求
我要把这个问题简化为:我不想改变我的代码库。可以理解。
虽然,我认为你别无选择。你有点陷入了困境。
结论

我认为你没有太多好的选择(它们都需要在宏之外做出更改,也就是说要修改你的代码库):

  1. 通过删除对self的任何引用并始终在block中使用它,创建一个块安全版本的宏
  2. 创建一个块安全版本的宏,它使用strongSelf,然后每当你使用它时,你还必须在block之外创建weakSelf引用,并在block内部创建一个strongSelf(就像上面的例子一样)。这比较麻烦,但可以保留现有的所有日志记录。

祝你好运!


感谢您对问题进行了很好的分析,您真正概括了这个问题。我担心正如您所指出的那样,我必须妥协一些要求并放弃其中一些。 - Ben Chester

0

在代码块之外:

__weak __typeof(self) weakSelf = self;

在代码块内部:

__typeof(self) self = weakSelf;

然后当宏使用self时,实际上是在使用块内定义的self。这将与原始self相同或为nil(因此要检查nil)。

如果您启用了GCC_WARN_SAHDOW,则会生成警告。出于许多原因,其中之一是这个,GCC_WARN_SHADOW不是一个有用的警告。请关闭它。


由于我想在各个地方使用这个宏,并且我正在寻找一种通用的解决方案,所以恐怕这不是一个真正的选择。当您处于块中并将self传递到宏中时,可以执行此操作,但不太优雅。 - Ben Chester
这是父函数的一行,然后每个块都有一行。 - Steven Fisher
我明白你的意思,但这个代码被用在一个相对较大的代码库中,我更喜欢零配置而不是额外的要求。如果我要更改每个块以包含这个额外的代码和标志,我可以编辑宏,使其接受self作为参数,并传入已经__weak引用到sel。这不需要我禁用可能有用的阴影警告,但如果我可以避免这样做,我更愿意。 - Ben Chester
祝你好运。我希望你能找到一个不需要代码的答案。然而,无论如何,我建议不要打开GCC_WARN_SHADOW。我发现它会使我的代码严重恶化,像是千刀万剐一般。更加追求完美并不总是更好的选择。 :) - Steven Fisher

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接