Application in the Background
우리는 2강에서 배웠던 내용을 생각해 봅시다
Refresh 버튼을 메뉴를 선택하면 AsyncTask를 사용하여 데이터를 업데이트 하였습니다
이것은 좋지 않고 나중에 수정할 것이라고 말한적이 있습니다
AsyncTask는 Activity의 Lifecycle과 무관하기 때문입니다
Loader를 배울때의 문제점이 계속 반복되는 구조가 됩니다
Web 서비스에서 Data를 받을 때와 같은 장시간 Background 작업은 좋은 방법이 아닙니다
AsyncTask는 Application 프로세스가 살아 있는 동안 계속 실행 됩니다
단 우선순위가 낮기 때문에 Background로 이동을 하는 상황이나 리소스가 부족한 상황에서는 죽게 됩니다
이것은 큰 문제입니다!!
Application을 시작할때 날씨를 업데이트 하지만 날씨가 빠르게 변화하면 바람직하지 않은 움직임을 할 수도 있습니다
날씨를 업데이트 하는 올바른 방법을 배웁시다
배터리 소모를 최소화하고 Background에서 정기적으로 Data를 업데이트 합시다
4강에서 활동양이 미비한 Application은 종료시킨다는 사실을 배웠습니다
Foreground Application을 위한 활동이라는 사실도 배웠습니다
Application이 보이지 않는 곳에서도 해야할 활동이 있다면?
예를 들어서 사진 업로드 / 다운로드, 음악 재생 같은 것이 있다면 어떨까요?
이런동작을 하기 위해서 Application의 Component가 있습니다
Activity, Content Provider, Broadcast Receiver, intent 같은 것들을 이미 다 배웠습니다
마지막 단계인 Services만 배우면 됩니다!
1 | startService(myService); |
이런식으로 서비스를 시작 / 정지 할 수 있습니다
Activity와 Services의 다른 점은 UI가 없고 Background Activity보다 우선순위도 높지 않습니다
실행중인 서비스를 가지고 있는 앱은 Foreground 활동을 위한 리소스 확보 때문에 종료될 가능성이 있습니다
하지만 기본 설정으로 시스템은 앱 내에서 중지된 서비스를 재시작 하려고 시도합니다
Activity와는 다르게 서비스는 방해 없이 작업을 수행하도록 설계되어 있습니다
보통 실행하고 싶은 백그라운드 작업을 시작하려고 할 때만 onStartCommand handler를 override 합니다
그런데 이 경우에는 앱 상태 변화를 모니터링하고 백그라운드 앱에 전달할 handler가 없다는 것을 알아야 합니다
실행중인 Service가 프레임워크에 다음과 같이 통지하기 때문입니다
“이 Application은 Background에 실행중인 Service를 가지지 않는 다른 Application보다 우선 순위가 높다”
Services는 작업 수행도 가능합니다
음악을 재생하거나 네비게이션 길안내를 수행하는 작업들이 가능합니다
이 작업은 UI를 가지지 않기 때문에 중단시 사용자 경험을 방해하게 됩니다
이러한 경우 startForeground() 호출에 의해 “Service가 Foreground에서 실행되고 있다”고 통지할수 잇습니다
Activity와 Receiver와 같이 Service도 Main-Thread 입니다
그래서 Service내에서 시간이 오래 걸리는 작업을 실행시키는 경우에는 Background Thread로 처리 할 수 있습니다
IntentService Class를 이용 할 수 있습니다 Practice Patterns을 이용해서 사용됩니다
intent를 이용해서 들어오는 intent의 목록을 만들고 startService()가 호출될 때 넘어 들어옵니다
큐 형식으로 되어있습니다
Services는 강력한 툴 입니다
배우는 것도 중요하지만 현실적으로 프레임워크를 통해 자기만의 Services implementation을 실행하는 방법도 종종 있습니다
백그라운드 작업을 실행할 IntentService이든 SyncAdapter든 간에 계속 배워 나가봅시다
Android에서는 App Priority를 3단계로 구분합니다
Low, High, Critical
- Critical : Active Activities Foreground Services
- High : Visible Activities, Running Services
- Low : Background Applications
Android 자원 관리의 3가지 법칙!!
- Android는 유저가 사용하고 있는 중인 앱들을 부드럽게 사용가능하도록 유지시킵니다
- 1번을 어기지 않는 이상 Android는 보이거나 실행중인 앱들은 그대로 내비둡니다
- 1번 2번을 어기지 않는 이상 Android는 Background에 있는 모든 앱들을 유지시킵니다
Service
Service를 이용해서 Applications을 어떻게 적용할까요?
WeatherProvider 내에 Content Notifier가 Content Observer에게 통보해 줍니다
FetchWeatherTask는 이미 UI와 독립적으로 실행되고 있습니다
이제 IntentService를 활용할 수 있습니다!
service 패키지를 만들어 봅시다
SunshineService class를 만들 때 IntentService를 상속받아서 해봅시다
기본적인 생성자와 handler를 override 합시다
AndroidManifest.xml 에 Service를 추가합니다
1 | <service android:name=".service.SunshineService"/> |
FetchWeatherTask.doInBackground() 내용을 SunshineService.onHandleIntent()로 이동합니다
나머지 부속 함수들도 이동합니다
마무리로 FetchWeatherTask class를 삭제합니다
ForecastFragment.java
1 | private void updateWeather() { |
이제 우리 Application에는 간단한 Service가 포함되어있습니다
하지만 이것은 스스로 작동하지는 않습니다
Alarm Manager를 통해서 작업을 진행해 봅시다
Alarm Manager는 일정 시간이 지난뒤 시작하려는 Application 구성 요소 시스템에 말하고 Background에서 몇가지 작업을 처리 할 수 있습니다
우리는 Background에서 어떤 작업을 시키면 될까요? 이것을 하기위해서 어떤것이 필요할까요?
Broadcast Receiver
Broadcast Receiver는 아주 특별한 역할을 합니다
다른 Applications에서 Intent Broadcast를 수신하는데 사용합니다
일반적으로 Broadcast Receiver는 Intent Filter에 등록합니다
또한 Application에서 알람을 listen것도 하나의 방법입니다
우리는 PendingIntent를 사용합니다
PendingIntent는 하나의 Application에서 또 다른 Application으로 전달하는 역할을 수행합니다
기존의 Intent와 다르게 permission, ID를 보내서 지정한 작업을 수행 할 권한을 부여받는 역할을 합니다
Android 내부에 보안 모델에 무단 액세스하지 않고 특정 비동기 방법으로 Application의 CallBack 프로세스에 허용합니다
SunshineService Class 안에 추가 합니다
1 | static public class AlarmReceiver extends BroadcastReceiver { |
AndroidManifest 에도 Receiver를 등록합니다
1 | <receiver |
연습
- AlarmReceiver.onReceive()를 완성하세요
- updateWeather()를 수정하세요
정답
SunshineService.java
1 | static public class AlarmReceiver extends BroadcastReceiver { |
ForecastFragment.java
1 | private void updateWeather() { |
이제 앱을 실행하면 5초후에 화면이 업데이트 되는걸 볼 수 있습니다
우리는 이것을 이용해서 Background에서 Update와 Service를 이용해서 폰의 기능을 더 유용하게 사용할 수 있습니다
우리 Android 휴대폰 내부에는 Cell Radio가 있습니다
고객이나 서비스 사이트의 변화를 무시할 수 없습니다
- 최소량의 상태 전송만 한다
- Prefetch, Batch를 이용한다
- 긴급하지 않은 전송은 대기
- 사용자나 서버가 시작한 긴급전송과 묶어서 전송
이것을 통해서 Cell Radio 변화의 빈도를 낮출 수 있습니다
크기에 상관 없이 데이터를 옮길 때마다 Radio는 30초까지 켜져 있을 수 있다는 사실을 알아야 합니다
어느정도 균형을 맞춰서 작업이 되어야합니다
현재 섹션에서 사용자가 필요료 할만한 모든 데이터를 한 번 연결된 상태에서 한꺼번에 최대한 다운 받아야 합니다
하지만, 사용하지 않을 데이터를 다운 받느라 배터리와 주파수를 낭비하면 안됩니다
Transferring Data Without Draining the Battery
Efficient Data Transfers
SyncAdapter
Background에서 Transaction을 쉽게 만드는 방법은 여러가지가 있습니다
Android는 여러가지 Practice들을 제공하는 SyncManager 프레임워크를 가지고 있습니다
SyncAdapter를 이용해서 그 프레임워크를 사용할 수 있습니다!!!
Application이 구글 Application이 이용하는 효과적은 싱크 기능을 이용할 수 있도록 만들어줍니다
Data Transfer을 한자리에 보관할 수 있는 중심적인 장소입니다
Android에 의해 효율적으로 관리 됩니다
Android SyncManager는 SyncAdapter를 이용해서 Sync 요청을 처리합니다
SyncManager는 이러한 요청을 배열하고 시간을 조정함으로서 Application에서 다른 Application으로 Data가 이동하게 하거나 System이 Radio를 switch 하는 빈도를 줄여 줍니다
메모리가 부족하다면 동시에 Sync 하는 수를 적게 합니다
SyncManager는 Network가 불안정할 때 Data Transfer나 Download 재시작 전에 Network 연결 상태를 점검합니다
Synchronization 프레임워크는 Content Provider와 Two-way Synchronization을 해주고 Android 계정 관리자가 그 계정에 알맞는 Synchronization 서비스를 제공하도록 합니다
우리 앱은 그런 것들을 하는건 아닙니다
이러 기능들의 복잡함을 다루기는 할 것입니다
SyncAdapter를 만드는것은 처음에는 어려울 수도 있습니다
한번 SyncAdapter를 구현해 봅시다
1 | <service |
1 | <provider |
1 | <uses-permission |
1 | <service |
ForecastFragment.java
1 | private void updateWeather() { |
한번 해보기
SyncAdapter를 이용해서 날씨 데이터를 가져오고 DB안에 저장하도록 하세요
ForecastFragment.updateWeather()를 수정해서 SyncAdapter와 동기화를 시작하도록 하세요
SunshineService.onHandleIntent()에 있는 코드를 SyncAdapter로 이동하면 됩니다
정답
스케쥴을 동기화 합시다
FetchWeatherTask, SunshineService class를 삭제하고 AndroidManifest에 등록한 service와 receiver도 삭제합니다
SunshineSyncAdapter에 method를 추가 합니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39/**
* Helper method to schedule the sync adapter periodic execution
*/
public static void configurePeriodicSync(Context context, int syncInterval, int flexTime) {
Account account = getSyncAccount(context);
String authority = context.getString(R.string.content_authority);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// we can enable inexact timers in our periodic sync
SyncRequest request = new SyncRequest.Builder().
syncPeriodic(syncInterval, flexTime).
setSyncAdapter(account, authority).
setExtras(new Bundle()).build();
ContentResolver.requestSync(request);
} else {
ContentResolver.addPeriodicSync(account,
authority, new Bundle(), syncInterval);
}
}
private static void onAccountCreated(Account newAccount, Context context) {
/*
* Since we've created an account
*/
SunshineSyncAdapter.configurePeriodicSync(context, SYNC_INTERVAL, SYNC_FLEXTIME);
/*
* Without calling setSyncAutomatically, our periodic sync will not be enabled.
*/
ContentResolver.setSyncAutomatically(newAccount, context.getString(R.string.content_authority), true);
/*
* Finally, let's do a sync to get things started
*/
syncImmediately(context);
}
public static void initializeSyncAdapter(Context context) {
getSyncAccount(context);
}getSyncAccount()에 코드를 추가합니다
1
2
3
4
5
6
7
8if (null == accountManager.getPassword(newAccount)) {
if (!accountManager.addAccountExplicitly(newAccount, "", null)) {
return null;
}
onAccountCreated(newAccount, context); // 이게 추가된 사항
}
return newAccount;시간에 대한 설정을 추가 합니다
1 | // Interval at which to sync with the weather, in seconds. |
MainActivity 마지막에 코드를 추가합니다
1 | SunshineSyncAdapter.initializeSyncAdapter(this); |
Control Flow
- MainActivity가 생성될때 SyncAdapter를 초기화합니다
- 초기화 중에, getSyncAccount() 가 호출됩니다
- getSyncAccount()는 계정이 없으면 onAccountCreated()를 이용해서 만든다
- onAccountCreated()는 동기화 스케쥴링을 구성하고, 즉시 동기화를 한다
FCM (Firebase Cloud Messaging)
SyncAdapter를 이용해서는 우리는 반복적인 작업에 대해서 구현했습니다
부정확한 반복 알람은 정확한 반복 알람보다는 훨씬 낫겠지만 여전히 완벽과는 거리가 멉니다
반복 알람들의 공통적인 문제는 여전히 업데이트를 확인하기 위해 서버를 폴링한다는 것입니다
자주 폴링하면 할수록 더 최신 데이터를 출력할 수 있지만 배터리 소모량이 많아집니다
배터리를 오래 쓰기 위해서 폴링 주기를 길게 늘리면 갱신되지 않은 데이터가 오랫동안 남아 있습니다
사용자로 하여금 스스로 업데이트 빈도를 결정하게 할 수도 있지만 앱이 다 알아서 해 준다는 느낌이 덜합니다
더 좋은 방법이 없을까요? 그런것이 가능은 할까요?
FCM은 다운로드될 준비가 된 데이터가 생기면 서버에서 앱에 알리도록 합니다
혹은 메세지 playload 안에 데이터를 포함시켜서 보내는 방법도 있습니다
FCM은 Firebase를 통해 서버에서 앱의 어느 인스턴스에라도 메세지를 보낼 수 있습니다
이런구조를 사용하면 폴링을 사용 안해도 됩니다
배터리 생명도 개선이 되고 앱의 최신성도 향상시켜 줍니다
그리고 동기화할 데이터가 있을 때 클라이언트에 알리는 일을 서버에게 맡길 수 있습니다
이러한 알림은 새로운 데이터가 있거나 다운로드가 필요하다고 앱에 알려주고 SyncAdapter를 발동시키는 단순한 메세지일 수도 있고 메세지 안에 새로운 데이터를 포함시켜 보내는 방법도 있습니다
우리 Application에서는 외부 서버를 이용하게 됩니다
그렇다고 해도 변경점을 발견하게 되면 소스를 끌어오고 설치된 앱 인스턴스에 알리는 중간 단계를 만드는 것이 좋습니다
Notification
Notification을 만들어 봅시다
string.xml
1 | <!-- Notification Format --> |
SunshineSyncAdapter에 Projection을 추가합니다
1 | private static final String[] NOTIFY_WEATHER_PROJECTION = new String[] { |
Constants 추가
1 | private static final long DAY_IN_MILLIS = 1000 * 60 * 60 * 24; |
Utility.formatTemperature() 수정
1 | public static String formatTemperature(Context context, double temperature) { |
**SunshineSyncAdapter.getWeatherDataFromJson()**에서 bulkInsert 진행 후에
1 | notifyWeather(); |
SunshineSyncAdapter에 메서드 추가
1 | private void notifyWeather() { |
알림
NotificationCompat.Builder
NotificationManager
PendingIntent
TaskStackBuilder
연습
- NotificationCompat.Builder를 이용해서 Notification을 만듭니다
- Notification이 선택되면 화면이 열리도록 explicit intent를 만들어 봅시다
- TaskStackBuilder를 이용해서 backstack을 만듭시다
- NotificationManager를 이용해서 Notification을 보여줍니다
정답
1 | NotificationCompat.Builder mBuilder = |
Background에서 App을 업데이트 했지만 Application 실행시키지 않으면 변화된 내용을 알 수 있는가?
알림은 Background 업데이트를 사용자에게 알려 주는 편리한 방식으로 시작했지만 가벼운 정도의 방식으로 App과 직접 상호작용하는 강력한 표준 지름길이 되었습니다
단순 한줄 알림에서 많은 정보를 포함할수 있도록 바뀌었고, 알림 내에 잇는 데이터에서 수행되는 작동을 포함 가능하게 합니다
잘 이용한다면 Android Wear에서 쉽게 Notification을 구성 할 수 있습니다
그러나 알림의 유용성과 스팸은 한끗 차이라는것을 명심하세요
연습
Application을 좀 더 좋게 만들기 위한 작업입니다
preference를 이용해서 Notification을 On / Off 하도록 만들어 보세요!
정답
사실 알고계신분도 있을 수 있지만 우리 Application의 Database가 무한정 증가 하고 있습니다
이것은 언젠가 용량부족을 겪게 됩니다
그러면 사용자들은 우리 Application을 당연히 삭제합니다… 이렇게 하면 안됩니다
이 문제를 해결해 봅시다
단순하게 생각하면 오늘 날짜 이전 데이터는 삭제를 하면 됩니다
1 | getContext().getContentResolver().delete(WeatherEntry.CONTENT_URI, |
이런식으로 코드를 추가하면 불필요 데이터는 삭제가 되기 때문에 문제가 해결이 됩니다
우리 Application에 아직도 불편한 부분이 있습니다
지도를 보여줄때 GPS좌표값이 아니라 단순 위치 쿼리를 이용한다는 점입니다
이것을 수정하여서 GPS 좌표값으로 넘겨주도록 합시다
또한, 더이상 필요없는 Refresh 버튼도 제거합시다!
일단 MainActivity에 있는 **openPreferredLocationInMap()**을 ForecastFragment로 이동합니다
**onOptionsItemSelected()**에서 action_map을 삭제합니다
menu_main.xml에서 action_map 부분을 삭제합니다
forecast_fragment_menu.xml 을 수정합니다
1 |
|
ForecastFragment.java
1 |
|
복습은 필수
- Service
- BroadcastReceiver
- SyncAdapter
- Notification
- AndroidStudio