Skip to content

Commit c2c4398

Browse files
authored
Fix unnecessary retains to prevent zombie Analytics instances (#164)
* Fixed memory leaks * Added leak testing * Removed unnecessary subscriber conformance from Timeline. * Some cleanup * Analytics property on plugins now needs to be marked `weak`. * Fixed leak test for linux.
1 parent 2b85609 commit c2c4398

31 files changed

+171
-48
lines changed

Examples/destination_plugins/AdjustDestination.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class AdjustDestination: NSObject, DestinationPlugin, RemoteNotifications {
4141
let timeline = Timeline()
4242
let type = PluginType.destination
4343
let key = "Adjust"
44-
var analytics: Analytics? = nil
44+
weak var analytics: Analytics? = nil
4545

4646
private var settings: AdjustSettings? = nil
4747

Examples/destination_plugins/ComscoreDestination.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class ComscoreDestination: DestinationPlugin {
4444
let timeline = Timeline()
4545
let type = PluginType.destination
4646
let key = "comScore"
47-
var analytics: Analytics? = nil
47+
weak var analytics: Analytics? = nil
4848

4949
private var comscoreSettings: ComscoreSettings?
5050
private var comscoreEnrichment: ComscoreEnrichment?

Examples/destination_plugins/ExampleDestination.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public class ExampleDestination: DestinationPlugin {
4646
public let type = PluginType.destination
4747
// TODO: Fill this out with your settings key that matches your destination in the Segment App
4848
public let key = "Example"
49-
public var analytics: Analytics? = nil
49+
public weak var analytics: Analytics? = nil
5050

5151
private var exampleSettings: ExampleSettings?
5252

Examples/destination_plugins/FlurryDestination.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class FlurryDestination: DestinationPlugin {
4646
let timeline = Timeline()
4747
let type = PluginType.destination
4848
let key = "Flurry"
49-
var analytics: Analytics? = nil
49+
weak var analytics: Analytics? = nil
5050

5151
var screenTracksEvents = false
5252

Examples/destination_plugins/IntercomDestination.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class IntercomDestination: DestinationPlugin {
4343
let timeline = Timeline()
4444
let type = PluginType.destination
4545
let key = "Intercom"
46-
var analytics: Analytics? = nil
46+
weak var analytics: Analytics? = nil
4747

4848
private var intercomSettings: IntercomSettings?
4949
private var configurationLabels = [String: Any]()

Examples/other_plugins/CellularCarrier.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import CoreTelephony
5454
class CellularCarrier: Plugin {
5555
var type: PluginType = .enrichment
5656

57-
var analytics: Analytics?
57+
weak var analytics: Analytics?
5858

5959
func execute<T: RawEvent>(event: T?) -> T? {
6060
guard var workingEvent = event else { return event }

Examples/other_plugins/ConsentTracking.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import UIKit
4646
*/
4747
class ConsentTracking: Plugin {
4848
let type = PluginType.before
49-
var analytics: Analytics? = nil
49+
weak var analytics: Analytics? = nil
5050

5151
var queuedEvents = [RawEvent]()
5252

Examples/other_plugins/ConsoleLogger.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import Segment
4242
class ConsoleLogger: Plugin {
4343
let type = PluginType.after
4444
let name: String
45-
var analytics: Analytics? = nil
45+
weak var analytics: Analytics? = nil
4646

4747
var identifier: String? = nil
4848

Examples/other_plugins/IDFACollection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import AppTrackingTransparency
4646
*/
4747
class IDFACollection: Plugin {
4848
let type = PluginType.enrichment
49-
var analytics: Analytics? = nil
49+
weak var analytics: Analytics? = nil
5050
@Atomic private var alreadyAsked = false
5151

5252
func execute<T: RawEvent>(event: T?) -> T? {

Examples/other_plugins/NotificationTracking.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import Segment
4040

4141
class NotificationTracking: Plugin {
4242
var type: PluginType = .utility
43-
var analytics: Analytics?
43+
weak var analytics: Analytics?
4444

4545
func trackNotification(_ properties: [String: Any], fromLaunch launch: Bool) {
4646
if launch {

Examples/other_plugins/UIKitScreenTracking.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class UIKitScreenTracking: UtilityPlugin {
5151
static let controllerKey = "controller"
5252

5353
let type = PluginType.utility
54-
var analytics: Analytics? = nil
54+
weak var analytics: Analytics? = nil
5555

5656
init() {
5757
setupUIKitHooks()

Sources/Segment/Plugins/Context.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Foundation
99

1010
public class Context: PlatformPlugin {
1111
public let type: PluginType = .before
12-
public var analytics: Analytics?
12+
public weak var analytics: Analytics?
1313

1414
internal var staticContext = staticContextData()
1515
internal static var device = VendorSystem.current

Sources/Segment/Plugins/DestinationMetadataPlugin.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Foundation
1313
*/
1414
public class DestinationMetadataPlugin: Plugin {
1515
public let type: PluginType = PluginType.enrichment
16-
public var analytics: Analytics?
16+
public weak var analytics: Analytics?
1717
private var analyticsSettings: Settings? = nil
1818

1919
public func update(settings: Settings, type: UpdateType) {

Sources/Segment/Plugins/DeviceToken.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Foundation
99

1010
public class DeviceToken: PlatformPlugin {
1111
public let type = PluginType.before
12-
public var analytics: Analytics?
12+
public weak var analytics: Analytics?
1313

1414
public var token: String? = nil
1515

Sources/Segment/Plugins/Logger/SegmentLog.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Foundation
1111

1212
internal class SegmentLog: UtilityPlugin {
1313
public var filterKind = LogFilterKind.debug
14-
var analytics: Analytics?
14+
weak var analytics: Analytics?
1515

1616
let type = PluginType.utility
1717

@@ -22,7 +22,7 @@ internal class SegmentLog: UtilityPlugin {
2222

2323
// For internal use only. Note: This will contain the last created instance
2424
// of analytics when used in a multi-analytics environment.
25-
internal static var sharedAnalytics: Analytics? = nil
25+
internal static weak var sharedAnalytics: Analytics? = nil
2626

2727
#if DEBUG
2828
internal static var globalLogger: SegmentLog {

Sources/Segment/Plugins/Platforms/Linux/LinuxLifecycleMonitor.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ import Foundation
1010
#if os(Linux)
1111
class LinuxLifecycleMonitor: PlatformPlugin {
1212
let type = PluginType.utility
13-
var analytics: Analytics?
13+
weak var analytics: Analytics?
1414
}
1515
#endif

Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleEvents.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class macOSLifecycleEvents: PlatformPlugin, macOSLifecycle {
1616
static var buildKey = "SEGBuildKeyV2"
1717

1818
let type = PluginType.before
19-
var analytics: Analytics?
19+
weak var analytics: Analytics?
2020

2121
/// Since application:didFinishLaunchingWithOptions is not automatically called with Scenes / SwiftUI,
2222
/// this gets around by using a flag in user defaults to check for big events like application updating,

Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class macOSLifecycleMonitor: PlatformPlugin {
4646
static var specificName = "Segment_macOSLifecycleMonitor"
4747
let type = PluginType.utility
4848
let name = specificName
49-
var analytics: Analytics?
49+
weak var analytics: Analytics?
5050

5151
private var application: NSApplication
5252
private var appNotifications: [NSNotification.Name] =

Sources/Segment/Plugins/Platforms/Vendors/AppleUtils.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ internal class watchOSVendorSystem: VendorSystem {
172172
}
173173

174174
override var requiredPlugins: [PlatformPlugin] {
175-
return [watchOSLifecycleMonitor()]
175+
return [watchOSLifecycleMonitor(), DeviceToken()]
176176
}
177177

178178
private func deviceModel() -> String {

Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class iOSLifecycleEvents: PlatformPlugin, iOSLifecycle {
1616
static var buildKey = "SEGBuildKeyV2"
1717

1818
let type = PluginType.before
19-
var analytics: Analytics?
19+
weak var analytics: Analytics?
2020

2121
/// Since application:didFinishLaunchingWithOptions is not automatically called with Scenes / SwiftUI,
2222
/// this gets around by using a flag in user defaults to check for big events like application updating,

Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public extension iOSLifecycle {
3838

3939
class iOSLifecycleMonitor: PlatformPlugin {
4040
let type = PluginType.utility
41-
var analytics: Analytics?
41+
weak var analytics: Analytics?
4242

4343
private var application: UIApplication? = nil
4444
private var appNotifications: [NSNotification.Name] = [UIApplication.didEnterBackgroundNotification,

Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleEvents.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class watchOSLifecycleEvents: PlatformPlugin, watchOSLifecycle {
1515
static var buildKey = "SEGBuildKeyV2"
1616

1717
let type = PluginType.before
18-
var analytics: Analytics?
18+
weak var analytics: Analytics?
1919

2020
func applicationDidFinishLaunching(watchExtension: WKExtension) {
2121
if analytics?.configuration.values.trackApplicationLifecycleEvents == false {

Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleMonitor.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ public extension watchOSLifecycle {
2828

2929

3030
class watchOSLifecycleMonitor: PlatformPlugin {
31-
var type = PluginType.utility
32-
var analytics: Analytics?
31+
let type = PluginType.utility
32+
weak var analytics: Analytics?
3333
var wasBackgrounded: Bool = false
3434

3535
private var watchExtension = WKExtension.shared()

Sources/Segment/Plugins/SegmentDestination.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class SegmentDestination: DestinationPlugin {
2525
public let type = PluginType.destination
2626
public let key: String = Constants.integrationName.rawValue
2727
public let timeline = Timeline()
28-
public var analytics: Analytics? {
28+
public weak var analytics: Analytics? {
2929
didSet {
3030
initialSetup()
3131
}
@@ -54,8 +54,8 @@ public class SegmentDestination: DestinationPlugin {
5454
guard let analytics = self.analytics else { return }
5555
storage = analytics.storage
5656
httpClient = HTTPClient(analytics: analytics)
57-
flushTimer = QueueTimer(interval: analytics.configuration.values.flushInterval) {
58-
self.flush()
57+
flushTimer = QueueTimer(interval: analytics.configuration.values.flushInterval) { [weak self] in
58+
self?.flush()
5959
}
6060
// Add DestinationMetadata enrichment plugin
6161
add(plugin: DestinationMetadataPlugin())

Sources/Segment/Plugins/StartupQueue.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ public class StartupQueue: Plugin, Subscriber {
1515

1616
public let type: PluginType = .before
1717

18-
public var analytics: Analytics? = nil {
18+
public weak var analytics: Analytics? = nil {
1919
didSet {
20-
analytics?.store.subscribe(self, handler: runningUpdate)
20+
analytics?.store.subscribe(self) { [weak self] (state: System) in
21+
self?.runningUpdate(state: state)
22+
}
2123
}
2224
}
2325

Sources/Segment/Startup.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ extension Analytics {
7676
// do the first one
7777
checkSettings()
7878
// set up return-from-background to do it again.
79-
NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { (notification) in
79+
NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { [weak self] (notification) in
8080
guard let app = notification.object as? UIApplication else { return }
8181
if app.applicationState == .background {
82-
self.checkSettings()
82+
self?.checkSettings()
8383
}
8484
}
8585
}
@@ -100,8 +100,8 @@ extension Analytics {
100100
// now set up a timer to do it every 24 hrs.
101101
// mac apps change focus a lot more than iOS apps, so this
102102
// seems more appropriate here.
103-
QueueTimer.schedule(interval: .days(1), queue: .main) {
104-
self.checkSettings()
103+
QueueTimer.schedule(interval: .days(1), queue: .main) { [weak self] in
104+
self?.checkSettings()
105105
}
106106
}
107107
}

Sources/Segment/Timeline.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Sovran
1111

1212
// MARK: - Main Timeline
1313

14-
public class Timeline: Subscriber {
14+
public class Timeline {
1515
internal let plugins: [PluginType: Mediator]
1616

1717
public init() {

Sources/Segment/Utilities/HTTPClient.swift

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public class HTTPClient {
2424
private var apiHost: String
2525
private var apiKey: String
2626
private var cdnHost: String
27-
private let analytics: Analytics
27+
28+
private weak var analytics: Analytics?
2829

2930
init(analytics: Analytics, apiKey: String? = nil, apiHost: String? = nil, cdnHost: String? = nil) {
3031
self.analytics = analytics
@@ -75,24 +76,24 @@ public class HTTPClient {
7576

7677
let dataTask = session.uploadTask(with: urlRequest, fromFile: batch) { [weak self] (data, response, error) in
7778
if let error = error {
78-
self?.analytics.log(message: "Error uploading request \(error.localizedDescription).")
79+
self?.analytics?.log(message: "Error uploading request \(error.localizedDescription).")
7980
completion(.failure(error))
8081
} else if let httpResponse = response as? HTTPURLResponse {
8182
switch (httpResponse.statusCode) {
8283
case 1..<300:
8384
completion(.success(true))
8485
return
8586
case 300..<400:
86-
self?.analytics.log(message: "Server responded with unexpected HTTP code \(httpResponse.statusCode).")
87+
self?.analytics?.log(message: "Server responded with unexpected HTTP code \(httpResponse.statusCode).")
8788
completion(.failure(HTTPClientErrors.statusCode(code: httpResponse.statusCode)))
8889
case 429:
89-
self?.analytics.log(message: "Server limited client with response code \(httpResponse.statusCode).")
90+
self?.analytics?.log(message: "Server limited client with response code \(httpResponse.statusCode).")
9091
completion(.failure(HTTPClientErrors.statusCode(code: httpResponse.statusCode)))
9192
case 400..<500:
92-
self?.analytics.log(message: "Server rejected payload with HTTP code \(httpResponse.statusCode).")
93+
self?.analytics?.log(message: "Server rejected payload with HTTP code \(httpResponse.statusCode).")
9394
completion(.failure(HTTPClientErrors.statusCode(code: httpResponse.statusCode)))
9495
default: // All 500 codes
95-
self?.analytics.log(message: "Server rejected payload with HTTP code \(httpResponse.statusCode).")
96+
self?.analytics?.log(message: "Server rejected payload with HTTP code \(httpResponse.statusCode).")
9697
completion(.failure(HTTPClientErrors.statusCode(code: httpResponse.statusCode)))
9798
}
9899
}
@@ -113,21 +114,21 @@ public class HTTPClient {
113114

114115
let dataTask = session.dataTask(with: urlRequest) { [weak self] (data, response, error) in
115116
if let error = error {
116-
self?.analytics.log(message: "Error fetching settings \(error.localizedDescription).")
117+
self?.analytics?.log(message: "Error fetching settings \(error.localizedDescription).")
117118
completion(false, nil)
118119
return
119120
}
120121

121122
if let httpResponse = response as? HTTPURLResponse {
122123
if httpResponse.statusCode > 300 {
123-
self?.analytics.log(message: "Server responded with unexpected HTTP code \(httpResponse.statusCode).")
124+
self?.analytics?.log(message: "Server responded with unexpected HTTP code \(httpResponse.statusCode).")
124125
completion(false, nil)
125126
return
126127
}
127128
}
128129

129130
guard let data = data, let responseJSON = try? JSONDecoder().decode(Settings.self, from: data) else {
130-
self?.analytics.log(message: "Error deserializing settings.")
131+
self?.analytics?.log(message: "Error deserializing settings.")
131132
completion(false, nil)
132133
return
133134
}

Sources/Segment/Utilities/Storage.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import Foundation
99
import Sovran
1010

1111
internal class Storage: Subscriber {
12-
let store: Store
1312
let writeKey: String
1413
let userDefaults: UserDefaults?
1514
static let MAXFILESIZE = 475000 // Server accepts max 500k per batch
@@ -21,11 +20,14 @@ internal class Storage: Subscriber {
2120
private var fileHandle: FileHandle? = nil
2221

2322
init(store: Store, writeKey: String) {
24-
self.store = store
2523
self.writeKey = writeKey
2624
self.userDefaults = UserDefaults(suiteName: "com.segment.storage.\(writeKey)")
27-
store.subscribe(self, handler: userInfoUpdate)
28-
store.subscribe(self, handler: systemUpdate)
25+
store.subscribe(self) { [weak self] (state: UserInfo) in
26+
self?.userInfoUpdate(state: state)
27+
}
28+
store.subscribe(self) { [weak self] (state: System) in
29+
self?.systemUpdate(state: state)
30+
}
2931
}
3032

3133
func write<T: Codable>(_ key: Storage.Constants, value: T?) {

0 commit comments

Comments
 (0)