Compose Snapshot 系统详解

Compose Snapshot 系统详解

目标:把「为什么读一下 state.value 就自动订阅了」这件事,从心智模型讲到源码,再讲到你日常遇到的每一个奇怪现象。


目录

  1. 先建立心智模型
  2. 三个必须分开看的问题
  3. [手写一个迷你 Snapshot 系统](#手写一个迷你 Snapshot 系统 "#%E4%B8%89%E6%89%8B%E5%86%99%E4%B8%80%E4%B8%AA%E8%BF%B7%E4%BD%A0-snapshot-%E7%B3%BB%E7%BB%9F")
  4. [订阅是怎么发生的:readObserver 全链路](#订阅是怎么发生的:readObserver 全链路 "#%E5%9B%9B%E8%AE%A2%E9%98%85%E6%98%AF%E6%80%8E%E4%B9%88%E5%8F%91%E7%94%9F%E7%9A%84readobserver-%E5%85%A8%E9%93%BE%E8%B7%AF")
  5. [通知是怎么发生的:apply 全链路](#通知是怎么发生的:apply 全链路 "#%E4%BA%94%E9%80%9A%E7%9F%A5%E6%98%AF%E6%80%8E%E4%B9%88%E5%8F%91%E7%94%9F%E7%9A%84apply-%E5%85%A8%E9%93%BE%E8%B7%AF")
  6. 冲突检测与合并
  7. 现象案例集
  8. 调试技巧

一、先建立心智模型

别把它想成"观察者模式"

大部分人第一反应是:mutableStateOf 内部有个 listener 列表,setValue 的时候遍历通知。

不是的。 State 对象自己完全不知道谁在观察它。它只是一个"能存多个版本的格子"。

真正的模型是 数据库的 MVCC(多版本并发控制)

数据库概念 Compose 对应
事务 Snapshot
事务 ID Snapshot.id(单调递增)
行的多版本 StateRecord 链表
快照读 readable()
事务提交 Snapshot.apply()
写写冲突 SnapshotApplyResult.Failure

订阅 ,是数据库不做、Compose 额外加的一层:事务在执行期间,记录自己读过哪些行 。这层就是 readObserver

一句话总结全局

「在一个装了 readObserver 的 Snapshot 里读过某个 State」= 订阅。

订阅关系存在 Snapshot 的观察者 里,不存在 State 里。


二、三个必须分开看的问题

初学者容易把三件事糊成一团,导致怎么看都看不懂。请把它们彻底分开:

css 复制代码
┌─────────────────────────────────────────────────────┐
│ 问题 A:隔离                                          │
│   不同线程/不同事务读到的值为什么互不干扰?             │
│   → 靠 StateRecord 多版本链表 + snapshotId 可见性规则   │
├─────────────────────────────────────────────────────┤
│ 问题 B:订阅                                          │
│   谁读了谁,是怎么记下来的?                            │
│   → 靠 Snapshot.readObserver 回调                     │
├─────────────────────────────────────────────────────┤
│ 问题 C:通知                                          │
│   改了值之后,怎么找到该重组的 scope?                  │
│   → 靠 modified 集合 + applyObserver + 反查依赖表       │
└─────────────────────────────────────────────────────┘

A 和 B 完全正交。你可以只实现 A(就是个 MVCC 存储),也可以只实现 B(就是个依赖收集器)。Compose 把两者叠在一起,才有了「事务内一致读 + 自动依赖收集」。


三、手写一个迷你 Snapshot 系统

这是理解全部原理最快的路。下面这份代码可以直接跑,只有约 100 行,覆盖了 A/B/C 三个问题的核心。

3.1 版本记录

kotlin 复制代码
/** 一个版本。真实源码里叫 StateRecord */
abstract class Record {
    var snapshotId: Int = 0     // 这个版本是哪个快照写的
    var next: Record? = null    // 单链表,头部最新
}

class IntRecord(var value: Int) : Record()

3.2 快照

关键点在 invalid 集合:它装的是"对我不可见的快照 id",也就是所有在我创建时还没提交的快照。

kotlin 复制代码
open class MiniSnapshot(
    val id: Int,
    val invalid: Set<Int>,          // 不可见的 id 集合
    val readObserver: ((Any) -> Unit)? = null
) {
    companion object {
        private var nextId = 1
        // 当前打开、尚未提交的快照 id
        val open = mutableSetOf<Int>()
        // 全局快照(默认没人开事务时用它)
        var global = MiniSnapshot(0, emptySet())

        private val threadLocal = ThreadLocal<MiniSnapshot?>()
        val current: MiniSnapshot get() = threadLocal.get() ?: global

        fun takeMutable(
            readObserver: ((Any) -> Unit)? = null,
            writeObserver: ((Any) -> Unit)? = null
        ): MutableMiniSnapshot {
            val id = nextId++
            // 我看不见:所有还开着的事务
            val snap = MutableMiniSnapshot(id, open.toSet(), readObserver, writeObserver)
            open += id
            return snap
        }

        fun <T> enter(snapshot: MiniSnapshot, block: () -> T): T {
            val prev = threadLocal.get()
            threadLocal.set(snapshot)
            try { return block() } finally { threadLocal.set(prev) }
        }

        // 提交后全局通知
        val applyObservers = mutableListOf<(Set<Any>) -> Unit>()
    }
}

class MutableMiniSnapshot(
    id: Int,
    invalid: Set<Int>,
    readObserver: ((Any) -> Unit)? = null,
    val writeObserver: ((Any) -> Unit)? = null
) : MiniSnapshot(id, invalid, readObserver) {

    val modified = mutableSetOf<MiniState>()

    fun apply() {
        // ★ 提交的本质:把自己的 id 从"未提交"集合里摘掉
        //   于是所有 snapshotId == 我的 id 的 record,对后来者立刻可见
        open -= id
        global = MiniSnapshot(id, open.toSet())
        if (modified.isNotEmpty()) {
            applyObservers.forEach { it(modified.toSet()) }
        }
    }
}

这里是全篇最重要的一句话: 提交(apply)不是"把数据写回去",数据早就写在链表里了。 提交是 把自己的 id 从 invalid 集合里移除,让已经写好的版本变得"可见"。 回滚(dispose 不 apply)也不需要撤销数据,因为那些 record 的 id 永远不会离开 invalid 集合,永远看不见。

3.3 状态对象

kotlin 复制代码
class MiniState(initial: Int) {
    private var firstRecord: Record = IntRecord(initial).apply { snapshotId = 0 }

    var value: Int
        get() {
            val snapshot = MiniSnapshot.current
            snapshot.readObserver?.invoke(this)          // ★ 订阅点(问题 B)
            return (readable(snapshot) as IntRecord).value
        }
        set(v) {
            val snapshot = MiniSnapshot.current as? MutableMiniSnapshot
                ?: error("当前快照只读")
            writable(snapshot).value = v                  // ★ 写新版本(问题 A)
            snapshot.modified += this                     // ★ 记录脏数据(问题 C)
            snapshot.writeObserver?.invoke(this)
        }

    /** 可见性规则:id <= 我的 id,且不在 invalid 里,取最大的那个 */
    private fun readable(snapshot: MiniSnapshot): Record {
        var candidate: Record? = null
        var r: Record? = firstRecord
        while (r != null) {
            if (r.snapshotId <= snapshot.id && r.snapshotId !in snapshot.invalid) {
                if (candidate == null || candidate.snapshotId < r.snapshotId) candidate = r
            }
            r = r.next
        }
        return candidate ?: error("没有可读版本")
    }

    private fun writable(snapshot: MutableMiniSnapshot): IntRecord {
        val cur = readable(snapshot) as IntRecord
        // 本快照已经写过一次 → 原地改,不再新建版本
        if (cur.snapshotId == snapshot.id) return cur
        // 否则新建一个版本挂到链表头
        return IntRecord(cur.value).also {
            it.snapshotId = snapshot.id
            it.next = firstRecord
            firstRecord = it
        }
    }
}

3.4 跑一下

kotlin 复制代码
fun main() {
    val count = MiniState(0)

    // ---- 问题 A:隔离 ----
    val snap = MiniSnapshot.takeMutable()
    MiniSnapshot.enter(snap) { count.value = 100 }
    println(count.value)                      // 0   ← 外面看不见未提交的修改
    println(MiniSnapshot.enter(snap) { count.value })  // 100 ← 事务内看得见
    snap.apply()
    println(count.value)                      // 100 ← 提交后可见

    // ---- 问题 B + C:订阅与通知 ----
    val deps = mutableMapOf<MiniState, MutableList<String>>()
    val scope = "MyComposable"

    val obsSnap = MiniSnapshot.takeMutable(
        readObserver = { state ->
            deps.getOrPut(state as MiniState) { mutableListOf() } += scope
        }
    )
    MiniSnapshot.enter(obsSnap) {
        println("组合中读到 count = ${count.value}")   // 这一读 → 建立订阅
    }
    obsSnap.apply()

    MiniSnapshot.applyObservers += { changed ->
        changed.forEach { s -> println("需要重组:${deps[s]}") }
    }

    val w = MiniSnapshot.takeMutable()
    MiniSnapshot.enter(w) { count.value = 200 }
    w.apply()          // 输出:需要重组:[MyComposable]
}

跑通这段,你就把 Compose Snapshot 的骨架吃透了。剩下的都是工程化细节:性能优化、并发安全、冲突合并、嵌套快照。


四、订阅是怎么发生的:readObserver 全链路

4.1 读的入口

kotlin 复制代码
// SnapshotState.kt
override var value: T
    get() = next.readable(this).value
kotlin 复制代码
// Snapshot.kt
fun <T : StateRecord> T.readable(state: StateObject): T {
    val snapshot = Snapshot.current
    snapshot.readObserver?.invoke(state)      // ★★★ 唯一的订阅点
    return readable(this, snapshot.snapshotId, snapshot.invalid) ?: readError()
}

注意传给 observer 的是 StateObject 本身 (也就是那个 MutableState 实例),不是它的值。所以依赖表的 key 是对象引用。

4.2 组合期间谁装了 observer

Recomposer / CompositionImpl.composeContent()

kotlin 复制代码
// CompositionImpl
private fun <T> composing(block: () -> T): T {
    val snapshot = Snapshot.takeMutableSnapshot(
        readObserverOf(this),            // ★ 装上
        writeObserverOf(this, modifiedValues)
    )
    try {
        return snapshot.enter(block)
    } finally {
        applyAndCheck(snapshot)
    }
}

readObserverOf 最终走到 Composer.recordReadOf

kotlin 复制代码
internal fun recordReadOf(value: Any) {
    if (!areChildrenComposing) {
        composer.currentRecomposeScope?.let { scope ->
            scope.used = true                       // 标记这个 scope 真的读了东西
            observations.add(value, scope)          // 建立 state → scope 映射
        }
    }
}

4.3 订阅粒度 = RecomposeScope

currentRecomposeScope最近一层被 @Composable 编译器插桩、且可跳过的函数。这解释了一个高频困惑:

kotlin 复制代码
@Composable
fun Screen() {
    var n by remember { mutableStateOf(0) }

    Column {
        Text("固定标题")        // ← 也会被"波及"?
        Text("$n")             // ← 读 n 的地方在这
        Button(onClick = { n++ }) { Text("+") }
    }
}

读发生在 Screen 这个 scope 里Column 的 content lambda 是 inline 的,不构成独立 scope),所以 n 变化时整个 Screen 重组,Text("固定标题") 也会被重新执行(只是它的参数没变,内部会跳过实际工作)。

想缩小粒度,把读推进一个非 inline 的 composable:

kotlin 复制代码
@Composable
fun Counter(value: () -> Int) {   // lambda 传递,读延后
    Text("${value()}")            // 读发生在 Counter 的 scope
}

这就是所谓 "defer reads"(延迟读取) 优化的原理。

4.4 存依赖的数据结构

observations: ScopeMap<Any, RecomposeScopeImpl>(旧版 IdentityScopeMap)。

它不是普通 HashMap,特点:

  • identityHashCode 而非 equals(避免 data class 的 equals 干扰)
  • 按 identity hash 排序存数组,二分查找,无节点分配
  • 一对多,且支持 反向删除(scope 失效时清掉它的所有依赖项)

五、通知是怎么发生的:apply 全链路

5.1 写的入口

kotlin 复制代码
override var value: T
    set(value) {
        val current = withCurrent { this }
        if (!policy.equivalent(current.value, value)) {   // ★ 相等性策略拦截
            next.overwritable(this, current) { this.value = value }
        }
    }

overwritablewritableRecordnotifyWrite(snapshot, state),把 state 加进 snapshot.modified

注意 policy.equivalent :默认是 structuralEqualityPolicy()(用 ==)。设同样的值不会触发任何通知。这也是为什么把可变对象(比如 ArrayList)塞进 mutableStateOf 然后原地 add 完全不会刷新------引用没变、equals 可能也没变,写路径压根没走。

三种策略:

策略 判等方式 场景
structuralEqualityPolicy() == 默认
referentialEqualityPolicy() === data class 频繁比较开销大时
neverEqualPolicy() 永远不等 每次赋值都要触发,比如可变对象

5.2 提交与全局通知

kotlin 复制代码
Snapshot.registerApplyObserver { changed: Set<Any>, snapshot: Snapshot -> ... }

RecomposerrunRecomposeAndApplyChanges() 里注册:

kotlin 复制代码
val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ ->
    synchronized(stateLock) {
        if (_state.value >= State.Idle) {
            snapshotInvalidations.add(changed)
            deriveStateLocked()          // 唤醒 recompose 协程
        }
    }
}

然后:

sql 复制代码
changed: Set<Any>
   ↓ 反查 composition.observations
Set<RecomposeScope>
   ↓ scope.invalidate()
标记 SlotTable 中对应区间为待重组
   ↓ withFrameNanos 到来
只重组这些 scope

5.3 为什么点击回调里改 state 也能生效

点击回调运行时你在 GlobalSnapshot 里,没人会调 apply()。靠 GlobalSnapshotManager 兜底:

kotlin 复制代码
// GlobalSnapshotManager.kt (androidMain)
fun ensureStarted() {
    if (started.compareAndSet(false, true)) {
        Snapshot.registerGlobalWriteObserver {
            if (!commitPending) {
                commitPending = true
                schedule {                       // AndroidUiDispatcher,主线程
                    commitPending = false
                    Snapshot.sendApplyNotifications()
                }
            }
        }
    }
}

sendApplyNotifications()advanceGlobalSnapshot() → 推进全局快照 id + 触发 applyObservers。

推论 :一个事件回调里连改 10 个 state,只会合并成 一次 通知(commitPending 去重)。


六、冲突检测与合并

多线程/嵌套快照场景下,两个快照可能改了同一个 state。

6.1 检测

MutableSnapshot.apply() 中的 innerApplyLocked

modified 里的每个 state,比较三个版本:

  • current:全局最新可见版本
  • previous:我 fork 时看到的版本
  • applied:我写的版本

current !== previous,说明我 fork 之后别人改过 → 冲突。

6.2 合并

先给 state 一次自救机会:

kotlin 复制代码
val merged = state.mergeRecords(previous, current, applied)

SnapshotMutableStateImpl 的实现:

kotlin 复制代码
override fun mergeRecords(previous, current, applied): StateRecord? {
    return if (policy.equivalent(current.value, applied.value)) current
           else policy.merge(previous.value, current.value, applied.value)
               ?.let { /* 生成新 record */ }
}

默认 policy 的 merge 返回 null → 合并失败 → apply() 返回 SnapshotApplyResult.Failure

SnapshotStateList / SnapshotStateMap 实现了 merge:它们记录了修改序号,能做类似三方合并的操作,所以并发追加往往能自动合并成功。

6.3 实用案例:事务性 UI

kotlin 复制代码
@Composable
fun EditForm(user: UserState) {
    var draft by remember { mutableStateOf<Snapshot?>(null) }

    Button(onClick = {
        // 开一个事务,用户编辑期间的修改对外不可见
        draft = Snapshot.takeMutableSnapshot()
    }) { Text("编辑") }

    Button(onClick = {
        draft?.apply()      // 提交,一次性生效
        draft = null
    }) { Text("保存") }

    Button(onClick = {
        draft?.dispose()    // 丢弃,自动回滚,无需任何撤销逻辑
        draft = null
    }) { Text("取消") }
}

或者更常用的语法糖(自动 apply / 异常时回滚):

kotlin 复制代码
Snapshot.withMutableSnapshot {
    account.balance -= 100
    order.status = PAID
    // 中途抛异常 → 两个修改全部作废,UI 永远不会看到中间态
}

6.4 案例:子线程安全地读写

kotlin 复制代码
lifecycleScope.launch(Dispatchers.IO) {
    val snapshot = Snapshot.takeMutableSnapshot()
    try {
        snapshot.enter {
            // 这里读到的所有 state 都是一致的时间点视图
            // 即使主线程同时在改,也不会读到"半个更新"
            repository.save(uiState.name, uiState.email)
            uiState.syncing = false
        }
        val result = snapshot.apply()
        if (result is SnapshotApplyResult.Failure) {
            // 冲突了,重试或提示
        }
    } finally {
        snapshot.dispose()   // ★ 必须 dispose,否则 id 永远留在 open 集合,
                             //    导致后续快照的 invalid 集合无限膨胀(内存泄漏)
    }
}

注意 :直接在子线程 state.value = x(不开快照)是可以的------写进全局快照,registerGlobalWriteObserver 会调度到主线程发通知。但你就失去了一致性和原子性。


七、现象案例集

案例 1:为什么 mutableStateOf(mutableListOf()) 不刷新

kotlin 复制代码
var list by remember { mutableStateOf(mutableListOf<String>()) }
Button(onClick = { list.add("x") }) { ... }   // ❌ 不刷新

list.add() 根本没走 value 的 setter,modified 集合是空的,没有任何写入被记录。

解法(按推荐度):

kotlin 复制代码
// ✅ 1. 用 SnapshotStateList,它自己就是 StateObject
val list = remember { mutableStateListOf<String>() }
list.add("x")

// ✅ 2. 用不可变集合 + 整体替换
var list by remember { mutableStateOf(listOf<String>()) }
list = list + "x"

// ⚠️ 3. neverEqualPolicy + 重新赋值(可行但易错)
var list by remember { mutableStateOf(mutableListOf<String>(), neverEqualPolicy()) }
list.add("x"); list = list

案例 2:derivedStateOf 到底省了什么

kotlin 复制代码
val listState = rememberLazyListState()

// ❌ 每滚动 1px 都重组
val showButton = listState.firstVisibleItemIndex > 0

// ✅ 只在 true/false 翻转时重组
val showButton by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 0 }
}

原理:DerivedSnapshotState 是一个"带缓存的 StateObject"。它的 ResultRecord 存了:

  • dependencies:计算时读过哪些 state 及它们当时的 currentValue 快照
  • result:上次的计算结果

外层读它时:

  1. 用自己的 readObserver 收集依赖
  2. 校验所有 dependency 的当前值是否变化;没变则直接返回缓存
  3. 变了才重算;重算结果若与旧结果 ==,不通知外层

所以「上游高频变化、下游结果低频变化」正是它的适用场景。反之如果结果和上游同频变化,derivedStateOf 只是纯开销。

案例 3:graphicsLayer { } 为什么能跳过重组

kotlin 复制代码
// ❌ 每帧重组整个 composable
Box(Modifier.offset(x = animatedX.dp))

// ✅ 完全不重组,只重跑 layer 属性
Box(Modifier.graphicsLayer { translationX = animatedX })

graphicsLayer 的 lambda 不在组合阶段执行,而在 绘制阶段SnapshotStateObserver 包裹执行:

kotlin 复制代码
observer.observeReads(scope, onValueChangedForScope) { block() }

animatedX 建立的依赖,指向的是 "重绘这个 layer" 这个 scope,而不是某个 RecomposeScope。值变了 → 只触发 invalidateDraw(),组合和布局阶段完全跳过。

同理还有:

  • Modifier.drawBehind { } → 只 invalidate draw
  • Modifier.layout { } 里读 state → 只 invalidate layout
  • Modifier.offset { IntOffset(...) }(lambda 版)→ 只 invalidate layout

通用原则:把 state 的读取推迟到尽可能晚的阶段。

案例 4:为什么 lambda 传参能减少重组

kotlin 复制代码
// ❌ Parent 重组 → Child 参数变 → Child 重组
@Composable fun Child(count: Int) { Text("$count") }

// ✅ Parent 重组,Child 的参数(lambda 引用)不变 → Child 跳过
//    读发生在 Child 内部,只有 Child 重组
@Composable fun Child(count: () -> Int) { Text("${count()}") }

前者读发生在 Parent 的 scope,后者读发生在 Child 的 scope。订阅位置变了,失效范围就变了。

案例 5:为什么 remember 里读 state 不订阅

kotlin 复制代码
val x = remember { someState.value }   // ⚠️ 只在第一次组合时读,之后永远是旧值

它确实订阅了(读发生在组合的快照里),所以 someState 变化时 触发重组。但 remember 的 calculation 不会重新执行,x 还是旧的。要么用 remember(someState.value),要么用 derivedStateOf

案例 6:snapshotFlow 是什么

kotlin 复制代码
LaunchedEffect(Unit) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .distinctUntilChanged()
        .collect { analytics.log(it) }
}

它就是把 SnapshotStateObserver 包成 Flow:在 Snapshot.takeSnapshot(readObserver) 里执行 block 收集依赖,注册 applyObserver,依赖变化就重新执行 block 并 emit。

只在依赖真的变化时才重跑 ,比在 composable 里写 LaunchedEffect(value) 更精确,且不占用组合阶段。

案例 7:为什么组合中读了又写会死循环

kotlin 复制代码
@Composable
fun Bad() {
    var n by remember { mutableStateOf(0) }
    Text("$n")
    n++          // ❌ 读了 n(订阅),又写了 n(触发失效)→ 无限重组
}

组合用的是 takeMutableSnapshot,写入被 writeObserverOf 记录进 modifiedValues,apply 后立刻反查到刚建立的依赖 → 立刻失效 → 再组合。这就是 Compose 反复强调"组合必须无副作用"的机制层原因。


八、调试技巧

查看某次变更影响了谁

kotlin 复制代码
Snapshot.registerApplyObserver { changed, _ ->
    changed.forEach { Log.d("Snapshot", "changed: $it") }
}

观察全局写

kotlin 复制代码
Snapshot.registerGlobalWriteObserver {
    Log.d("Snapshot", "global write: $it", Throwable())  // 带堆栈,定位是谁改的
}

编译器重组计数

Gradle 里开:

kotlin 复制代码
composeCompiler {
    reportsDestination = layout.buildDirectory.dir("compose_reports")
    metricsDestination = layout.buildDirectory.dir("compose_metrics")
}

产出的 *-classes.txt 会标注每个 composable 是 restartable / skippable,参数是 stable / unstable。不 skippable 的 composable,再精细的订阅粒度也白搭。

Layout Inspector

Android Studio 的 Layout Inspector 有 Recomposition Counts 列,能直接看到哪个节点重组次数异常。


附:一页速查

python 复制代码
mutableStateOf(x)
   └─ SnapshotMutableStateImpl : StateObject
        └─ firstStateRecord ──► Record(id=5, v=3) ──► Record(id=2, v=1) ──► ...
                                    最新              较旧

读 value:
   Snapshot.current.readObserver?.invoke(stateObject)   ← 订阅
   找 id <= 当前id 且 不在 invalid 中 的最大 id 的 record

写 value:
   policy.equivalent(旧, 新) ? 直接返回 : {
       本快照已有 record ? 原地改 : 新建 record 挂链表头
       snapshot.modified += stateObject
   }

apply():
   冲突检测(current !== previous ?)
   → mergeRecords 尝试合并
   → 成功: id 移出 open 集合,推进全局快照,触发 applyObservers
   → 失败: SnapshotApplyResult.Failure

applyObserver 收到 changed: Set<Any>
   → 反查 observations: ScopeMap<Any, RecomposeScope>
   → scope.invalidate()
   → 下一帧 withFrameNanos 只重组这些 scope
相关推荐
艾莉丝努力练剑4 小时前
【MYSQL】MYSQL学习的一大重点:基本查询(下)
android·数据库·学习·mysql·面试·八股文
三少爷的鞋4 小时前
Android 中coroutineScope 和 supervisorScope 到底怎么选?先搞清楚 Exception 和 Result
android
其实防守也摸鱼6 小时前
KMP全栈开发:从Android到AI Agent的技术演进与实践
android·运维·网络·人工智能·学习·安全·macos
YM52e11 小时前
鸿蒙Flutter Padding内边距:EdgeInsets详解
android·学习·flutter·华为·harmonyos·鸿蒙
Rex叶然15 小时前
ScreenMatch适配安卓屏幕脚本,用于老项目
android·screenmatch·安卓屏幕适配
阿pin15 小时前
Android随笔-Activity启动流程深度分析
android·activity启动流程
zzq779716 小时前
加固包闪退四象限定位:targetSdk 与保护策略实战解析
android·安全·安卓·安全架构
我命由我1234517 小时前
Android 开发问题:Cannot resolve method ‘repeat‘ in ‘String‘
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
光头闪亮亮18 小时前
Fyne ( go跨平台GUI )项目实战-scanner摄像头扫码组件开发技术详解
android·go