上文分析了车载设置按键音的实现,对 Android 的源码进行了部分修改,此文趁热打铁,继续对 Android 源码进行分析,并了解 Anddroid 的休眠机制。想了解 Android 按键音的实现,可阅读上文 车载设置–按键提示音
使用场景
一般在车载应用设置中,有一个 自动关屏 的选项,通常有 30s,60s,2分钟等选项,当我们选择了 30s 选项后,在屏幕无操作后或者面板无操作 30s 后自动关屏,进入屏保界面。当点击屏保或者操作方控键,空调面板,屏保消失从而进入相应的界面。如果按照 Android 休眠机制来做,当屏幕无操作后黑屏后此时触摸无作用,除非按电源键解锁,此时做任何操作也无法点亮屏幕,基于这一点关屏后无触摸就不符合车载的需求,所以需要利用源码来达到类似的效果。
Android 休眠机制
通常我们在手机上观看视频时,无论我们多久没触摸,手机将一直操持亮屏状态,那么手机上这个不黑屏是怎么实现的呢?其实就是因为该播放界面持有 wakeLock ,wakeLock 通常的用法为:
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, TAG);
mWakeLock.acquire();
以此就来分析 Android 休眠的机制。
1. WakeLock 的初始化
public WakeLock newWakeLock(int levelAndFlags, String tag) {
validateWakeLockParameters(levelAndFlags, tag);
return new WakeLock(levelAndFlags, tag, mContext.getOpPackageName());
}
WakeLock(int flags, String tag, String packageName) {
mFlags = flags;
mTag = tag;
mPackageName = packageName;
mToken = new Binder();
}
可以看到,newWakeLock 方法首先检测 LevelAndFlags 和 Tag 的合法性,代码很简单,
其实就是:tag 不能为空,Level必须用 PowerManager 提供的几个Level。
/**
* 当在应用调用 acquire ,申请锁,执行的就是这个函数
*/
public void acquire() {
synchronized (mToken) {
acquireLocked();
}
}
private void acquireLocked() {
if (!mRefCounted || mCount++ == 0) {
// Do this even if the wake lock is already thought to be held (mHeld == true)
// because non-reference counted wake locks are not always properly released.
// For example, the keyguard's wake lock might be forcibly released by the
// power manager without the keyguard knowing. A subsequent call to acquire
// should immediately acquire the wake lock once again despite never having
// been explicitly released by the keyguard.
mHandler.removeCallbacks(mReleaser);
try {
mService.acquireWakeLock(mToken, mFlags, mTag, mPackageName, mWorkSource);
} catch (RemoteException e) {
}
mHeld = true;
}
}
从源码中可以看出在应用层调用 acquire ,调用的是mService.acquireWakeLock(mToken, mFlags, mTag, mPackageName, mWorkSource),而真正的执行是在 PowerManagerService 。那么就来深入了解 PowerManagerService 的实现。
public void acquireWakeLock(IBinder lock, int flags, String tag, String packageName,
WorkSource ws) {
//代码省略
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final long ident = Binder.clearCallingIdentity();
try {
acquireWakeLockInternal(lock, flags, tag, packageName, ws, uid, pid);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private void acquireWakeLockInternal(IBinder lock, int flags, String tag, String packageName,
WorkSource ws, int uid, int pid) {
synchronized (mLock) {
WakeLock wakeLock;
int index = findWakeLockIndexLocked(lock);
if (index >= 0) {
wakeLock = mWakeLocks.get(index);
if (!wakeLock.hasSameProperties(flags, tag, ws, uid, pid)) {
// Update existing wake lock. This shouldn't happen but is harmless.
notifyWakeLockReleasedLocked(wakeLock);
wakeLock.updateProperties(flags, tag, packageName, ws, uid, pid);
notifyWakeLockAcquiredLocked(wakeLock);
}
} else {
wakeLock = new WakeLock(lock, flags, tag, packageName, ws, uid, pid);
try {
lock.linkToDeath(wakeLock, 0);
} catch (RemoteException ex) {
throw new IllegalArgumentException("Wake lock is already dead.");
}
notifyWakeLockAcquiredLocked(wakeLock);
mWakeLocks.add(wakeLock);
}
applyWakeLockFlagsOnAcquireLocked(wakeLock);
mDirty |= DIRTY_WAKE_LOCKS;
updatePowerStateLocked();
}
updatePowerStateLocked 更新电源状态,该函数是 PMS 的核心代码。 当手指触摸时 mDirty = DIRTY_USER_ACTIVITY
private void updatePowerStateLocked() {
if (!mSystemReady || mDirty == 0) { //未启动完毕,或者 mDirty 没有变化
return;
}
// Phase 0: Basic state updates. // 更新基本状态
updateIsPoweredLocked(mDirty);//只有 mDirty 为 RTY_BATTERY_STATE 才执行
updateStayOnLocked(mDirty); 只有 mDirty 为 DIRTY_BATTERY_STATE | DIRTY_SETTINGS 才执行
// Phase 1: Update wakefulness.
// Loop because the wake lock and user activity computations are influenced
// by changes in wakefulness. // 2、更新 wakelock 和用户活动
final long now = SystemClock.uptimeMillis();
int dirtyPhase2 = 0;
for (;;) {
int dirtyPhase1 = mDirty;
dirtyPhase2 |= dirtyPhase1;
mDirty = 0; //mDirty 重新赋值为 0
updateWakeLockSummaryLocked(dirtyPhase1); //调用PMS的acquireWakeLock时,就会将dirty的DIRTY_WAKE_LOCKS位置1
updateUserActivitySummaryLocked(now, dirtyPhase1); //要根据用户最后的活动来决定当前屏幕的状态 重点分析
if (!updateWakefulnessLocked(dirtyPhase1)) {
break;
}
}
// Phase 2: Update dreams and display power state.
updateDreamLocked(dirtyPhase2); // 更新dream state
updateDisplayPowerStateLocked(dirtyPhase2);
// Phase 3: Send notifications, if needed.
if (mDisplayReady) {
sendPendingNotificationsLocked();
}
// Phase 4: Update suspend blocker.
// Because we might release the last suspend blocker here, we need to make sure
// we finished everything else first!
updateSuspendBlockerLocked(); // 更新suspend blocker
//updateBootFastSuspendBlockerLocked();
}
updateUserActivitySummaryLocked 的分析:
// Update the status of the user activity timeout timer.
if ((dirty & (DIRTY_USER_ACTIVITY | DIRTY_WAKEFULNESS | DIRTY_SETTINGS)) != 0) {
mHandler.removeMessages(MSG_USER_ACTIVITY_TIMEOUT);
long nextTimeout = 0;
if (mWakefulness != WAKEFULNESS_ASLEEP) {
//获取进入休眠状态的时间sleepTimeout
//getSleepTimeoutLocked中会判断休眠时间和屏幕熄灭时间的关系
//如果休眠时间sleepTimeout小于屏幕熄灭时间screenOfftime,
//则休眠时间被调整为屏幕熄灭时间,因为屏幕亮屏状态下,终端不能进入休眠
//获取屏幕熄灭的时间
final int screenOffTimeout = getScreenOffTimeoutLocked();
//获取屏幕变暗的时间
final int screenDimDuration = getScreenDimDurationLocked(screenOffTimeout);
//将当前的用户事件,转化为PMS可以处理的屏幕状态
mUserActivitySummary = 0;
//在唤醒的状态下,发生过用户事件
if (mLastUserActivityTime >= mLastWakeTime) {
nextTimeout = mLastUserActivityTime
+ screenOffTimeout - screenDimDuration;
if (now < nextTimeout) {
//如果没有到达需要变暗的时间,那么当前屏幕的状态为USER_ACTIVITY_SCREEN_BRIGHT(亮屏)
mUserActivitySummary |= USER_ACTIVITY_SCREEN_BRIGHT;
} else {
//到达变暗的时间,则计算出屏幕熄灭的时间
nextTimeout = mLastUserActivityTime + screenOffTimeout;
if (now < nextTimeout) {
//还没到熄灭的时间,则当前屏幕的状态为USER_ACTIVITY_SCREEN_DIM(暗屏)
mUserActivitySummary |= USER_ACTIVITY_SCREEN_DIM;
}
}
}
if (mUserActivitySummary == 0
&& mLastUserActivityTimeNoChangeLights >= mLastWakeTime) {
nextTimeout = mLastUserActivityTimeNoChangeLights + screenOffTimeout;
if (now < nextTimeout
&& mDisplayPowerRequest.screenState
!= DisplayPowerRequest.SCREEN_STATE_OFF) {
mUserActivitySummary = mDisplayPowerRequest.screenState
== DisplayPowerRequest.SCREEN_STATE_BRIGHT ?
USER_ACTIVITY_SCREEN_BRIGHT : USER_ACTIVITY_SCREEN_DIM;
}
}
从上面的代码可以看出,在该函数中用 mUserActivitySummary 变量存储当前屏幕的状态。
一共有3中基本状态:
* USER_ACTIVITY_SCREEN_BRIGHT 点亮屏幕
* USER_ACTIVITY_SCREEN_DIM 屏幕变暗
* USER_ACTIVITY_SCREEN_DREAM 屏保状态
从代码可以看出,屏幕变化和 userActivity 活动有关,它根据最后的 userActivity 活动的时间决定点亮屏幕、调暗屏幕或熄灭屏幕 。
private boolean updateWakefulnessLocked(int dirty) {
boolean changed = false;
if ((dirty & (DIRTY_WAKE_LOCKS | DIRTY_USER_ACTIVITY | DIRTY_BOOT_COMPLETED
| DIRTY_WAKEFULNESS | DIRTY_STAY_ON | DIRTY_PROXIMITY_POSITIVE
| DIRTY_DOCK_STATE)) != 0) {
if (mWakefulness == WAKEFULNESS_AWAKE && isItBedTimeYetLocked()) {
if (DEBUG_SPEW) {
Slog.d(TAG, "updateWakefulnessLocked: Bed time...");
}
final long time = SystemClock.uptimeMillis();
//主要根据设置信息,判断是否满足进入Dream状态的条件
if (shouldNapAtBedTimeLocked()) {
//将mWakefullness的值置为WAKEFULNESS_DREAMING,修改mDirty变量,并进行通知等
changed = napNoUpdateLocked(time);
} else {
//将mWakefullness的值置为WAKEFULNESS_DOZING
//如果系统设置了跳过Dozing态,则将mWakefullness置为WAKEFULNESS_ASLEEP
//同时修改mDirty变量,并进行通知等
changed = goToSleepNoUpdateLocked(time,
changed = goToSleepNoUpdateLocked(time,
PowerManager.GO_TO_SLEEP_REASON_TIMEOUT);
}
}
}
return changed;
}
在用户发生触摸时,会调用 userActivityInternal:
public void userActivity(long eventTime, int event, int flags) {
// 此处省略
final int uid = Binder.getCallingUid();
final long ident = Binder.clearCallingIdentity();
try {
userActivityInternal(eventTime, event, flags, uid);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private void userActivityInternal(long eventTime, int event, int flags, int uid) {
synchronized (mLock) {
if (userActivityNoUpdateLocked(eventTime, event, flags, uid)) {
updatePowerStateLocked();
}
}
}
继续往下跟:userActivityNoUpdateLocked 代码如下:
private boolean userActivityNoUpdateLocked(long eventTime, int event, int flags, int uid) {
if (eventTime < mLastSleepTime || eventTime < mLastWakeTime
|| mWakefulness == WAKEFULNESS_ASLEEP || !mBootCompleted || !mSystemReady) {
return false;
}
mNotifier.onUserActivity(event, uid);
if ((flags & PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS) != 0) {
if (eventTime > mLastUserActivityTimeNoChangeLights
&& eventTime > mLastUserActivityTime) {
mLastUserActivityTimeNoChangeLights = eventTime;
mDirty |= DIRTY_USER_ACTIVITY;
return true;
}
} else { //此时 flags=0x0
if (eventTime > mLastUserActivityTime) { //和上次触摸时间比较
mLastUserActivityTime = eventTime; //记住触摸时间
mDirty |= DIRTY_USER_ACTIVITY;
return true;
}
}
return false;
}
移植流程:
1.参考源码自定义一个变量,用来存储休眠时间 mScreenOffTimeoutXX,初始化的位置与休眠时间一致:
mScreenOffTimeoutSetting = Settings.System.getIntForUser(resolver,
Settings.System.SCREEN_OFF_TIMEOUT, DEFAULT_SCREEN_OFF_TIMEOUT,
UserHandle.USER_CURRENT);
mScreenOffTimeoutSoling = Settings.System.getIntForUser(resolver,"XX_timeout", -1,UserHandle.USER_CURRENT);
if(mNotifier!=null)
mNotifier.onUserActivity(mScreenOffTimeoutSoling);
2.监听关屏时间的改变,同监听休眠时间 一样:
resolver.registerContentObserver(Settings.System.getUriFor(Settings.System.SCREEN_OFF_TIMEOUT),
false, mSettingsObserver, UserHandle.USER_ALL);
resolver.registerContentObserver(Settings.System.getUri"XX_timeout"),false, mSettingsObserver, UserHandle.USER_ALL);
3.当在用户申请锁时,存储包名,方便过滤本地应用,即 acquireWakeLockInternal 发送通知
if(mNotifier!=null)
mNotifier.onWakeLockAcquired(wakeLock.mPackageName);
4.当用户发生触摸操作时,重新计时,即在 userActivityNoUpdateLocked 开头添加如下代码:
int timeout = mScreenOffTimeoutSoling;
mNotifier.onUserActivity(timeout);
//后面为源码部分,不在后面添加,避免被 return
if (eventTime < mLastSleepTime || eventTime < mLastWakeTime
|| mWakefulness == WAKEFULNESS_ASLEEP || !mBootCompleted || !mSystemReady) {
return false;
}
5.当锁被释放时,不在对相应的包名过滤,即在 releaseWakeLockInternal 添加代码:
f(mNotifier!=null)
mNotifier.onWakeLockReleased(wakeLock.mPackageName,mScreenOffTimeoutXX);
mWakeLocks.remove(index);
notifyWakeLockReleasedLocked(wakeLock);
wakeLock.mLock.unlinkToDeath(wakeLock, 0);
这个代码的移植都是参照源码,而真正发送消息的地方是在 Notifier 类中,在此类中,做延迟操作,发送广播通知。对对事件的处理分为如下几种:
1. 当本地应用如视频,申请锁时,取消计时,永不黑屏;
2. 当用户有点击事件时,重新计时,同时判断本地应用是否申请了锁,如果申请了,仍然不计时;
3. 当本地应用释放锁时,重新计时。
基于上述分析,开始一直 Notifier 代码。
private static final int MSG_USER_ACTIVITY_XX = 6; 用于 handler 用于发送的 MSG
private String[] mXXApps = {,"com.xx.call,"com.xx.video"}; //用于过滤包名,包名相同,就不黑屏
/**
* 当应用申请锁时,判断是不是本地应用,是本地应用,就存储起来,同时取消计时
*/
public void onWakeLockAcquired(String pkgName){
if(isXXApp(pkgName)){
mLockPkgName = pkgName;
mHandler.removeMessages(MSG_USER_ACTIVITY_SOLING);
}
}
/**
* 当触摸发生时,如果是本地应用,则不处理,
* 不是本地应用,重新计时
*/
public void onUserActivity(int timeout){
synchronized (mLock){
if(!isXXApp(mLockPkgName)){
mHandler.removeMessages(MSG_USER_ACTIVITY_XX);
if(timeout!=-1&&timeout!=0)
mHandler.sendEmptyMessageDelayed(MSG_USER_ACTIVITY_XX,timeout);
, }
}
}
/**
* 释放锁时,重新计时
*/
public void onWakeLockReleased(String pkgName,int timeout){
if(isSolingApp(pkgName)){
mLockPkgName = "";
mHandler.removeMessages(MSG_USER_ACTIVITY_SOLING);
if(timeout!=-1&&timeout!=0)
mHandler.sendEmptyMessageDelayed(MSG_USER_ACTIVITY_SOLING,
}
}
/**
* 在 timeout 时间到了之后,发送通知,在接受通知中,处理逻辑:比如关闭背光
*/
private void sendUserActivityToSoling() {
Intent intent = new Intent();
intent.setAction("com.action.xx.screen.off");
if(mContext!=null)
mContext.sendBroadcast(intent);
}
/**
* 白名单
*/
private boolean isXXApp(String pkgName){
for(int i = 0;i<mSolingApps.length;i++){
if(mSolingApps[i].equals(pkgName))
return true;
}
return false;
}
总结
其实自己本身对 WakeLock 机制也并不是很了解,也是通过不断的打印 log 分析而来,在分析 log 中才有了看似可行的思路:
1. 当本地应用申请锁时,取消计时;
2. 当用户有操作时,会调用 userActivity 方法,在此方法中,重新计时;
3. 当本地应用释放锁时,重新计时;
4. 监听 timeout 的改变,重新计时。
有了上面的思路,整个移植过程看起来就很清晰明了,同时配合 log 分析,基本就能达到预期效果。后续如果有 bug ,再来分析,可能会对 wakeLock 机制有更深的了解,到时再来更新博客。