iOS/Common

iOS 앱의 메모리 사용량 최적화

TDCIAN 2024. 4. 3. 22:19

 

 

iOS 앱의 메모리 사용량을 최적화하는 방법은 여러 가지가 있으며, 이는 앱의 성능과 사용자 경험에 직접적인 영향을 미칩니다.

 

1. 메모리 캐싱 기법

메모리 캐싱은 데이터를 메모리에 저장하여 빠른 접근을 가능하게 하는 기법입니다. iOS에서는 NSCache를 사용하여 이미지 캐싱을 구현할 수 있습니다. NSCache는 메모리 부족 시 자동으로 캐시된 데이터를 제거하여 메모리 사용량을 관리합니다.

 

예시 코드

import UIKit

class ImageCache {
	static let shared = ImageCache()
    private var cache = NSCache<NSString, UIImage>()
    
    // 이미지를 캐시에 저장
    func setImage(_ image: UIImage, forKey key: String) {
    	self.cache.setObject(image, forKey: key as NSString)
    }
    
    // key와 일치하는 캐시된 이미지를 반환
    func getImage(forKey key: String) -> UIImage? {
    	return self.cache.object(forKey: key as NSString)
    }
}

 

 

2. 대용량 데이터 처리 시 메모리 최적화

대용량 데이터(예: 이미지, 비디오)를 처리할 때는 메모리 사용량을 최소화하는 방법이 중요합니다. 이를 위해 lazy loading과 썸네일 활용 등의 기법을 사용할 수 있습니다.

 

예시 코드

import UIKit

// 비동기적으로 이미지 로드
class ImageLoader {
	static let shared = ImageLoader()
    
    func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
    	DispatchQueue.global().async {
        	if let data = try? Data(contentsOf: url),
               let image = UIImage(data: data) {
               	DispatchQueue.main.async {
                	completion(image)
                }
            } else {
            	completion(nil)
            }
        }
    }
}

 

 

다음으로 원본 이미지 대신 작은 크기의 이미지를 사용하여 메모리 사용량을 줄이는 방법입니다. 이미지를 로드할 때 썸네일 크기로 조정하여 메모리 사용량을 줄일 수 있습니다.

import UIKit

extension UIImage {
	func thumbnail(size: CGSize) -> UIImage? {
    	UIGraphicBeginImageContext(size)
        self.draw(in: CGRect(origin: .zero, size: size))
        let thumbnail = UIGraphicGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return thumbnail
    }
}

 

 

이 코드는 원본 이미지에서 썸네일 크기로 조정된 이미지를 생성하여 메모리 사용량을 줄일 수 있습니다.

 

 

3. Lazy loading

Lazy loading은 객체의 생성이나 작업의 실행을 실제로 필요할 때까지 지연시키는 방식입니다. 이는 앱의 성능을 향상시키고 메모리 사용량을 줄이는 데 도움이 됩니다. Swift에서는 lazy 키워드를 사용하여 lazy loading을 구현할 수 있습니다.

 

예시 코드

class Example {
	lazy var expensiveProperty: ExpensiveClass = {
    	return ExpensiveClass()
    }()
}

 

위 예시 코드에서 expensiveProperty는 ExpensiveClass의 인스턴스를 생성하는 데 비용이 많이 드는 작업을 포함하고 있습니다. lazy 키워드를 사용하여 이 속성을 지연 초기화하였습니다. 이렇게 하면 Example 클래스의 인스턴스가 생성될 때 expensiveProperty는 즉시 초기화되지 않고, 처음 접근할 때 초기화됩니다.

 

이미지 로딩에 적용된 lazy loading 예시 코드

class ImageLoader {
	lazy var image: UIImage? = {
    	return UIImage(named: "exampleImage")
    }()
}

 

위 예시 코드에서 ImageLoader 클래스는 이미지를 지연 로드하는 속성 image를 가지고 있습니다. 이 속성은 처음 접근될 때 이미지를 로드하고, 이후에는 캐시된 이미지를 반환합니다. 이렇게 하면 불필요하면 이미지 로딩을 피할 수 있습니다.

 

Lazy loading의 위험성(?)

Lazy loading을 사용할 때 발생할 수 있는 문제 중 하나는 멀티스레드 환경에서의 스레드 안전성 문제입니다. 여러 스레드가 동시에 lazy loading된 속성에 접근하면, 속성이 두 번 이상 초기화될 수 있으며, 이는 예상치 못한 동작을 초래할 수 있습니다.

 

문제를 일으킬 수 있는 코드의 예시

class ThreadSafetyClass {
	lazy var threadSafetyVariable: SomeType = SomeType()
}

 

위 예시 코드에서 threadSafetyVariable은 SomeType의 인스턴스를 생성하는 데 비용이 많이 드는 작업을 포함하고 있습니다. 이 속성은 처음 접근할 때 초기화 됩니다. 그러나 멀티스레드 환경에서 여러 스레드가 동시에 이 속성에 접근하면, 속성이 두 번 이상 초기화될 수 있습니다. 이는 예상치 못한 동작을 초래할 수 있으며, 특히 참조 타입의 경우 여러 스레드가 서로 다른 인스턴스를 참조하게 되어 문제가 발생할 수 있습니다.

 

이러한 문제를 해결하기 위해서는 초기화 코드를 동기화하는 방법을 사용해야 합니다.

Swift에서는 @DispatchQueue를 사용하여 스레드 안전성을 보장해야 합니다.

 

스레드 안전한 Lazy Loading 코드의 예시

class ThreadSafeLazyInitialization {
	private var threadSafetyVariable: SomeType?
    private let lock = NSLock()
    
    var threadSafetyVariable: SomeType {
    	lock.lock()
        defer { lock.unlock() }
        if threadSafetyVariable == nil {
        	threadSafetyVariable = SomeType()
        }
        return threadSafetyVariable
    }
}

 

위 예시 코드에서는 NSLock을 사용하여 초기화 코드를 동기화하고 있습니다. 이렇게 하면 여러 스레드가 동시에 속성에 접근할 때 속성이 한 번만 초기화되도록 보장할 수 있습니다. 이는 멀티스레드 환경에서 Lazy loading을 안전하게 사용할 수 있게 해줍니다.

 

 

정보 출처: phind