Skip to content

Commit

Permalink
PluginizedComponent should release its non-core component based on bo…
Browse files Browse the repository at this point in the history
…und lifecycle

Fixes #143
  • Loading branch information
neakor committed Aug 22, 2018
1 parent 8962d0c commit 6efa89f
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 56 deletions.
8 changes: 4 additions & 4 deletions Foundation/NeedleFoundation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
41F0681520D45AD100FD67C7 /* PluginizedComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F0681020D45AD100FD67C7 /* PluginizedComponent.swift */; };
41F0681620D45AD100FD67C7 /* PluginExtensionProviderRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F0681220D45AD100FD67C7 /* PluginExtensionProviderRegistry.swift */; };
41F0681720D45AD100FD67C7 /* NonCoreComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F0681320D45AD100FD67C7 /* NonCoreComponent.swift */; };
41F0681820D45AD100FD67C7 /* PluginizedLifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F0681420D45AD100FD67C7 /* PluginizedLifecycle.swift */; };
41F0681820D45AD100FD67C7 /* PluginizedScopeLifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F0681420D45AD100FD67C7 /* PluginizedScopeLifecycle.swift */; };
41F0681B20D45B1300FD67C7 /* PluginizedComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F0681A20D45B1300FD67C7 /* PluginizedComponentTests.swift */; };
OBJ_25 /* Bootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Bootstrap.swift */; };
OBJ_26 /* Component.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Component.swift */; };
Expand All @@ -34,7 +34,7 @@
41F0681020D45AD100FD67C7 /* PluginizedComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PluginizedComponent.swift; sourceTree = "<group>"; };
41F0681220D45AD100FD67C7 /* PluginExtensionProviderRegistry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PluginExtensionProviderRegistry.swift; sourceTree = "<group>"; };
41F0681320D45AD100FD67C7 /* NonCoreComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonCoreComponent.swift; sourceTree = "<group>"; };
41F0681420D45AD100FD67C7 /* PluginizedLifecycle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PluginizedLifecycle.swift; sourceTree = "<group>"; };
41F0681420D45AD100FD67C7 /* PluginizedScopeLifecycle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PluginizedScopeLifecycle.swift; sourceTree = "<group>"; };
41F0681A20D45B1300FD67C7 /* PluginizedComponentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PluginizedComponentTests.swift; sourceTree = "<group>"; };
"NeedleFoundation::NeedleFoundation::Product" /* NeedleFoundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = NeedleFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
"NeedleFoundation::NeedleFoundationTests::Product" /* NeedleFoundationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = NeedleFoundationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -70,7 +70,7 @@
41F0681120D45AD100FD67C7 /* Internal */,
41F0681320D45AD100FD67C7 /* NonCoreComponent.swift */,
41F0681020D45AD100FD67C7 /* PluginizedComponent.swift */,
41F0681420D45AD100FD67C7 /* PluginizedLifecycle.swift */,
41F0681420D45AD100FD67C7 /* PluginizedScopeLifecycle.swift */,
);
path = Pluginized;
sourceTree = "<group>";
Expand Down Expand Up @@ -225,7 +225,7 @@
files = (
41F0681620D45AD100FD67C7 /* PluginExtensionProviderRegistry.swift in Sources */,
OBJ_25 /* Bootstrap.swift in Sources */,
41F0681820D45AD100FD67C7 /* PluginizedLifecycle.swift in Sources */,
41F0681820D45AD100FD67C7 /* PluginizedScopeLifecycle.swift in Sources */,
OBJ_26 /* Component.swift in Sources */,
41F0681520D45AD100FD67C7 /* PluginizedComponent.swift in Sources */,
41F0681720D45AD100FD67C7 /* NonCoreComponent.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,18 @@ import Foundation
/// a pluginized component generic without having to specify the nested
/// generics.
public protocol PluginizedComponentType: ComponentType {
/// Bind the plugnizable component to the a lifecycle. This ensures
/// the associated non-core component is released when the given
/// scope is deallocated.
/// Bind the pluginized component to the given lifecycle. This ensures
/// the associated non-core component is notified and released according
/// to the given scope's lifecycle.
///
/// - note: This method must be invoked when using a `PluginizedComponent`,
/// to avoid memory leak of the component and the non-core component.
/// - note: This method is required, because the non-core component reference
/// cannot be made weak. If the non-core component is weak, it is deallocated
/// before the plugin points are created lazily.
/// - parameter lifecycle: The `PluginizedLifecycle` to bind to.
func bind(to lifecycle: PluginizedLifecycle)

/// Signal this pluginized component that the corresponding consumer
/// is about to deinit. This allows the pluginized component to release
/// its corresponding non-core component, breaking the retain cycle
/// between it and its non-core component.
func consumerWillDeinit()
/// - note: This method is required, because the non-core component
/// reference cannot be made weak. If the non-core component is weak,
/// it is deallocated before the plugin points are created lazily.
/// - parameter observable: The `PluginizedScopeLifecycleObervable` to
/// bind to.
func bind(to observable: PluginizedScopeLifecycleObervable)
}

/// The base protocol of a plugin extension, enabling Needle's parsing process.
Expand Down Expand Up @@ -75,44 +70,40 @@ open class PluginizedComponent<DependencyType, PluginExtensionType, NonCoreCompo
pluginExtension = createPluginExtensionProvider()
}

/// Bind the plugnizable component to the a lifecycle. This ensures
/// the associated non-core component is released when the given
/// scope is deallocated.
/// Bind the pluginized component to the given lifecycle. This ensures
/// the associated non-core component is notified and released according
/// to the given scope's lifecycle.
///
/// - note: This method must be invoked when using a `PluginizedComponent`,
/// to avoid memory leak of the component and the non-core component.
/// - note: This method is required, because the non-core component reference
/// cannot be made weak. If the non-core component is weak, it is deallocated
/// before the plugin points are created lazily.
/// - parameter lifecycle: The `PluginizedLifecycle` to bind to.
public func bind(to lifecycle: PluginizedLifecycle) {
/// - note: This method is required, because the non-core component
/// reference cannot be made weak. If the non-core component is weak,
/// it is deallocated before the plugin points are created lazily.
/// - parameter observable: The `PluginizedScopeLifecycleObervable` to
/// bind to.
public func bind(to observable: PluginizedScopeLifecycleObervable) {
guard lifecycleObserverDisposable == nil else {
return
}

lifecycleObserverDisposable = lifecycle.observe { (isActive: Bool) in
if isActive {
lifecycleObserverDisposable = observable.observe { (event: PluginizedScopeLifecycle) in
switch event {
case .active:
self.releasableNonCoreComponent?.scopeDidBecomeActive()
} else {
case .inactive:
self.releasableNonCoreComponent?.scopeDidBecomeInactive()
case .deinit:
// Only release the non-core component after the consumer, which should
// be the owner reference to the component is released. Cannot release
// the non-core component when the bound lifecyle is deactivated. The
// consumer may later require the same instance of this component again.
// In that case, this component will try to access its released non-core
// component to recreate plugins.
self.releasableNonCoreComponent = nil
}
}
}

/// Signal this pluginized component that the corresponding consumer
/// is about to deinit. This allows the pluginized component to release
/// its corresponding non-core component, breaking the retain cycle
/// between it and its non-core component.
public func consumerWillDeinit() {
// Only release the non-core component after the consumer, which should
// be the owner reference to the component is released. Cannot release
// the non-core component when the bound lifecyle is deactivated. The
// consumer may later require the same instance of this component again.
// In that case, this component will try to access its released non-core
// component to recreate plugins.
self.releasableNonCoreComponent = nil
}

// MARK: - Private

private var lifecycleObserverDisposable: ObserverDisposable?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@

import Foundation

/// The lifecycle of a pluginized scope. This represents the lifecycle of
/// the scope that utilizes a pluginized DI component. In the case of an
/// iOS MVC application, the lifecycle events should be mapped to the view
/// controller lifecycles.
public enum PluginizedScopeLifecycle {
/// The active lifecycle. This can be represented as a view controller's
/// `viewDidAppear` lifecycle.
case active
/// The inactivate lifecycle. This can be represented as a view
/// controller's `viewDidDisappear` lifecycle.
case inactive
/// The deinit lifecycle. This can be represented as a view controller's
/// `deinit` lifecycle.
case `deinit`
}

/// The object that allows an observer to be disposed, thereby ending the
/// observation.
public protocol ObserverDisposable {
Expand All @@ -24,13 +40,13 @@ public protocol ObserverDisposable {
func dispose()
}

/// The lifecycle of a pluginized scope.
public protocol PluginizedLifecycle {
/// The observable of the lifecycle events of a pluginized scope.
public protocol PluginizedScopeLifecycleObervable {

/// Observe the lifecycle with given observer.
/// Observe the lifecycle events with given observer.
///
/// - parameter observer: The observer closure to invoke when the lifecycle
/// changes.
/// - returns: The disposable object that can end the observation.
func observe(_ observer: @escaping (Bool) -> Void) -> ObserverDisposable
func observe(_ observer: @escaping (PluginizedScopeLifecycle) -> Void) -> ObserverDisposable
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,33 +45,33 @@ class PluginizedComponentTests: XCTestCase {
func test_bindTo_verifyNonCoreComponentLifecycle() {
let mockPluginizedComponent = MockPluginizedComponent()
let mockDisposable = MockObserverDisposable()
let mockLifecycle = MockPluginizedLifecycle(disposable: mockDisposable)
let mockLifecycle = MockPluginizedScopeLifecycleObervable(disposable: mockDisposable)
let noncoreComponent: MockNonCoreComponent? = mockPluginizedComponent.nonCoreComponent as? MockNonCoreComponent
mockPluginizedComponent.bind(to: mockLifecycle)

XCTAssertEqual(noncoreComponent!.scopeDidBecomeActiveCallCount, 0)
XCTAssertEqual(noncoreComponent!.scopeDidBecomeInactiveCallCount, 0)

mockLifecycle.observer!(true)
mockLifecycle.observer!(.active)

XCTAssertEqual(noncoreComponent!.scopeDidBecomeActiveCallCount, 1)
XCTAssertEqual(noncoreComponent!.scopeDidBecomeInactiveCallCount, 0)

mockLifecycle.observer!(false)
mockLifecycle.observer!(.inactive)

XCTAssertEqual(noncoreComponent!.scopeDidBecomeActiveCallCount, 1)
XCTAssertEqual(noncoreComponent!.scopeDidBecomeInactiveCallCount, 1)
}

func test_consumerWillDeinit_verifyReleasingNonCoreComponent() {
func test_bindTo_verifyReleasingNonCoreComponent() {
let mockPluginizedComponent = MockPluginizedComponent()
var noncoreComponent: MockNonCoreComponent? = mockPluginizedComponent.nonCoreComponent as? MockNonCoreComponent
var noncoreDeinitCallCount = 0
noncoreComponent!.deinitHandler = {
noncoreDeinitCallCount += 1
}
let mockDisposable = MockObserverDisposable()
let mockLifecycle = MockPluginizedLifecycle(disposable: mockDisposable)
let mockLifecycle = MockPluginizedScopeLifecycleObervable(disposable: mockDisposable)
mockPluginizedComponent.bind(to: mockLifecycle)

XCTAssertNotNil(noncoreComponent)
Expand All @@ -82,7 +82,7 @@ class PluginizedComponentTests: XCTestCase {
XCTAssertNil(noncoreComponent)
XCTAssertEqual(noncoreDeinitCallCount, 0)

mockPluginizedComponent.consumerWillDeinit()
mockLifecycle.observer!(.deinit)

XCTAssertNil(noncoreComponent)
XCTAssertEqual(noncoreDeinitCallCount, 1)
Expand Down Expand Up @@ -123,17 +123,17 @@ class MockPluginizedComponent: PluginizedComponent<EmptyDependency, EmptyPluginE
}
}

class MockPluginizedLifecycle: PluginizedLifecycle {
class MockPluginizedScopeLifecycleObervable: PluginizedScopeLifecycleObervable {

let disposable: ObserverDisposable

init(disposable: ObserverDisposable) {
self.disposable = disposable
}

var observer: ((Bool) -> Void)?
var observer: ((PluginizedScopeLifecycle) -> Void)?

func observe(_ observer: @escaping (Bool) -> Void) -> ObserverDisposable {
func observe(_ observer: @escaping (PluginizedScopeLifecycle) -> Void) -> ObserverDisposable {
self.observer = observer
return disposable
}
Expand Down

0 comments on commit 6efa89f

Please sign in to comment.