做Android开发这些年,我一直觉得AccountManager是那种“平时用不到、但一旦用到就绕不开”的系统服务。你手机上的联系人多端同步、云相册备份、企业应用的单点登录,背后几乎都是AccountManagerService在统一管理账号和Token。这篇文章我打算从Framework角度把这个服务彻底拆开:它为什么存在、在系统里怎么被启动和存储数据、读账户和取Token的源码链路长什么样、如果想给自家App接入一套系统账户体系应该怎么落地,以及日常排错有哪些直接有效的招数。适合正在啃Framework源码的开发者,也适合需要在App里接入账号体系、或者想实现多应用统一登录的团队参考。
1. Android为什么要造出AccountManager:从“各自登录”到“统一认证”
1.1 没有账户体系的日子:密码散落与重复开发
想象一下手机里装了二三十个App,每个都要注册账号和密码。如果系统层不提供任何统一机制,每个App就得各自维护一套登录状态和会话凭证,密码散落在各个应用私有目录里,安全策略完全由各团队自己定——有的明文存,有的简单哈希,有的干脆只依赖服务器下发的Token。最头疼的是跨应用协作:日历应用要读取邮箱的会议安排,就得想尽办法从邮箱App那里借授权;同一个产品矩阵想做统一登录,只能靠每个App自己维护SDK和回调协议,登录态不互通,用户换个App就得重新登录。
在Android早期,这个痛点被放得很大。系统想在“设置”里给用户一个统一的账户入口,同时也希望联系人、日历这些系统组件能够跨应用访问账户数据。单靠应用各自为政根本做不到,必须在操作系统层面抽象出一套“账户”模型,把账户的存储、认证、授权集中管理起来,再以Service的形式开放给所有应用。AccountManagerService就是这套模型的落地实现。
1.2 账户类型:一个命名空间定义的第三方世界
AccountManager的核心概念非常朴素:一个账户(Account)由两个字段唯一标识——name(账户名称,通常是用户名或邮箱)和type(账户类型)。type是字符串标识,约定采用反域名命名,比如“com.example.cloud”。
type的作用有两层。第一是命名空间隔离,两个不同账户类型即使name相同也不会冲突,因为系统在数据库层面用name + type做唯一键;第二是路由,系统拿到type,就知道该把这个账户的认证工作交给哪一个认证器(Authenticator)服务。你打开系统设置里的账户页,能看到的每一个账户类型,背后都对应一个注册到系统里的认证器服务。
我最初看这块时有个误解,以为AccountManager会自动帮应用完成登录和校验。实际上它只负责“存储”和“路由”,真正的认证逻辑全部交给第三方注册的认证器。这就是Android账户体系能同时容纳邮箱、社交、企业OA等完全不同的认证协议的原因——系统只关心账户长什么样、属于哪个类型,认证细节一概不管。
1.3 统一认证的关键:认证器机制
认证器的本质是一个独立的Service,对外暴露IAccountAuthenticator接口,由应用在Manifest里声明并注册到系统。AccountManagerService遇到下面几种情况时,才会去调用认证器:
- 用户通过系统界面添加账户;
- 某应用请求获取AuthToken,但本地缓存里没有;
- 需要校验密码或刷新凭据。
这种设计把“认证”和“存储”解耦了。AccountManagerService像一个酒店前台,负责登记房客信息(账户数据)、分发房卡(Token),而真正的身份核验员是各个认证器——它们藏在各个应用进程里,前台无法直接看到,只能通过内部电话(Binder绑定)联系。应用侧拿到Token之后,就可以用这个Token去请求自己的业务服务器,整个体系完美地把“系统能力”和“业务逻辑”隔离开。
2. AccountManagerService的系统基石:启动注册、数据库与权限模型
2.1 服务的出生:SystemServer里的启动与发布
Android系统服务的标准归宿是system_server进程。AccountManagerService在SystemServer的startOtherServices阶段被创建,然后通过publishBinderService发布为“account”服务。对应的代码路径大致是:SystemServer会创建AccountManagerService实例,调用它的systemServiceReady,再把它注册到ServiceManager。Context.ACCOUNT_SERVICE这个常量对应的值就是”account“,应用层拿到的AccountManager对象就是它的Binder代理。
一个容易被忽略的细节是,AccountManagerService在构造阶段就会注册一批系统级监听:用户切换监听(多用户场景下需要切换AccountsDb)、包移除监听(应用卸载后清理它对账户的授权记录)、以及锁屏状态监听(和CE/DE数据库的读取时机有关)。这些监听决定了账户数据的生命周期边界,理解了这个,后面遇到“为什么我卸载了App但账户还在”这类问题就有了解释。
2.2 客户端如何与服务端建立连接
应用层拿到的AccountManager,位于framework/base/core/java/android/accounts/AccountManager.java。你new不出来它,只能通过Context.getSystemService(Context.ACCOUNT_SERVICE)获取。AccountManager内部持有AccountManagerService的Binder引用,所有API最终都是跨进程调用的。
这套客户端封装做了几件很重要的事:参数封装(把方法调用转成Parcelable的调用参数)、异步回调调度(很多账户API是异步的,需要Handler和回调调度)、以及对部分返回结果的本地解析。我建议你把它当作框架层客户端的标准范本去读——它没有过度设计,但把Binder调用的繁琐细节包装得相当干净。
2.3 账户数据落在哪:AccountsDb与CE/DE分区
AccountManagerService把账户数据存在AccountsDb里。在Android 7.0之前,主要是accounts.db;之后为了支持Direct Boot模式,数据库拆分成了两份:
- accounts_de.db:存放设备解锁前就能读取的非敏感数据,比如账户的name、type;
- accounts_ce.db:存放设备解锁后才能读取的数据,比如Token和密码哈希,直接与锁屏安全绑定。
这个拆分的意义是:系统在用户未解锁时根本不会去读ce数据库,所以如果你的同步逻辑跑在锁屏前的Direct Boot阶段,就别指望能拿到Token。对于Framework开发者来说,这是一个关键认知边界。
AccountsDb内部有几张核心表,我把作用和关键字段整理如下:
| 表名 | 作用 | 关键数据 |
|---|---|---|
| accounts | 账户主表 | name、type、密码哈希、上一次认证时间 |
| authtokens | 账户令牌表 | 账户id、Token类型、Token内容、过期策略 |
| grants | 应用授权表 | 账户id、授权应用的uid、授权标志 |
| uids | 账户类型与uid映射 | 声明了某一账户类型的应用包名和uid |
| visibility | 账户可见性 | 某个账户类型对某个应用包名的可见状态 |
2.4 权限与账户可见性:不是拿到服务就能读账户
AccountManagerService的权限体系分两个维度:系统权限和账户可见性。
系统权限对应Manifest声明,常用的有四个:
| 权限 | 能力 |
|---|---|
| android.permission.GET_ACCOUNTS | 读取账户列表 |
| android.permission.AUTHENTICATE_ACCOUNTS | 向认证器发起认证请求 |
| android.permission.MANAGE_ACCOUNTS | 管理账户,添加/删除账户 |
| android.permission.USE_CREDENTIALS | 使用系统凭据,新版本中已被弱化 |
账户可见性是Android 8.0之后引入的机制。即使应用声明了GET_ACCOUNTS权限,如果系统或用户将该账户类型对这个应用设为不可见,AccountManagerService返回的账户列表仍然是空的。这个设计防止了账户类型信息被无关应用批量收集。你可以通过AccountManager.setAccountVisibility方法动态调整可见性,但注意这个方法本身也需要系统级权限。
3. 读列表、拿Token、加账户:三条核心链路的源码级拆解
3.1 读取账户列表:一次典型的Binder往返
先看最常用的读取账户列表的调用:
AccountManager am = AccountManager.get(context); Account[] accounts = am.getAccountsByType("com.example.cloud");AccountManager.getAccountsByType内部会先去拿AccountManagerService的Binder引用,然后调accountManagerService.getAccountsByType,传入调用者包名和userId。服务端在进入业务逻辑前会先做权限检查:如果调用方没有GET_ACCOUNTS权限,也没有Manifest里声明账户类型与调用者uid的映射关系,就直接返回空数组或抛SecurityException。
权限检查通过后,AccountManagerService会查AccountsDb的accounts表,按type过滤账户,组装成Account[]数组。Account实现了Parcelable,所以能跨Binder传递。整个流程本质上就是一次标准的系统服务调用:客户端封装参数、Binder传输、服务端权限校验、数据库查询、结果回传。
每次调用都走一次跨进程,开销不小,所以AccountManager在客户端还有一层账户缓存。但缓存就意味着可能不是最新数据,系统会在账户数据变化时发广播通知其他进程清理缓存。这就是为什么你的App里即使没有主动刷新,过一段时间账户列表也会自动准确——前提是监听并处理了缓存失效逻辑。
3.2 获取AuthToken:缓存命中与认证器回调两条路径
AuthToken是账户体系里最重要的产物,通常代表某服务的访问令牌。App在需要调用业务接口时,会向AccountManager请求Token:
AccountManager am = AccountManager.get(context); am.getAuthToken(account, "authtokenType", null, true, callback, handler);流程可以拆成两步。
第一步,AccountManagerService先查authtokens表,看该账户下有没有对应Token类型且没过期的缓存。如果命中,直接把Token回传,整个过程不需要唤醒认证器。
第二步,缓存未命中或Token已过期,AccountManagerService就会通过IAccountAuthenticator向该账户类型注册的认证器发起getAuthToken请求。认证器拿到请求后,通常会带上本地的refreshToken去服务器换取新的accessToken;服务器返回后认证器把Token交给AccountManagerService,后者写入authtokens表做缓存,再回调客户端。
这中间有一个很重要的实现细节:AccountManagerService内部通过Session机制管理异步请求。它先挂起调用方的Binder会话,等认证器跨进程回调完成后,再resolve这个会话,把Token推给客户端。如果你在读源码时看到SessionManager、onResult、onError这一系列类名,它们就是这套异步编排的载体。
3.3 添加账户:从addAccount到addAccountExplicitly的完整链路
完整添加账户的流程,是系统界面、认证器、调用方三方协作的过程:
- App调用AccountManager.addAccount(type, ...),AccountManagerService检查权限后去绑定该类型的认证器;
- 认证器的addAccount回调可以返回一个Bundle,Bundle里通常带一个PendingIntent或Intent,指向自己的登录Activity;
- 这个Intent通过AccountManager传给调用方,调用方一般会在onActivityResult里把它作为result启动;
- 用户在登录页输入账号密码,业务请求成功后,登录页调用AccountManager.addAccountExplicitly(account, password, extraData),把账户写入系统数据库;
- 系统写入成功后,会通过AccountsDb的accounts表记录账户,并向相关应用发送账户变化广播。
如果认证器不需要UI,也可以直接在addAccount回调里构造一个账户并调addAccountExplicitly,系统界面就不会弹出登录页。
addAccountExplicitly是API 23才引入的,它把“直接向系统账户池写入账户”的能力从认证器内部暴露给了普通应用。即便你的应用不是认证器提供方,只要持有权限,也可以把账户写进系统。这个能力用得好的话,可以搭建出跨应用的统一登录体系;用不好的话也容易把账户数据写脏,所以使用时要谨慎控制写入时机和权限范围。
4. 手写一个账户认证器:把AccountManager接入自家业务
4.1 认证器的工程结构:Service、XML资源与Manifest
一个标准的认证器工程包含三个部分:一个继承AbstractAccountAuthenticator的实体类、一个承载它的Service、一份描述账户类型的XML资源。
先看Manifest里的声明:
<service android:name=".NoteAuthenticatorService" android:exported="true" android:permission="android.permission.BIND_ACCOUNT_AUTHENTICATOR"> <intent-filter> <action android:name="android.accounts.AccountAuthenticator" /> </intent-filter> <meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/authenticator" /> </service>注意两个关键点:第一,android:permission必须是BIND_ACCOUNT_AUTHENTICATOR,这个权限只有系统才能持有,普通应用无法绑定你的认证器,防止恶意应用伪装;第二,intent-filter的action必须是AccountAuthenticator,AccountManagerService通过这个action和meta-data定位认证器。
然后是res/xml/authenticator.xml:
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" android:accountType="com.example.note" android:icon="@mipmap/ic_launcher" android:smallIcon="@mipmap/ic_launcher" android:label="@string/app_name" />accountType就是你的账户类型命名空间,建议用域名反写,避免冲突。label和icon会显示在系统设置里的账户列表页。
4.2 实现AbstractAccountAuthenticator:四个关键回调
现在写实体类。AbstractAccountAuthenticator是系统提供的一个抽象基类,它已经做好了Binder适配层的解析,我们只需要实现核心回调:
public class NoteAuthenticator extends AbstractAccountAuthenticator { public NoteAuthenticator(Context context) { super(context); } @Override public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) { // 构造登录Activity的Intent,通过response携带给调用方 Intent intent = new Intent(mContext, LoginActivity.class); intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); Bundle bundle = new Bundle(); bundle.putParcelable(AccountManager.KEY_INTENT, intent); return bundle; } @Override public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) { // 先从本地取refreshToken,再请求服务器换accessToken // 模拟返回一个写死的Token,方便验证链路 Bundle result = new Bundle(); result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); result.putString(AccountManager.KEY_AUTHTOKEN, "token_" + account.name); return result; } @Override public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) { Bundle result = new Bundle(); result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true); return result; } @Override public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) { return new Bundle(); } }addAccount返回的Bundle里带了登录Intent,调用方拿到后会启动它。登录成功后,LoginActivity会调用addAccountExplicitly把账户写进系统。
Service的实现更简单,onBind返回认证器的Binder即可:
public class NoteAuthenticatorService extends Service { private NoteAuthenticator mAuthenticator; @Override public void onCreate() { super.onCreate(); mAuthenticator = new NoteAuthenticator(this); } @Override public IBinder onBind(Intent intent) { return mAuthenticator.getIBinder(); } }这套骨架跑通后,你的账户类型就会出现在系统设置里,可以被任何声明了相应权限的应用读取和路由。
4.3 接入AccountManager并完成端到端验证
把认证器部署到设备后,推荐用这几步验证链路:
- 打开系统设置里的“账户”入口,能看到新增的账户类型“示例笔记”;
- 通过设置页添加账户,会跳转到你的登录Activity;
- 登录成功后,账户出现在账户列表里;
- 回到你的App里,用AccountManager读取该类型账户。
代码验证如下:
AccountManager am = AccountManager.get(this); Account[] accounts = am.getAccountsByType("com.example.note"); if (accounts != null && accounts.length > 0) { // 账户添加成功 am.getAuthToken(accounts[0], "note_auth", null, false, (AccountManagerCallback<Bundle>) future -> { Bundle result = future.getResult(); String token = result.getString(AccountManager.KEY_AUTHTOKEN); // 走通Token获取链路 }, null); }这里要提醒一件事:Demo里为了方便验证,认证器直接返回了写死Token;生产环境必须在getAuthToken里用refreshToken调自己的服务器换取新的accessToken,并且处理刷新失败、账户已失效等异常情况。
5. 同步、Token生命周期与多用户:登录之外的实战细节
5.1 SyncAdapter与AccountManager如何协同工作
系统账户和同步是一对孪生兄弟。SyncAdapter是另一套框架,它允许一个账户在后台定期与服务器同步数据。两者协作的经典方式是:AccountManager负责账户与Token,SyncAdapter负责业务数据同步。
典型场景是邮箱App:用户添加账户成功后,App调ContentResolver.requestSync触发同步;SyncAdapter在onPerformSync回调里,先从AccountManager拿当前账户,再通过getAuthToken取Token,最后带着Token请求服务器的未读邮件接口,把结果写进ContentProvider。
这里有个需要认清的边界:AccountManagerService本身不管同步策略、不管数据格式、更不知道你的业务字段。它只负责把账户和Token可靠地维持好,真正的业务同步逻辑还是在你的SyncAdapter里。这也是框架设计得很清爽的地方。
5.2 Token失效、账户变更与多账号切换
Token不是永久的,服务端随时可能让它失效。常见做法是监听下面这两个广播:
- AccountManager.ACTION_AUTH_TOKEN_CHANGED:Token刷新后系统发出的广播;
- AccountManager.ACTION_ACCOUNT_REMOVED:账户被移除后系统发出的广播。
App在收到Token失效通知时,应该清理本地Token缓存,重新走一次getAuthToken流程。很多线上问题都出在这:Token在服务端已经过期了,本地还在用旧Token请求接口,直到连续收到401才开始重新认证。与其等接口报错,不如在客户端主动监听Token变更广播,提前刷新。
多账号切换是个很容易出体验问题的点。很多App允许用户添加多个账户,但切换时只改了UI上的账号标识,Token还是原来那个,结果导致业务请求串号。我建议在切换账号时做三件事:重新用目标账户去getAuthToken、清掉全局业务缓存、通知所有业务页面刷新数据源。这三步一条链路做完,多账号切换才会真正干净。
5.3 多用户空间下账户隔离与可见性控制
AccountManagerService天然支持多用户。每个用户下有一套独立的AccountsDb,用户A的账户对用户B不可见。这包括两个层面:一是数据文件隔离,目前提到过的de和ce数据库都是per-user的,路径在/data/system/users/ /下;二是接口层面的隔离,AccountManagerService的所有账户接口都会校验调用方属于哪个userId,默认只操作当前活跃用户的数据。
账户可见性除了前面提到的系统级控制外,还有一个包名维度的配置:某个账户类型可以声明对某个包名的可见状态,可选值包括NONE、NORMAL和ALWAYS。系统设置里用户手动关闭某个应用对账户的访问,实质就是把这个状态改成了NONE。如果你在开发中遇到“同一个账户类型,有的应用能看到账户,有的应用看不到”,优先查这个可见性配置。
6. 排错与调试:把AccountManager从黑盒变成透明
6.1 高频异常与它们的真实成因
账户模块出现的线上问题通常并不复杂,但表象五花八门。我把这些年遇到的高频问题整理成一张对照表,排查时可以按图索骥:
| 现象 | 真实成因 | 解决方向 |
|---|---|---|
| getAccountsByType返回空列表 | 未声明GET_ACCOUNTS权限,或账户可见性为NONE | 检查Manifest和可见性配置 |
| SecurityException: caller uid X is not allowed | 没有AUTHENTICATE/MANAGE权限越权访问 | 补充相应权限 |
| 认证器绑定失败,添加账户无响应 | Service未导出,或缺少BIND_ACCOUNT_AUTHENTICATOR权限 | 检查认证器Manifest声明 |
| 设置了账户但同步不触发 | 未调用requestSync,或SyncAdapter不匹配 | 检查SyncAdapter的authority和accountType |
| Token失效后接口持续401 | 本地缓存未在Token变更时清理 | 监听ACTION_AUTH_TOKEN_CHANGED并重建Token |
6.2 dumpsys account:一线调试的主战场
账户模块的运行时状态,用日志看不全,直接用系统服务dump反而最直观。在命令行里执行:
adb shell dumpsys account输出会包含当前用户下的所有账户列表、每种账户类型对应的认证器包名、每个账户已授权的应用uid列表、Token缓存情况、账户可见性配置。
举个例子,排查“某个App为什么读不到账户”时,先看输出里有没有这个账户,再看该App的uid和包名是否在授权名单里,最后查可见性字段。三步下来基本能定位是数据没写入、权限不够还是可见性被关掉。这个命令也可以配合具体UID过滤,比如:
adb shell dumpsys account | grep your.package.name6.3 一次“账户同步失败”的完整排查链路
下面用案例说明排查思路。某企业应用反馈,部分用户出现“已登录但无法同步数据”的问题。
排查链路按顺序走:
- 先用dumpsys account确认账户存在,且账户类型正确;
- 再用dumpsys account看authtoken是否还在本地缓存中;
- 对比服务器日志,确认Token是否已被服务端判失效;
- 检查客户端是否监听了Token变更广播;
- 把问题定位为“Token在服务端已过期,但客户端缓存的Token仍然在本地,没有触发重新认证”;
- 修复方式是增加401拦截后强制刷新Token,同时在Token变更广播里清理缓存。
排查终点通常不是“去服务器重置Token”,而是“让客户端具备Token失效的自愈能力”。AccountManagerService已经提供了Token缓存和变化广播,剩下要做的,是让业务逻辑在Token生命周期变化时能够及时响应。
这套链路走下来,你会发现AccountManagerService虽然叫Service,但它的难点根本不在服务本身,而在“系统如何与第三方认证器协作”的机制设计上。把账户类型、可见性、权限、Token生命周期这几个概念串起来,账户模块就不再是一个黑盒了。尤其是做多应用统一登录的时候,先向系统注册自己的账户类型,再让需要登录的App通过AccountManager去读取和获取Token,整个体系会比自建登录态清晰得多可靠得多。