안드로이드 PowerManagerService 전원 관리 원리
요약
본문은 안드로이드 프레임워크 소스 코드 관점에서 PowerManagerService(PMS)의 전원 관리 메커니즘을 전반적으로 분석합니다. PMS는 기기의 전원 상태, 화면 밝기, 잠금 해제, 절전 전략 등 핵심 기능을 관리하며 안드로이드 시스템 배터리 관리의 핵심 서비스입니다. 본문에서는 PowerManagerService의 시작 절차, WakeLock 메커니즘 구현, 화면 상태 관리, Doze 절전 모드, SuspendBlocker 원리, 그리고 다른 서비스와의 협력 관계를 상세히 분석합니다. 소스 코드 수준 분석과 실제 사례를 통해 개발자들이 안드로이드 전원 관리의 저수준 논리를 깊이 이해하고 절전 최적화의 모범 사례를 익힐 수 있도록 돕습니다.
키워드: PowerManagerService, WakeLock, Doze 모드, 화면 관리, SuspendBlocker
1. PowerManagerService 아키텍처 개요
1.1 PMS 시스템 내 위치
안드로이드 시스템에서 PowerManagerService는 SystemServer 프로세스 내에서 실행되는 핵심 서비스 중 하나입니다. 시스템 부팅 시 SystemServer에 의해 초기화되며, 다른 시스템 서비스와 협력하여 전원 관리를 수행합니다. 주요 서비스와의 관계는 다음과 같습니다:
- ActivityManagerService: 애플리케이션 생명주기 관리와 연계하여 전원 상태 변경 시 알림
- Window Manager: 화면 상태 변화와 UI 렌더링과의 연동
- BatteryStatsService: 배터리 사용량 통계 수집 및 분석
- DisplayManager: 화면 전원 상태와 실제 디스플레이 제어
1.2 PMS 핵심 책임
PowerManagerService는 다음과 같은 주요 기능을 담당합니다:
- 전원 상태 관리: 기기가 활성 상태(Awake), 잠자는 상태(Asleep), Dozing 상태 등으로 전환
- WakeLock 제어: 애플리케이션의 전원 상태 요청 처리 및 관리
- 화면 밝기 제어: 자동 밝기 조절 및 수동 밝기 설정 관리
- 절전 전략 구현: Doze 모드, App Standby 등 절전 기능 실행
- 절차적 잠금 해제(SuspendBlocker): 시스템 절대 모드 방지 메커니즘 제어
- 전원 이벤트 분산: 다른 시스템 구성 요소에 전원 관련 이벤트 전달
2. PowerManagerService 시작 절차
2.1 시작 시퀀스 다이어그램
PowerManagerService의 시작은 시스템 부팅 과정에서 발생하며, 다음과 같은 순서로 진행됩니다:
- SystemServer 초기화
- PowerManagerService 인스턴스 생성
- 네이티브 레이어 초기화
- 시스템 준비 완료(systemReady) 호출
- 설정 관찰자 등록
- 전원 상태 초기화
2.2 PMS 생성자
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
public final class PowerManagerService extends SystemService implements Watchdog.Monitor {
public PowerManagerService(Context context) {
super(context);
// 1. 컨텍스트 초기화
mContext = context;
mHandlerThread = new ServiceThread("PowerManagerHandler", Process.THREAD_PRIORITY_DISPLAY, false);
mHandlerThread.start();
mHandler = new PowerManagerHandler(mHandlerThread.getLooper());
// 2. 시스템 절대 모드 차단기 생성
mSuspendBlocker = createSuspendBlockerLocked("PowerManagerService.WakeLocks");
mDisplaySuspendBlocker = createSuspendBlockerLocked("PowerManagerService.Display");
// 3. 각성 상태 초기화
mWakefulness = WAKEFULNESS_AWAKE;
mWakefulnessChanging = false;
// 4. 네이티브 레이어 초기화
nativeInit();
nativeSetPowerBoost(Boost.INTERACTION, 0);
nativeSetPowerMode(Mode.INTERACTIVE, true);
// 5. 설정 관찰자 초기화
mSettingsObserver = new SettingsObserver(mHandler);
mAttentionDetector = new AttentionDetector(mContext, this, mLock);
// 6. 배터리 통계
mBatteryStats = BatteryStatsService.getService();
// 7. 알림기 (브로드캐스트 전송)
mNotifier = new Notifier(Looper.getMainLooper(), mContext, mBatteryStats,
mAppOps, createSuspendBlockerLocked("PowerManagerService.Broadcasts"),
mPolicy);
// 8. 무선 충전 감지기
mWirelessChargerDetector = new WirelessChargerDetector(mSensorManager,
createSuspendBlockerLocked("PowerManagerService.WirelessChargerDetector"),
mHandler);
// 9. 디스플레이 관리 초기화
mDisplayPowerCallbacks = new DisplayPowerCallbacks();
// 10. 와치독 모니터링
Watchdog.getInstance().addMonitor(this);
Watchdog.getInstance().addThread(mHandler);
}
}
2.3 systemReady 절차
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
public void systemReady(IAppOpsService appOpsService) {
synchronized (mLock) {
mSystemReady = true;
mAppOps = appOpsService;
mDreamManager = getLocalService(DreamManagerInternal.class);
mDisplayManagerInternal = getLocalService(DisplayManagerInternal.class);
mPolicy = getLocalService(WindowManagerPolicy.class);
mBatteryManagerInternal = getLocalService(BatteryManagerInternal.class);
// 설정 읽기
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mScreenBrightnessSettingMinimum = pm.getMinimumScreenBrightnessSetting();
mScreenBrightnessSettingMaximum = pm.getMaximumScreenBrightnessSetting();
mScreenBrightnessSettingDefault = pm.getDefaultScreenBrightnessSetting();
// 사용자 설정 읽기
final ContentResolver resolver = mContext.getContentResolver();
mStayOnWhilePluggedInSetting = Settings.Global.getInt(resolver,
Settings.Global.STAY_ON_WHILE_PLUGGED_IN, BatteryManager.BATTERY_PLUGGED_AC);
mScreenOffTimeoutSetting = Settings.System.getInt(resolver,
Settings.System.SCREEN_OFF_TIMEOUT, DEFAULT_SCREEN_OFF_TIMEOUT);
// 관찰자 등록
resolver.registerContentObserver(Settings.Global.getUriFor(
Settings.Global.STAY_ON_WHILE_PLUGGED_IN),
false, mSettingsObserver, UserHandle.USER_ALL);
// DisplayPowerController 생성
final int displayId = mDisplayManagerInternal.getDisplayIds()[0];
mDisplayPowerControllers.put(displayId, new DisplayPowerController(
mContext, mDisplayPowerCallbacks, mHandler,
mSensorManager, mDisplayManagerInternal.getDisplayBlCompany()));
// 전원 상태 업데이트
updatePowerStateLocked();
}
// ActivityManagerService에 준비 완료 알림
mBatteryManagerInternal.getPlugType();
}
3. WakeLock 메커니즘 상세 분석
3.1 WakeLock 유형
// frameworks/base/core/java/android/os/PowerManager.java
public final class PowerManager {
// CPU 유지 실행 (화면과 키보드는 꺼질 수 있음)
public static final int PARTIAL_WAKE_LOCK = 0x00000001;
// 화면 유지 밝기 (사용되지 않음)
@Deprecated
public static final int SCREEN_DIM_WAKE_LOCK = 0x00000006;
// 화면 최대 밝기 유지 (사용되지 않음)
@Deprecated
public static final int SCREEN_BRIGHT_WAKE_LOCK = 0x0000000a;
// 화면 최대 밝기와 키보드 유지 (사용되지 않음)
@Deprecated
public static final int FULL_WAKE_LOCK = 0x0000001a;
// 근접 센서 WakeLock (통화 시 자동 화면 꺼짐)
public static final int PROXIMITY_SCREEN_OFF_WAKE_LOCK = 0x00000020;
// Doze 모드 WakeLock
public static final int DOZE_WAKE_LOCK = 0x00000040;
// 화면 켜짐 유지 (권장 사용 방식)
public static final int SCREEN_ON_WAKE_LOCK = 0x00000080;
}
3.2 WakeLock 신청 절차
애플리케이션에서 WakeLock을 신청하는 과정은 다음과 같습니다:
- PowerManagerService에 IPC 호출을 통해 WakeLock 요청
- PMS 내부에서 WakeLock 객체 생성 및 등록
- 적절한 SuspendBlocker 획득
- 전원 상태 업데이트
- 필요한 경우 다른 서비스에 알림 전달
3.3 WakeLock 소스 코드 구현
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private void acquireWakeLockInternal(IBinder token, int flags, String tag, String packageName,
WorkSource workSource, String historyTag, int uid, int pid) {
synchronized (mLock) {
// 기존 WakeLock 검색 또는 새로 생성
int index = findWakeLockIndexLocked(token);
WakeLock wakeLock;
boolean notifyAcquire;
if (index < 0) {
wakeLock = new WakeLock(token, flags, tag, packageName, workSource, historyTag, uid, pid);
try {
token.linkToDeath(wakeLock, 0);
} catch (RemoteException ex) {
throw new IllegalArgumentException("Wake lock is already dead.");
}
mWakeLocks.add(wakeLock);
notifyAcquire = true;
} else {
wakeLock = mWakeLocks.get(index);
if (!wakeLock.hasSameProperties(flags, tag, workSource, uid, pid)) {
notifyWakeLockChangingLocked(wakeLock, flags, tag, packageName,
uid, pid, workSource, historyTag);
}
wakeLock.updateProperties(flags, tag, packageName, workSource, historyTag, uid, pid);
notifyAcquire = false;
}
// WakeLock 적용
applyWakeLockFlagsOnAcquireLocked(wakeLock, uid);
mDirty |= DIRTY_WAKE_LOCKS;
updatePowerStateLocked();
// 배터리 통계에 알림
if (notifyAcquire) {
notifyWakeLockAcquiredLocked(wakeLock);
}
}
}
private final class WakeLock implements IBinder.DeathRecipient {
public final IBinder mToken;
public int mFlags;
public String mTag;
public final String mPackageName;
public WorkSource mWorkSource;
public String mHistoryTag;
public final int mOwnerUid;
public final int mOwnerPid;
public long mAcquireTime;
public boolean mNotifiedAcquired;
public boolean mDisabled;
public WakeLock(IBinder token, int flags, String tag, String packageName,
WorkSource workSource, String historyTag, int ownerUid, int ownerPid) {
mToken = token;
mFlags = flags;
mTag = tag;
mPackageName = packageName;
mWorkSource = copyWorkSource(workSource);
mHistoryTag = historyTag;
mOwnerUid = ownerUid;
mOwnerPid = ownerPid;
}
@Override
public void binderDied() {
// WakeLock 소유자 프로세스가 종료된 경우 자동 해제
PowerManagerService.this.handleWakeLockDeath(this);
}
}
3.4 SuspendBlocker 원리
SuspendBlocker는 시스템 절대 모드(Suspend)를 방지하는 메커니즘입니다. PowerManagerService는 내부적으로 여러 SuspendBlocker를 사용합니다:
// frameworks/base/services/core/java/com/android/server/power/SuspendBlocker.java
interface SuspendBlocker {
void acquire();
void release();
}
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private SuspendBlocker createSuspendBlockerLocked(String name) {
SuspendBlocker suspendBlocker = new SuspendBlockerImpl(name);
mSuspendBlockers.add(suspendBlocker);
return suspendBlocker;
}
private final class SuspendBlockerImpl implements SuspendBlocker {
private final String mName;
private int mReferenceCount;
public SuspendBlockerImpl(String name) {
mName = name;
}
@Override
public void acquire() {
synchronized (this) {
mReferenceCount += 1;
if (mReferenceCount == 1) {
nativeAcquireSuspendBlocker(mName);
}
}
}
@Override
public void release() {
synchronized (this) {
mReferenceCount -= 1;
if (mReferenceCount == 0) {
nativeReleaseSuspendBlocker(mName);
} else if (mReferenceCount < 0) {
Slog.wtf(TAG, "Suspend blocker \"" + mName
+ "\" was released without being acquired!", new Throwable());
mReferenceCount = 0;
}
}
}
}
네이티브 구현은 커널의 sysfs 인터페이스를 통해 작동합니다:
// frameworks/base/services/core/jni/com_android_server_power_PowerManagerService.cpp
static void nativeAcquireSuspendBlocker(JNIEnv *env, jclass /* clazz */, jstring nameStr) {
const char* name = env->GetStringUTFChars(nameStr, NULL);
// 커널 sysfs 노드에 쓰기
int fd = open("/sys/power/wake_lock", O_WRONLY | O_APPEND);
if (fd >= 0) {
write(fd, name, strlen(name));
close(fd);
}
env->ReleaseStringUTFChars(nameStr, name);
}
static void nativeReleaseSuspendBlocker(JNIEnv *env, jclass /* clazz */, jstring nameStr) {
const char* name = env->GetStringUTFChars(nameStr, NULL);
int fd = open("/sys/power/wake_unlock", O_WRONLY | O_APPEND);
if (fd >= 0) {
write(fd, name, strlen(name));
close(fd);
}
env->ReleaseStringUTFChars(nameStr, name);
}
4. 화면 상태 관리
4.1 전원 상태 머신
PowerManagerService는 다음과 같은 전원 상태 머신을 사용합니다:
- WAKEFULNESS_AWAKE: 완전히 활성화된 상태
- WAKEFULNESS_DREAMING: 스크린 세이버나 꿈(드림) 상태
- WAKEFULNESS_DOZING: Doze 모드로 전환 중인 상태
- WAKEFULNESS_ASLEEP: 절전 모드 (깨어나려면 터치 필요)
상태 전환은 다음과 같은 이벤트에 의해 트리거됩니다:
- 사용자 활동 (터치, 버튼 누름 등)
- WakeLock 해제
- 화면 끝기 타이머 만료
- 시스템 명령에 의한 절전
4.2 updatePowerStateLocked 핵심 로직
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private void updatePowerStateLocked() {
if (!mSystemReady || mDirty == 0) {
return;
}
// 단계 0: 초기화
final long now = SystemClock.uptimeMillis();
int dirtyPhase2 = 0;
// 단계 1: WakeLock 업데이트
if ((mDirty & DIRTY_WAKE_LOCKS) != 0) {
updateWakeLockSummaryLocked(dirtyPhase2);
}
// 단계 2: 사용자 활동 업데이트
if ((mDirty & (DIRTY_WAKE_LOCKS | DIRTY_USER_ACTIVITY | DIRTY_WAKEFULNESS
| DIRTY_ATTENTIVE | DIRTY_BOOT_COMPLETED)) != 0) {
updateUserActivitySummaryLocked(now, dirtyPhase2);
}
// 단계 3: 디스플레이 전원 상태 업데이트
if ((mDirty & (DIRTY_WAKE_LOCKS | DIRTY_USER_ACTIVITY | DIRTY_WAKEFULNESS
| DIRTY_ACTUAL_DISPLAY_POWER_STATE_UPDATED | DIRTY_BOOT_COMPLETED
| DIRTY_SETTINGS | DIRTY_SCREEN_BRIGHTNESS_BOOST | DIRTY_VR_MODE_CHANGED
| DIRTY_QUIESCENT)) != 0) {
updateDisplayPowerStateLocked(dirtyPhase2);
}
// 단계 4: 드림(스크린 세이버) 업데이트
if ((mDirty & (DIRTY_WAKEFULNESS)) != 0) {
updateDreamLocked(dirtyPhase2, false);
}
// 단계 5: 상태 변경 완료
finishWakefulnessChangeIfNeededLocked();
// 단계 6: SuspendBlocker 업데이트
if ((mDirty & (DIRTY_WAKE_LOCKS | DIRTY_SUSPEND_BLOCKER)) != 0) {
updateSuspendBlockerLocked();
}
// 완료
mDirty = dirtyPhase2;
}
4.3 goToSleep 절차
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private void goToSleepInternal(long eventTime, int reason, int flags, int uid) {
synchronized (mLock) {
if (goToSleepNoUpdateLocked(eventTime, reason, flags, uid)) {
updatePowerStateLocked();
}
}
}
private boolean goToSleepNoUpdateLocked(long eventTime, int reason, int flags, int uid) {
if (eventTime > mLastWakeTime && mWakefulness != WAKEFULNESS_ASLEEP) {
// 이유 기록
mLastSleepTime = eventTime;
mLastSleepReason = reason;
mSandmanSummoned = true;
// DOZING 또는 ASLEEP 상태로 전환
setWakefulnessLocked(WAKEFULNESS_DOZING, reason);
// 브로드캐스트 전송
mNotifier.onGoToSleepStarted(reason);
// 다른 서비스에 알림
mPolicy.startedGoingToSleep(reason);
return true;
}
return false;
}
5. Doze 절전 모드
5.1 Doze 모드 상태 머신
Doze 모드는 다음과 같은 상태 머신을 따릅니다:
- 활성 상태(ACTIVE): 일반적인 작동 상태
- 비활성 상태(INACTIVE): 사용자 활동이 없는 상태
- 대기 중(IDLE_PENDING): Doze 모드로 진입 예정
- 감지 중(SENSING): 사용자 활동 감지
- 위치 확인 중(LOCATING): GPS 사용 등 위치 확인
- 유휴 상태(IDLE): Doze 모드 활성화
- 유지 관리(IDLE_MAINTENANCE): 제한된 작업 수행
5.2 Doze 모드 구현
// frameworks/base/services/core/java/com/android/server/DeviceIdleController.java
public class DeviceIdleController extends SystemService {
// Doze 상태 상수
private static final int STATE_ACTIVE = 0;
private static final int STATE_INACTIVE = 1;
private static final int STATE_IDLE_PENDING = 2;
private static final int STATE_SENSING = 3;
private static final int STATE_LOCATING = 4;
private static final int STATE_IDLE = 5;
private static final int STATE_IDLE_MAINTENANCE = 6;
// 시간 상수
private long mInactiveTimeout; // 30분
private long mIdleAfterInactiveTimeout; // 30분
private long mIdlePendingTimeout; // 5분
private long mMaxIdlePendingTimeout; // 10분
private long mIdlePendingFactor; // 2배 증가
private long mIdleTimeout; // 1시간
private long mMaxIdleTimeout; // 6시간
private long mIdleFactor; // 2배 증가
private long mMinTimeToAlarm; // 다음 알람 시간
private long mNextIdlePendingDelay;
private long mNextIdleDelay;
private int mState;
void stepIdleStateLocked(String reason) {
final long now = SystemClock.elapsedRealtime();
switch (mState) {
case STATE_INACTIVE:
// Doze 절차 시작
startMonitoringMotionLocked();
scheduleAlarmLocked(mInactiveTimeout, false);
mState = STATE_IDLE_PENDING;
break;
case STATE_IDLE_PENDING:
// 움직임 감지
mState = STATE_SENSING;
scheduleSensingTimeoutAlarmLocked(mConstants.SENSING_TIMEOUT);
break;
case STATE_SENSING:
// Idle 상태로 진입
cancelSensingTimeoutAlarmLocked();
cancelLocatingLocked();
mState = STATE_IDLE;
scheduleAlarmLocked(mNextIdleDelay, true);
mNextIdleDelay = (long)(mNextIdleDelay * mIdleFactor);
if (mNextIdleDelay > mMaxIdleTimeout) {
mNextIdleDelay = mMaxIdleTimeout;
}
break;
case STATE_IDLE:
// 유지 창
scheduleAlarmLocked(mConstants.IDLE_AFTER_INACTIVE_TIMEOUT, false);
mState = STATE_IDLE_MAINTENANCE;
EventLogTags.writeDeviceIdleOnStart();
addEvent(EVENT_DEEP_MAINTENANCE);
mMaintenanceStartTime = SystemClock.elapsedRealtime();
break;
case STATE_IDLE_MAINTENANCE:
// 다시 Idle로 돌아감
scheduleAlarmLocked(mNextIdleDelay, true);
mState = STATE_IDLE;
break;
}
}
}
5.3 Doze 화이트리스트 메커니즘
// frameworks/base/services/core/java/com/android/server/DeviceIdleController.java
// 화이트리스트에 추가 (Doze 제한 받지 않음)
public void addPowerSaveWhitelistApp(String packageName) {
synchronized (this) {
int uid = getUidLocked(packageName);
if (uid >= 0) {
setAppIdWhitelistStateLocked(UserHandle.getAppId(uid), true);
}
}
}
// 화이트리스트 여부 확인
public boolean isPowerSaveWhitelistApp(String packageName) {
synchronized (this) {
int uid = getUidLocked(packageName);
return uid >= 0 && mPowerSaveWhitelistUserAppIdArray.get(UserHandle.getAppId(uid));
}
}
6. 밝기 제어 메커니즘
6.1 DisplayPowerController 아키텍처
// frameworks/base/services/core/java/com/android/server/display/DisplayPowerController.java
final class DisplayPowerController implements AutomaticBrightnessController.Callbacks {
// 밝기 모드
private boolean mUseSoftwareAutoBrightnessConfig; // 자동 밝기
private boolean mColorFadeFadesConfig;
// 현재 밝기
private int mScreenBrightnessSetting;
private float mTemporaryScreenBrightness;
private float mScreenBrightnessScaleFactor;
// 자동 밝기 컨트롤러
private AutomaticBrightnessController mAutomaticBrightnessController;
// 디스플레이 장치
private DisplayDevice mDisplayDevice;
private Light mBacklight;
private void updatePowerState() {
// 1. 목표 밝기 계산
int brightness = mScreenBrightnessSetting;
if (mUseSoftwareAutoBrightnessConfig) {
brightness = mAutomaticBrightnessController.getAutomaticScreenBrightness();
}
// 2. 밝기 적용
if (mBacklight != null) {
mBacklight.setBrightness(brightness);
}
// 3. 화면 상태 업데이트
if (mPowerRequest.policy == DisplayPowerRequest.POLICY_OFF) {
// 화면 꺼짐
mDisplayDevice.requestDisplayStateLocked(Display.STATE_OFF, brightness);
} else if (mPowerRequest.policy == DisplayPowerRequest.POLICY_DOZE) {
// Doze 모드
mDisplayDevice.requestDisplayStateLocked(Display.STATE_DOZE, brightness);
} else {
// 정상 표시
mDisplayDevice.requestDisplayStateLocked(Display.STATE_ON, brightness);
}
}
}
6.2 자동 밝기 알고리즘
// frameworks/base/services/core/java/com/android/server/display/AutomaticBrightnessController.java
public class AutomaticBrightnessController implements SensorEventListener {
// 주변 밝기 -> 밝기 매핑 곡선
private BrightnessMapper mBrightnessMapper;
@Override
public void onSensorChanged(SensorEvent event) {
if (mLightSensorEnabled) {
final long time = SystemClock.uptimeMillis();
final float lux = event.values[0];
// 주변 밝기 기록 업데이트
mAmbientLightRingBuffer.push(time, lux);
// 평균 주변 밝기 계산
mAmbientLux = calculateAmbientLux(time);
// 목표 밝도로 매핑
int targetBrightness = mBrightnessMapper.getBrightness(mAmbientLux);
// 부드러운 전환
if (targetBrightness != mScreenAutoBrightness) {
animateScreenBrightness(targetBrightness,
BRIGHTNESS_ADJUSTMENT_SAMPLE_DEBOUNCE_MILLIS);
}
}
}
private float calculateAmbientLux(long now) {
final int N = mAmbientLightRingBuffer.size();
if (N == 0) {
return -1;
}
float sum = 0;
float totalWeight = 0;
long endTime = AMBIENT_LIGHT_PREDICTION_TIME_MILLIS;
for (int i = N - 1; i >= 0; i--) {
long eventTime = mAmbientLightRingBuffer.getTime(i);
if (eventTime < (now - AMBIENT_LIGHT_HORIZON)) {
break;
}
float weight = calculateWeight(now, eventTime);
float lux = mAmbientLightRingBuffer.getLux(i);
sum += lux * weight;
totalWeight += weight;
}
return sum / totalWeight;
}
}
7. 실전 사례
7.1 사례 1: 올바른 WakeLock 사용
public class WakeLockExample {
private PowerManager.WakeLock mWakeLock;
// WakeLock 획득
public void acquireWakeLock(Context context) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, // 타입
"MyApp::MyWakeLock" // 태그(:: 포함 필수)
);
// 타임아웃 자동 해제 권장
mWakeLock.acquire(10 * 60 * 1000L); // 10분
}
// WakeLock 해제
public void releaseWakeLock() {
if (mWakeLock != null && mWakeLock.isHeld()) {
mWakeLock.release();
}
}
// Activity에서 사용 (화면 켜짐 유지)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 방법 1: FLAG_KEEP_SCREEN_ON (권장)
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// 방법 2: WakeLock (권장되지 않음, 권한 필요)
// PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
// mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "MyApp::Screen");
// mWakeLock.acquire();
}
}
AndroidManifest.xml:
<!-- WakeLock 권한 -->
<uses-permission android:name="android.permission.WAKE_LOCK"/>
7.2 사례 2: Doze 모드 감지
public class DozeDetector {
private Context mContext;
public boolean isDeviceIdleMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
return pm.isDeviceIdleMode();
}
return false;
}
public boolean isIgnoringBatteryOptimizations() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
String packageName = mContext.getPackageName();
return pm.isIgnoringBatteryOptimizations(packageName);
}
return true;
}
// 화이트리스트 요청 (사용자 확인 필요)
public void requestIgnoreBatteryOptimizations(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + mContext.getPackageName()));
activity.startActivity(intent);
}
}
// Doze 모드 변화 리스너 등록
public void registerDozeReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
mContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (isDeviceIdleMode()) {
Log.d(TAG, "Doze 모드 진입");
// 비필수 작업 중지
} else {
Log.d(TAG, "Doze 모드 종료");
// 작업 복원
}
}
}, filter);
}
}
7.3 사례 3: 배터리 상태 모니터링
public class BatteryMonitor extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
// 배터리 잔량
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
float batteryPct = level / (float) scale;
// 충전 상태
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL;
// 충전 유형
int chargePlug = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
boolean usbCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_USB;
boolean acCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_AC;
boolean wirelessCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_WIRELESS;
Log.d(TAG, String.format("배터리: %.0f%%, 충전 중: %b", batteryPct * 100, isCharging));
} else if (Intent.ACTION_POWER_CONNECTED.equals(action)) {
Log.d(TAG, "충전기 연결됨");
} else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) {
Log.d(TAG, "충전기 분리됨");
}
}
public void register(Context context) {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
filter.addAction(Intent.ACTION_POWER_CONNECTED);
filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
context.registerReceiver(this, filter);
}
}
8. 성능 최적화 및 절전 전략
8.1 WakeLock 사용 모범 사례
// ✅ 권장: 타임아웃 WakeLock 사용
mWakeLock.acquire(10 * 60 * 1000L); // 10분 후 자동 해제
// ❌ 피하기: 영구 WakeLock
mWakeLock.acquire(); // 해제 잊을 수 있어 과도한 배터리 소모
// ✅ 권장: try-finally로 해제 보장
try {
mWakeLock.acquire();
doWork();
} finally {
if (mWakeLock.isHeld()) {
mWakeLock.release();
}
}
// ✅ 권장: WorkManager로 WakeLock 대체
OneTimeWorkRequest workRequest = new OneTimeWorkRequestBuilder<MyWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build();
WorkManager.getInstance(context).enqueue(workRequest);
8.2 Doze 모드 적응
// 1. FCM 고우 프라이오리티 메시지 사용 (Doze 통과 가능)
RemoteMessage message = new RemoteMessage.Builder(to)
.setPriority(RemoteMessage.PRIORITY_HIGH)
.setData(data)
.build();
// 2. AlarmManager 정확한 알람 사용
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
}
// 3. JobScheduler 사용 (유지 창에서 실행)
JobInfo jobInfo = new JobInfo.Builder(JOB_ID, componentName)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.setPersisted(true)
.build();
JobScheduler js = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
js.schedule(jobInfo);
8.3 배터리 최적화 체크리스트
// 1. 화면 잦은 깨움 피하기
// 2. WakeLock 합리적으로 사용하고 즉시 해제
// 3. 백그라운드 작업에 JobScheduler/WorkManager 사용
// 4. 위치 업데이트는 Fused Location Provider 사용하고 빈도 낮추기
// 5. 네트워크 요청을 일괄 처리하여 깨움 횟수 줄이기
// 6. 센서 감시는 불필요 시 해제
// 7. Doze 모드와 App Standby에 적응
// 8. Battery Historian으로 배터리 소모 분석
9. 결론
9.1 PMS 핵심 아키텍처
PowerManagerService는 안드로이드 시스템의 전원 관리를 총괄하는 핵심 서비스입니다. 전원 상태 머신을 통해 기기의 전원 상태를 관리하고, WakeLock 메커니즘을 통해 애플리케이션의 전원 요청을 처리하며, Doze 모드를 통해 절전을 구현합니다. DisplayPowerController는 화면 밝기와 전원 상태를 관리하고, SuspendBlocker는 시스템 절대 모드를 방지합니다.
9.2 핵심 요점
- WakeLock 메커니즘: SuspendBlocker를 통해 시스템 절대 모드를 방지하며, 반드시 시간 내에 해제해야 합니다.
- Doze 모드: 단계별로 깊은 절전 모드로 진입하며, 화이트리스트 앱은 제한을 받지 않습니다.
- 화면 관리: 활성(Awake)/드림(Dreaming)/Dozing/수면(Asleep) 네 가지 상태를 관리합니다.
- 밝기 제어: 주변 센서 + 매핑 곡선 + 부드러운 애니메이션을 사용합니다.
- 절전 전략: Doze 적응, JobScheduler 사용, 일괄 네트워크 요청 등이 있습니다.
9.3 개발자 제안
- FLAG_KEEP_SCREEN_ON 우선 사용: WakeLock 권한 신청을 피하세요.
- WakeLock에 반드시 타임아웃 설정: 해제를 잊지 않도록 하세요.
- 백그라운드 작업에 WorkManager 사용: Doze 호환을 자동으로 처리합니다.
- 화이트리스트 신중 추가: 메신저, 내비게이션, 알람 등 핵심 기능에만 사용하세요.
- Battery Historian 사용: 배터리 문제를 분석하세요.
PowerManagerService는 안드로이드 전원 관리의 핵심으로, 그 원리를 이해하는 것은 배터리 효율적인 애플리케이션 개발과 사용자 경험 향상에 매우 중요합니다.
참고 자료:
- 안드로이드 소스 코드:
frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java - 안드로이드 소스 코드:
frameworks/base/services/core/java/com/android/server/DeviceIdleController.java - 안드로이드 소스 코드:
frameworks/base/services/core/java/com/android/server/display/DisplayPowerController.java - 안드로이드 공식 문서: Optimize for Doze and App Standby