AppOps介绍
首先,先介绍下AppOps机制,在Android中,已经有一套Android Runtime运行时权限机制,对应Framework中的PermissionManager和PermissionService服务;其实还有一套AppOpsManager和AppOpsService服务,关于这个服务官方的介绍是如下:App-ops提供两个用途:一个是访问控制,这个具体是和runtime运行时权限配合;这个在下篇文章介绍;一个是跟踪,这里翻译是电量耗电跟踪,我自己个人的理解是和电量耗电相关的权限,比如定位权限,本篇文章介绍的就是应用退到后台以后,如何通过Appops机制跟踪限制后台使用定位。
/**
* App-ops are used for two purposes: Access control and tracking.
*
* <p>App-ops cover a wide variety of functionality from helping with runtime permissions access
* control and tracking to battery consumption tracking.
后台位置权限
在ard10上,权限新增了后台位置权限,需要额外申请ACCESS_BACKGROUND_LOCATION权限,此时权限弹窗会展示始终允许和使用期间这两个选项;选择相应选项相应权限会授权;这里除了运行时权限会授权外,AppOps权限机制也会授权,这里定位权限在AppOps机制上对应的授权结果为:
- 始终允许:
MODE_ALLOWED
- 使用期间:
MODE_FOREGROUND
- 拒绝:
MODE_IGNORED
这几个值在源码定义处
/frameworks/base/core/java/android/app/AppOpsManager.java
public static final int MODE_ALLOWED = 0;
public static final int MODE_IGNORED = 1;
public static final int MODE_ERRORED = 2;
public static final int MODE_DEFAULT = 3;
public static final int MODE_FOREGROUND = 4;
获取位置
在Android设备上,获取设备位置,具体可以看下这片博客:Android获取位置 ,接下来从获取设备位置API去推怎么限制应用退到后台访问位置
private LocationManager locationManager;
locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
//获取Location
Location location = locationManager.getLastKnownLocation(locationProvider);
可以看到这里通过LocationManager调用下来,最后会调用到LocationManagerService.getLastLocation()
在这里面会去检查权限是否授权,否则返回为空
/frameworks/base/services/core/java/com/android/server/LocationManagerService.java
@Override
public Location getLastLocation(LocationRequest r, String packageName) {
...
// Don't report location access if there is no last location to deliver.
if (lastLocation != null) {
//这里会检查权限,如果不通过则拿到的location是null的
if (!reportLocationAccessNoThrow(
pid, uid, packageName, allowedResolutionLevel)) {
if (D) {
Log.d(TAG, "not returning last loc for no op app: " + packageName);
}
lastLocation = null;
}
}
return lastLocation;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
这里看下是怎么检查权限的
private boolean reportLocationAccessNoThrow(
int pid, int uid, String packageName, int allowedResolutionLevel) {
//这里先做个转换得到FINE/CORSE对应的op值
int op = resolutionLevelToOp(allowedResolutionLevel);
if (op >= 0) {
//这里判断如果值不等于MODE_ALLOWED则会返回false
if (mAppOps.noteOpNoThrow(op, uid, packageName) != AppOpsManager.MODE_ALLOWED) {
return false;
}
}
return getAllowedResolutionLevel(pid, uid) >= allowedResolutionLevel;
}
这里(AppOpsManager)mAppOps.noteOpNoThrow() -> AppOpsService.noteOperationUnchecked(),如下代码的第一点和第二点是关键,展开讨论之前,我们先看下应用在退到前后台的时候怎么更新状态;
private int noteOperationUnchecked(int code, int uid, String packageName,
int proxyUid, String proxyPackageName, @OpFlags int flags) {
synchronized (this) {
//1.getOpsRawLocked()这里会去查询并更新前后台状态
final Ops ops = getOpsRawLocked(uid, packageName, true /* edit */,
false /* uidMismatchExpected */);
...
final Op op = getOpLocked(ops, code, true);
...
final UidState uidState = ops.uidState;
...
final int switchCode = AppOpsManager.opToSwitch(code);
// If there is a non-default per UID policy (we set UID op mode only if
// non-default) it takes over, otherwise use the per package policy.
if (uidState.opModes != null && uidState.opModes.indexOfKey(switchCode) >= 0) {
//2.UidState.evalMode()这里会得到结果MODE_ALLOWED或者MODE_IGNORED;
final int uidMode = uidState.evalMode(code, uidState.opModes.get(switchCode));
...
应用前后台状态更新
- 切换应用前后台时会走ActivityManagerService.noteUidProcessState()-> mAppOpsService.updateUidProcState(uid, state);
- 这里通过AppOpsService更新对应uid的应用进程前后台状态,
- 如从前台切到后台,前台状态是200,后台状态是700
- 这里是关于状态码的定义,主要关注下200-对应应用在前台
/frameworks/base/core/java/android/app/AppOpsManager.java
* Uid state: The UID is top foreground app. The lower the UID
* state the more important the UID is for the user.
* @hide
*/
@TestApi
@SystemApi
public static final int UID_STATE_TOP = 200;
/**
* Uid state: The UID is running a foreground service of location type.
* The lower the UID state the more important the UID is for the user.
* @hide
*/
@TestApi
@SystemApi
public static final int UID_STATE_FOREGROUND_SERVICE_LOCATION = 300;
/**
* Uid state: The UID is running a foreground service. The lower the UID
* state the more important the UID is for the user.
* @hide
*/
@TestApi
@SystemApi
public static final int UID_STATE_FOREGROUND_SERVICE = 400;
/**
* The max, which is min priority, UID state for which any app op
* would be considered as performed in the foreground.
* @hide
*/
public static final int UID_STATE_MAX_LAST_NON_RESTRICTED = UID_STATE_FOREGROUND_SERVICE;
/**
* Uid state: The UID is a foreground app. The lower the UID
* state the more important the UID is for the user.
* @hide
*/
@TestApi
@SystemApi
public static final int UID_STATE_FOREGROUND = 500;
/**
* Uid state: The UID is a background app. The lower the UID
* state the more important the UID is for the user.
* @hide
*/
@TestApi
@SystemApi
public static final int UID_STATE_BACKGROUND = 600;
/**
* Uid state: The UID is a cached app. The lower the UID
* state the more important the UID is for the user.
* @hide
*/
@TestApi
@SystemApi
public static final int UID_STATE_CACHED = 700;
public void updateUidProcState(int uid, int procState) {
synchronized (this) {
final UidState uidState = getUidStateLocked(uid, true);
int newState = PROCESS_STATE_TO_UID_STATE[procState];
if (uidState != null && uidState.pendingState != newState) {
final int oldPendingState = uidState.pendingState;
//设置对应uid的进程uidState的pendingState,state为当前状态,pendingState为即将变成的状态
uidState.pendingState = newState;
//如果从后台切到前台,是200<700,则直接通过commitUidPendingStateLocked()更新进程前后台状态信息并且pendingStateCommitTime = 0;
if (newState < uidState.state
|| (newState <= UID_STATE_MAX_LAST_NON_RESTRICTED
&& uidState.state > UID_STATE_MAX_LAST_NON_RESTRICTED)) {
// We are moving to a more important state, or the new state may be in the
// foreground and the old state is in the background, then always do it
// immediately.
commitUidPendingStateLocked(uidState);
} else if (uidState.pendingStateCommitTime == 0) {
// We are moving to a less important state for the first time,
// delay the application for a bit.
final long settleTime;
//如果是从前台切到后台,则这里断点看了下设置settleTime为30000即30s,这里会延迟30s获取到的进程状态才会更新成后台,主要是通过pendingStateCommitTime来判断
if (uidState.state <= UID_STATE_TOP) {
settleTime = mConstants.TOP_STATE_SETTLE_TIME;
} else if (uidState.state <= UID_STATE_FOREGROUND_SERVICE) {
settleTime = mConstants.FG_SERVICE_STATE_SETTLE_TIME;
} else {
settleTime = mConstants.BG_STATE_SETTLE_TIME;
}
uidState.pendingStateCommitTime = SystemClock.elapsedRealtime() + settleTime;
BinderCallCacheAgent.removeCheckPackageBinderCache(uid);
}
...
1. 如上后台切前台,则进入if (newState < uidState.state...
会调用commitUidPendingStateLocked();
private void commitUidPendingStateLocked(UidState uidState) {
...
//即将变成的状态pendingState变成了当前的状态
uidState.state = uidState.pendingState;
uidState.pendingStateCommitTime = 0;
}
2. 此时如果是从前台切后台,则700<200不满足,是进入else if (uidState.pendingStateCommitTime == 0) {
此时设置uidState.pendingStateCommitTime为当前时间加30s延时
getOpsRawLocked()
这里继上面的获取位置的两个关键点展开讲,第一个://1.getOpsRawLocked()这里会去查询并更新前后台状态;看下这个方法
private @Nullable UidState getUidStateLocked(int uid, boolean edit) {
UidState uidState = mUidStates.get(uid);
if (uidState == null) {
if (!edit) {
return null;
}
uidState = new UidState(uid);
mUidStates.put(uid, uidState);
} else {
//这里获取uidState状态前会判断是否更新state,如果当前时间是超过了之前设置的pendingStateCommitTime,则会更新,这里断点看是30s
if (uidState.pendingStateCommitTime != 0) {
if (uidState.pendingStateCommitTime < mLastRealtime) {
commitUidPendingStateLocked(uidState);
} else {
mLastRealtime = SystemClock.elapsedRealtime();
if (uidState.pendingStateCommitTime < mLastRealtime) {
commitUidPendingStateLocked(uidState);
}
}
}
}
return uidState;
}
这里和上面的uidState.pendingStateCommitTime的值串起来了,前面从前台切到后台,设置的pendingStateCommitTime是30s,也就是说之前从前台切到后台,则需要30s之后状态才会更新为后台状态;如果是后台切到前台,则是马上调用commitUidPendingStateLocked()即时更新,看官方解释原因如下:
if (newState < uidState.state
|| (newState <= UID_STATE_MAX_LAST_NON_RESTRICTED
&& uidState.state > UID_STATE_MAX_LAST_NON_RESTRICTED)) {
//后台->前台,认为是变化到一个更重要的状态,所以需要即时更新
// We are moving to a more important state, or the new state may be in the
// foreground and the old state is in the background, then always do it
// immediately.
commitUidPendingStateLocked(uidState);
} else if (uidState.pendingStateCommitTime == 0) {
//前台->后台,认为是变化到不那么重要的状态,所以可以延迟更新
// We are moving to a less important state for the first time,
// delay the application for a bit.
final long settleTime;
if (uidState.state <= UID_STATE_TOP) {
settleTime = mConstants.TOP_STATE_SETTLE_TIME;
UidState.evalMode()
继上面的第二个关键点讲,evalMode()
//2.UidState.evalMode()这里会得到结果MODE_ALLOWED或者MODE_IGNORED;
final int uidMode = uidState.evalMode(code, uidState.opModes.get(switchCode));
这个方法加断点可以看到,evalMode()第一个参数是对应的位置权限值,第二个参数是之前权限弹窗设置的值;
- 如果之前设置的是使用期间对应AppOpsManager.MODE_FOREGROUND,则需要判断state状态,这个state指的是当前的前后台状态,如果是前台200<=300,则返回MODE_ALLOWED,允许访问位置;如果是后台700<=300不成立,则返回MODE_IGNORED,不允许访问位置;
- 如果之前设置的是始终允许对应,则不需要检查state前台状态,直接返回允许或拒绝;
这里也做个引申解释下为什么下面这种方式能做到,原因是使用后台定位服务则当前应用进程状态是300,如上判断300<=300,所以返回MODE_ALLOWED,允许访问位置;
这里也做个引申解释下为什么下面这种方式能做到,原因是使用后台定位服务则当前应用进程状态是300,如上判断300<=300,所以返回MODE_ALLOWED,允许访问位置;
最后
如何学习Framework?
由于许多Android开发者日常工作主要集中在业务层面,大量时间用于编写基础代码、应用现成框架,导致对底层技术如Framework、Handler源码、Binder机制等了解不足,仅停留在表面认知阶段。
为此,为了帮助广大开发者弥补这一短板,特此准备了一份详尽的Android Framework内核源码知识体系图解,以及配套的《Android Framework源码开发解析》学习笔记,旨在引导大家系统性地攻克Android Framework领域的核心技术,从而提升自身的竞争力,从容应对金三银四的求职挑战。
【有需要的朋友,扫描下方二维码即可领取!!】👇👇

《Android Framework源码开发揭秘》
第一章 系统启动流程分析
- 第一节 Android启动概括
- 第二节 init.rc解析
- 第三节 Zygote
- 第四节 面试题
第二章 跨进程通信IPC解析
- 第一节 Service还可以这么理解
- 第二节 Binder基础
- 第三节 Binder应用
- 第四节 AIDL应用(上)
- 第五节 AIDL应用(下)
- 第六节 Messenger原理及应用
- 第七节 服务端回调
- 第八节 获取服务(IBinder)
- 第九节 Binder面试题全解析
第三章 Handler源码解析
- 第一节 源码分析
- 第二节 难点问题
- 第三节 Handler常问面试题
第四章 AMS源码解析
- 第一节 引言
- 第二节 Android架构
- 第三节 通信方式
- 第四节 系统启动系列
- 第五节 AMS
- 第六节 AMS面试题解析
第五章 WMS源码解析
- 第一节 WMS与activity启动流程
- 第二节 WMS绘制原理
- 第三节 WMS角色与实例化过程
- 第四节 WMS工作原理
第六章 Surface源码解析
- 第一节 创建流程及软硬件绘制
- 第二节 双缓冲及Surface View解析
- 第三节 Android图形系统综述
第七章 基于Android12.0的SurfaceFlinger源码解析
- 第一节 应用建立和SurfaceFlinger的沟通桥梁
- 第二节 SurfaceFlinger的启动和消息队列处理机制
- 第三节 SurfaceFlinger之VSyns(上)
- 第四节 SurfaceFlinger之VSyns(中)
- 第五节 SurfaceFlinger之VSyns(下)
第八章 PKMS源码解析
- 第一节 PKMS调用方式
- 第二节 PKMS启动过程分析
- 第三节 APK的扫描
- 第四节 APK的安装
- 第五节 PKMS之权限扫描
- 第六节 静默安装
- 第七节 requestPermissions源码流程解析
- 第八节 PKMS面试题
第九章 InputManagerService源码解析
- 第一节 Android Input输入事件处理流程(1)
- 第二节 Android Input输入事件处理流程(2)
- 第三节 Android Input输入事件处理流程(3)
第十章 DisplayManagerService源码解析
- 第一节 DisplayManagerService启动
- 第二节 DisplayAdepter和DisplayDevice的创建
- 第三节 DMS部分亮灭屏流程
- 第四节 亮度调节
- 第五节 Proximity Sensor灭屏原理
- 第六节 Logical Display和Physical Display配置的更新
【有需要的朋友,扫描下方二维码即可领取!!】👇👇
