앱이 네트워크에 전송하는 요청은 전력 소모가 많은 모바일 또는 Wi-Fi 무선 기능을 사용하기 때문에 배터리 소모의 주요 원인입니다. 이 무선 기능은 패킷을 보내고 받는 데 필요한 전력 외에는 기능을 켜고 켜진 상태로 유지하는 데에만 추가 전력을 소비합니다. 모바일 무선 기능에서 15초마다 발생하는 네트워크 요청과 같이 단순한 것일지라도 지속적으로 빠르게 배터리 전력을 소모할 수 있습니다.
정기 업데이트에는 세 가지 일반적인 유형이 있습니다.
- 사용자 시작. 새로고침을 위한 드래그 동작과 같은 일부 사용자 동작에 따라 업데이트를 실행합니다.
- 앱 시작. 정기적으로 업데이트를 수행합니다.
- 서버 시작 서버의 알림에 대한 응답으로 업데이트를 실행합니다.
이 주제에서는 각 항목을 살펴보고 배터리 소모를 줄이기 위해 최적화할 수 있는 추가 방법을 설명합니다.
사용자가 시작한 요청 최적화
사용자 시작 요청은 일반적으로 일부 사용자 동작에 대한 응답으로 발생합니다. 예를 들어 최신 뉴스 기사를 읽는 데 사용되는 앱은 사용자가 아래로 당겨 새로고침 동작을 실행하여 새 기사를 확인할 수 있도록 허용할 수 있습니다. 다음 기법을 사용하여 네트워크 사용을 최적화하면서 사용자가 시작한 요청에 응답할 수 있습니다.
사용자 요청 제한
현재 데이터가 아직 최신 상태인데 새 데이터를 확인하기 위해 짧은 시간 동안 여러 번 새로고침 동작을 하는 등 사용자 시작 요청이 필요하지 않은 경우 이를 무시할 수 있습니다. 각 요청에 따라 작동하면 무선 기능이 계속 켜져 있어 상당한 전력이 낭비될 수 있습니다. 더 효율적인 접근 방식은 일정 기간 동안 하나의 요청만 이루어지도록 사용자 시작 요청을 제한하여 무선 통신이 사용되는 빈도를 줄이는 것입니다.
캐시 사용
앱의 데이터를 캐시하면 앱이 참조해야 하는 정보의 로컬 복사본이 생성됩니다. 그러면 앱이 새 요청을 위해 네트워크 연결을 열지 않고도 동일한 로컬 정보 사본에 여러 번 액세스할 수 있습니다.
정적 리소스와 원본 크기의 이미지와 같은 주문형 다운로드를 비롯한 데이터를 최대한 많이 캐시해야 합니다. HTTP 캐시 헤더를 사용하여 캐싱 전략으로 인해 앱에 오래된 데이터가 표시되지 않도록 할 수 있습니다. 네트워크 응답 캐싱에 관한 자세한 내용은 중복 다운로드 방지를 참고하세요.
Android 11 이상에서 앱은 머신러닝 및 미디어 재생과 같은 사용 사례를 위해 다른 앱에서 사용하는 것과 동일한 대규모 데이터 세트를 사용할 수 있습니다. 앱이 공유 데이터 세트에 액세스해야 할 때 새 사본을 다운로드하기 전에 먼저 캐시된 버전을 확인할 수 있습니다. 공유 데이터 세트에 대해 자세히 알아보려면 공유 데이터 세트에 액세스하기를 참고하세요.
많은 데이터를 낮은 빈도로 다운로드하기 위해 큰 대역폭 사용
무선 통신을 연결하는 경우 일반적으로 더 높은 대역폭을 사용하면 더 큰 배터리 비용이 듭니다. 즉, 5G는 일반적으로 LTE보다 더 많은 에너지를 소비하고 LTE는 3G보다 더 큰 비용이 듭니다.
즉, 기반 무선 통신 상태는 무선 통신 기술에 따라 달라지는 반면 일반적으로 상태 변경 테일-타임에 따른 배터리 영향은 더 높은 대역폭의 무선 통신에서 더 큽니다. 테일 시간에 관한 자세한 내용은 무선 상태 머신을 참고하세요.
동시에 대역폭이 높다는 것은 더 많은 데이터를 미리 가져와서 같은 시간 동안 더 많이 다운로드할 수 있음을 의미합니다. 또한 직관적이지는 않지만, 테일-타임 배터리 비용이 상대적으로 높기 때문에 업데이트 빈도를 줄이기 위해 각 전송 세션 동안 무선 통신을 더 장시간 활성 상태로 유지하는 것이 더 효율적입니다.
예를 들어 LTE 무선 통신이 3G보다 대역폭과 에너지 비용이 두 배라면 각 세션 동안 네 배의 데이터 또는 10MB 정도의 데이터를 다운로드해야 합니다. 이렇게 많은 양의 데이터를 다운로드할 때, 데이터를 미리 가져오는 것이 사용 가능한 로컬 저장소에 미치는 영향을 고려하는 것과 미리 가져오기용 캐시를 정기적으로 플러시하는 것이 중요합니다.
ConnectivityManager
를 사용하여 기본 네트워크의 리스너를 등록하고 TelephonyManager
를 사용하여 PhoneStateListener
를 등록하여 현재 기기 연결 유형을 확인할 수 있습니다. 연결 유형을 알게 되면 그에 따라 미리 가져오기 루틴을 수정할 수 있습니다.
Kotlin
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val tm = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager private var hasWifi = false private var hasCellular = false private var cellModifier: Float = 1f private val networkCallback = object : ConnectivityManager.NetworkCallback() { // Network capabilities have changed for the network override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities ) { super.onCapabilitiesChanged(network, networkCapabilities) hasCellular = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) hasWifi = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } } private val phoneStateListener = object : PhoneStateListener() { override fun onPreciseDataConnectionStateChanged( dataConnectionState: PreciseDataConnectionState ) { cellModifier = when (dataConnectionState.networkType) { TelephonyManager.NETWORK_TYPE_LTE or TelephonyManager.NETWORK_TYPE_HSPAP -> 4f TelephonyManager.NETWORK_TYPE_EDGE or TelephonyManager.NETWORK_TYPE_GPRS -> 1/2f else -> 1f } } private class NetworkState { private var defaultNetwork: Network? = null private var defaultCapabilities: NetworkCapabilities? = null fun setDefaultNetwork(network: Network?, caps: NetworkCapabilities?) = synchronized(this) { defaultNetwork = network defaultCapabilities = caps } val isDefaultNetworkWifi get() = synchronized(this) { defaultCapabilities?.hasTransport(TRANSPORT_WIFI) ?: false } val isDefaultNetworkCellular get() = synchronized(this) { defaultCapabilities?.hasTransport(TRANSPORT_CELLULAR) ?: false } val isDefaultNetworkUnmetered get() = synchronized(this) { defaultCapabilities?.hasCapability(NET_CAPABILITY_NOT_METERED) ?: false } var cellNetworkType: Int = TelephonyManager.NETWORK_TYPE_UNKNOWN get() = synchronized(this) { field } set(t) = synchronized(this) { field = t } private val cellModifier: Float get() = synchronized(this) { when (cellNetworkType) { TelephonyManager.NETWORK_TYPE_LTE or TelephonyManager.NETWORK_TYPE_HSPAP -> 4f TelephonyManager.NETWORK_TYPE_EDGE or TelephonyManager.NETWORK_TYPE_GPRS -> 1 / 2f else -> 1f } } val prefetchCacheSize: Int get() = when { isDefaultNetworkWifi -> MAX_PREFETCH_CACHE isDefaultNetworkCellular -> (DEFAULT_PREFETCH_CACHE * cellModifier).toInt() else -> DEFAULT_PREFETCH_CACHE } } private val networkState = NetworkState() private val networkCallback = object : ConnectivityManager.NetworkCallback() { // Network capabilities have changed for the network override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities ) { networkState.setDefaultNetwork(network, networkCapabilities) } override fun onLost(network: Network?) { networkState.setDefaultNetwork(null, null) } } private val telephonyCallback = object : TelephonyCallback(), TelephonyCallback.PreciseDataConnectionStateListener { override fun onPreciseDataConnectionStateChanged(dataConnectionState: PreciseDataConnectionState) { networkState.cellNetworkType = dataConnectionState.networkType } } connectivityManager.registerDefaultNetworkCallback(networkCallback) telephonyManager.registerTelephonyCallback(telephonyCallback) private val prefetchCacheSize: Int get() { return when { hasWifi -> MAX_PREFETCH_CACHE hasCellular -> (DEFAULT_PREFETCH_CACHE * cellModifier).toInt() else -> DEFAULT_PREFETCH_CACHE } } }
자바
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); private boolean hasWifi = false; private boolean hasCellular = false; private float cellModifier = 1f; private ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() { @Override public void onCapabilitiesChanged( @NonNull Network network, @NonNull NetworkCapabilities networkCapabilities ) { super.onCapabilitiesChanged(network, networkCapabilities); hasCellular = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR); hasWifi = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_WIFI); } }; private PhoneStateListener phoneStateListener = new PhoneStateListener() { @Override public void onPreciseDataConnectionStateChanged( @NonNull PreciseDataConnectionState dataConnectionState ) { switch (dataConnectionState.getNetworkType()) { case (TelephonyManager.NETWORK_TYPE_LTE | TelephonyManager.NETWORK_TYPE_HSPAP): cellModifier = 4; Break; case (TelephonyManager.NETWORK_TYPE_EDGE | TelephonyManager.NETWORK_TYPE_GPRS): cellModifier = 1/2.0f; Break; default: cellModifier = 1; Break; } } }; cm.registerDefaultNetworkCallback(networkCallback); tm.listen( phoneStateListener, PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE ); public int getPrefetchCacheSize() { if (hasWifi) { return MAX_PREFETCH_SIZE; } if (hasCellular) { return (int) (DEFAULT_PREFETCH_SIZE * cellModifier); } return DEFAULT_PREFETCH_SIZE; }
앱에서 시작한 요청 최적화
앱 시작 요청은 일반적으로 백엔드 서비스에 로그 또는 분석을 전송하는 앱과 같이 일정에 따라 발생합니다. 앱에서 시작된 요청을 처리할 때는 이러한 요청의 우선순위, 일괄 처리 가능 여부, 기기가 충전 중이거나 무제한 네트워크에 연결될 때까지 지연 가능 여부를 고려하세요. 이러한 요청은 신중한 예약과 WorkManager와 같은 라이브러리를 사용하여 최적화할 수 있습니다.
네트워크 요청 일괄 처리
휴대기기에서는 무선 기능을 사용 설정하고, 연결하고, 무선 기능을 켜진 상태로 유지하는 과정에서 많은 전력을 사용하게 됩니다. 따라서 개별 요청을 그때그때 처리하면 전력이 많이 소모되고 배터리 수명이 단축될 수 있습니다. 더 효율적인 방법은 일련의 네트워크 요청을 대기열에 넣어 함께 처리하는 것입니다. 이를 통해 시스템에서는 무선 기능을 사용 설정할 때 드는 전력 비용을 한 번 지불하는 것으로도 앱에서 요청한 모든 데이터를 받을 수 있습니다.
WorkManager 사용
WorkManager
라이브러리를 사용하면 네트워크 가용성 및 전원 상태와 같은 특정 조건이 충족되는지 고려하는 효율적인 일정에 따라 작업을 실행할 수 있습니다. 예를 들어 최신 뉴스 헤드라인을 가져오는 DownloadHeadlinesWorker
라는 Worker
서브클래스가 있다고 가정해 보겠습니다. 이 작업자는 기기가 무제한 네트워크에 연결되어 있고 기기의 배터리가 부족하지 않은 경우 매시간 실행되도록 예약할 수 있습니다. 데이터를 가져오는 데 문제가 있는 경우 아래와 같이 맞춤 재시도 전략을 사용합니다.
Kotlin
val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .setRequiresBatteryNotLow(true) .build() val request = PeriodicWorkRequestBuilder<DownloadHeadlinesWorker>(1, TimeUnit.HOURS) .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.LINEAR, 1L, TimeUnit.MINUTES) .build() WorkManager.getInstance(context).enqueue(request)
자바
Constraints constraints = new Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .setRequiresBatteryNotLow(true) .build(); WorkRequest request = new PeriodicWorkRequest.Builder(DownloadHeadlinesWorker.class, 1, TimeUnit.HOURS) .setBackoffCriteria(BackoffPolicy.LINEAR, 1L, TimeUnit.MINUTES) .build(); WorkManager.getInstance(this).enqueue(request);
Android 플랫폼은 WorkManager 외에도 폴링과 같은 네트워킹 작업 완료를 위한 효율적인 일정을 만드는 데 도움이 되는 여러 도구를 제공합니다. 이러한 도구의 사용법을 자세히 알아보려면 백그라운드 처리 가이드를 참고하세요.
서버에서 시작한 요청 최적화
서버에서 시작한 요청은 일반적으로 서버의 알림에 대한 응답으로 발생합니다. 예를 들어 최신 뉴스 기사를 읽는 데 사용되는 앱은 사용자의 맞춤설정 환경설정에 맞는 새로운 기사 배치에 관한 알림을 수신한 후 이를 다운로드할 수 있습니다.
Firebase 클라우드 메시징을 사용하여 서버 업데이트 전송
Firebase 클라우드 메시징(FCM)은 서버에서 특정 앱 인스턴스로 데이터를 전송하는 데 사용되는 간단한 메커니즘입니다. FCM을 사용하면 서버는 특정 기기에서 실행 중인 앱에 사용 가능한 새 데이터가 있음을 알려줄 수 있습니다.
앱이 정기적으로 서버에 핑하여 새로운 데이터가 있는지 쿼리해야 하는 폴링에 비해 이 이벤트 기반 모델을 사용하면 앱이 다운로드할 데이터가 있는 것을 알았을 때만 새 연결을 생성하면 됩니다. 이 모델은 앱의 정보를 업데이트할 때 불필요한 연결을 최소화하고 지연 시간을 줄여줍니다.
FCM은 지속적인 TCP/IP 연결을 사용하여 구현합니다. 이를 통해 지속적 연결 수를 최소화하고 플랫폼에서 대역폭을 최적화하고 배터리 수명에 미치는 영향을 최소화할 수 있습니다.