我在应用程序中遇到内存问题,已经能够将其分解为NSCalendar。
像这样的简单视图控制器:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
while Calendar.current.component(.year, from: Date()) > 0
{
// why does the memory keep increasing?
}
}
}
似乎导致内存泄漏。
此示例将明显阻塞UI线程,但不会导致内存不断增加,或者至少应在循环完成后释放该内存。至少从我的理解来看,不应该这样。我在这里误解了一些基本的东西吗?还是一个错误?
如何解决这个问题?
评论摘录:
仅供参考-您的问题与NSCalendar无关。您的问题是while循环永远不允许清理内存
所有这些Date实例也占用了内存
好吧,但是如果我运行的只是一个日期比较的循环,那么我就不会遇到相同的问题。这是因为优化程序介入了吗?
while Date() > Date(timeIntervalSince1970: 200)
{
// no increase of memory here
}
答案 0 :(得分:2)
乍一看,它看起来像问题是因为您的while
循环永远不会结束,这可能会阻止自动释放的对象有机会被释放。或类似的东西。为了测试这一点,我使用了for
循环重写了您的代码,该循环仅运行一百万次迭代。 (print
语句只会使速度变慢,从而使图形更好。)下面是代码:
for i in 1...1000000 //Date() < d
{
Calendar.current.component(.year, from:Date())
print("running")
}
当我以这种方式运行代码时,我在Xcode中获得了一个内存图,如下所示:
如果泄漏意味着已分配但从未释放过的内存,那么这肯定看起来像泄漏。我的下一步是更改代码,以免每次循环都不会创建新的Date
对象:
let d = Date(timeIntervalSinceNow: 30)
for i in 1...1000000 //Date() < d
{
Calendar.current.component(.year, from:d)
print("running")
}
给出完全相同的图。如果这里有泄漏,则不是Date
对象被泄漏。现在是时候使用更强的药物了。我使用“分配和泄漏”工具在Instruments中分析了相同的代码,并获得了以下分配:
因此,恰好创建了一百万个NSDateComponents
对象,这与循环中的迭代次数相同。如果有泄漏,那可能就是泄漏了。但是“泄漏”工具说没有物体泄漏:
这意味着所有这些对象都已考虑在内,并且可能只是尚未释放的自动释放对象。只要有足够的内存,并且没有明确释放池的对象,这些对象将继续存在。但是当需要其他内存时,池最终将被释放,因此这些旧对象的存活时间比您所需的更长,这确实不是问题。
我通过Instruments进行了仔细研究,您可以从图中看到最终释放出相关对象:
这证实了我们的怀疑-Calendar.current.component()
创建了一些自动释放的对象,这些对象将在当前自动释放池耗尽时随后释放。同样,这里没有泄漏。之所以看起来像是泄漏,是因为代码中的循环从未退出过,自动释放池也从未被清空过。
还请注意,Xcode的调试导航器中的内存图表有些令人误解:它没有显示实际使用了多少内存,而是当前为进程分配了多少内存。因此,您没有看到它减少的事实并不意味着您的应用程序仍在使用如此大的内存,而仅仅是该应用程序当前有那么多的工作量。
答案 1 :(得分:2)
正如其他人指出的那样,问题在于Calendar.current.component(_:from:)
在幕后引入了一个自动释放对象,该对象直到耗尽自动释放池后才释放。
早在引用计数的Objective-C代码的早期,返回新分配的对象(在调用者完成操作后将自动释放)的常见方法是返回“自动释放”对象。这个对象只有在您退回到运行循环时才会被释放,这会耗尽autorelease pool。而且,您可以通过添加自己的手动自动释放池来控制重复创建自动释放对象的大型循环上的高水位标记。
Swift本身并不创建自动释放对象,因此此问题有点像Objective-C时代错误,这在我们自己的Swift代码中通常不会遇到。但是,每当编写循环并调用可能在后台使用自动释放对象的Cocoa API的代码时(例如在这种情况下),我们就必须对此保持敏感。
在深入探讨解决方案之前,我将调整您的示例以确保可以最终退出。例如,让我们编写一个例程,该例程一直旋转直到与当前时间关联的minute
发生变化(例如,当当前分钟结束并且下一分钟开始时)。假设previousValue
包含当前的minute
值。
诀窍是我们需要将autoreleasepool
放入循环中。在以下示例中,我们利用了autoreleasepool
是泛型的事实,它返回在其闭包内部返回的内容:
while autoreleasepool(invoking: { Calendar.current.component(.minute, from: Date()) }) == previousValue {
// do something
}
请注意,如果您发现该模式难以阅读(习惯于将闭包作为方法的参数花费一些时间),则可以使用repeat
-until
循环来完成大部分同一件事:
var minute: Int!
repeat {
minute = autoreleasepool {
Calendar.current.component(.minute, from: Date())
}
// do something
} while minute == previousValue
顺便说一句,这种循环快速旋转的过程效率极低。当然,正如您提到的那样,您永远不会在主线程上执行此操作(因为我们永远不想阻塞主线程)。但是您通常不会在 any 线程上执行此操作,因为旋转的计算量很大。有时您必须这样做(例如,无论如何都要在后台线程上执行一些复杂的计算,并且您希望它在某个特定的时间停止),但是十分之九的代码在设计中更深层地困扰着代码。通常,明智地使用计时器等可以实现所需的效果,而无需计算开销。
很难为您提供最佳解决方案的建议,因为我们不知道您要解决的是什么更广泛的问题。但请注意,通常不建议在线程上旋转。
您问:
好吧,但是如果我运行的只是一个日期比较的循环,那么我就不会遇到相同的问题。这是因为优化程序介入了吗?
不,这仅仅是因为Date()根本不会像Calendar.current.component(_:from:)
那样向混合引入任何自动释放对象。 (顺便说一句,Apple一直擅长在其代码库中缓慢删除自动释放对象,因此您很可能会在将来的某个日期发现它,甚至可能不需要手动自动释放池。)
答案 2 :(得分:1)
没有内存泄漏。这是由于您正在滥用主线程这一事实:每秒创建数千个昂贵的NSDateComponents
对象,每个对象占用176字节,这在几秒钟内达到数十兆字节。 Calendar.current.component(_:from:)
是负责所有这些分配的对象,它在_NSCopyOnWriteCalendarWrapper
上调用Calendar.current
。
我怀疑/认为这与ARC的工作方式有关:由于在每个循环中都引用了Calendar.current
,所以调用了许多写时复制,然后从新的NSDateComponents获取日期()。再加上ARC没有释放对象,它认为它仍然是必需的,而您已经做到了。
关于您的反例while Date() > Date(timeIntervalSince1970: 200)
:Date()
很便宜,因为一天结束时要花几秒钟的时间,比较不相关的Date
实例不需要复制任何副本-写时。
解决方案是使用autoreleasepool。苹果表示,借助ARC,开发人员无需手动从内存中释放对象。但是,在某些情况下,创建自动释放池可以帮助减少应用程序的峰值内存占用量。自动释放池很有用,因为您不是在等待系统释放所创建的任何对象,而是在关闭结束时告诉系统释放这些对象。在iOS应用中,主要功能在自动释放池中运行。该池的清空操作将在每个主运行循环结束时进行。当创建的对象多于消耗的对象时,就会出现问题。
因此,对于占用大量内存的代码部分,应使用autoreleasepool
:
autoreleasepool {
while Calendar.current.component(.year, from: Date()) > 0 {
print("Hello world")
}
}
这将减慢内存的填充速度。
答案 3 :(得分:0)
这个想法是自动释放每个变量或函数,从而导致峰值内存消耗。
var myDate: Date = Date()
var condition : Bool {
get{
return {
autoreleasepool{
Calendar .current.component(.year, from: myDate) > 0
}
}()
}
}
while condition {
autoreleasepool{
myDate = Date();
print (condition) }
}