Android 中大型项目架构梳理<一>
Android 项目架构之用户信息模块<二>
背景介绍:
个人35+的老年Android开发.日常开发中,维护和开发和我码领差不多的远古代码,着实被恶心的不轻.加上这几年的姑且称之为工作经验的东西吧,梳理一下项目中所遇到的一些问题,以及自己所想到的一些解决方式方法.
本章提要:用户信息模块搭建
原因:
在庞大的项目中因为快速和频繁的获取用户信息,当内存不足等特殊情况导致用户信息为空,造成项目稳定性不足.
1:用户信息模块搭建思路
公司项目是一个直播项目,IM/直播/动画等会占据大量内存,进而频繁的GC,有时候会出现内存释放本不该释放的对象又没有重建,导致用户信息为null的情况出现.
因为内存不足导致被释放,就加载本地不就好了嘛 类似多级缓存的思路.
调用->内存->数据库/SP->网络
主要功能:
- 内存缓存
- 数据库存储(
Room,存储) - 多用户存储
- 快速/安全获取(
协程获取)
2.具体实现
为方便后期扩展,采取数据库形式存储数据(多账户,加密等).采取数据库存储+缓存存储保证数据不为空,用Kotlin+协程实现数据库数据的获取
2.1 Room 实现用户信息的存储
核心代码:
UserDatabase数据库创建和更新UserEntity用户信息数据UserDao数据库操作类UserRepository操作工具类
2.1.1 用户信息数据
注意:
因为数据库,表中增加字段需要更新表,所以采用了常用字段和扩展字段(extraJson)的形式,减少因为增加字段而修改表结构带来的风险
package com.wkq.user.data.entity
import androidx.room.*
/**
* UserEntity:用户账号实体类
*/
@Entity(tableName = "user_accounts")
data class UserEntity(
@PrimaryKey
val userId: String, // 用户 ID (唯一标识)
val userName: String?="", // 用户名
val avatar: String? = "", // 头像地址
/**
* 是否为当前激活状态的账号
*/
val isCurrent: Boolean = false,
/**
* 扩展字段 (JSON 字符串),用于存储非结构化的额外信息
*/
val extraJson: String? = null,
/**
* 账号过期时间(毫秒时间戳)
*/
val expireTime: Long? = null,
/**
* 最后一次活跃时间(毫秒时间戳)
*/
val lastActiveTime: Long? = System.currentTimeMillis()
)
2.1.2 UserDao 用户信息表的Dao
注意:
internal 修饰Module外部不可见,减少调用者的干扰
package com.wkq.user.data.dao
import androidx.room.*
import com.wkq.user.data.entity.UserEntity
import kotlinx.coroutines.flow.Flow
/**
* UserDao:用户数据库访问接口
*/
@Dao
internal interface UserDao {
/**
* 插入或更新用户
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrUpdate(user: UserEntity)
/**
* 查询所有用户
*/
@Query("SELECT * FROM user_accounts")
fun getAllUsersFlow(): Flow<List<UserEntity>>
/**
* 查询当前激活用户
*/
@Query("SELECT * FROM user_accounts WHERE isCurrent = 1 LIMIT 1")
fun getCurrentUserFlow(): Flow<UserEntity?>
/**
* 查询当前激活用户(挂起函数)
*/
@Query("SELECT * FROM user_accounts WHERE isCurrent = 1 LIMIT 1")
suspend fun getCurrentUser(): UserEntity?
/**
* 根据 ID 查询用户
*/
@Query("SELECT * FROM user_accounts WHERE userId = :userId LIMIT 1")
suspend fun getUserById(userId: String): UserEntity?
/**
* 删除用户
*/
@Delete
suspend fun deleteUser(user: UserEntity)
/**
* 切换账号:
* 1. 将所有账号设为非当前
* 2. 将指定 ID 账号设为当前
*/
@Transaction
suspend fun switchAccount(userId: String) {
clearCurrentStatus()
setCurrentStatus(userId)
}
@Query("UPDATE user_accounts SET isCurrent = 0")
suspend fun clearCurrentStatus()
@Query("UPDATE user_accounts SET isCurrent = 1 WHERE userId = :userId")
suspend fun setCurrentStatus(userId: String)
}
2.1.3 UserDatabase 数据库的帮助类
- 创建数据库
- 预留后期表变动的方法(
MIGRATION_1_2)
package com.wkq.user.data.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.wkq.user.data.dao.UserDao
import com.wkq.user.data.entity.UserEntity
/**
* UserDatabase:用户本地数据库 (Room 实现)
*/
@Database(entities = [UserEntity::class], version = 1, exportSchema = true)
internal abstract class UserDatabase : RoomDatabase() {
/** 获取用户数据操作接口 */
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: UserDatabase? = null
// 升级 模板代码(Room + Migration)
// 1: version=1 -->version=2
//2: 创建val MIGRATION_1_2=Migration(1,2)
//3: .addMigrations(MIGRATION_1_2)
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// -----------------------------
// 1. 新增字段(简单)
// -----------------------------
db.execSQL(
"""
ALTER TABLE UserEntity
ADD COLUMN age INTEGER NOT NULL DEFAULT 0
""".trimIndent()
)
// -----------------------------
// 2. 如果要新增表
// -----------------------------
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS UserProfile (
id INTEGER PRIMARY KEY NOT NULL,
userId INTEGER NOT NULL,
nickname TEXT,
avatar TEXT
)
""".trimIndent()
)
// -----------------------------
// 3. 复杂表修改(改字段名/删字段/改类型)
// -----------------------------
// 举例:UserEntity 表删掉 name 字段
db.execSQL(
"""
CREATE TABLE UserEntity_new (
id INTEGER PRIMARY KEY NOT NULL,
age INTEGER NOT NULL DEFAULT 0
)
""".trimIndent()
)
db.execSQL(
"""
INSERT INTO UserEntity_new (id, age)
SELECT id, age FROM UserEntity
""".trimIndent()
)
db.execSQL("DROP TABLE UserEntity")
db.execSQL("ALTER TABLE UserEntity_new RENAME TO UserEntity")
}
}
fun getDatabase(context: Context): UserDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext, UserDatabase::class.java, "multi_user_db"
).fallbackToDestructiveMigration(true)
// .addMigrations(MIGRATION_1_2)
.build()
INSTANCE = instance
instance
}
}
}
}
2.1.4 UserRepository 数据库数据的操作类
package com.wkq.user.repository
import com.wkq.user.cache.UserCache
import com.wkq.user.data.dao.UserDao
import com.wkq.user.data.entity.UserEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
/**
* UserRepository:用户数据仓库
*
* 职责:
* 1. 封装底层数据源 (Room DAO) 和内存缓存 (UserCache)。
* 2. 确保数据库更新时,内存缓存能同步得到刷新。
* 3. 提供统一的业务接口供上层 UserManager 调用。
*/
internal class UserRepository(
private val userDao: UserDao,
private val userCache: UserCache
) {
/**
* 获取当前用户的 Flow
* 内部使用 onEach 观察数据流变化,自动同步更新到 UserCache
*/
fun getCurrentUserFlow(): Flow<UserEntity?> {
return userDao.getCurrentUserFlow().onEach { user ->
userCache.updateCache(user)
}
}
/**
* 获取所有账户信息的 Flow (直接来自数据库)
*/
fun getAllUsersFlow(): Flow<List<UserEntity>> = userDao.getAllUsersFlow()
/**
* 登录或更新用户信息
* @param user 用户实体,若 isCurrent 为 true,则会清除数据库中其他账号的当前激活状态
*/
suspend fun saveUser(user: UserEntity) {
if (user.isCurrent) {
// 如果设置为当前用户,先通过 DAO 清除其他用户的激活标志
userDao.clearCurrentStatus()
}
userDao.insertOrUpdate(user)
// 同步更新内存缓存
if (user.isCurrent) {
userCache.updateCache(user)
} else if (userCache.getCurrentUser()?.userId == user.userId) {
// 如果原本是当前用户但现在被设为非激活,则清除缓存
userCache.clear()
}
}
/**
* 切换账户
* @param userId 目标用户 ID
*/
suspend fun switchAccount(userId: String) {
userDao.switchAccount(userId)
val user = userDao.getUserById(userId)
userCache.updateCache(user)
}
/**
* 手动刷新内存缓存(从数据库拉取最新数据)
*/
suspend fun refreshCache() {
val user = userDao.getCurrentUser()
userCache.updateCache(user)
}
/**
* 退出登录
* @param userId 退出登录的用户 ID,将 mark 为非激活状态
*/
suspend fun logout(userId: String) {
val user = userDao.getUserById(userId)
if (user != null) {
userDao.insertOrUpdate(user.copy(isCurrent = false))
// 如果退出的正好是当前缓存的用户,则同步清理内存
if (userCache.getCurrentUser()?.userId == userId) {
userCache.clear()
}
}
}
/**
* 同步加载当前用户(查库)并刷新缓存
*/
suspend fun loadCurrentUser(): UserEntity? {
val user = userDao.getCurrentUser()
userCache.updateCache(user)
return user
}
}
2.2 UserCache 缓存信息暴露的方法
package com.wkq.user.cache
import com.wkq.user.data.entity.UserEntity
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* UserCache:用户内存缓存类
*
* 功能:
* 使用 StateFlow 在内存中持有当前登录用户的实时状态,供 UI 层进行响应式监听。
*/
internal class UserCache {
/**
* 当前登录用户的 MutableStateFlow (私有)
*/
private val _currentUser = MutableStateFlow<UserEntity?>(null)
/**
* 对外公开的只读 StateFlow
*/
val currentUserFlow: StateFlow<UserEntity?> = _currentUser.asStateFlow()
/**
* 更新缓存值
*/
fun updateCache(user: UserEntity?) {
_currentUser.value = user
}
/**
* 同步获取缓存中的当前用户对象
*/
fun getCurrentUser(): UserEntity? {
return _currentUser.value
}
/**
* 清空当前用户缓存
*/
fun clear() {
_currentUser.value = null
}
companion object {
@Volatile
private var INSTANCE: UserCache? = null
/** 传统的双重校验锁单例获取方式 */
fun getInstance(): UserCache {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: UserCache().also { INSTANCE = it }
}
}
}
}
2.3 用户信息操作的主操作类
package com.wkq.user.manager
import android.content.Context
import com.google.gson.Gson
import com.wkq.user.cache.UserCache
import com.wkq.user.data.db.UserDatabase
import com.wkq.user.data.entity.UserEntity
import com.wkq.user.repository.UserRepository
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.ConcurrentHashMap
/**
* UserManager:用户管理核心单例类
*
* 职责:
* 1. 维护当前登录用户和所有用户的响应式状态 (StateFlow)。
* 2. 提供线程安全的同步/异步接口进行用户登录、切换、退出。
* 3. 自动同步底层数据库变化到内存内存 Flow。
* 4. 支持用户自定义扩展数据的 JSON 解析与缓存。
*
* 优化点:
* - 线程安全:使用 ConcurrentHashMap 处理扩展数据缓存。
* - 性能优化:通过 stateIn 将冷流转成热流,并在内存中共享数据。
* - 并发保护:使用 Mutex 解决冷启动时的“缓存击穿”读库问题。
*/
class UserManager private constructor(context: Context) {
// ------------------ 内部依赖 ------------------
private val database = UserDatabase.getDatabase(context)
private val userDao = database.userDao()
private val userCache = UserCache.getInstance()
private val repository = UserRepository(userDao, userCache)
private val gson = Gson()
/** 并发锁:确保多线程下 getSafeCurrentUser() 仅触发一次查库 */
private val mutex = Mutex()
/** 全局协程处理器,防止异常导致 Scope 崩溃 */
private val handler = CoroutineExceptionHandler { _, throwable -> throwable.printStackTrace() }
/** 单例作用域:使用 SupervisorJob 确保子协程互不影响 */
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + handler)
/** 扩展数据并发缓存:Clazz Name -> 解析后的实体 */
private val extraDataCache = ConcurrentHashMap<String, Any?>()
// ------------------ 对外公开 Flow ------------------
/**
* 当前用户 Flow (StateFlow)
* 每次用户更新时会自动清空扩展数据缓存
*/
val currentUserFlow: StateFlow<UserEntity?> = repository.getCurrentUserFlow()
.onEach { extraDataCache.clear() } // 用户变更,清理对应的扩展缓存
.stateIn(scope, SharingStarted.Eagerly, null)
/** 所有用户列表 Flow (StateFlow) */
val allUsersFlow: StateFlow<List<UserEntity>> = repository.getAllUsersFlow()
.stateIn(scope, SharingStarted.Eagerly, emptyList())
// ------------------ 核心功能接口 ------------------
/**
* 挂起函数:获取当前用户(带并发保护)
* 优先从内存 Flow 获取,若无则持有锁进行数据库加载
*/
suspend fun getSafeCurrentUser(): UserEntity? {
currentUserFlow.value?.let { return it }
return mutex.withLock {
// 双重校验,解决“并发缓存击穿”问题
currentUserFlow.value ?: repository.loadCurrentUser()
}
}
/**
* 挂起函数:获取所有用户列表
* 优先从内存 Flow 获取,若为空则从数据库初次加载
*/
suspend fun getAllUsers(): List<UserEntity> =
allUsersFlow.value.takeIf { it.isNotEmpty() } ?: repository.getAllUsersFlow().first()
/** 回调函数:获取当前用户(主线程安全回调) */
fun getUserAsync(onResult: (UserEntity?) -> Unit) =
runAsyncMain({ getSafeCurrentUser() }, onResult)
/** 回调函数:获取用户列表(主线程安全回调) */
fun getAllUsersAsync(onResult: (List<UserEntity>) -> Unit) =
runAsyncMain({ getAllUsers() }, onResult)
/** 保存或更新用户(异步执行) */
fun saveUser(user: UserEntity) {
scope.launch(Dispatchers.IO) { repository.saveUser(user) }
}
/** 切换账号(异步执行) */
fun switchAccount(userId: String) {
scope.launch(Dispatchers.IO) { repository.switchAccount(userId) }
}
/** 退出登录(异步执行) */
fun logout(userId: String) {
scope.launch(Dispatchers.IO) { repository.logout(userId) }
}
// ------------------ 扩展数据接口 ------------------
/**
* 获取当前用户扩展信息(泛型解析 + 内存缓存)
* @param clazz 目标解析类型
* 注意:当前用户变更时会自动清理该缓存
*/
@Suppress("UNCHECKED_CAST")
fun <T> getExtraData(clazz: Class<T>): T? {
val key = clazz.name
// 1. 命中内存缓存直接返回
extraDataCache[key]?.let { return it as T? }
// 2. 无缓存则从 UserEntity.extraJson 进行解析
val json = currentUserFlow.value?.extraJson ?: return null
val parsed = try { gson.fromJson(json, clazz) } catch (e: Exception) { null }
// 3. 写入缓存提高下次读取效率
if (parsed != null) {
extraDataCache[key] = parsed
}
return parsed
}
/** 刷新内存缓存(手动从数据库同步一次最新状态) */
fun refreshCache() {
scope.launch(Dispatchers.IO) { repository.refreshCache() }
}
/** 清理资源(通常仅在 Application 结束时调用) */
fun clear() {
scope.cancel()
}
// ------------------ 内部工具方法 ------------------
/** 辅助方法:将协程任务结果分发到主线程 */
private fun <T> runAsyncMain(block: suspend () -> T, onResult: (T) -> Unit) {
scope.launch {
val result = block()
withContext(Dispatchers.Main) { onResult(result) }
}
}
// ------------------ 单例管理 ------------------
companion object {
@Volatile
private var INSTANCE: UserManager? = null
/** 模块初始化(应在 Application.onCreate 中调用) */
fun init(context: Context): UserManager =
INSTANCE ?: synchronized(this) {
INSTANCE ?: UserManager(context).also { INSTANCE = it }
}
/** 获取单例对象 */
fun getInstance(): UserManager =
INSTANCE ?: throw IllegalStateException(
"UserManager must be initialized first. Call init(context) in Application."
)
}
}
3.调用方式
package com.wkq.user.util
import android.util.Log
import com.wkq.user.data.entity.UserEntity
import com.wkq.user.manager.UserManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/**
* UserManager 调用示例
*
* 展示了如何在不同场景下调用 UserManager 的公开 API。
*/
object UserManagerDemo {
private const val TAG = "UserManagerDemo"
private val scope = CoroutineScope(Dispatchers.Main)
/**
* 1. 响应式监听当前用户变化 (推荐)
*/
fun observeUser() {
scope.launch {
UserManager.getInstance().currentUserFlow.collectLatest { user ->
if (user != null) {
Log.d(TAG, "当前用户更新: ${user.userName}")
} else {
Log.d(TAG, "当前未登录")
}
}
}
}
/**
* 2. 在协程中同步安全地获取当前用户 (挂起函数)
*/
suspend fun fetchUser() {
val user = UserManager.getInstance().getSafeCurrentUser()
Log.d(TAG, "获取到最新用户: ${user?.userName}")
}
/**
* 3. 使用回调方式获取当前用户 (传统异步方式)
*/
fun getUserAsync() {
UserManager.getInstance().getUserAsync { user ->
Log.d(TAG, "异步回调获取用户: ${user?.userName}")
}
}
/**
* 4. 获取用户自定义扩展数据 (自动解析 JSON 并缓存)
*/
fun showExtraData() {
// 假设 UserEntity 中的 extraJson 存储了 UserSettings 的 JSON
data class UserSettings(val theme: String = "dark", val language: String = "zh")
val settings = UserManager.getInstance().getExtraData(UserSettings::class.java)
Log.d(TAG, "用户设置: 主题=${settings?.theme}, 语言=${settings?.language}")
}
/**
* 5. 执行账号操作 (保存、切换、退出)
*/
fun accountOperations() {
val userManager = UserManager.getInstance()
// 保存/更新用户
val newUser = UserEntity(userId = "1001", userName = "张三", isCurrent = true)
userManager.saveUser(newUser)
// 切换账号
// userManager.switchAccount("1002")
// 退出登录 (清理指定用户状态)
// userManager.logout("1001")
}
}
总结
模块简单说明
-
UserManager(外) -> UserRepository(中) -> UserDao/Cache(内存/内) 的标准架构,实现了业务逻辑与存储细节的彻底分离。
-
通过
internal关键字隐藏了仓库、缓存和数据库等实现细节,将模块的公开 API 收窄至 -
启用了 Room 的
exportSchema功能并配置了文件路径,为后续的版本迭代和数据库迁移提供扩展 -
UserManager,UserEntity(仅公开业务入口和必要的数据模型)。
内存+Room 缓存保证数据安全,internal关键句只暴露对外调用,Room+extraJson字段扩展 保证追加字段的稳定性.
暂时就想到这么多,后期有想到的地方再追加