LifetimeTracker

Demo (bar) Demo (circular)
条形样式 圆形样式

LifetimeTracker 可以在您开发应用程序时立即发现 retain cycle / 内存问题,并立即向您展示这些问题,以便您可以更轻松地找到它们。

Instruments 和 Memory Graph Debugger 非常棒,但是很多时候,开发人员在完成功能实现后忘记检查问题。

如果您零星地使用这些工具,它们发现的许多问题将需要您调查原因,并在过程中花费大量时间。

其他工具,例如 FBRetainCycleDetector,依赖于 objc 运行时魔法来查找问题,但这意味着它们实际上不能用于纯 Swift 类。这个小工具只专注于跟踪对象的生命周期,这意味着它可以用于 Objective-C 和 Swift 代码库,并且它不依赖于任何复杂的或自动的魔法行为。

如果您想支持我的工作并改进您的工程工作流程,请查看我的 SwiftyStack 课程

安装

CocoaPods

pod 'LifetimeTracker' 添加到您的 Podfile。

Carthage

github "krzysztofzablocki/LifetimeTracker" 添加到您的 Cartfile。

Swift Package Manager

LifetimeTracker" 添加到您的 Package.swift 的 dependencies 值中。

dependencies: [
    .package(url: "https://github.com/krzysztofzablocki/LifetimeTracker.git", .upToNextMajor(from: "1.8.0"))
]

集成

要集成可视化通知,只需在 AppDelegate(didFinishLaunchingWithOptions:) 的开头添加以下行,如果您使用的是 iOS 13+ SceneDelegates,则在 scene(willConnectTo:options:) 中添加。

Swift

#if DEBUG
	LifetimeTracker.setup(
        onUpdate: LifetimeTrackerDashboardIntegration(
            visibility: .alwaysVisible,
            style: .bar,
            textColorForNoIssues: .systemGreen,
            textColorForLeakDetected: .systemRed
        ).refreshUI
    )
#endif

Objective-C

LifetimeTrackerDashboardIntegration *dashboardIntegration = [LifetimeTrackerDashboardIntegration new];
[dashboardIntegration setVisibleWhenIssueDetected];
[dashboardIntegration useBarStyle];
[LifetimeTracker setupOnUpdate:^(NSDictionary<NSString *,EntriesGroup *> * groups) {
    [dashboardIntegration refreshUIWithTrackedGroups: groups];
}];

您可以控制仪表板的可见性:alwaysVisiblealwaysHiddenvisibleWithIssuesDetected

有两种样式可用。一种是覆盖条视图,它直接在屏幕上显示问题的详细列表;另一种是圆形视图,它仅显示问题数量,并将详细列表作为模态视图控制器打开。

跟踪关键参与者

通常,您希望仅使用 LifetimeTracker 跟踪应用程序中的关键参与者,例如 ViewModels / Controllers 等。当您有超过 maxCount 个活跃项目时,跟踪器会通知您。

Swift

您需要遵循 LifetimeTrackable 协议,并在 init 函数的末尾调用 trackLifetime()

class SectionFrontViewController: UIViewController, LifetimeTrackable {
    class var lifetimeConfiguration: LifetimeConfiguration {
        return LifetimeConfiguration(maxCount: 1, groupName: "VC")
    }
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        /// ...
        trackLifetime()
    }
}

Objective-C

您需要遵循 LifetimeTrackable 协议,并在 init 函数的末尾调用 [self trackLifetime]

@import LifetimeTracker;

@interface SectionFrontViewController() <LifetimeTrackable>

@implementation SectionFrontViewController

+(LifetimeConfiguration *)lifetimeConfiguration
{
    return [[LifetimeConfiguration alloc] initWithMaxCount:1 groupName:@"VC"];
}

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
    	/// …
        [self trackLifetime];
    }
    return self;
}
@end

Danger 集成

如果您正在使用 Danger,您可以使用它为每个 PR 添加复选框,以确保人们已验证没有创建 retain cycle,并且在有人忘记调用 trackLifetime() 函数时通知您。

#
# ** FILE CHECKS **
# Checks for certain rules and warns if needed.
# Some rules can be disabled by using // danger:disable rule_name
#
# Rules:
# - Check if the modified file is a View and doesn't implement LifetimeTrackable (lifetime_tracking)

# Sometimes an added file is also counted as modified. We want the files to be checked only once.
files_to_check = (git.modified_files + git.added_files).uniq
(files_to_check - %w(Dangerfile)).each do |file|
	next unless File.file?(file)
	# Only check inside swift files
  next unless File.extname(file).include?(".swift")

  # Will be used to check if we're inside a comment block.
	is_comment_block = false

	# Collects all disabled rules for this file.
	disabled_rules = []

	filelines = File.readlines(file)
	filelines.each_with_index do |line, index|
		if is_comment_block
			if line.include?("*/")
				is_comment_block = false
			end
		elsif line.include?("/*")
			is_comment_block = true
		elsif line.include?("danger:disable")
			rule_to_disable = line.split.last
			disabled_rules.push(rule_to_disable)
		else
			# Start our custom line checks
			# e.g. you could do something like check for methods that only call the super class' method
			#if line.include?("override") and line.include?("func") and filelines[index+1].include?("super") and filelines[index+2].include?("}")
			#	warn("Override methods which only call super can be removed", file: file, line: index+3)
			#end
    end
	end

	# Only continue checks for Lifetime Trackable types
	next unless (File.basename(file).include?("ViewModel") or File.basename(file).include?("ViewController") or File.basename(file).include?("View.swift")) and !File.basename(file).include?("Node") and !File.basename(file).include?("Tests") and !File.basename(file).include?("FlowCoordinator")

	if disabled_rules.include?("lifetime_tracking") == false
		if File.readlines(file).grep(/LifetimeTrackable/).any?
			fail("You forgot to call trackLifetime() from your initializers in " + File.basename(file, ".*") + " (lifetime_tracking)") unless File.readlines(file).grep(/trackLifetime()/).any?
		else
			warn("Please add support for LifetimeTrackable to " + File.basename(file, ".*") + " . (lifetime_tracking)")
		end
		markdown("- [ ] I've verified that showing and hiding " + File.basename(file, ".*") + " doesn't surface any [LifetimeTracker](https://github.com/krzysztofzablocki/LifetimeTracker) issues")
	end

end

显示堆栈中的最后一个通知

有时,获取有关最后一个 retain cycle 的信息以将其记录到外部来源(例如分析/跟踪器)非常有用。为了做到这一点,我们可以使用 onLeakDetected 更新初始配置

[LifetimeTracker setupOnLeakDetected:^(Entry * entry, EntriesGroup * group) {
    NSLog(@"POSSIBLE LEAK ALERT: %@ - current count %li, max count %li", entry.name, (long)entry.count, (long)entry.maxCount);
} onUpdate:^(NSDictionary<NSString *,EntriesGroup *> * groups) {
    [dashboardIntegration refreshUIWithTrackedGroups: groups];
}];
LifetimeTracker.setup(onLeakDetected: { entity, _ in
	log.warning("POSSIBLE LEAK ALERT: \(entity.name) - current count: \(entity.count), max count: \(entity.maxCount)")
}, onUpdate: LifetimeTrackerDashboardIntegration(visibility: .alwaysVisible, style: .bar).refreshUI)

分组跟踪对象

您可以将跟踪的对象分组在一起。默认情况下,组的 maxCount 将按每个成员的 maxCount 计算。但是,您可以覆盖它并为组提供一个单独的值 groupMaxCount

当您有一组子类,每个子类可以出现 x 次,但总共少于所有子类的总和时,您可能想要这样做

// DetailPage: UIViewController

// VideoDetailPage: DetailItem
LifetimeConfiguration(maxCount: 3, groupName: "Detail Page")

// ImageDetailPage: DetailItem
LifetimeConfiguration(maxCount: 3, groupName: "Detail Page")

=> Group warning if 7 DetailPage objects are alive

// VideoDetailPage: DetailItem
LifetimeConfiguration(maxCount: 3, groupName: "Detail Page", groupMaxCount: 3)

// ImageDetailPage: DetailItem
LifetimeConfiguration(maxCount: 3, groupName: "Detail Page", groupMaxCount: 3)

=> Group warning if 4 DetailPage object are alive

为内存泄漏编写集成测试

您可以使用可访问性标识符 LifetimeTracker.summaryLabel 访问摘要标签,这允许您编写集成测试来查找是否发现了任何问题。

许可证

LifetimeTracker 在 MIT 许可证下可用。有关更多信息,请参阅 LICENSE

致谢

我使用了 SwiftPlate 来生成与 CocoaPods 和 Carthage 兼容的 xcodeproj。