前言:SSO 是什么

SSO(Single Sign-On,单点登录)本质上是一种票据中转机制:用户在公司统一登录页输入密码,登录页生成一张一次性凭证(ticket),App 拿到这张票去服务器换取真正的身份 Cookie。全程用户的密码只交给公司统一登录页,App 本身永远看不到密码。


一、参与方

在理解流程之前,先明确有哪些系统参与了这件事:

角色 是什么 地址(生产环境)
SSO 服务 公司统一登录页,负责验证密码并颁发 ticket https://sso.example.com
UserService 公司内部用户体系服务,负责用 ticket 换 Cookie https://userservice.example.com
MyApp App 公司员工用的内部 Android App
AuthCallbackServer App 内部在 127.0.0.1 上开的临时 HTTP 服务器 http://127.0.0.1:{随机端口}/callback
系统浏览器(Chrome 等) 用来打开 SSO 登录页,让用户输密码

二、完整时序图(文字版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
用户              App(UI)          AuthViewModel     AuthRepository      系统浏览器          SSO服务            UserService
| | | | | | |
| 点击"SSO登录" | | | | | |
|---------------->| | | | | |
| | startSsoLogin() | | | | |
| |------------------>| | | | |
| | | prepareSsoLogin()| | | |
| | |----------------->| | | |
| | | | 开ServerSocket | | |
| | | | 构造SSO URL | | |
| | | | | | |
| | | SsoEffect. | | | |
| | | LaunchUrl(url) | | | |
| |<------------------| | | | |
| | startActivity | | | | |
| |------------------------------------------------->| | |
| | | | | | |
| | | awaitSsoCallback()| | | |
| | |----------------->| | | |
| | | | accept()阻塞等待 | | |
| | | | | | |
| | | | | 打开SSO登录页 | |
| 输入账号密码 | | | |----------------->| |
|------------------------------------------------------------------------------------------------>| |
| | | | | | 验证密码 |
| | | | | | 颁发ticket |
| | | | | redirect到 | |
| | | | | 127.0.0.1/callback?ticket=xxx |
| | | |<-----------------| | |
| | | | 收到HTTP请求 | | |
| | | | 提取ticket | | |
| | | | 返回成功HTML | | |
| | | |----------------->| | |
| 看到"登录成功"页| | | | | |
|<--------------------------------------------------------| | | |
| | | | | | |
| | | | exchangeTicket() | | |
| | | |---------------------------------------------------->|
| | | | | | 验证ticket |
| | | | | | 返回Cookie+用户信息
| | | |<----------------------------------------------------|
| | | | 存加密本地存储 | | |
| | 状态→Idle | | | | |
| |<------------------| | | | |
| 进入主界面 | | | | | |
|<-----------------| | | | | |

三、逐步拆解每个环节

第 1 步:用户点击按钮

LoginScreen.kt 中的 SSO 登录按钮绑定了 onSsoLoginClick 回调,点击后调用:

1
authViewModel.startSsoLogin(subsystemAlias = "userservice")

subsystemAlias 固定为 “userservice”,用于告知 SSO 服务和 UserService 这次登录属于哪个子系统。


第 2 步:并发保护检查

startSsoLogin 第一件事就是防止重复触发:

1
2
3
4
5
6
7
fun startSsoLogin(subsystemAlias: String) {
if (ssoLoginJob?.isActive == true || _ssoLoginState.value.isBusy()) {
return // ← 直接忽略,什么都不做
}
_ssoLoginState.value = SsoLoginState.LaunchingLogin
ssoLoginJob = viewModelScope.launch { runSsoLogin(subsystemAlias) }
}

“忙碌”的定义是:当前状态是 LaunchingLogin、HandlingCallback、LaunchingImAuthorization、HandlingImCallback 其中任意一种。也就是说,从点击按钮到登录完成的整个过程中,再次点击都会被静默忽略。


第 3 步:准备阶段——在本机开一个 HTTP 服务器

这是 SSO 流程最特殊的地方,也是和 IM 登录最大的区别。

authRepository.prepareSsoLogin(“userservice”) 做了这几件事:

3a. 关闭旧服务器(防止上次未正常关闭留下的残留)

1
callbackServer?.close()

3b. 创建新的 AuthCallbackServer

1
2
val server = AuthCallbackServer()
callbackServer = server

AuthCallbackServer 的构造函数:

1
2
3
4
5
6
7
8
9
10
11
private val serverSocket = ServerSocket(
0, // ← 端口 0 = 让 OS 随机分配一个空闲端口
1, // ← backlog=1,同时只接受 1 个连接
InetAddress.getByName("127.0.0.1") // ← 只监听本机回环地址,外部无法访问
)
val port: Int = serverSocket.localPort // 拿到 OS 分配的实际端口,比如 54321
val callbackUrl: String = "http://127.0.0.1:$port/callback"

init {
serverSocket.soTimeout = 5 * 60 * 1000 // 5 分钟超时
}

为什么要在本机开服务器?

SSO 的安全机制要求 ticket 必须被”redirect”回一个预先登记的地址(service 参数)。桌面端可以用 localhost,移动端也用同样的思路——在本机开一个临时 HTTP 服务器监听回调。

3c. 构造 SSO 登录 URL

1
2
3
4
fun buildLoginUrl(callbackUrl: String, subsystemAlias: String): String {
val encodedService = URLEncoder.encode(callbackUrl, "UTF-8")
return "${config.ssoHost}?service=$encodedService&subsystem=$subsystemAlias"
}

最终 URL 长这样(生产环境):

1
2
3
https://sso.example.com
?service=http%3A%2F%2F127.0.0.1%3A54321%2Fcallback
&subsystem=userservice

这个 URL 告诉 SSO 服务:用户登录成功后,把 ticket 带到 http://127.0.0.1:54321/callback 这个地址。


第 4 步:打开系统浏览器

prepareSsoLogin 返回 URL 后,ViewModel 通过 Channel 发出副作用:

1
2
_ssoEffect.send(SsoEffect.LaunchUrl(url))
_ssoLoginState.value = SsoLoginState.HandlingCallback // 状态更新

LoginScreen.kt 中的 LaunchedEffect 收到后:

1
2
3
is SsoEffect.LaunchUrl -> {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(effect.url)))
}

此刻 App 切到后台,系统浏览器打开了 https://sso.example.com?service=...&subsystem=userservice。

💡 深入:startActivity 打开浏览器的底层机制

Intent.ACTION_VIEW 配合 https:// URL,Android 系统会查询已安装 App 中哪些声明了处理 https scheme 的 Activity(通常是 Chrome 或系统默认浏览器),然后启动它。这和你在手机上点一个网页链接完全一样——App 本身不渲染页面,只是把 URL 丢给系统,自己退到后台。

此时 App 进入后台,但协程仍在 Dispatchers.IO 线程上挂起等待 ServerSocket.accept(),App 进程并没有被销毁。

📦 深入:TCP 只管传输,不管消费——为什么必须主动 read()?

一个常见误解:「连接建立了,数据不就被消费了吗?」——并不是。

TCP 做的事只是:把字节从发送方的内核发送缓冲区,搬到接收方的内核接收缓冲区。搬完之后 TCP 的工作就结束了。数据躺在接收缓冲区里,等应用程序来主动取

类比:TCP 是快递员,接收缓冲区是门口的快递柜。快递员把包裹放进柜子之后,你得自己去开柜取包裹——快递员不会帮你搬进房间,也不知道你有没有取。柜子满了他就没地方放新包裹,只能在外面等(TCP 流量控制)。

所以 accept() 只是「接受了连接」,数据还在缓冲区里:

这也是 headers 必须全部读完的根本原因:如果缓冲区里还有未读的 headers,缓冲区可能满,TCP 流量控制会让浏览器的发送窗口缩为 0,发送方卡住等接收方腾空间,而接收方又在等浏览器读响应——双方互相等待,死锁。读完所有 headers 是消除这个隐患的防御性写法。


第 5 步:用户在浏览器输入账号密码

这一步完全在浏览器内发生,App 对此一无所知,也看不到任何密码。

SSO 服务验证密码成功后,做两件事:

  1. 生成一个一次性 ticket(例如 ST-12345-abcdef-sso)

  2. 把浏览器 redirect 到:

1
http://127.0.0.1:54321/callback?ticket=ST-12345-abcdef-sso

这个 redirect 实际上是浏览器发起的一次 HTTP 请求,目标是本机的 ServerSocket。

💡深入:302 重定向到本机的完整过程

这里没有任何魔法,就是最普通的 HTTP 302 重定向。

用户点击「确认登录」后,浏览器向 SSO 服务提交表单(POST 请求)。SSO 服务验证密码正确后,不返回 200,而是返回 302

浏览器收到 302,会自动向 Location 里的地址发起一个新的 GET 请求:

这个请求的目标是本机 127.0.0.1,走的是回环网卡(loopback),不经过任何网络,直接被同一台设备上正在 accept() 的 ServerSocket 接住。浏览器完全不知道对面是一个 Android App,它只认 HTTP 协议——只要对方在那个端口上,能建立 TCP 连接、返回合法的 HTTP 响应,浏览器就会处理它。


第 6 步:AuthCallbackServer 接收 ticket

就在用户登录的同时,App 在 Dispatchers.IO 上挂起等待:

1
2
3
4
5
6
7
8
9
10
11
12
13
override suspend fun awaitSsoCallback(): Result<Unit> {
acquireWakeLock() // ← 先拿 WakeLock,防止 CPU 休眠
return try {
runCatching {
val server = callbackServer ?: error("No SSO callback server running")
val result = server.awaitCallback() // ← 阻塞在这里等浏览器回来
callbackServer = null
exchangeTicket(result.ticket, result.callbackUrl).getOrThrow()
}
} finally {
releaseWakeLock()
}
}

WakeLock 的作用:Android 系统可能在屏幕关闭时让 CPU 进入休眠,导致 ServerSocket.accept() 永远不会被唤醒。PARTIAL_WAKE_LOCK 让 CPU 保持运行,超时设置 6 分钟(略长于服务器的 5 分钟超时)。

awaitCallback() 内部的 HTTP 解析过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 读取请求行
val requestLine = reader.readLine()
// 内容类似: "GET /callback?ticket=ST-12345-abcdef-sso HTTP/1.1"

// 2. 消耗所有 header 行(必须读完,否则浏览器会一直等待)
while (true) {
val line = reader.readLine() ?: break
if (line.isEmpty()) break // 空行 = headers 结束
}

// 3. 解析路径和 ticket 参数
val ticket = parseQueryParam(queryString, "ticket")
// ticket = "ST-12345-abcdef-sso"

// 4. 向浏览器返回成功 HTML
sendResponse(writer, SUCCESS_HTML)
// HTML 里有 <meta http-equiv="refresh" content="2;url=myapp://auth/callback">
// 用户会看到"登录成功,正在返回 myapp..."

// 5. 返回结果
CallbackResult(ticket = ticket, callbackUrl = "http://127.0.0.1:54321/callback")

💡深入:ServerSocket 为什么能控制浏览器显示什么?

浏览器向 127.0.0.1:54321 发出 GET 请求后,会等待对方返回 HTTP 响应,再把响应 body 渲染出来。AuthCallbackServer 里的 sendResponse() 手动拼了一个合法的 HTTP 响应:

浏览器收到这个响应,看到 Content-Type: text/html,就按 HTML 把 body 渲染出来——也就是用户看到的「登录成功,正在返回 myapp…」页面。AuthCallbackServer 扮演的角色就是一个只活一次的微型 HTTP 服务器,接完 ticket 就关门,顺手把成功页发给浏览器。

为什么 headers 必须全部读完? TCP 是双向流。浏览器把请求发完后,要等服务端把请求全部读走,才会开始等响应。如果 App 不消耗完 header 行就直接写响应,可能触发 TCP 流量控制,双方互相等待(死锁)。所以代码里有那段 while 循环,必须一行一行读到空行才能写响应。

callbackUrl 为什么要带回去? UserService 的 internal_login 接口需要用它做二次校验——确认 ticket 确实是颁发给这个地址的,防止 ticket 被其他人截获后拿来冒用。


第 7 步:用 ticket 换 Cookie(exchangeTicket)

这是整个流程的核心兑换步骤:

1
2
3
4
5
6
7
8
POST https://userservice.example.com/auth/internal_login
Content-Type: application/json

{
"ticket": "ST-12345-abcdef-sso",
"validateService": "http://127.0.0.1:54321/callback",
"subsystemAlias": "userservice"
}

UserService 收到请求后:

  1. 拿着 ticket 去 SSO 服务验证(这是服务端对服务端的校验,客户端看不到)

  2. 验证 validateService 与当初颁发 ticket 时记录的 service 一致

  3. 查出对应的用户信息

  4. HTTP 响应头里下发 Set-Cookie,在 响应体里返回用户数据

响应示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
HTTP/1.1 200 OK
Set-Cookie: app-access-token-prod=eyJhbGciOi...; Path=/; Domain=.example.com; Secure; HttpOnly
Content-Type: application/json

{
"data": {
"name": "张三",
"email": "zhangsan@example.com",
"userNameAlias": "zhangsan",
"userId": "xxx",
"access-token": "eyJhbGciOi..."
}
}

兼容性处理:部分 UserService 环境只在 JSON 里返回 access-token,不下发 Set-Cookie。CookieCodec.withAccessTokenFallback 会把 JSON 里的值补合成一条标准格式的 Set-Cookie:

1
2
// 如果 Set-Cookie 里没有 access token,就自己造一条
"app-access-token-prod=eyJhbGciOi...; Path=/; Domain=.example.com; Secure; HttpOnly"

用户信息解析:兼容两种响应格式

  • internal_login:data.name、data.email 直接在 data 下

  • user_info(会话恢复时用):data.internalUser.name、data.internalUser.email 在嵌套层


第 8 步:持久化登录态

凭据换取成功后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 把 Cookie 列表和用户信息存入 EncryptedSharedPreferences
saveSsoSession(cookies, user)
// 用 AES256-GCM 加密,key 用 Android Keystore 保护

// 2. 更新内存状态
_authSnapshot.value = AuthSnapshot(
stage = Stage.SSO_AUTHENTICATED,
ssoSession = SsoSession(cookies, user, updatedAt = System.currentTimeMillis()),
gatewayCredential = null,
)

// 3. 尝试拉取 Gateway 凭据(失败不影响登录成功)
refreshGatewayCredential()
// POST /instance/get → 获取 Gateway URL 和 token
// 成功后 stage 升为 GATEWAY_READY

存储的 key 一览:

1
2
3
4
5
"sso_cookies_json"   → JSON 序列化的 Cookie 字符串列表
"user_snapshot_json" → JSON 序列化的用户信息
"auth_env" → 当前环境(PROD/BETA/SIT)
"gateway_url" → Gateway 地址
"gateway_token" → Gateway token

第 9 步:状态回到 Idle,UI 跳转主界面

1
_ssoLoginState.value = SsoLoginState.Idle

LoginScreen 收到状态变化,跳转主界面。整个 SSO 流程结束。


四、各种失败场景的处理

场景 A:用户 5 分钟内没有完成登录

ServerSocket.soTimeout = 5 * 60 * 1000,超时后 accept() 抛出 SocketTimeoutException,被 runCatching 捕获,状态切为 Error,显示”登录失败”。

场景 B:用户在 SSO 页面点了取消/关闭了 WebView

cancelSsoLoginIfWaiting() 被 UI 调用:

1
2
3
4
5
6
7
8
9
fun cancelSsoLoginIfWaiting() {
if (!_ssoLoginState.value.isBusy()) return
try {
authRepository.cancelSsoLogin() // 关闭 ServerSocket
} finally {
ssoLoginJob?.cancel() // 取消协程
_ssoLoginState.value = SsoLoginState.Idle
}
}

场景 C:ticket 已过期或被用过(一次性)

UserService 返回非 2xx,error(“internal_login failed: ${response.code}”) 被 runCatching 捕获,状态切为 Error。

场景D:协程被取消(App 进后台被系统杀死等)

CancellationException 被单独捕获,状态回 Idle 并重新 throw,确保协程取消语义正确:

1
2
3
4
} catch (e: CancellationException) {
_ssoLoginState.value = SsoLoginState.Idle
throw e // ← 必须重新抛出,否则协程无法正常取消
}

五、下次启动如何恢复登录态(restoreSession)

App 不是每次都要走完整的 SSO 流程。下次启动时:

1
2
3
4
5
1. 从 EncryptedSharedPreferences 读出 cookies + user
2. POST /auth/user_info(带上已存的 Cookie)
→ UserService 验证 Cookie 是否仍有效
3. 有效 → 直接恢复 SSO_AUTHENTICATED,跳过登录页
4. 无效 → clearAuth(),显示登录页,要求重新 SSO

旧版本兼容:如果本地只有旧二维码凭据(gateway_url 有值但 sso_cookies_json 为空),说明是从旧版本升级来的用户,直接清除并强制重新登录。


六、一句话总结每个类的职责

类/文件 职责
LoginScreen.kt 按钮点击 → 调 ViewModel;收 SsoEffect → 打开浏览器
AuthViewModel 状态机控制;协程生命周期管理;并发保护
AuthRepositoryImpl 实现所有登录逻辑;持久化;协调各 API 类
AuthCallbackServer 在 127.0.0.1 开临时 HTTP 服务器,等浏览器回调,提取 ticket
AuthApi 封装两个 UserService HTTP 接口:internal_login(换 Cookie)和 user_info(校验 Cookie)
AuthEnvConfig 管理不同环境(PROD/BETA/SIT)的服务地址和 Cookie 名
CookieCodec Set-Cookie 字符串的编解码工具;兜底合成缺失的 access token Cookie
SsoLoginState 登录流程的状态枚举
SsoEffect UI 副作用通道(LaunchUrl / LaunchExternalUrl)

七、IM 登录(App-to-App)完整流程

与 SSO 登录的本质区别

SSO 登录的回调靠的是 HTTP redirect → 本机 ServerSocket,整个过程在当前 App 进程里闭环。

IM 登录完全不同:App 跳出去,IM App 完成授权,再用 deep link 把 token 直接带回来。没有 ServerSocket,没有 HTTP 服务器,中间 App 可以被系统暂停甚至进程重启,token 依然能安全回来。

第 1 步:用户点击「IM 登录」

1
authViewModel.startImAuthorization()

同样先做并发保护检查,忙碌状态直接忽略。通过后状态切到 LaunchingImAuthorization。

第 2 步:prepareImAuthorization —— 构造授权 URL

① 生成防重放的 state

1
2
val state = UUID.randomUUID().toString().replace("-", "")
// 例如:a1b2c3d4e5f6...(32 位随机字符串)

state 的作用是防止 CSRF 攻击——确保回跳回来的数据确实是这次授权发出去的,不是别人伪造的。

② state 双重持久化

1
2
pendingImAuthorizationState = state                              // 内存(进程存活时用)
prefs?.edit()?.putString(KEY_PENDING_IM_STATE, state)?.apply() // 加密磁盘(进程重启后也能校验)

为什么要存磁盘?因为跳到 IM App 后,Android 系统可能把 MyApp 进程回收(内存不足)。用户在 IM App 里完成授权、回跳时进程重新启动,内存里的 state 已经没了,必须从磁盘读回来校验。

③ 构造 imapp://corp-im/auth?… URL

ImAuthorizationUrlCodec.buildAuthorizationUrl() 把所有参数编码进 URL:

1
2
3
4
5
6
7
8
9
10
11
12
imapp://corp-im/auth
?callback=myapp%3A%2F%2Fauth-callback%3Fstate%3D...%26provider%3Dhi
&state=a1b2c3d4e5f6...
&subsystem=myapp
&source=android
&mode=local_token
&cookie_name=app-access-token-prod
&client_name=MyApp
&client_icon_url=https://...(myapp 的图标)
&client_subtitle=使用当前 IM 账号继续访问 MyApp
&client_body=MyApp 仅会使用本次授权完成企业登录和工作区连接。
&consent_version=myapp_mobile_v1
参数 含义
callback 授权完成后 IM App 把 token 带到这个 URL 回跳,scheme 是 myapp://
state 防重放标识,IM App 必须原样带回
mode=local_token 告诉 Hi:不走服务端接口,直接在本地把 token 放进回调 URL
cookie_name 告诉 Hi 要回传哪个 Cookie 的 token
client_* IM 授权页展示的 App 信息(图标、名称、说明文字)

第 3 步:跳转 IM App

1
_ssoEffect.send(SsoEffect.LaunchExternalUrl(url))

UI 层收到后:

1
2
3
4
5
6
7
is SsoEffect.LaunchExternalUrl -> {
try {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(effect.url)))
} catch (e: ActivityNotFoundException) {
authViewModel.handleImAuthorizationLaunchFailed() // IM 未安装兜底
}
}

imapp:// 是 IM App 注册的 URL scheme,Android 系统找到 IM App 并把它拉起来。此时 MyApp 退到后台,不需要等待,没有挂起的协程,没有 ServerSocket

第 4 步:在 IM App 里发生了什么

这部分完全在 IM App 内部,MyApp 代码看不到,但从参数可以推断:

  1. IM App 解析 imapp://corp-im/auth?… 里的参数

  2. 展示授权确认页(用 client_name、client_icon_url、client_subtitle、client_body 渲染)

  3. 用户点击「同意授权」

  4. IM App 从本地取出当前登录用户的 access token(mode=local_token 决定了这一步在本地完成,不走网络)

  5. IM App 把 token 和用户信息拼入 callback URL,用 startActivity 打开它:

1
2
3
4
5
6
7
8
9
10
myapp://auth-callback
?state=a1b2c3d4e5f6... ← 原样带回,用于校验
&provider=im
&email=zhangsan@example.com
&userserviceAccessToken=eyJhbGci... ← access token
&name=张三
&userNameAlias=zhangsan
&avatar=https://...
&userId=xxx
&sessionId=yyy

myapp:// 是 MyApp 注册的 URL scheme。Android 系统收到这个 Intent,把 MyApp 拉到前台,把整个 URL 传给 MainActivity。

App 层拿到 callbackUrl 后调用:

1
authViewModel.handleImAuthorizationCallback(callbackUrl)

状态切到 HandlingImCallback。

第 6 步:parseCallback —— 校验 + 解析

ImAuthorizationUrlCodec.parseCallback() 做了一系列防御性检查,任何一步失败就返回 Failure

1
2
3
4
5
6
7
8
9
10
11
12
13
① scheme 必须是 myapp,host 必须是 auth-callback
↓ 通过
② pendingState 不能为空(内存 or 磁盘读回)
↓ 通过
③ provider 必须是 "im"
↓ 通过
④ 检查 error / error_description 参数(IM 授权失败时会带这两个)
↓ 无错误
⑤ state 必须与 pendingState 完全一致(防 CSRF 核心)
↓ 一致
⑥ email 和 access token 不能为空
↓ 都有值
⑦ 返回 Success,携带所有字段

token 字段的多键名兼容:IM App 不同版本回传的字段名可能不同,代码做了多键名兼容:

1
2
private val tokenKeys = listOf("userserviceAccessToken", "access_token", "accessToken", "ssoAccessToken")
private val emailKeys = listOf("email", "userEmail", "mail")

URL 参数位置的双重解析:token 既可能在 query string(?key=value),也可能在 fragment(#?key=value),代码两个位置都解析,query 优先级高于 fragment:

1
2
3
4
5
private fun parseParams(uri: URI): Map<String, String> {
val queryParams = parseQuery(uri.rawQuery)
val fragmentParams = parseFragment(uri.rawFragment)
return fragmentParams + queryParams // query 覆盖 fragment
}

第 7 步:构造 Cookie(IM 登录专有)

与 SSO 登录最大的区别在这里:SSO 的 Cookie 由 UserService 服务端下发,IM 登录的 Cookie 由客户端本地构造

1
2
3
4
5
6
7
private fun buildImAuthorizationCookies(result: ImAuthorizationCallbackResult.Success): List<String> =
buildList {
add("${result.cookieName}=${result.accessToken}; Path=/; Domain=.example.com; Secure; HttpOnly")
if (result.sessionId.isNotBlank()) {
add("session_id=${result.sessionId}; Path=/; Domain=.example.com; Secure; HttpOnly")
}
}

构造完之后,持久化 + 更新状态 + 拉 Gateway 凭据,和 SSO 登录完全一样。

IM 登录 vs SSO 登录核心差异

对比维度 SSO 登录 IM 登录
用户操作 在浏览器输账号密码 在 IM App 点「同意授权」
回调机制 HTTP redirect → 本机 ServerSocket deep link → MainActivity
token 来源 UserService 服务端 Set-Cookie 下发 IM App 本地填入回调 URL
进程存活要求 必须一直活着(WakeLock 保活) 可被杀死,state 持久化到磁盘
超时机制 5 分钟 ServerSocket 超时 无超时,依赖 Hi 响应
防重放 靠本机端口唯一性 UUID state + 磁盘持久化双重校验

八、URL Scheme 注册与系统路由机制

注册方式:AndroidManifest.xml 中的 Intent Filter

App 安装时,Android 系统扫描 AndroidManifest.xml,把所有 <intent-filter> 登记进系统的包管理器数据库(PackageManager)。MyApp 在 MainActivity 上声明了两条 deep link:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- IM App-to-App 授权回调 -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="auth-callback" />
</intent-filter>

<!-- SSO 回调(冗余保留)-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="auth" android:pathPrefix="/callback" />
</intent-filter>

两条都用 myapp://,靠 host 区分用途:

  • auth-callback → IM 登录回调
  • auth + /callback 路径前缀 → SSO 相关回调

系统如何路由这个 URL?

IM App 完成授权后,执行的代码等价于:

1
2
3
// IM App 内部
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("myapp://auth-callback?state=xxx&..."))
startActivity(intent)

Android 系统收到后按以下流程处理:

1
2
3
4
5
1. 从 Intent 里提取:action=VIEW,scheme=myapp,host=auth-callback
2. 查 PackageManager:谁注册了能处理这个 Intent 的 Activity?
3. 找到 MyApp 的 MainActivity
4. 进程还活着 → 拉到前台,调 onNewIntent(intent)
进程被杀了 → 重启进程,调 onCreate(intent)

launchMode="singleTop" 的关键作用

1
2
3
4
<activity
android:name=".MainActivity"
android:launchMode="singleTop"这里
...>

singleTop 的意思:如果返回栈顶已经有一个 MainActivity 实例,不新建,直接复用,通过 onNewIntent() 把新 Intent 传进来。

如果用默认的 standard 模式,每次 deep link 回来都会创建新的 MainActivity 实例,用户返回时会出现异常的页面栈。

<queries> 声明是干什么的?

1
2
3
4
5
6
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="imapp" />
</intent>
</queries>

这是反向的:MyApp 要跳出去打开 IM App(imapp://)。Android 11+ 引入了包可见性限制——App 默认看不到其他 App 的存在。声明 <queries> 之后,MyApp 才能:

  • 通过 packageManager.resolveActivity() 检查 IM App 是否已安装
  • startActivity(Intent(ACTION_VIEW, Uri.parse("imapp://..."))) 成功跳转

不加这个声明,startActivity 会直接抛 ActivityNotFoundException,触发代码里 IM App 未安装的兜底分支。

两个 App 的双向关系

1
2
3
4
5
6
7
8
9
10
11
12
13
MyApp                                     IM App
│ │
│ 注册 intent-filter: │ 注册 intent-filter:
│ myapp://auth-callback │ imapp://corp-im/auth
│ │
│ <queries> 声明能看见 imapp:// │
│ │
│──── startActivity(imapp://...) ───────>│ 被系统拉起,展示授权页
│ │
│<─── startActivity(myapp://...) ────────│ 授权完成,回跳
│ │
│ onNewIntent() 收到 deep link │
│ parseCallback() 校验 state │

系统扮演的角色是路由器:谁注册了能处理这个 URL scheme 的 Activity,Intent 就发给谁。两个 App 通过 scheme 互相「拨号」,系统负责「接线」。

runBlocking 的阻塞实现原理是 Kotlin 协程基础架构中非常精妙的一部分。它并不是通过简单的“忙等待”(busy-wait)来实现阻塞,而是通过在当前线程上启动一个事件循环(Event Loop) 来实现的。

下面我们分层来解析它的实现原理。

核心原理:线程局部事件循环 (Thread-Local Event Loop)

当调用 runBlocking 时,它的核心工作是在当前线程上安装并运行一个事件循环。这个事件循环负责调度和执行其代码块内所有的协程任务,直到所有任务完成。

1. 阻塞的入口:runBlocking 调用

当你调用 runBlockBlocking { ... } 时,会发生以下几步:

a. 创建事件循环和调度器

-

runBlocking 会创建一个 BlockingEventLoop 作为当前线程的局部事件循环。

-

同时,它会创建一个 BlockingCoroutine 实例,它使用这个新创建的事件循环作为其协程上下文中的 ContinuationInterceptor(调度器)。

b. 启动协程

  • 你传入的 lambda 代码块被包装成一个协程(StandaloneCoroutine),并在这个新创建的、使用阻塞事件循环的上下文中启动。

c. 进入事件循环

-

这是最关键的一步。runBlocking 会调用 eventLoop.join() 或类似的机制(在旧版本中是 joinBlocking)。

-

这个 join() 方法会启动一个 while 循环,这个循环会持续运行,直到满足两个条件:

1.

事件循环中没有可立即执行的任务(协程)。

2.

事件循环已经退出isCompleted 为 true),即所有子协程都已完成。

2. 事件循环的内部工作方式

这个 while 循环就是“阻塞”发生的地方,但它内部是高效的:

|

1
// 概念上的伪代码,简化自 kotlinx.coroutines.EventLoopImplBaseprivate fun joinBlocking() {    val eventLoop = ThreadLocalEventLoop.current() as BlockingEventLoop    while (true) {        // 1. 检查是否所有工作都已完成        if (eventLoop.isCompleted) break // 条件2满足,退出循环,解除阻塞        // 2. 处理所有队列中的任务        val task = eventLoop.processNextEvent()        if (task != null) {            // 执行任务(例如,恢复一个挂起的协程)            task.run()        } else {            // 3. 如果没有立即要处理的任务,但事件循环还未结束            // 就让线程真正地“睡眠”一小段时间,而不是忙等待            // 在JVM上,这可能会调用 LockSupport.parkNanos()            eventLoop.parkNanos()         }    }}

|

-

有任务时:循环会不断地从事件队列中取出任务(例如,一个 delay 定时器到期了,需要恢复某个协程)并执行它。此时线程是在“工作”的。

-

无任务且未完成时:线程会通过 parkNanos 等方法被短暂挂起,避免 100% 的 CPU 占用。一旦有新任务被提交到事件循环(例如,一个定时器到期),线程会被唤醒并继续工作。

3. 如何结束阻塞?

当 runBlocking 代码块内部的所有协程(包括所有子协程)都执行完毕后,根协程的状态会变为“已完成”。这会触发事件循环将自己的 isCompleted 标志设置为 true

外层的 while 循环检测到这个条件后,就会 break 退出循环。runBlocking 方法随之返回,调用线程得以继续执行后面的代码。阻塞解除

与挂起 (Suspend) 的关键区别

为了更好理解,我们对比一下 runBlocking 和普通协程构建器(如 launch)的区别:

|
特性
| runBlocking
| launch / async
|

|
线程行为
| 阻塞 (Block)
| 挂起 (Suspend)
|

|
实现机制
| 在当前线程启动一个事件循环并运行 while 循环。
| 将协程体包装成状态机,通过 Continuation.resumeWith() 在调度器线程池中执行。
|

|
资源占用
| 占用调用线程,但通过事件循环高效调度,无任务时线程会短暂休眠。
| 不占用调用线程。当协程挂起时,底层线程立即被释放,可去执行其他任务。
|

|
使用场景
| 主函数、测试、集成阻塞代码与协程世界。
| 常规的异步、并发编程。
|

-

挂起:是协程的行为。一个挂起函数释放了它当前占用的线程,当结果准备好时,它会在另一个线程(由调度器决定)上恢复执行。

-

阻塞:是线程的行为。一个阻塞操作占用了线程,在操作完成之前,这个线程不能做任何其他事情。runBlocking 的“阻塞”是阻塞在它自己的事件循环上。

总结:runBlocking 的阻塞原理

1.

安装事件循环runBlocking 在当前线程上设置了一个专属于它的 **BlockingEventLoop**。

2.

运行循环至完成:它启动一个 while 循环,这个循环会持续运行,处理事件循环中的任务(恢复协程、执行定时操作等)。

3.

高效等待:循环在没有立即任务但协程未全部完成时,会通过 park 等操作让线程短暂休眠,避免CPU空转。

4.

退出条件:一旦它内部的所有协程工作完成,循环退出,runBlocking 返回,线程解除阻塞。

所以,runBlocking 的阻塞是一种有生产力的阻塞——线程虽然被“卡”在了一个循环里,但这个循环正在高效地驱动着整个协程世界的运转。这正是它不能用在已有协程环境中的原因,因为你会把一个本该用于处理大量协程的线程(如 Dispatchers.IO 中的线程)浪费在运行这一个事件循环上。

由Deepseek生成,已理解。

获取trace文件

1、获取ANR日志 adb bugreport,导出trace文件,分析trace, pid, tid

2、解压日志后/data/anr路径下根据发生时间选择对应文件

3、—– pid 17172 at 2025-07-08 15:53:02 —–

根据当前pid分析出17172进程出现问题并且记录发生时间

分析ANR

  1. 通过进程ID17172搜索定位到

|

1
"main" prio=5 tid=1 Native | group="main" sCount=1 dsCount=0 flags=1 obj=0x7261bba8 self=0x7e35a97c00 | sysTid=17472 nice=-10 cgrp=default sched=0/0 handle=0x7e37005ed0 | state=S schedstat=( 13473368664 317028758 13398 ) utm=1066 stm=281 core=5 HZ=100 | stack=0x7fdb9c2000-0x7fdb9c4000 stackSize=8192KB | held mutexes=

|

  1. 解释字段含义
    “main” prio=5 tid=1 Native
    |
1
"main":线程名称(主线程)。 prio=5:线程优先级(5 是默认优先级,范围 1~10,值越大优先级越高)。 tid=1:线程 ID(1 通常是主线程)。 Native:线程当前在执行 本地代码(JNI/NDK),而非 Java/Kotlin 代码。

|

group=”main” sCount=1 dsCount=0 flags=1 obj=0x7261bba8 self=0x7e35a97c00

|

1
group="main":线程所属组(主线程组)。 sCount=1:线程被挂起(suspend)的次数。dsCount=0:调试器挂起次数(0 表示未被调试器暂停)。 flags=1:线程状态标志(1 通常表示线程处于活跃状态)。 obj=0x7261bba8:线程关联的 Java 对象地址。 self=0x7e35a97c00:线程本身的 Native 地址。

|

sysTid=17172 nice=-10 cgrp=default sched=0/0 handle=0x7e37005ed0

|

1
sysTid=17172:系统级线程 ID(与 ps -t 命令看到的 ID 一致)。 nice=-10:线程的调度优先级(-20~19,值越小优先级越高,0 是默认值)。 cgrp=default:线程所属的 CPU 调度组(默认组)。 sched=0/0:调度策略(0 表示 SCHED_NORMAL,即普通优先级)。 handle=0x7e37005ed0:线程句柄的内存地址。

|

state=S schedstat=( 13473368664 317028758 13398 ) utm=1066 stm=281 core=5 HZ=100

|

1
state=S:线程当前状态(S 表示休眠/Sleeping,其他常见状态:R=运行中,D=不可中断休眠)。 schedstat=( 13473368664 317028758 13398 ):调度统计信息,格式为 (运行时间ns, 等待时间ns, 执行次数)。 utm=1066 stm=281:线程在用户态(utm)和内核态(stm)的累计运行时间(单位:jiffies,1 jiffy=10ms)。 core=5:线程最后运行的 CPU 核心编号。 HZ=100:系统时钟频率(100 表示每秒 100 次 tick)。

|

|
字段
| 当前值
| 正常参考值
| 问题指示
|

|
nice
| -10
| 0
| 异常提高优先级,需审查代码
|

|
schedstat
| 13.47秒运行
| <100ms
| 主线程重度占用 CPU
|

|
utm/stm
| 10.66s/2.81s
| <1s
| 存在未异步化的耗时任务
|

人际交往的基本技巧

1. 如果想采蜂蜜,就不要捅蜂窝

动物对那些它们认为是正确的行为的学习会十分迅速,而且记得更加牢固。这远比通过惩罚它们的错误行为来教育它们更为迅速有效。

人们厌恶批评就如同渴望认可一样。

批评带来的怨恨会打击员工的积极性,影响你和家人、朋友的感情。同时,你所批评的境况仍得不到改善。

己所不欲,勿施于人。

2. 每个人都渴望得到赞美

所有和我们共事的人都是普普通通的人,他们都渴望得到肯定。每个灵魂都渴望获得赞美。

普天之下,只有一个方法可以让别人做事,那就是让别人想做这件事。

每个人对赞美的渴望,正如同对美食的渴望一样强烈。

我们日常最容易忽略的一种美德就是赞赏别人。

真心地给予别人肯定,不要吝啬你的赞美。人们会为你的肯定和赞美而欢呼雀跃,并且会用心铭记你的话语,直到永远。

3. 激起别人急切的欲望

如果成功有什么秘诀的话,那就是要先去了解对方的需求,从他们的角度来思考问题。首先激起对方急切的欲望。能做到这点的人,将掌握世界;不能做到这点的人,将四处碰壁。

唯一能影响别人的方法就是跟他们谈论那些他们想要的东西,并让他们知道怎么样才能得到它。

让别人喜欢你的六种方法

1. 用热情和真诚的态度对待他人

一个人可以通过真诚地关心他人来赢得关注、时间,甚至是合作。

如果你希望别人喜欢你,如果你希望赢得真正的友情,如果你希望在帮助别人的同时帮助自己,请牢记这条原则:真诚地关心别人。

2. 微笑的价值

喜欢微笑的人,在管理、教育和推销方面都比较成功,并且教育出来的孩子也更加开朗、乐观。

笑容比起皱眉更能传情达意,鼓励比惩罚更有效。

一个人脸上的神情,要远远比她身上所穿的衣服重要得多。

微笑会让人觉得你非常友善,会让你感受到一种温暖,它表示“我喜欢你,你令我非常开心,很高兴能见到你”。

微笑是你传播善意的信使,你的笑容可以照亮每一个看到它的人。对于那些看惯了皱眉、愁容满面,或是转过脸避开别人目光的人,你的笑容就如同阳光驱散雾霾般照进他们的心里。

3. 记住他人的姓名

姓名对于一个人来说是极为特殊、极为重要的。名字把人跟人区别开,使得一个人成为世上独一无二的个体。记得一个人的姓名,在人际交往上同样重要。

记住一个人的姓名,并轻易地叫出来,对这个人来说就是一种巧妙而有效的恭维。

4. 成为一名出色的听众

千万别忘记,那个与你交谈的人,他对于自己的需求和问题,比起对你以及你的问题的感兴趣程度要超过百倍。

认真地倾听别人谈话,是我们能给对方的一种最大的恭维。很少有人能够拒绝那种带有恭维性的认真聆听。

如果你想要成为一位出色的谈论家,一个善于倾听的人,想要别人对你感兴趣,那么你首先需要对他人感兴趣,提出一些别人愿意作答的问题,鼓励对方多谈论自己,以及他们的成就。

5. 让别人觉得自己重要

生活中,存在着一个举足轻重的原则,如果我们遵从这条原则,就永远也不会遇到麻烦;要是不遵从它,就会陷入无尽的麻烦。这个原则就是:让别人感受到他们自己的重要性。

人类本性中最深层的渴望,就是试图让自己变得重要。

主要让别人觉得自己重要,那么很多人的命运就很有可能因此而改变。

几乎每个你遇到的人都会认为他在某些地方比你强,这是一条永恒不变的真理。赢得这种人心的办法就是,以一种微妙的方式让他们意识到了你了解他们的重要性,并真诚地认同他们。

三人行,必有我师焉。要会向别人学习。

走出孤独忧虑的人生

1. 让自己忙起来

让自己不停地忙着。忧虑的人一定要让自己沉浸在工作中,否则只有在绝望中挣扎。

2. 不要为小事烦恼

不要让自己因为一些应该抛弃和忘记的小事而忧虑。要记住:生命如此短暂,不要再为小事而烦恼。

3. 不要自寻烦恼

要使我们能够停止忧虑,就可以根据事情发生的概率来评估我们的忧虑究竟值不值,这样,我想我们应该可以减少99%的忧虑。

让我们看看以前的记录,让我们根据概率问问自己,现在担心会发生的事情,到底真正发生的有多少?

4. 适应不可避免的事实

不要为月亮哭泣,也不要因事而后悔。

顺应时势,就是你在踏上人生旅途时最重要的一件事。

很显然,环境本身并不能使我们快乐或不快乐,只有我们对周围环境的反应才决定了我们的感受。

在必要的时候,我们都应该经受得住灾难和悲剧,甚至要战胜它们。也许我们会认为自己办不到,但事实上,我们内在的力量却强大得惊人,只要我们愿意利用,他就能帮助我们克服一切困难。

当我们不再反抗那些不可避免的事实之后,我们就可以节省精力,创造更丰富的生活。

5. 不要做无用功

在避免忧虑方面,没有比“船到桥头自然直”和“不要为打翻的牛奶哭泣”更基本,也更有用的话。

让过去的错误产生价值的唯一方法,就是平静地分析过去的错误,并从中吸取教训,然后再忘记错误。

不要试着去锯木屑。

不要为工作和金钱而烦恼

1. 做自己喜欢的工作

一定要记住,在做你生命中最重要而且影响最深远的决定之前,请务必多花点时间了解真实的真相。如果不这么做,那么你的决定将可能让你痛苦不已。

如果人们从事他们自己无限热爱的工作,他们都可以获得成功。

避免选择那些早就很激烈并且拥挤的职业和行业;避免选择只有10%的生存机会的行业。

克服“你只适合一项职业”的错误认知。每个人都可以在多项职业上取得成功。

2. 处理好金钱引发的烦恼

人们的大多数烦恼都和金钱有关。

使大多数人烦恼的并不是他们没有足够的金钱,而是他们不知道如何支配手中已有的金钱。

结构化并发

让我们先将思路转为日常业务开发中,比如在某某业务中,可能存在好几个需要同时处理的逻辑,比如同时请求两个网络接口,同时操作两个子任务等。我们暂且称上述学术化概念为 多个并发操作。

而每个并发操作其实都是在处理一个单独的任务,这个任务中,可能还存在子任务 ; 同样对于这个子任务来说,它又是其父任务的子单元。每个任务都有自己的生命周期,子任务的生命周期会继承父任务的生命周期,比如如果父任务关闭,子任务也会被取消。而如果满足这样特性,我们就称其就是 结构化并发。

异常传递流程

默认情况下,任意一个协程发生异常时都会影响到整个协程树,而异常的传递通常是双向的,也即协程会向子协程与父协程共同传递。

整体流程如下:

  1. 先 cancel 子协程

  2. 取消自己

  3. 将异常传递给父协程

  4. (重复上述过程,直到根协程关闭)

异常传递形式

-

launch : 层层向上传递,在launch外部try-catch无效

-

async : 根协程启动或加SupervisorJob启动,会优先当前暴露,可在await()捕获;否则会将异常传递给父协程,导致异常没有在调用处暴露。

异常处理

是否是根协程启动

async + 是否是根协程启动

  • async 时内部也是新的作用域,如果 async 对应的是根协程(scope.async),那么我们可以在 await() 时直接捕获异常

  • async 时如果不是根协程启动,则会将异常传递给父协程,导致异常没有在调用处暴露。可以在async时声明SupervisorJob,此时即可在 await() 时捕获异常

launch + 是否是根或父协程自带异常处理器

  • 在launch外部try-catch无用

  • launch自带的异常处理器不会处理当前协程发生的异常,除非当前协程显式添加SupervisorJob

SupervisorJob

根协程自带的SupervisorJob,子协程会继承

非根协程的父协程声明的SupervisorJob,子协程不会继承: 子协程在 launch 时会创建新的协程作用域,其会使用默认新的 Job 替代父协程传递 SupervisorJob ,

所以导致我们传递的 SupervisorJob 被覆盖。所以如果我们想让子协程不影响父协程或者其他子协程,此时就必须再显示添加 SupervisorJob。

场景实践

严格意义上来说,所有异常都可以用 tryCatch 去处理

严格意义上来说,所有异常都可以用 tryCatch 去处理,只要我们的处理位置得当。但这并不是所有方式的最优解,特别是如果你想更优雅的处理异常时,此时就可以考虑 CoroutineExceptionHandler 。

什么时候该用 SupervisorJob ,什么时候该用 Job?

引用官方的一句话就是:想要避免取消操作在异常发生时被传播,记得使用 SupervisorJob ;反之则使用 Job。

SupervisorJob + tryCatch

我们有两个接口 A,B 需要同时请求,当接口A异常时,需要不影响B接口的正常展示,当接口B异常时,此时界面展示异常信息

SupervisorJob + CoroutineExceptionHandler

如果你有一个顶级协程,并且需要自动捕获所有的异常,则此时可以选用上述方式

参考

普通callback实现

与协程异步代码同步写法对比

callback实现代码

|

1
//CallbackImpl.kt‭​​‌‌‎‭‬​abstract class CallbackImpl(val callBack: CallBack<Any>): CallBack<Any> {‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​    override fun onResult(result: Any?) {‭​​‌‌‎‭‬​        val cb = callBack‭​​‌‌‎‭‬​        while (true) {‭​​‌‌‎‭‬​            val outcome = invoke(result)‭​​‌‌‎‭‬​            if (outcome === CALLBACK_FLAG) {‭​​‌‌‎‭‬​                return‭​​‌‌‎‭‬​            }‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​            cb.onResult(outcome)‭​​‌‌‎‭‬​            return‭​​‌‌‎‭‬​        }‭​​‌‌‎‭‬​    }‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​    protected abstract fun invoke(result: Any?): Any?‭​​‌‌‎‭‬​}‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​val CALLBACK_FLAG = CoroutineSingletons.CALLBACK‭​​‌‌‎‭‬​enum class CoroutineSingletons { CALLBACK }‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​interface CallBack<T> {‭​​‌‌‎‭‬​    fun onResult(result: T?)‭​​‌‌‎‭‬​}‭​​‌‌‎‭‬​object UserInfoHelper {‭​​‌‌‎‭‬​    fun launch() {‭​​‌‌‎‭‬​        Log.e("MainActivity", "launch run")‭​​‌‌‎‭‬​        requestUserCallback(object : CallBack<Any> {‭​​‌‌‎‭‬​            override fun onResult(result: Any?) {‭​​‌‌‎‭‬​                Log.e("MainActivity", "launch result: $result")‭​​‌‌‎‭‬​            }‭​​‌‌‎‭‬​        })‭​​‌‌‎‭‬​    }‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​    val handler = Handler(Looper.getMainLooper())‭​​‌‌‎‭‬​    private fun delay(timeMillis: Long, callBack: CallBack<Any>): Any {‭​​‌‌‎‭‬​        handler.postDelayed({‭​​‌‌‎‭‬​            callBack.onResult(Unit)‭​​‌‌‎‭‬​        }, timeMillis)‭​​‌‌‎‭‬​        return CALLBACK_FLAG‭​​‌‌‎‭‬​    }‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​    //正常情况下应该保持到协程的context中,这样写为了简单处理‭​​‌‌‎‭‬​    private var userCallback:UserCallback? = null‭​​‌‌‎‭‬​    abstract class UserCallback(callBack: CallBack<Any>) : CallbackImpl(callBack) {‭​​‌‌‎‭‬​        var result: Any? = null‭​​‌‌‎‭‬​        var flag: Int = 0‭​​‌‌‎‭‬​    }‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​    fun requestUserCallback(callBack: CallBack<Any>): Any {‭​​‌‌‎‭‬​        val cb = if (userCallback == null) {‭​​‌‌‎‭‬​            val c = object : UserCallback(callBack) {‭​​‌‌‎‭‬​                override fun invoke(result: Any?): Any? {‭​​‌‌‎‭‬​                    this.result = result‭​​‌‌‎‭‬​                    ++flag‭​​‌‌‎‭‬​                    return requestUserCallback(this)‭​​‌‌‎‭‬​                }‭​​‌‌‎‭‬​            }‭​​‌‌‎‭‬​            userCallback = c‭​​‌‌‎‭‬​            c‭​​‌‌‎‭‬​        }else {‭​​‌‌‎‭‬​            userCallback!!‭​​‌‌‎‭‬​        }‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​        var returnResult = cb.result‭​​‌‌‎‭‬​        when (cb.flag) {‭​​‌‌‎‭‬​            0 -> {‭​​‌‌‎‭‬​                if (requestFriendListCallback(cb) == CALLBACK_FLAG) {‭​​‌‌‎‭‬​                    return CALLBACK_FLAG‭​​‌‌‎‭‬​                }‭​​‌‌‎‭‬​            }‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​            1 -> {‭​​‌‌‎‭‬​                returnResult = cb.result‭​​‌‌‎‭‬​            }‭​​‌‌‎‭‬​        }‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​        return "user: $returnResult"‭​​‌‌‎‭‬​    }‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​    //正常情况下应该保持到协程的context中,这样写为了简单处理‭​​‌‌‎‭‬​    var requestFriendListCallback: RequestFriendListCallback? = null‭​​‌‌‎‭‬​    abstract class RequestFriendListCallback(callBack: CallBack<Any>) : CallbackImpl(callBack) {‭​​‌‌‎‭‬​        var result: Any? = null‭​​‌‌‎‭‬​        var flag: Int = 0‭​​‌‌‎‭‬​    }‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​    fun requestFriendListCallback(callBack: CallBack<Any>): Any {‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​        val cb = if ( requestFriendListCallback == null) {‭​​‌‌‎‭‬​            val c = object : RequestFriendListCallback(callBack) {‭​​‌‌‎‭‬​                override fun invoke(result: Any?): Any? {‭​​‌‌‎‭‬​                    this.result = result‭​​‌‌‎‭‬​                    ++flag‭​​‌‌‎‭‬​                    return requestFriendListCallback(this)‭​​‌‌‎‭‬​                }‭​​‌‌‎‭‬​            }‭​​‌‌‎‭‬​            requestFriendListCallback = c‭​​‌‌‎‭‬​            c‭​​‌‌‎‭‬​        } else {‭​​‌‌‎‭‬​            requestFriendListCallback!!‭​​‌‌‎‭‬​        }‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​        when (cb.flag) {‭​​‌‌‎‭‬​            0 -> {‭​​‌‌‎‭‬​                if (delay(3000, cb) == CALLBACK_FLAG) {‭​​‌‌‎‭‬​                    return CALLBACK_FLAG‭​​‌‌‎‭‬​                }‭​​‌‌‎‭‬​            }‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​            1 -> {‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​            }‭​​‌‌‎‭‬​        }‭​​‌‌‎‭‬​ ‭​​‌‌‎‭‬​        return "friendList"‭​​‌‌‎‭‬​    }‭​​‌‌‎‭‬​}‭​​‌‌‎‭‬​

|

协程反编译字节码

|

1

|

1. suspendCoroutine作用及用法

suspendCoroutine 是 Kotlin 中用于挂起协程并与外部世界进行交互的函数之一。它的主要作用是允许你在协程中将异步非挂起的操作转化为挂起操作,从而可以方便地与协程上下文集成。具体来说,suspendCoroutine 的主要作用包括以下几点:

  1. 将回调式的异步操作转化为挂起操作:当你需要在协程中执行某些异步操作,例如调用回调函数,处理回调式 API,或等待某个事件发生时,suspendCoroutine 允许你将这些异步操作封装为挂起操作,使得你可以像调用普通的挂起函数一样调用它们,从而简化了异步编程的代码。

  2. 挂起当前协程:suspendCoroutine 在执行时会挂起当前协程,这意味着协程的执行会暂停,直到回调操作完成。这有助于防止阻塞线程,并允许其他协程在执行。

  3. 传递回调函数:suspendCoroutine 接受一个 Lambda 表达式,该 Lambda 表达式需要传递一个函数参数,通常是一个回调函数,该回调函数将在异步操作完成时被调用。在 Lambda 表达式内部,你可以使用 resume 来恢复协程的执行并传递结果,或者使用 resumeWithException 来传递异常,以便协程可以处理结果或错误。

suspendCoroutine案例

|

1
suspend fun main() {  	withContext(Dispatchers.IO) {  		val result = async { getResult() }.await()  		println("result=$result")  	}  }    suspend fun getResult() = suspendCoroutine<Boolean> {  	val callback = object : Callback {  		override fun onSuccess() {  			it.resume(true)  		}  	  		override fun onFail() {  			it.resumeWithException(Exception("fail"))  		}  	  		override fun onCancel() {  			it.resume(false)  		}  	}  	handleCallback(callback)  }    fun handleCallback(callback: Callback) {  	callback.onFail()  }    interface Callback {  	fun onSuccess()  	fun onFail()  	fun onCancel()  }

|

suspendCancellableCoroutine作用和用法

suspendCancellableCoroutine 是 Kotlin 中的一个函数,它允许你在一个协程中创建一个可取消的挂起操作,通常用于实现自定义的挂起函数或处理异步操作。基本功能和suspendCoroutine一样,只是多了一个支持监听外部协程取消和取消内部协程执行的能力。

suspendCancellableCoroutine案例

|

1
// 自定义的可取消挂起函数  suspend fun customCancelableOperation(): String {  	return suspendCancellableCoroutine { continuation ->  		val job = GlobalScope.launch {  			try {  				// 模拟异步操作  				delay(1000)  				// 如果操作成功,恢复挂起操作并传递结果  				continuation.resume("Operation completed successfully")  			} catch (e: CancellationException) {  				// 如果操作被取消,处理取消操作  				continuation.resumeWithException(e)  			} catch (e: Exception) {  				// 如果操作失败,传递异常  				continuation.resumeWithException(e)  			}  		}  		  		// 当外部协程取消时,取消内部协程  		continuation.invokeOnCancellation {  		job.cancel()  		}  	}  }    fun main() = runBlocking {  	val job = launch {  		val result = customCancelableOperation()  		println(result)  	}  	  	delay(500) // 等待一段时间  	job.cancel() // 取消外部协程  	job.join()  }

|

处理resumeWithException的通用方案

除了通过try catch的方式来捕获异常,也可以在launch协程的context中添加exceptionHandler来统一处理异常。 例如:

|

1
fun main() = runBlocking {  	val exceptionHandler = CoroutineExceptionHandler { _, exception ->  		println("Caught exception: $exception")  	}  	  	val job = GlobalScope.launch(exceptionHandler) {  		println(data)  	}  	  	job.join()  }

|

其他

在日常开发中,在协程上下文将异步非挂起的操作转化为挂起操作时遇到过多次回调完成的情况,导致一个continuation被多次resume,从而出现异常java.lang.IllegalStateException: Already resumed...

这时,可以通过使用suspendCancellableCoroutine并通过判断其状态决定是否需要真正resume来避免重复多次resume。

|

1
fun <T> Continuation<T>.safeResume(value: T) {  	if (this is CancellableContinuation) {  		if (isActive) {  			resume(value)  		} else {  			CapaLog.d("Continuation", "continuation is not active")  		}  	} else throw Exception("Must use suspendCancellableCoroutine instead of suspendCoroutine")  }

|

0%