레이블이 누가인 게시물을 표시합니다. 모든 게시물 표시
레이블이 누가인 게시물을 표시합니다. 모든 게시물 표시

2016년 10월 12일 수요일

[안드로이드] Doze 모드

Doze 모드 최적화

안드로이드 6.0(API 23)부터 배터리를 절약하기 위해 Doze 모드가 추가되었다. 사용자가 휴대폰을 사용하고 있지 않다고 판단되면, 시스템은 모든 앱의 배터리를 많이 잡아먹는 백그라운드 작업과 네트워킹을 일시 중단시킨다. Doze 모드는 타겟 API가 23 이하의 앱이더라도 동일하게 적용되므로, Doze 모드에서의 앱 동작을 확인해봐야 한다.

Doze 모드 이해하기

디바이스에 전원공급이 없고, 스크린이 꺼진 채로 아무 움직임 없이 일정 시간이 지나면, 디바이스는 Doze 모드로 진입하게 된다. Doze 모드에 진입하게 되면, 시스템은 배터리를 절약하기 위해 모든 앱의 네트워크 사용과 CPU를 과도하게 이용하는 백그라운드 서비스, Sync, 기본 알람까지 제한한다.
주기적으로, 시스템은 Doze모드에서 깨어나, 모든 앱이 미뤄뒀던 작업을 완료할 수 있는 Maintenance Window라 불리는 짧은 시간을 가진다. 이 짧은 시간동안, 시스템은 Doze 모드에 진입하면서 멈췄던 작업들을 다시 시작하도록 수행한다.
디바이스의 화면이 켜지거나, 전원공급이 시작되면 앱은 Doze 모드에서 벗어나 원래대로 모든 작업을 수행하게 된다.
6.0 Doze
최근, 7.0(API 24)에서는 디바이스에 전원공급이 없고 화면만 꺼져있으면, 디바이스가 움직이더라도 Doze 모드로 진입한다. 그리고 Doze 모드가 2단계로 세분화되었다.
1단계에서는 네트워크 엑세스, Sync, Job 스케쥴링 정지, 2단계에서는 GPS, Wifi 스캐닝, wake lock 등이 제한된다.
7.0 Doze

Doze 모드에서 제한되는 동작

  • 네트워크 엑세스
  • Wake Lock
  • AlarmManager의 기본 알람
    • setAndAllowWhileIdle() / setExactAndAllowWhileIdle() 메서드를 이용한 알람은 Doze모드에서도 받을 수 있다.
    • setAlarmClock() 메서드를 이용한 알람은 정상적으로 동작한다. 이 알람이 울리기 전에, 시스템은 Doze 모드를 빨리 빠져나온다.
  • Wifi 스캐닝
  • Sync Adapter 동작
  • Job Scheduler 동작

Doze 모드 적용

Doze 모드에 대한 영향은 앱마다 다 다를것이다. 대부분의 앱들은 아무런 변경 없이도 Doze 모드 주기안에 잘 동작할 것이다. 하지만, 네트워크, 알람, Job, Sync 를 이용하고 있다면 Doze 모드를 확인해 볼 필요가 있다. 이 앱들은 각maintenence window 동안에 효율적으로 동작하도록 만들어야 한다.
AlarmManager의 경우, 기존에 있던 API 메서드를 이용하면, Doze 모드에서는 알람이 정확하게 울리지 않는다. 그래서 Doze 모드에서도 알람이 제대로 동작하도록 하려면 6.0(API 23)에서 추가된setAndAllowWhileIdle()setExactAndAllowWhileIdle() 메서드를 이용해야 한다.
앱이 실시간으로 연결을 유지하며 메시지를 받을 필요가 있다면, Google Cloud Messageing 을 고려해본다.

Doze 모드 감지
브로드캐스트 리시버를 등록하고, PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED 액션을 수신하도록 등록하면, Doze 모드일 때, 디바이스가 대기 상태로 변경되며, 해당 액션이 발생하게 된다.
if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
    registerReceiver(mDeviceIdleModeReceiver, new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
}

App Standby 이해하기

App Standby는 사용자가 앱을 이용하지 않을 때, 앱이 대기상태인지를 결정한다. 시스템은 일정시간 동안 앱에 아무런 동작이 없고, 다음의 상황이 아닐 때, 앱을 대기상태로 인지한다.
  • 사용자가 명시적으로 앱을 실행
  • 앱이 현재 포그라운드 프로세스를 가지고 있음
  • 사용자가 Notification 영역을 보고있고, 그 영역에 Notification을 출력해야 할 때
디바이스가 전원공급이 시작되면, 시스템은 이 앱들을 standby 상태로부터 해제하고, 앱의 네트워크 액세스, 스케쥴링 등 제한했던 동작을 수행하도록 한다.

디바이스가 대기 상태일 때, GCM을 이용해 인터렉션하기

GCM은 클라우드와 하나의 영구적인 연결을 유지하여, 실시간 메시징이 필요한 모든 앱들이 이 연결을 공유할 수 있도록 한다. 연결을 공유하는 이 기법은 앱마다 자신만의 연결을 유지하는 것보다 배터리를 훨씬 절약할 수 있도록 한다. 이러한 이유때문에, 구글은 실시간 메시징이 필요하다면, GCM을 이용하는걸 추천한다.
GCM 메시지의 우선순위를 높여주게 되면, 디바이스가 Doze 모드에 있거나, 앱이 Standby 상태 이더라도 네트워크에 연결할 수 있도록 수행한다. 동작 수행 후, 다시 대기 상태로 돌아간다. 즉, GCM 메시지는 Doze 모드나 App의 상태에 영향을 받지 않고, 효율적으로 상호작용 할 수 있다.

Doze & App Standby 모드 테스트하기

Doze 모드 테스트

1.앱을 실행하고, 아무 동작도 하지 않는다.
2.디바이스 스크린을 끈다.(앱은 그대로 Active 상태로 둔다.)
3.다음 명령어를 command에 입력하여 adb를 이용해 강제로 Doze모드 발생 상태를 만든다. 단, 디바이스의 상태가 변할 때까지 두번째 문장은 계속 실행한다.
$ adb shell dumpsys battery unplug
$ adb shell dumpsys deviceidle step
4.디바이스를 다시 활성화시킨 후에 앱이 정상동작 하는지 확인한다.

App Standby 모드 테스트

1.앱을 실행하고, 아무 동작도 하지 않는다.
2.command에 다음 명령어를 실행하여 강제로 앱을 Standby 상태로 만든다.
$ adb shell dumpsys battery unplug
$ adb shell am set-inactive <packageName> true
3.command에 다음의 명령어를 실행하여 앱을 깨운다.
$ adb shell ab set-inactive <packageName> false
$ adb shell ab get-inactive <packageName>
4.앱이 원상태로 돌아온 후에 정상동작 하는지 확인한다. 특히, Notification이나 Job 등이 생각한대로 동작하는지 확인한다.

2016년 9월 28일 수요일

[안드로이드] 7.0(누가) 백그라운드 최적화

안드로이드에서 7.0버전을 출시하면서 백그라운드를 최적화하기 위해 다음의 2가지 사항을 변경했다.
  • 매니페스트 파일에 등록된 CONNECTIVITY_ACTION(android.net.conn.CONNECTIVITY_CHANGE) 브로드캐스트 수신을 무시한다.(앱이 백그라운드에서 이 브로드캐스트를 더이상 받지 못한다.) 단, Context.registerReceiver()메서드를 이용해 메인스레드에서 등록된 리시버는 앱이 실행중일 때, 똑같이 CONNECTIVITY_ACTION 브로드캐스트를 받을 수 있다.
  • 앱은 더이상 ACTION_NEW_PICTUREACTION_NEW_VIDEO 브로드캐스트를 보내거나 받을 수 없다. 이는 targetSdkVersion에 상관없이 모든 앱에 영향을 끼친다.
안드로이드 7.0을 대응해야 한다면, 반드시 위의 두가지 사항을 확인해서 대응해 주어야 한다. 애응하지 않으면 어떤 부작용이 있을지 알 수 없다.

CONNECTIVITY_ACTION 제한

targetSdkVersion을 24로 설정한 앱의 경우, 매니페스트에 설정한 CONNECTIVITY_ACTION 브로드캐스트를 더 이상 받지 못하게 된다. 이 상황은 백그라운드에서 CONNECTIVITY_ACTION 처리를 하고있는 앱이라면, 반드시 확인해봐야 한다.
Note : 위에서도 언급했지만, 앱이 실행중일 때에는 Context의 registerReceiver() 메서드를 이용해 CONNECTIVITY_ACTION 수신 리시버를 등록하고, 계속 브로드캐스트 받을 수 있다.

네트워크 Job 스케쥴링

안드로이드 5.1 이상

안드로이드 5.1(API 21) 버전에서 새로 추가된 JobScheduler 관련 API를 이용해 처리할 수 있다. JobInfo.builder 로 jobInfo인스턴스를 생성할 때, setRequiredNetworkType() 메서드를 이용해 네트워크 타입을 지정하고 생성한 후에 그 인스턴스를 스케쥴에 등록한다. 그러면, 스케쥴러가 해당 네트워크 타입이 되었을 때, Job을 수행한다. 다음 예제코드는 네트워크 타입이 WIFI 일때의 JobInfo를 등록하는 내용이다.
public static final int MY_BACKGROUND_JOB = 0;
...
public static void scheduleJob(Context context) {
JobScheduler js =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo job = new JobInfo.Builder(
MY_BACKGROUND_JOB,
new ComponentName(context, MyJobService.class))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
.build();
js.schedule(job);
}
위의 예제에서 네트워크 타입이 UNMETERED(무과금 -> WIFI) 가 되면, JobInfo를 만들때 등록했던 JobService 클래스의 onStartJob()메서드로 콜백된다. (JobService 클래스 참고)

안드로이드 5.1 미만

안드로이드 5.1(API 21)버전 미만에서는 JobScheduler 클래스를 사용할 수 없으므로, 구글플레이 서비스를 이용해 구현된 GcmNetworkManager 클래스를 이용해야 한다. GcmNetworkManager는 Task 추상클래스의 하위 클래스 객체를 스케쥴링하게된다. Task 클래스 하위 인스턴스는 다음과 같이 스케쥴링한다.
     OneoffTask myTask = new OneoffTask.Builder()
.setService(MyGcmTaskService.class)
.setRequiredNetwork(NETWORK_STATE_UNMETERED)
.setTag("test-upload")
.build();
GcmNetworkManager.getInstance(this).schedule(myTask);
해당 태스크의 순서가 되면, GcmTaskService 클래스의 onRunTask() 메서드로 콜백된다. (GcmTaskService 클래스 참고) 단, GcmTaskService 클래스가 콜백을 받으려면 다음과 같이 매니페스트에 등록되어야 한다.
     <service android:name=".MyUploadService"
android:exported="true"
<!-- 다른 코드로부터 이 서비스가 실행되지 않도록 하기 위해서 추가 -->
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
<!-- 이 서비스가 콜백을 받기 위해 추가 -->
<intent-filter>
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
</intent-filter>
</service>

앱이 실행중일 때, Network Connectivity 모니터링하기

앱 실행중에 브로드캐스트 리시버로 CONNECTIVITY_ACTION을 수신하는 방법 외에도, ConnectivityManager 클래스의 registerNetworkCallback()메서드를 이용해, 해당 상태에서의 콜백을 받을 수 있다. (이 메서드는 API 21에서 추가되었다.)
NetworkRequest.Builder 를 이용하여 NetworkRequest 인스턴스를 생성한 후에, 그 인스턴스를 registerNetworkCallback()메서드의 첫번째 인자로 넘기고, 두번째 인자로는 첫번째 인자에 명시된 상태로 네트워크가 변할 때, 실행될 콜백 메서드를 넘긴다. 앱이 종료될 때, unregisterNetworkCallback()메서드를 호출해 주어야 한다.

NEW_PICTURE / NEW_VIDEO 제한

안드로이드 7.0에서는 ACTION_NEW_PICTUREACTION_NEW_VIDEO 브로드캐스트를 주고받을 수 없도록 변경되었다. 이러한 현상은, 어떤 애플리케이션이 새로운 이미지나 비디오를 프로세싱할 때, 이 액션을 등록한 모든 리시버에게 브로드캐스트 되어 많은 앱들이 깨어나야만 했다.

새로운 JobInfo 메서드

안드로이드 7.0에서는 Content URI의 변화를 연결하기 위해 JobInfo API를 확장하여 다음의 메서드들을 추가하였다.
  • JobInfo.TriggerContentUri() : content URI의 변화를 감지하기 위해 요구되는 파라미터들을 캡슐화한다.
  • JobInfo.Builder.addTriggerContentUri() : TriggerContentUri 객체를 JobInfo로 전달한다. ContentObserver는 캡슐화된 content URI를 모니터링한다. 만약 Job과 관련된 TriggerContentUri가 여러개 있으면, 시스템은 한개의 URI가 변하더라도 콜백을 전달한다. 어떤 URI의 하위 컨텐트의 변화를 감지하고 싶으면, TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS 플래그를 추가하면 된다. 이 플래그는, ContentResolver.registerContentObserver()메서드의 매개변수인 notifyForDescendants와 대응하는 역할을 한다.
Note : TriggerContentUri() 메서드는 setPeriodic()setPersisted() 메서드와 같이 사용할 수 없다. 컨텐트의 변경을 계속 감지하려면, JobService가 콜백을 끝내기 전에 새로운 JobInfo 인스턴스를 만들어 스케쥴링한다.
다음 예제는 MEDIA_URI가 변경될떄를 감지하기 위해 스케쥴링하는 코드이다.
public static final int MY_BACKGROUND_JOB = 0;
...
public static void scheduleJob(Context context) {
JobScheduler js =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder builder = new JobInfo.Builder(
MY_BACKGROUND_JOB,
new ComponentName(context, MediaContentJob.class));
builder.addTriggerContentUri(
new JobInfo.TriggerContentUri(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
js.schedule(builder.build());
}

새로운 JobParameter 메서드

안드로이드 7.0에서는 JobParameter를 확장하여, Job 콜백이 왔을 때 해당 컨텐트의 권한이나 URI같은 유용한 정보를 볼 수 있도록 확장되었다.
  • Uri[] getTriggeredContentUris() : 이 Job에서 연결된 URI를 배열로 반환한다. 만약 연결된 URI가 없다면 null을 리턴한다.
  • String[] getTriggeredContentAuthorities() : 이 Job에서 연결된 URI들의 권한ㅇ르 배열로 반환한다. 반환된 결과가 null이 아니라면, getTriggeredContentUris() 메서드를 사용하면 된다.
다음 예제는 변경된 URI의 권한과 URI를 JobService.onStartJob() 메서드에서 받아서 기록하는 코드이다.
@Override
public boolean onStartJob(JobParameters params) {
StringBuilder sb = new StringBuilder();
sb.append("Media content has changed:\n");
if (params.getTriggeredContentAuthorities() != null) {
sb.append("Authorities: ");
boolean first = true;
for (String auth :
params.getTriggeredContentAuthorities()) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(auth);
}
if (params.getTriggeredContentUris() != null) {
for (Uri uri : params.getTriggeredContentUris()) {
sb.append("\n");
sb.append(uri);
}
}
} else {
sb.append("(No content)");
}
Log.i(TAG, sb.toString());
return true;
}

[안드로이드] 누가(7.0) 멀티윈도우 대응

안드로이드가 7.0으로 업데이트 되면서, 갤럭시 s3 이상 휴대폰에서만 볼수 있었던 멀티윈도우기능을 공식적으로 지원하게 되었다. 이로써, 멀티윈도우 기능은 모든 안드로이드 디바이스에 탑재될 것이고, 사용자의 안드로이드 UX로 자리매김할 것이다.

멀티윈도우?

  • 한 화면에 여러 앱이 실행 가능하도록 하는 기능
  • 동시에 실행되는 앱들은 독립적으로 실행되며, 드래그&드롭으로 데이터를 주고 받을 수 있다.
  • 화면 크기가 큰 기기는 제조사가 자유형식 모드를 이용하도록 할 수 있다.
    • 자유형식 모드 : 사용자가 엑티비티의 크기를 자유롭게 조정 가능
multiwindow

멀티윈도우 구현

사전 준비

  • 모듈 하위의 build.gradle을 열고, compileSdkVersiontargetSdkVersion을 24 로 변경한다.
    • targetSdkVersion가 24보다 낮으면, compileSdkVersion이 24로 설정되어 있더라도, 시스템은 이 앱을 멀티윈도우 지원불가로 인식하고, 무조건 전체화면으로만 앱을 실행하게 된다.
    • targetSdkVersion을 24로 설정하면, compileSdkVersion에 상관없이, 시스템은 이 앱을 멀티윈도우 지원가능으로 인식한다. 단, 기본 크기로 고정되어 있어서 크기변경이 불가능하고, 어떤 부작용이 나타날지 알 수 없다.
android {
    compileSdkVersion 24
    
    defaultConfig {
        targetSdkVersion 24
        ...
    }
    ...
}

멀티윈도우 사용

  • compileSdkVersion이 24로 설정되어 있어야 한다.
  • 매니페스트 파일에 <activity> / <application> 태그에서 resizeableActivity 속성을 이용하여 멀티윈도우 기능을 활성/비활성시킬 수 있다.
android:resizeableActivity=["true" | "false"]
  • targetSdkVersion이 24이면, 이 속성을 지정하지 않았을 경우 기본값은 true이다.

Picture In Picture(PIP) 사용

  • PIP : 동영상처럼 사용자의 상호작용이 없더라도, 앱이 자동으로 계속 실행되도록 해주는 기능
  • 매니페스트 파일에 <activity> 태그에서 supportsPictureInPicture 속성을 이용하여 해당 엑티비티가 이 모드를 지원하는지 여부를 지정한다.
android:supportsPictureInPicture=["true" | "false"]
  • resizeableActivity 속성이 false인 경우, 이 속성은 무시된다.

멀티윈도우 크기 설정

  • 멀티윈도우 모드에서 앱의 크기를 변경할 수 있도록 설정하려면, / 태그 하위에 <layout> 태그를 추가하고, 다음과 같은 속성을 통해 크기를 지정해준다.
    • defaultWidth : 자유형식 모드에서 엑티비티가 시작될 때의 기본 너비
    • defaultHeight : 자유형식 모드에서 엑티비티가 시작될 때의 기본 높이
    • gravity : 자유형식 모드에서 엑티비티의 초기 배치
    • minWidth : 자유형식 / 화면분할 모드에서 엑티비티의 최소 너비
    • minHeight : 자유형식 / 화면분할 모드에서 엑티비티의 최소 높이

멀티윈도우에서의 life-cycle

  • 멀티윈도우 모드에서 Activity, Fragment의 생명주기는 변하지 않고 그대로 동작한다.
  • 현재 사용자와 상호작용중인 엑티비티가 최상단 엑티비티가 되어 활성화되고, 다른 화면의 엑티비티는 화면은 표시되지만 Pause 상태로 있다.
  • 멀티윈도우 상태에서는 Pause 상태이지만, 화면에 계속 보이는 상태이므로, 동영상 플레이어 등은 pause에서 동영상을 중지하면 안된다.

멀티윈도우 모드에서 비활성화되는 기능

  • 시스템 UI 사용자지정 옵션 비활성화
  • screenOrientation 속성 변경 무시

멀티윈도우 변경 콜백

  • 멀티윈도우 상태 변경에 따른 콜백메서드 및 상태확인 메서드가 존재한다.
  • 다음 메서드는 엑티비티, 프레그먼트 모두 추가되었다.
    • onMultiWindowModeChanged() : 현재 화면이 멀티윈도우 모드로 들어가거나, 나올때 호출되는 메서드
    • onPictureInPictureModeChanged() : 현재 화면이 PIP 모드로 들어가거나, 나올때 호출되는 메서드
    • isInMultiWindowMode() : 현재 화면이 멀티윈도우 모드인지 확인
    • isInPictureInPictureMode() : 현재 화면이 PIP 모드인지 확인 - 이 때, isInMultiWindowMode()는 무조건 true