Android 项目架构之<用户信息模块>

Android 中大型项目架构梳理<一>

Android 项目架构之用户信息模块<二>

背景介绍:

个人35+的老年Android开发.日常开发中,维护和开发和我码领差不多的远古代码,着实被恶心的不轻.加上这几年的姑且称之为工作经验的东西吧,梳理一下项目中所遇到的一些问题,以及自己所想到的一些解决方式方法.


本章提要:用户信息模块搭建

原因:

在庞大的项目中因为快速和频繁的获取用户信息,当内存不足等特殊情况导致用户信息为空,造成项目稳定性不足.

1:用户信息模块搭建思路

公司项目是一个直播项目,IM/直播/动画等会占据大量内存,进而频繁的GC,有时候会出现内存释放本不该释放的对象又没有重建,导致用户信息为null的情况出现.

因为内存不足导致被释放,就加载本地不就好了嘛 类似多级缓存的思路.

调用->内存->数据库/SP->网络

主要功能:

  • 内存缓存
  • 数据库存储(Room,存储)
  • 多用户存储
  • 快速/安全获取(协程获取)

2.具体实现

为方便后期扩展,采取数据库形式存储数据(多账户,加密等).采取数据库存储+缓存存储保证数据不为空,用Kotlin+协程实现数据库数据的获取

2.1 Room 实现用户信息的存储

核心代码:

  • UserDatabase 数据库创建和更新
  • UserEntity 用户信息数据
  • UserDao 数据库操作类
  • UserRepository 操作工具类
2.1.1 用户信息数据

注意:

因为数据库,表中增加字段需要更新表,所以采用了常用字段和扩展字段(extraJson)的形式,减少因为增加字段而修改表结构带来的风险

less 复制代码
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外部不可见,减少调用者的干扰

kotlin 复制代码
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)
kotlin 复制代码
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 数据库数据的操作类
kotlin 复制代码
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 缓存信息暴露的方法

kotlin 复制代码
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 用户信息操作的主操作类

kotlin 复制代码
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.调用方式

kotlin 复制代码
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字段扩展 保证追加字段的稳定性.

暂时就想到这么多,后期有想到的地方再追加

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui