• 티스토리 홈
  • 프로필사진
    2료일
  • 방명록
  • 공지사항
  • 태그
  • 블로그 관리
  • 글 작성
2료일
  • 프로필사진
    2료일
    • 분류 전체보기 (118)
      • SWIFT개발 (29)
      • 알고리즘 (25)
      • Design (6)
      • ARkit (1)
      • 면접준비 (32)
      • UIkit (2)
      • Vapor-Server with swift (3)
      • 디자인패턴 (5)
      • 반응형프로그래밍 (12)
  • 방문자 수
    • 전체:
    • 오늘:
    • 어제:
  • 최근 댓글
      등록된 댓글이 없습니다.
    • 최근 공지
        등록된 공지가 없습니다.
      # Home
      # 공지사항
      #
      # 태그
      # 검색결과
      # 방명록
      • MemoryLeak을 찾아보자(강한참조순환인가?) - 실전편2
        2025년 01월 16일
        • 2료일
        • 작성자
        • 2025.01.16.:17

        나의 앱 볼레또는 여행 날짜가 되면 CLMointior인스턴스에 해당하는 지역의 위도 경도를 넣어 모너터링을 키고 유저가 해당 지역에 가면 푸쉬 알람을 보내주는 기능이 핵심이다. 이전에는 모니터링 이외의 메모리 릭을 살펴봤는데 이번에는 모니터링에서 발생하는 Memory Leak을 살펴볼 예정이다.

        저번에 했던 Memory  debugger Graph를 다시 열어보자. 막 거미줄마냥 엮여있다.

        총 16개로 저번에 1개 고치는데에도 하루종일 사용했는데 이번엔 얼마나 걸릴지 막막하다. 나의 과거를 욕해야지..

        일단 이번에는 Command Line Tool사용하여 더 살펴보려 한다. 메모리 그래프 디버거 킨 상태에서 File > Export Memory Graph를 선택하여 스냅샷을 파일로 저장하고 해당하는 것을

        vmmap --summary 해당이름.memgraph

        로 열어주었다.

        VIRTUAL SIZE = 시스템이 해당 메모리 영역을 위해 예약한 가상메모리 공간의 총량. 실제로 사용중인 메모리 크기는 아니다.

        RESIDENT SIZE = 현재 실제로 사용중인 메모리 양. (사용중인 물리적메모리)

        DIRTY SIZE = 수정된 메모리의 크기, 예를 들어, 캐시 데이터가 변경되었거나 아직 디스크에 기록되지 않은 상태의 메모리.

        SWAPPED SIZE = 메모리 디스크로 스왑된 크기

        1. MALLOC 관련 데이터

        • MALLOC_SMALL, MALLOC_TINY 같은 영역은 동적으로 메모리를 할당할 때 사용.

        • 메모리 최적화를 위해 할당된 메모리 크기와 사용 여부를 분석할 수 있다.

        • 예: MALLOC_SMALL의 21.3MB가 RESIDENT(사용 중) 상태이므로, 할당된 블록의 크기와 효율성을 점검할 수 있습니다.

        2. Stack 사용량 분석

        • 스택은 함수 호출에 따라 동적으로 증가 및 감소합니다.

        • 예: Stack의 RESIDENT SIZE가 512KB로 적절한 크기로 유지되고 있습니다.

        • 만약 이 값이 비정상적으로 크다면, 재귀 함수 호출 또는 비효율적인 스택 사용의 가능성을 의심할 수 있습니다.

        3. dyld private memory

        • dyld private memory는 동적 라이브러리 로딩과 관련된 메모리입니다.

        • 2.5GB로 가장 큰 메모리 영역을 차지하고 있는 점이 눈에 띄었다.

        • 이는 앱에서 사용하는 라이브러리의 크기와 관련이 있습니다.

        ㄴ 이렇게 봐서는 어디가 문제인지 모르겠다. 그래서 다시 이전에 했던 것처럼 Profile을 해주자.

        일단 어떠한 상황에서 메모리릭이 형성되는지를 파악하기위해 수많은 시뮬레이션을 했다.

        1. 자동로그인상태에서 현재날짜의 티켓을 만들거나 날짜를 수정하여 위치 모니터링 하는 경우에서는 메모리릭이 발견되지 않았다.

        2. 현재 여행중인 날짜를 편집해서 날짜를 미여행 날짜로 바꿀때 위치모니터링이 꺼지지가 않았다.(메모리 릭은 아님)

        um..이건 내가 로직을 잘못짜서 발생한 이슈였다. 기존에는 여행을 편집을 완료하면 해당 시작일과 종료일로부터 현재 여행중일때만 위치 모니터링을 켜주는 동작을 하면 그것을 Appfeature에서 보고 켜주었지만 끄는 동작은 없었다. 

               case .successTicket :
                        guard let startDate = state.startDate, let endDate = state.endDate, let arrivalSpot = state.arrivialSpot else {return .none}
                        if state.mode == .add {
                            return .run { _ in
                                await dismiss()
                            }
                        } else {
                            //편집모드
                            if Date.isTraveling(startDate: startDate, endDate: endDate) {
                                return .concatenate(
                                    .send(.startMonitoring(arrivalSpot)),
                                    .send(.dismissView)
                                )
                            }
                            else if state.isMonitoring {
                                return .run {send in
                                    await locationClient.stopMonitoring()
                                     await send(.dismissView)
                                }
                            } else {
                                return .run {send in
                                    await send(.dismissView)}
                            }
                        }

        수정후 코드

        이 문제를 해결하기 위해 isMonitoring 상태를 추가하고, 날짜 편집 후 isMonitoring 상태를 확인하여 현재 여행 중인 날짜가 포함되지 않으면 위치 모니터링을 끄는 로직을 추가했습니다. 이때 중요한 점은 여러 여행 중 하나만 날짜가 변경되어 현재 여행 중인 티켓만 영향을 받아야 한다는 점입니다. 모든 여행의 모니터링을 끄는 동작을 방지하기 위해 isMonitoring 상태를 통해 이를 분리 처리했습니다.

        기존코드(문제의 코드)

        import ComposableArchitecture
        import CoreLocation
        import SwiftUI
        
        var backgroundActivitySession: CLBackgroundActivitySession?
        @DependencyClient
        struct LocationClient {
            var authorizationStatus: @Sendable () async -> CLAuthorizationStatus = {.denied}
            var requestauthorzizationStatus: @Sendable  () async -> Void
            var startMonitoring: @Sendable (SpotType) async throws -> AsyncStream<MonitorEvent>
            var stopMonitoring: @Sendable (SpotType) async -> Void
            var disableLocationServices: @Sendable () -> Void
             
            private static var monitor: CLMonitor?
        }
        
        enum MonitorEvent: Equatable {
            case didEnterFrameRegion
            case didEnterBadgeRegion(StickerCodes)
        }
        
        extension LocationClient: DependencyKey {
            static let liveValue: Self = {
                let locationManager = LocationManager()
                
                return Self(
                    authorizationStatus: {
                        return locationManager.manager.authorizationStatus
                    }, requestauthorzizationStatus: {
                        locationManager.manager.requestAlwaysAuthorization()
                    },
                    startMonitoring: {spot in
                        AsyncStream { continuation in
                            @Dependency(\.notificationClient.add) var notificationClient
                            Task {
                                backgroundActivitySession = CLBackgroundActivitySession()
                                let spot = spot.spot
                                // 기존 Monitor가 존재하면 먼저 제거
                                      if let existingMonitor = monitor {
        //                                  existingMonitor.() // 기존 이벤트 제거
                                          monitor = nil
                                      }
                                monitor = await CLMonitor(spot.upperString)
            
                                let frameCondition = CLMonitor.CircularGeographicCondition(center: spot.coordinate, radius: 3000.0)
                                monitor?.add(frameCondition, identifier: "Frame")
                                for landmark in spot.landmarks {
                                    let badgeCenter = CLLocationCoordinate2D(latitude: landmark.latitude, longitude: landmark.longtitude)
                                    let landmarkCondition = CLMonitor.CircularGeographicCondition(center: badgeCenter, radius: 1000.0)
                                     monitor?.add(landmarkCondition, identifier: landmark.badgetype.rawValue)
                                }
                                if let events =  monitor?.events {
                                    for try await event in events {
                                        switch event.state {
                                        case .satisfied:
                                            if event.identifier == "Frame" {
                                                monitor?.remove("Frame")
                                                continuation.yield(.didEnterFrameRegion)
                                            } else if let badgeType = StickerCodes(rawValue: event.identifier) {
                                                monitor?.remove(event.identifier)
                                                try await notificationClient(BadgeNotification(id: badgeType.rawValue, stickerImageType: badgeType))
                                                continuation.yield(.didEnterBadgeRegion(badgeType))
                                            }
                                        default:
                                            break
                                        }
                                    }
                                }
                            }}
                    },
                    stopMonitoring: {spottype in
                        let spot  = spottype.spot
                         monitor?.remove(spot.upperString)
                        backgroundActivitySession?.invalidate()
                    }, disableLocationServices:  {
                        guard let appSettingsURL = URL(string: UIApplication.openSettingsURLString) else { return }
                        DispatchQueue.main.async {
                            UIApplication.shared.open(appSettingsURL)
                        }
                    }
                )
            }()
        }
        
        enum LocationError: Error {
            case authorizationDenied
        }
        
        extension DependencyValues {
            var locationClient: LocationClient {
                get { self[LocationClient.self] }
                set { self[LocationClient.self] = newValue }
            }
        }
        
        
        private class LocationManager: NSObject, CLLocationManagerDelegate {
            let manager = CLLocationManager()
            private var authorizationContinuation: CheckedContinuation<CLAuthorizationStatus, Never>?
            override init() {
                super.init()
                manager.delegate = self
                manager.desiredAccuracy = kCLLocationAccuracyKilometer
                manager.pausesLocationUpdatesAutomatically = false
                manager.allowsBackgroundLocationUpdates = true
            }
        
        
            func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
                guard let continuation = authorizationContinuation else { return }
                authorizationContinuation = nil
                continuation.resume(returning: manager.authorizationStatus)
            }
            
        
        }

         

        3. 로그아웃 후  로그인을 하였을때 여행중인 티켓이 있을때 메모리 릭이 생긴다.

        -> 로그아웃할때 backgroundActivitySession nil로 하였지만 AsyncStream의 Task가 종료되지 않고 남아있어서 발생하는 이슈였다. 근데 왜 종료되지 않았지? 이는 AsyncStream의 continuation이 yield를 계속 대기하고, 강한 참조순환이 발생하였다. 해당 이유로는 continuation.finish()이 호출이 안되었다. 이로 인해 AsyncStream이 계속 메모리에 남아 있어 관련 리소스 해제되지 않았다.

        4. 여행중인 여행티켓을 다른 날짜로 수정하면 메모리 릭이 생긴다. 

        -> 이것도 마찬가지로 이전 AsyncStream의 Task와 강한참조순환이 일어났다 왜 났을까

        기존 코드를 보면 Monitor 객체를 Client의 Static변수로 만들어 주었다. 즉 monitor가 locationClient의 self에 의존하는 형태.

        또 보면 locationClient 메서드가 monitor를 사용한다. 즉 강한 순환 참조가 발생한다. monitor는 CLMonitor인스턴스로 외부에서 참조가 유지될수 있었다. stopMonitoring에서 모니터 자체를 nil로 설정하여 해제하는 로직이 없었다.

         

        솔루션

        1. LocationActor를 도입하여 monitor, backgroundSession, activeContinuation같은 상태를 관리하도록 분리하였다. 그래서 상태를 액터내부에서 관리하여 리소스 명시적으로 해제할 수 있도록 구현하였다.

        2. continutaiton.onTermination을 설정해 스트림이 종료될 때 연관된 Task 취소하도록 했다. 또한 stopMonitoring에서는 명시적으로 스트림을 종료하도록 했다. 그래서 스트림이 종료되면 즉시 해제되도록 메모리 누수를 방지했다.

        기존 코드에서는 AsyncStream의 Continuation을 종료(finish())하지 않아 내부의 비동기 작업(Task)이 계속 실행되는 문제가 있었습니다. 이를 해결하기 위해 Task를 명시적으로 변수로 선언하고, Continuation을 Actor의 변수로 관리하도록 변경하였습니다. stopMonitoring() 메서드가 호출되면 Continuation을 종료하고(finish() 호출), onTermination에서 해당 Task를 취소(cancel())하여 모든 비동기 작업을 안전하게 정리하도록 설계하였습니다!!

        import ComposableArchitecture
        import CoreLocation
        import SwiftUI
        
        var backgroundActivitySession: CLBackgroundActivitySession?
        @DependencyClient
        struct LocationClient {
            var authorizationStatus: @Sendable () async -> CLAuthorizationStatus = {.denied}
            var requestauthorzizationStatus: @Sendable  () async -> Void
            var startMonitoring: @Sendable (SpotType) async throws -> AsyncStream<MonitorEvent>
            var stopMonitoring: @Sendable () async -> Void
            var disableLocationServices: @Sendable () -> Void
            var isMonitoringActive: @Sendable () async -> Bool = { false }
            
            @CasePathable
            enum MonitorEvent: Equatable {
                case didEnterFrameRegion(String)
                case didEnterBadgeRegion(StickerCodes)
            }
        }
        
        
        
        extension LocationClient: DependencyKey {
            static let liveValue: Self = {
                return Self(
                    authorizationStatus: {
                        await LocationActor.shared.authorizationStatus()
                    }, requestauthorzizationStatus: {
                        await LocationActor.shared.requestAuthorizationStatus()
                    },
                    startMonitoring: {spot in
                      try await LocationActor.shared.startMonitoring(spot: spot)
                    },
                    stopMonitoring: {
                        LocationActor.shared.stopMonitoring()
                    }, disableLocationServices:  {
                        guard let appSettingsURL = URL(string: UIApplication.openSettingsURLString) else { return }
                        DispatchQueue.main.async {
                            UIApplication.shared.open(appSettingsURL)
                        }
                    }, isMonitoringActive: {
                        LocationActor.shared.isMonitoring()
                    }
                )
                @globalActor final actor LocationActor {
                    static let shared = LocationActor()
                    private var delegate = Delegate()
                    private var monitor: CLMonitor?
                    private var backgroundSession: CLBackgroundActivitySession?
                    private var activeContinuation: AsyncStream<MonitorEvent>.Continuation?
                    
                    private final class Delegate: NSObject, @unchecked Sendable, CLLocationManagerDelegate {
                        let manager = CLLocationManager()
                        var authorizationContinuation: CheckedContinuation<CLAuthorizationStatus, Never>?
                        
                        
                        override init() {
                            super.init()
                            manager.delegate = self
                            manager.desiredAccuracy = kCLLocationAccuracyKilometer
                            manager.pausesLocationUpdatesAutomatically = false
                            manager.allowsBackgroundLocationUpdates = true
                        }
                        
                        nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
                            guard let continuation = authorizationContinuation else { return }
                            authorizationContinuation = nil
                            //                    continuation.resume(returning: manager.authorizationStatus)
                        }
                    }
                    func isMonitoring() -> Bool {
                        guard let monitor = monitor else {return false}
                        return true
                        
                    }
                    func authorizationStatus() -> CLAuthorizationStatus {
                        delegate.manager.authorizationStatus
                    }
                    func requestAuthorizationStatus() {
                        delegate.manager.requestAlwaysAuthorization()
                    }
                    
                    func startMonitoring(spot: SpotType) async throws -> AsyncStream<MonitorEvent> {
                        
                        let stream = AsyncStream<MonitorEvent> { continuation in
                            activeContinuation = continuation
                            backgroundSession = CLBackgroundActivitySession()
                        let task = Task {
                            monitor = await CLMonitor(spot.spot.upperString)
                            let frameCondition = CLMonitor.CircularGeographicCondition(
                                center: spot.spot.coordinate,
                                radius: 3000.0
                            )
                            await monitor?.add(frameCondition, identifier: "Frame")
                            for landmark in spot.spot.landmarks {
                                let badgeCenter = CLLocationCoordinate2D(
                                    latitude: landmark.latitude,
                                    longitude: landmark.longtitude
                                )
                                let landmarkCondition = CLMonitor.CircularGeographicCondition(
                                    center: badgeCenter,
                                    radius: 1000.0
                                )
                                await monitor?.add(landmarkCondition, identifier: landmark.badgetype.rawValue)
                            }
                                guard let events = await monitor?.events else {
                                    continuation.finish()
                                    return
                                }
                                for try await event in events {
                                    switch event.state {
                                    case .satisfied:
                                        if event.identifier == "Frame" {
                                            await monitor?.remove("Frame")
                                            continuation.yield(.didEnterFrameRegion(spot.spot.name))
                                        } else if let badgeType = StickerCodes(rawValue: event.identifier) {
                                            await monitor?.remove(event.identifier)
                                            continuation.yield(.didEnterBadgeRegion(badgeType))
                                        }
                                    default:
                                        break
                                    }
                                }
                            }
                            continuation.onTermination = { _ in
                                print("마치인디고")
                                task.cancel()
                            }
                        }
                   
                        
                        return stream
                    }
                    func stopMonitoring()  {
                        backgroundSession?.invalidate()
                        backgroundSession = nil
                        activeContinuation?.finish()
                        activeContinuation = nil
                        monitor = nil
                    }
                }
            }()
        }
        
        
        enum LocationError: Error {
            case authorizationDenied
        }
        
        extension DependencyValues {
            var locationClient: LocationClient {
                get { self[LocationClient.self] }
                set { self[LocationClient.self] = newValue }
            }
        }

        영감을 얻은 것은 TCA공식문서를 보면 Socket통신이 있다. 계속 AsyncStream을 열어두고 응답을 받는 형식이였당

        결국 이번엔 내가 강한순환참조 이슈를 저질렀나 했지만 그냥 비동기 통신이 메모리에 계속남아있는 메모리 누수였다..

         

        'SWIFT개발' 카테고리의 다른 글

        Preview는 어떻게 그림을 그리는 걸까? is that hotreload?  (0) 2025.03.01
        ScrollView 꾸미기? Deep Dive  (0) 2025.02.22
        Memory Leak을 찾아보자 실전편(1)  (0) 2025.01.14
        Alamofire error code handling  (3) 2024.12.17
        Preference Key  (0) 2024.11.25
        다음글
        다음 글이 없습니다.
        이전글
        이전 글이 없습니다.
        댓글
      조회된 결과가 없습니다.
      스킨 업데이트 안내
      현재 이용하고 계신 스킨의 버전보다 더 높은 최신 버전이 감지 되었습니다. 최신버전 스킨 파일을 다운로드 받을 수 있는 페이지로 이동하시겠습니까?
      ("아니오" 를 선택할 시 30일 동안 최신 버전이 감지되어도 모달 창이 표시되지 않습니다.)
      목차
      표시할 목차가 없습니다.
        • 안녕하세요
        • 감사해요
        • 잘있어요

        티스토리툴바