从项目崩盘到团队效率翻倍手机app架构设计避坑指南开发流程规范与性能优化实操
说实话,看到你这个标题,我仿佛又回到了三年前那个凌晨三点。
那是我参与的项目,用户量刚破百万,突然有一天,线上反馈炸了——崩溃率飙到12%,客服群被用户骂穿了,老板在群里@全组”明天必须上线修复版”。我们团队15个人,连续加了半个月班,最后花了40万做紧急重构,才把这个项目从死亡线上拉回来。
那次事故之后,我开始研究大量成功和失败的项目案例,也带出了现在效率翻倍、上线零事故的技术团队。今天,我就把这个过程中踩过的坑、总结的方法,全部掏心窝子分享给你。
一、先看看我们当年是怎么”作”死的
别急着听干货,先听我讲个故事。
我们当年那个App叫”快享生活”,是个本地生活服务平台,集外卖、打车、家政于一体。刚启动的时候,代码写成这样:
// 某段历史悠久的"祖传代码"
public class UserService {
private static UserService instance;
private List<User> allUsers;
private Map<String, Order> allOrders;
// 单例模式?随便写的
public static UserService getInstance() {
if (instance == null) {
instance = new UserService();
}
return instance;
}
// 一个方法里干了三件事:查用户、查订单、更新状态
public User getUserAndOrderAndStatus(String userId) {
// 同步请求,耗时3秒
User user = db.queryUser(userId);
Order order = db.queryOrder(userId);
// 没有异常处理,数据库断了整个方法就崩了
user.setStatus(order.getStatus());
// 还顺手存了个单例里的全局变量
allUsers.add(user);
allOrders.put(userId, order);
return user;
}
}
看到这段代码,你是不是也觉得眼熟?别尴尬,我见过80%的创业团队都写过类似的代码。
问题在哪里?让我一个个给你拆解:
第一个坑:没有分层架构
你看那个UserService,数据库操作、业务逻辑、数据缓存全塞在一个类里。随着功能越来越多,这个类膨胀到了3000多行。有人要改用户头像的逻辑,得在3000行代码里找位置;有人要加订单功能,直接把代码塞在方法末尾。
结果就是:A改用户信息,B的订单功能突然崩了;C加了个推送逻辑,D的登录功能失效了。
第二个坑:同步阻塞,用户体验极差
// 问题代码:主线程直接发网络请求
public void loadUserProfile(String userId) {
// 这是在UI线程!
User user = network.call("/api/user/" + userId); // 可能阻塞2-3秒
avatarView.setImageBitmap(user.getAvatar());
nameView.setText(user.getName());
// ...还有10个字段要设置
}
Android和iOS都不允许在主线程做耗时操作,但那时候我们为了”快”,直接就这么写了。用户点进个人中心,经常看到界面卡住2秒,然后”唰”一下全部显示出来。用户反馈:”这App怎么这么卡?”
第三个坑:没有统一的异常处理
// 每个接口自己try-catch,处理方式五花八门
try {
Order order = api.getOrder(orderId);
showOrder(order);
} catch (Exception e) {
Toast.makeText(this, "出错了" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
// 另一个地方
try {
User user = api.getUser(userId);
bindData(user);
} catch (Exception e) {
// 直接print,完全没处理
System.out.println(e.getMessage());
}
数据库连不上?接口超时?权限拒绝?各种异常散落在几百个地方,每个地方处理方式都不一样。用户看到的错误提示五花八门,有的弹Toast,有的直接闪退,有的什么都不显示。
第四个坑:代码重复严重
同样是”登录”功能,登录页写了一套逻辑,注册页又写了一套,第三方登录还写了一套。三套代码,三个版本号,三个维护者。有一次修登录bug,只改了登录页,注册页的bug没人知道,用户投诉来了才发现问题。
二、架构设计:我们是怎么翻盘的
崩盘之后,我们花了两个月时间做架构重构。以下是我们总结出来的核心架构设计原则,以及具体的代码实现。
2.1 采用MVVM + Clean Architecture分层架构
我们引入了Clean Architecture的思想,把整个项目分成四层:
┌─────────────────────────────────────┐
│ Presentation Layer │ ← UI层:Activity/Fragment + ViewModel
├─────────────────────────────────────┤
│ Domain Layer │ ← 业务逻辑层:UseCase + Repository接口
├─────────────────────────────────────┤
│ Data Layer │ ← 数据层:Repository实现 + DataSource
├─────────────────────────────────────┤
│ Network / Database / Cache │ ← 基础设施层:Retrofit / Room / DataStore
└─────────────────────────────────────┘
为什么要这样分层? 核心原因是可测试性和可维护性。每一层只依赖下一层,不跨层调用。这样测试的时候,可以单独测试每一层,不用启动整个App。
2.2 Presentation层:ViewModel + LiveData/MediatorLiveData
UI层只负责”展示”和”接收用户输入”,所有业务逻辑都放到ViewModel里。
// 正确的做法:ViewModel负责所有业务逻辑
class UserProfileViewModel(
private val getUserUseCase: GetUserUseCase,
private val updateAvatarUseCase: UpdateAvatarUseCase
) : ViewModel() {
// 用StateFlow管理状态,替代LiveData,更现代
private val _uiState = MutableStateFlow<UserProfileUiState>(UserProfileUiState.Loading)
val uiState: StateFlow<UserProfileUiState> = _uiState.asStateFlow()
// 用SharedFlow管理一次性事件,比如Toast、导航
private val _events = MutableSharedFlow<UserProfileEvent>()
val events: SharedFlow<UserProfileEvent> = _events.asSharedFlow()
// 加载用户信息
fun loadUserProfile(userId: String) {
viewModelScope.launch {
_uiState.value = UserProfileUiState.Loading
try {
val user = getUserUseCase.execute(userId)
_uiState.value = UserProfileUiState.Success(user)
} catch (e: Exception) {
_uiState.value = UserProfileUiState.Error(e.message ?: "加载失败")
_events.emit(UserProfileEvent.ShowToast(e.message ?: "加载失败"))
}
}
}
// 更新头像
fun updateAvatar(imageUri: Uri) {
viewModelScope.launch {
try {
updateAvatarUseCase.execute(userId, imageUri)
_events.emit(UserProfileEvent.ShowToast("头像更新成功"))
_events.emit(UserProfileEvent.RefreshProfile)
} catch (e: Exception) {
_events.emit(UserProfileEvent.ShowToast("头像更新失败"))
}
}
}
}
// UI层非常简洁,只负责观察状态
class UserProfileFragment : Fragment() {
private val viewModel: UserProfileViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 观察状态,根据状态更新UI
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState.collect { state ->
when (state) {
is UserProfileUiState.Loading -> showLoading()
is UserProfileUiState.Success -> bindData(state.user)
is UserProfileUiState.Error -> showError(state.message)
}
}
}
// 观察事件,处理一次性操作
viewLifecycleOwner.lifecycleScope.launch {
viewModel.events.collect { event ->
when (event) {
is UserProfileEvent.ShowToast -> showToast(event.message)
is UserProfileEvent.RefreshProfile -> viewModel.loadUserProfile(currentUserId)
}
}
}
}
}
看到区别了吗?UI层只有不到100行代码,而且完全不包含任何业务逻辑。业务逻辑全在ViewModel里,这样测试起来就非常简单——只需要测试ViewModel,不需要启动Activity/Fragment。
2.3 Domain层:UseCase是核心
// UseCase是Domain层的核心,每个UseCase只做一件事
abstract class UseCase<out Type, in Params> {
abstract suspend fun execute(params: Params): Type
}
// 具体UseCase:获取用户信息
class GetUserUseCase(
private val userRepository: UserRepository
) : UseCase<User, String>() {
override suspend fun execute(params: String): User {
return userRepository.getUser(params)
}
}
// 具体UseCase:更新用户头像
class UpdateAvatarUseCase(
private val userRepository: UserRepository
) : UseCase<Unit, UpdateAvatarParams>() {
override suspend fun execute(params: UpdateAvatarParams) {
userRepository.updateAvatar(params.userId, params.imageUri)
}
}
data class UpdateAvatarParams(
val userId: String,
val imageUri: Uri
)
为什么用UseCase? 因为UseCase把业务逻辑从ViewModel里抽出来,可以单独测试,也可以被多个ViewModel复用。比如”获取用户信息”这个逻辑,可能订单页、个人中心页、分享页都要用到,写成UseCase之后,三个页面共用同一个UseCase,修改bug只需要改一个地方。
2.4 Data层:Repository模式
// Repository接口(Domain层依赖这个接口,实现由Data层提供)
interface UserRepository {
suspend fun getUser(userId: String): User
suspend fun updateAvatar(userId: String, imageUri: Uri)
suspend fun refreshUserCache(userId: String)
}
// Repository实现(Data层提供)
class UserRepositoryImpl(
private val userRemoteDataSource: UserRemoteDataSource,
private val userLocalDataSource: UserLocalDataSource,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : UserRepository {
override suspend fun getUser(userId: String): User {
return try {
// 先查本地缓存
val cachedUser = withContext(dispatcher) {
userLocalDataSource.getUser(userId)
}
if (cachedUser != null) {
// 缓存命中,返回缓存数据,同时后台刷新
refreshUserCache(userId)
cachedUser
} else {
// 缓存未命中,从网络获取
val remoteUser = withContext(dispatcher) {
userRemoteDataSource.getUser(userId)
}
// 写入本地缓存
withContext(dispatcher) {
userLocalDataSource.saveUser(remoteUser)
}
remoteUser
}
} catch (e: Exception) {
// 网络失败,返回本地缓存(如果有的话)
withContext(dispatcher) {
userLocalDataSource.getUser(userId)
?: throw UserRepositoryException("用户数据获取失败", e)
}
}
}
override suspend fun updateAvatar(userId: String, imageUri: Uri) {
// 1. 上传图片到服务器
val uploadedUrl = withContext(dispatcher) {
userRemoteDataSource.uploadAvatar(imageUri)
}
// 2. 更新本地缓存
withContext(dispatcher) {
userLocalDataSource.updateAvatarUrl(userId, uploadedUrl)
}
}
override suspend fun refreshUserCache(userId: String) {
viewModelScope.launch(dispatcher) {
try {
val remoteUser = userRemoteDataSource.getUser(userId)
userLocalDataSource.saveUser(remoteUser)
} catch (e: Exception) {
// 刷新失败不影响当前使用
Log.w("UserRepository", "后台刷新失败", e)
}
}
}
}
// 数据源分层:Remote和Local分开
interface UserRemoteDataSource {
suspend fun getUser(userId: String): User
suspend fun uploadAvatar(imageUri: Uri): String
}
interface UserLocalDataSource {
suspend fun getUser(userId: String): User?
suspend fun saveUser(user: User)
suspend fun updateAvatarUrl(userId: String, avatarUrl: String)
}
这个设计的精妙之处:
- 依赖倒置:Domain层的
UserRepository是接口,Data层实现它。这样Domain层不依赖任何具体实现,可以随时替换数据源。 - 缓存策略:
getUser方法先查本地缓存,缓存命中就返回并后台刷新;缓存未命中才请求网络。这样大部分情况下用户看到的是秒开的数据。 - 异常处理:网络失败时返回本地缓存,保证用户体验不断档。
2.5 网络层:Retrofit + OkHttp的规范配置
// Retrofit工厂类,统一管理
object RetrofitClient {
private const val BASE_URL = "https://api.example.com/"
// 超时配置:连网超时30秒,读写超时60秒
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
// 添加日志拦截器(只在DEBUG模式开启)
.addInterceptor { chain ->
val request = chain.request()
val response = chain.proceed(request)
if (BuildConfig.DEBUG) {
Log.d("HTTP", "${request.method} ${request.url} -> ${response.code}")
}
response
}
// 添加请求头拦截器(统一添加token、版本号等)
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Content-Type", "application/json")
.addHeader("App-Version", BuildConfig.VERSION_NAME)
.addHeader("Device-Id", DeviceManager.getDeviceId())
.apply {
val token = TokenManager.getToken()
if (!token.isNullOrEmpty()) {
addHeader("Authorization", "Bearer $token")
}
}
.build()
chain.proceed(request)
}
// 添加响应拦截器(统一处理错误码)
.addInterceptor { chain ->
val response = chain.proceed(chain.request())
when (response.code) {
401 -> {
// token过期,清除token,跳转到登录页
TokenManager.clearToken()
// 这里用EventBus或者SharedFlow通知UI层跳转
}
429 -> {
// 请求太频繁,提示用户稍后再试
throw TooManyRequestsException("请求过于频繁,请稍后再试")
}
500..599 -> {
// 服务器错误,提示用户
throw ServerException("服务器错误,请稍后重试")
}
}
response
}
.build()
val apiService: ApiService by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(
GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
.create()
))
.build()
.create(ApiService::class.java)
}
}
// API接口定义
interface ApiService {
@GET("users/{userId}")
suspend fun getUser(@Path("userId") userId: String): ApiResponse<User>
@Multipart
@POST("users/avatar")
suspend fun uploadAvatar(
@Part image: MultipartBody.Part
): ApiResponse<UploadResult>
}
// 统一的响应包装类
data class ApiResponse<T>(
val code: Int,
val message: String,
val data: T?
) {
fun isSuccess() = code == 200
fun getDataOrThrow(): T = data
?: throw ApiResultException(message, code)
}
关键设计点:
- 超时时间:30秒连接超时,60秒读写超时。太短容易误判,太长用户体验差。
- 统一拦截器:通过OkHttp的拦截器,统一添加请求头、处理错误码,避免在每个接口里重复写同样的代码。
- 响应包装:
ApiResponse<T>把所有接口的响应统一成一个格式,前端处理起来更方便。
2.6 本地存储:Room + DataStore规范
// Entity定义
@Entity(tableName = "users")
data class User(
@PrimaryKey val userId: String,
val name: String,
val avatarUrl: String,
val email: String,
val createdAt: Long,
val updatedAt: Long
)
// DAO
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE userId = :userId")
suspend fun getUserById(userId: String): User?
@Upsert
suspend fun saveUser(user: User)
@Query("DELETE FROM users WHERE userId = :userId")
suspend fun deleteUser(userId: String)
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
}
// DataStore存储用户偏好设置
object UserPreferencesDataStore {
private const val PREFERENCES_NAME = "user_preferences"
val preferencesFlow: Flow<UserPreferences> = context.dataStore(
preferencesName = PREFERENCES_NAME
).map { preferences ->
UserPreferences(
lastLoginTime = preferences[LAST_LOGIN_TIME_KEY] ?: 0L,
themeMode = preferences[THEME_MODE_KEY] ?: "system"
)
}
suspend fun updateLastLoginTime(time: Long) {
context.dataStore.edit { preferences ->
preferences[LAST_LOGIN_TIME_KEY] = time
}
}
}
// Repository实现本地数据源
class UserLocalDataSourceImpl(
private val userDao: UserDao,
private val application: Application
) : UserLocalDataSource {
override suspend fun getUser(userId: String): User? {
return userDao.getUserById(userId)
}
override suspend fun saveUser(user: User) {
userDao.saveUser(user)
}
override suspend fun updateAvatarUrl(userId: String, avatarUrl: String) {
val user = userDao.getUserById(userId) ?: return
userDao.saveUser(user.copy(avatarUrl = avatarUrl, updatedAt = System.currentTimeMillis()))
}
}
三、开发流程规范:让团队效率翻倍的秘密
架构设计好之后,开发流程也至关重要。我们团队在规范这套流程之后,开发效率提升了2.3倍,线上事故率从每月3-5起降到了每月0-1起。
3.1 Git分支管理规范
我们采用GitFlow的改进版,每个项目必须遵循以下规范:
main ← 生产环境代码,只能从develop合并,禁止直接修改
│
develop ← 开发主分支,每天必须保持可编译状态
│
├── feature/xxx ← 功能分支,从develop创建,合并回develop
├── bugfix/xxx ← Bug修复分支,从develop创建,合并回develop
├── release/xxx ← 发布分支,从develop创建,用于测试,合并到main和develop
└── hotfix/xxx ← 紧急修复分支,从main创建,用于线上紧急修复,合并到main和develop
具体规范:
分支命名规范:
- 功能分支:
feature/功能描述,如feature/user-profile-refactor - Bug修复:
bugfix/问题描述,如bugfix/login-crash-on-android12 - 发布分支:
release/版本号,如release/v2.3.0 - 紧急修复:
hotfix/问题描述,如hotfix/payment-api-down
- 功能分支:
提交信息规范: “` // 格式:类型(范围): 描述
// 类型:feat(新功能)、fix(修复)、docs(文档)、style(格式)、refactor(重构)、test(测试)、chore(构建/工具)
feat(user): 添加用户头像上传功能 fix(payment): 修复iOS支付回调延迟问题 refactor(network): 重构网络层,统一错误处理 docs(readme): 更新API文档 test(auth): 增加登录接口的单元测试
3. **Code Review规范**:
- 所有合并到`develop`和`main`的代码必须经过至少**2人**Code Review
- Code Review使用模板,检查清单包括:
- [ ] 代码是否符合编码规范
- [ ] 是否有单元测试
- [ ] 是否有潜在的内存泄漏
- [ ] 是否有安全隐患
- [ ] 性能是否有明显问题
- [ ] 是否处理了异常情况
- Code Review在Pull Request中完成,禁止私下沟通
### 3.2 代码规范:Kotlin/Java双规范
```kotlin
// 我们使用detekt作为静态代码分析工具,配置如下:
# detekt配置示例
config:
warningsAsErrors: false
processors:
active: true
exclude:
- 'DetektProgressListener'
console-reports:
active: true
exclude:
- 'ProjectStatisticsReport'
- 'NotificationReport'
- 'FindingsReport'
comments:
active: true
AbsentFriend:
active: false
EndOfSentenceFormat:
active: false
KDocReferencesNonPublicProperty:
active: false
OutdatedDocumentation:
active: false
complexity:
active: true
ComplexCondition:
active: true
threshold: 4 # 条件表达式最多4个条件
ComplexMethod:
active: true
threshold: 15 # 方法最多15行
ignoreSimpleWhenEntries: true
ignoreWhenStatements: false
LongMethod:
active: true
threshold: 60 # 方法最多60行
LongParameterList:
active: true
functionThreshold: 5 # 函数参数最多5个
constructorThreshold: 7 # 构造函数参数最多7个
ignoreDefaultParameters: true
NamedArguments:
active: false
NestedBlockDepth:
active: true
threshold: 4 # 嵌套深度最多4层
ReplaceSafeCallChainWithRun:
active: false
TooManyFunctions:
active: true
thresholdInFiles: 15 # 文件最多15个函数
thresholdInClasses: 15 # 类最多15个函数
thresholdInInterfaces: 10 # 接口最多10个函数
thresholdInObjects: 10 # 对象最多10个函数
thresholdInEnums: 10 # 枚举最多10个函数
empty-blocks:
active: true
EmptyCatchBlock:
active: true
allowedExceptionNameRegex: "_|ignore"
EmptyClassBlock:
active: false
EmptyDefaultConstructor:
active: false
EmptyDoWhileBlock:
active: false
EmptyElseBlock:
active: false
EmptyFinallyBlock:
active: false
EmptyForBlock:
active: false
EmptyFunctionBlock:
active: true
ignoreOverridden: false
EmptyIfBlock:
active: false
EmptyInitBlock:
active: false
EmptyWhenBlock:
active: false
EmptyWhileBlock:
active: false
exceptions:
active: true
ExceptionRaisedInUnexpectedLocation:
active: true
methodNames: [toString, hashCode, equals, finalize]
InstanceOfCheckForException:
active: false
NotImplementedDeclaration:
active: false
PrintStackTrace:
active: false
SwallowedException:
active: false
ThrowingExceptionFromFinally:
active: true
ThrowingExceptionsWithoutMessageOrCause:
active: true
exceptions: [IllegalArgumentException, IllegalStateException, IOException]
ThrowingNewInstanceOfSameException:
active: true
TooGenericExceptionCaught:
active: true
exceptionNames:
- ArrayIndexOutOfBoundsException
- Error
- Exception
- IllegalMonitorStateException
- NullPointerException
- IndexOutOfBoundsException
- RuntimeException
- Throwable
allowedExceptionNameRegex: "_|ignore"
TooGenericExceptionThrown:
active: true
exceptionNames:
- Error
- Exception
- Throwable
- RuntimeException
naming:
active: true
BooleanPropertyNaming:
active: false
ClassNaming:
active: true
classPattern: '[A-Z][a-zA-Z0-9]*'
ConstructorParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
privateParameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
EnumNaming:
active: true
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
ForbiddenClassName:
active: false
FunctionParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
InvalidPackageDeclaration:
active: false
excludedPackages: ['kotlin']
MatchingDeclarationName:
active: true
MemberNameEqualsClassName:
active: false
ignoreOverridden: true
ObjectPropertyNaming:
active: true
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '_[A-Za-z][_A-Za-z0-9]*'
PackageNaming:
active: true
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
TopLevelPropertyNaming:
active: true
constantPattern: '[A-Z][_A-Z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '_[A-Za-z][_A-Za-z0-9]*'
VariableMaxLength:
active: false
VariableNaming:
active: true
variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '_[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
performance:
active: true
ArrayPrimitive:
active: true
ForEachOnRange:
active: true
SpreadOperator:
active: true
UnnecessaryTemporaryInstantiation:
active: true
potential-bugs:
active: true
Deprecation:
active: false
DuplicateCaseInWhenExpression:
active: true
EqualsAlwaysReturnsTrueOrFalse:
active: true
EqualsWithHashCodeExist:
active: true
ExplicitGarbageCollectionCall:
active: true
HasPlatformType:
active: false
IgnoredReturnValue:
active: false
ImplicitDefaultLocale:
active: false
ImplicitUnitReturnType:
active: false
InvalidRange:
active: true
IteratorHasTheUsesRemovedMethod:
active: true
MapGetWithNotNullAssertionOperator:
active: false
NotEqualsShortNotation:
active: false
UnconditionalJumpStatementInLoop:
active: false
UnnecessaryNullableCall:
active: false
UnreachableCode:
active: true
UnsafeCallOnNullableType:
active: true
UnsafeCast:
active: true
UselessPostfixExpression:
active: true
WrongEqualsTypeParameter:
active: true
style:
active: true
ClassOrdering:
active: false
CollapsibleIfStatements:
active: false
DataClassContainsFunctions:
active: false
conversionFunctionPrefix: 'to'
DataClassShouldBeImmutable:
active: false
DestructuringDeclarationWithTooManyEntries:
active: false
maxDestructuringEntries: 3
EqualsNullCall:
active: false
EqualsOnSignatureLine:
active: false
ExplicitCollectionElementAccessMethod:
active: false
ExplicitItLambdaParameter:
active: true
ExpressionBodySyntax:
active: false
includeLineWrapping: false
ForbiddenComment:
active: true
values:
- 'FIXME:'
- 'STOPSHIP:'
- 'TODO:'
allowedPatterns: ''
customMessage: '禁止使用注释中的TODO/FIXME/STOPSHIP标记'
ForbiddenImport:
active: false
ForbiddenMethodCall:
active: false
ForbiddenPublicDataClass:
active: false
ForbiddenSuppress:
active: false
ForbiddenVoid:
active: true
ignoreOverridden: false
ignoreUsageInGenerics: false
FunctionOnlyReturningConstant:
active: true
excludeOverridden: false
ignoreDeprecation: false
ignoreInternal: false
LoopWithTooManyJumpStatements:
active: true
maxJumpCount: 1
MagicNumber:
active: false
ignoreNumbers: ['-1', '0', '1', '2']
ignoreHashCodeFunction: true
ignorePropertyDeclaration: false
ignoreLocalVariableDeclaration: false
ignoreConstantDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true
ignoreAnnotation: false
ignoreNamedArgument: true
ignoreEnums: false
ignoreRanges: false
MandatoryBracesIfStatements:
active: false
MandatoryBracesLoops:
active: false
MaxChainedCallsOnSameLine:
active: false
maxChainedCalls: 5
MaxLineLength:
active: true
maxLineLength: 120 # 每行最多120个字符
excludePackageStatements: true
excludeImportStatements: true
excludeCommentStatements: false
MayBeConst:
active: true
ModifierOrder:
active: true
MultilineLambdaItParameter:
active: false
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
active: true
NoTabs:
active: false
OptionalAbstractKeyword:
active: true
OptionalUnit:
active: false
OptionalWhenBraces:
active: false
PreferToOverPairSyntax:
active: false
ProtectedMemberInFinalClass:
active: true
RedundantExplicitType:
active: false
RedundantHigherOrderMapUsage:
active: true
RedundantVisibilityModifierRule:
active: false
ReturnCount:
active: true
max: 3 # 方法最多3个return
excludedFunctions: 'equals'
excludeLabeled: false
excludeReturnFromLambda: true
excludeGuardClauses: false
SafeCast:
active: true
SerialVersionUIDInSerializableClass:
active: false
SpacingBetweenPackageAndImports:
active: false
ThrowsCount:
active: true
max: 2 # 方法最多2个throws
excludeGuardClauses: false
TrailingWhitespace:
active: false
UnderscoresInNumericLiterals:
active: false
acceptableLength: 5
UnnecessaryAbstractClass:
active: true
UnnecessaryAnnotationClassReference:
active: false
UnnecessaryApply:
active: true
UnnecessaryFilter:
active: false
UnnecessaryInheritance:
active: true
UnnecessaryLet:
active: false
UnnecessaryParentheses:
active: false
UntilInsteadOfRangeTo:
active: false
UnusedImports:
active: false
UnusedPrivateClass:
active: true
UnusedPrivateMember:
active: false
allowedNames: '(_|ignored|expected|all)'
UseAnyOrNoneInsteadOfFind:
active: false
UseArrayLiteralInForEach:
active: false
UseCheckOrError:
active: false
UseDataClass:
active: false
allowVars: false
UseEmptyCounterpart:
active: false
UseIfEmptyOrIfBlank:
active: false
UseIfInsteadOfWhen:
active: false
ignoreWhenContainingVariableDeclaration: false
UseIsNullOrEmpty:
active: false
UseOrEmpty:
active: false
UseRequire:
active: false
UseRequireNotNull:
active: false
UseSumOfInsteadOfFlatMapSize:
active: false
UselessCallOnNotNull:
active: true
UtilityClassWithPublicConstructor:
active: true
VarCouldBeVal:
active: true
ignoreTestFiles: true
WildcardImport:
active: true
excludeImports:
- 'java.util.*'
- 'kotlinx.android.synthetic.*'
formatting:
active: true
android: false
autoImport: false
blankLineBeforeDeclaration:
active: false
ChainWrapping:
active: true
ClassName:
active: false
CommentSpacing:
active: true
DocStyle:
active: false
Double括号Wrapping:
active: false
EnumEntryNameCase:
active: false
Filename:
active: false
FinalNewline:
active: true
insertFinalNewLine: true
ImportOrdering:
active: false
Indentation:
active: true
indentSize: 4
continuationIndentSize: 4
MaximumLineLength:
active: true
maxLineLength: 120
ignoreBackTickedIdentifier: false
ModifierOrder:
active: true
MultiLineIfElse:
active: false
NoBlankLineBeforeRbrace:
active: true
NoBlankLinesInChainedMethodCalls:
active: false
NoConsecutiveBlankLines:
active: true
NoEmptyClassBody:
active: true
NoEmptyFirstLineInMethodBlock:
active: false
NoLineBreakAfterElse:
active: true
NoLineBreakBeforeAssignment:
active: true
NoMultipleSpaces:
active: true
NoSemicolons:
active: true
NoTrailingSpaces:
active: true
NoUnusedImports:
active: true
NoWildcardImports:
active: true
packagesToImportFrom: ['java.util.*', 'kotlinx.android.synthetic.*']
PackageName:
active: false
ParameterListWrapping:
active: true
indentSize: 4
SpacingAroundAngleBrackets:
active: true
SpacingAroundColon:
active: true
SpacingAroundComma:
active: true
SpacingAroundDot:
active: true
SpacingAroundDoubleColon:
active: true
SpacingAroundKeyword:
active: true
SpacingAroundOperators:
active: true
SpacingAroundParens:
active: true
SpacingAroundRangeOperator:
active: true
SpacingAroundUnaryOperator:
active: true
SpacingBetweenDeclarationsWithAnnotations:
active: false
SpacingBetweenDeclarationsWithComments:
active: false
StringTemplate:
active: true
3.3 持续集成/持续部署(CI/CD)
# GitHub Actions示例:完整的CI/CD流程
name: Android CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
# 代码检查
detekt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run detekt
run: ./gradlew detekt
# 单元测试
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run unit tests
run: ./gradlew testDebugUnitTest
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v3
with:
name: test-reports
path: app/build/reports/tests/
# 构建APK
build:
needs: [detekt, unit-tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build debug APK
run: ./gradlew assembleDebug
- name: Build release APK
run: ./gradlew assembleRelease
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
- name: Upload APK
uses: actions/upload-artifact@v3
with:
name: apk
path: app/build/outputs/apk/
# 发布到内测平台
deploy-staging:
needs: build
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Download APK
uses: actions/download-artifact@v3
with:
name: apk
path: apk/
- name: Upload to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
token: ${{ secrets.FIREBASE_TOKEN }}
groups: testers
file: apk/app-debug.apk
# 发布到生产环境
deploy-production:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Download APK
uses: actions/download-artifact@v3
with:
name: apk
path: apk/
- name: Upload to Google Play
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
packageName: com.example.app
releaseFiles: apk/app-release.apk
track: production
status: completed
3.4 测试策略
我们团队严格执行”测试金字塔”策略:
/\
/ \ ← UI测试(少量,覆盖核心流程)
/----\
/ \ ← 集成测试(适量,覆盖关键接口)
/--------\
/ \ ← 单元测试(大量,覆盖所有业务逻辑)
/------------\
单元测试示例:
class UserProfileViewModelTest {
private lateinit var getUserUseCase: GetUserUseCase
private lateinit var updateAvatarUseCase: UpdateAvatarUseCase
private lateinit var viewModel: UserProfileViewModel
@Before
fun setUp() {
getUserUseCase = mock()
updateAvatarUseCase = mock()
viewModel = UserProfileViewModel(getUserUseCase, updateAvatarUseCase)
}
@Test
fun `loadUserProfile should emit Success state when user is found`() = runTest {
// 准备数据
val testUser = User("123", "张三", "https://example.com/avatar.jpg", "zhangsan@example.com", 0L, 0L)
coEvery { getUserUseCase.execute("123") } returns testUser
// 触发操作
viewModel.loadUserProfile("123")
// 验证结果
val state = viewModel.uiState.test { awaitItem() }
assert(state is UserProfileUiState.Success)
assert((state as UserProfileUiState.Success).user == testUser)
}
@Test
fun `loadUserProfile should emit Error state when network fails`() = runTest {
// 准备数据
coEvery { getUserUseCase.execute("123") } throws IOException("网络错误")
// 触发操作
viewModel.loadUserProfile("123")
// 验证结果
val state = viewModel.uiState.test { awaitItem() }
assert(state is UserProfileUiState.Error)
}
}
集成测试示例:
class UserRemoteDataSourceTest {
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.url("https://jsonplaceholder.typicode.com" + chain.request().url.encodedPath)
.build()
chain.proceed(request)
}
.build()
private val retrofit = Retrofit.Builder()
.baseUrl("https://fake-api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
private val apiService = retrofit.create(ApiService::class.java)
private val dataSource = UserRemoteDataSourceImpl(apiService)
@Test
fun `getUser should return correct user data`() = runTest {
val userId = "1"
val user = dataSource.getUser(userId)
assertNotNull(user)
assertEquals("1", user.userId)
assertEquals("Leanne Graham", user.name)
}
}
四、性能优化:从卡顿到丝滑的实操经验
架构和流程优化完之后,性能优化是最后一个关键步骤。下面是我们踩过的坑和总结出来的优化方案。
4.1 内存优化
内存泄漏是Android性能问题的头号杀手。 我们项目初期,内存泄漏导致App在低端机上运行10分钟后就开始卡顿甚至崩溃。
常见内存泄漏场景及修复:
// ❌ 错误示例:ViewModel持有Activity引用导致泄漏
class BadViewModel : ViewModel() {
// 这个Activity引用在ViewModel生命周期内一直存在
private var activity: Activity? = null
fun setActivity(activity: Activity) {
this.activity = activity
}
}
// ✅ 正确做法:使用WeakReference或者不持有Activity引用
class GoodViewModel : ViewModel() {
// 通过LiveData/StateFlow传递数据,而不是持有Activity引用
private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message
fun sendMessage(msg: String) {
_message.value = msg
}
}
// ❌ 错误示例:单例持有Context导致泄漏
object BadSingleton {
private var context: Context? = null
fun init(context: Context) {
this.context = context // 永远不释放!
}
}
// ✅ 正确做法:使用ApplicationContext
object GoodSingleton {
private var applicationContext: Context? = null
fun init(context: Context) {
this.applicationContext = context.applicationContext // 生命周期跟随Application
}
fun get(): Context = applicationContext ?: throw IllegalStateException("Not initialized")
}
// ❌ 错误示例:匿名内部类/lambda持有外部类引用
class BadPresenter {
private var handler = Handler(Looper.getMainLooper())
fun startLoading() {
handler.postDelayed({
// 这个lambda隐式持有了BadPresenter的引用
// 即使BadPresenter已经被销毁,handler的延迟任务还在运行
doSomething()
}, 10000)
}
}
// ✅ 正确做法:使用可取消的Job
class GoodPresenter(
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
) {
private var loadingJob: Job? = null
fun startLoading() {
loadingJob = scope.launch {
delay(10000)
doSomething()
}
}
fun cancelLoading() {
loadingJob?.cancel() // 可以主动取消,防止泄漏
}
}
// ❌ 错误示例:监听器未注销
class BadFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 注册了监听器,但Fragment销毁时没有注销
EventBus.getDefault().register(this)
}
@Subscribe
fun onEvent(event: MyEvent) {
// ...
}
}
// ✅ 正确做法:在onDestroyView中注销
class GoodFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
EventBus.getDefault().register(this)
}
override fun onDestroyView() {
super.onDestroyView()
EventBus.getDefault().unregister(this) // 及时注销
}
}
内存泄漏检测工具:
// build.gradle中添加LeakCanary依赖
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:2.12'
}
LeakCanary会自动检测内存泄漏,并在发现泄漏时弹出通知,点击可以看到详细的泄漏路径。这是我们团队的必备工具。
4.2 启动速度优化
App启动速度直接影响用户留存率。我们项目启动时间从3.2秒优化到了0.8秒。
冷启动优化策略:
// 启动页的优化
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. 先设置内容视图(最快展示)
setContentView(R.layout.activity_main)
// 2. 初始化必须立即使用的组件
initRequiredComponents()
// 3. 检查登录状态(异步)
checkLoginStatus()
}
private fun initRequiredComponents() {
// 初始化必须立即使用的组件
// 比如:主题设置、字体加载
ThemeManager.applyTheme(this)
}
private fun checkLoginStatus() {
// 检查登录状态,异步处理
lifecycleScope.launch {
val isLoggedIn = AuthManager.checkLoginStatus()
if (isLoggedIn) {
navigateToHome()
} else {
navigateToLogin()
}
}
}
}
// Application的优化
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// ❌ 不要在Application里做耗时操作!
// initDatabase() // 太慢了
// initNetwork() // 太慢了
// ✅ 只初始化轻量级、必须立即使用的组件
// 1. 初始化LeakCanary(只在debug模式)
if (BuildConfig.DEBUG) {
LeakCanary.install(this)
}
// 2. 初始化 Timber日志(轻量级)
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
// 3. 初始化数据绑定的基类(如果需要)
DataBindingUtil.setDefaultComponent(DiHolder.component)
// 4. 延迟初始化重组件(使用WorkManager或者懒初始化)
LazyInitManager.initialize(this)
}
}
// 懒初始化管理器
object LazyInitManager {
private var initialized = false
fun initialize(application: Application) {
// 使用WorkManager在后台线程延迟初始化
val workRequest = OneTimeWorkRequestBuilder<InitWork>()
.setInitialDelay(500, TimeUnit.MILLISECONDS) // 延迟500ms
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.NOT_REQUIRED)
.build()
)
.build()
WorkManager.getInstance(application).enqueueUniqueWork(
"app_init",
ExistingWorkPolicy. Replace,
workRequest
)
}
}
class InitWork(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// 在这里初始化耗时的组件
// 1. 初始化数据库
DatabaseManager.initialize(applicationContext)
// 2. 初始化网络层
NetworkManager.initialize(applicationContext)
// 3. 初始化缓存
CacheManager.initialize(applicationContext)
return Result.success()
}
}
预加载优化:
// 利用App在后台运行时预加载数据
class PreloadService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// 当App进入后台时,启动预加载服务
lifecycleScope.launch {
preloadData()
}
return START_NOT_STICKY
}
private suspend fun preloadData() {
// 预加载首页数据
val homeData = repository.loadHomeData()
cacheManager.cacheHomeData(homeData)
// 预加载用户信息
val userId = preferences.getUserId()
if (userId != null) {
val userInfo = repository.loadUserInfo(userId)
cacheManager.cacheUserInfo(userInfo)
}
}
}
4.3 列表性能优化
列表是最常见的性能问题来源。我们优化的数据:
| 优化前 | 优化后 |
|---|---|
| 滑动帧率 30fps | 滑动帧率 60fps |
| 内存占用 80MB | 内存占用 35MB |
| 列表刷新时间 800ms | 列表刷新时间 200ms |
ViewHolder优化:
// ❌ 错误做法:每次创建新的ViewHolder,不复用
class BadAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
// 每次都inflate新的布局
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_bad, parent, false)
return BadViewHolder(view)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
// 每次都要findViewByid
val viewHolder = holder as BadViewHolder
viewHolder.imageView.setImageResource(items[position].imageRes) // 每次都要解码图片
viewHolder.textView.text = items[position].text
}
}
// ✅ 正确做法:复用ViewHolder,使用DiffUtil
class GoodAdapter(
private val items: MutableList<Item> = mutableListOf()
) : ListAdapter<Item, GoodAdapter.ItemViewHolder>(ItemDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_good, parent, false)
return ItemViewHolder(view)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.bind(getItem(position))
}
class ItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val imageView: ImageView = view.findViewById(R.id.image)
private val textView: TextView = view.findViewById(R.id.text)
fun bind(item: Item) {
// Glide自动管理图片加载和缓存
Glide.with(itemView.context)
.load(item.imageUri)
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
.into(imageView)
textView.text = item.text
}
}
}
// DiffCallback:只更新变化的item
class ItemDiffCallback : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
// 如果有局部刷新需求,可以重写getChangePayload
override fun getChangePayload(oldItem: Item, newItem: Item): Any? {
return when {
oldItem.text != newItem.text -> "text"
oldItem.imageUri != newItem.imageUri -> "image"
else -> null
}
}
}
图片加载优化:
// Glide配置优化
object GlideConfig {
fun init(context: Context) {
Glide.get(context).apply {
// 内存缓存策略
memoryCache = LruResourceCache(32 * 1024 * 1024) // 32MB内存缓存
// 磁盘缓存策略
diskCache = InternalCacheDiskCacheFactory(context, "glide_cache", 100 * 1024 * 1024) // 100MB磁盘缓存
}
}
}
// 使用示例
class OptimizedImageLoader {
fun loadImage(
view: ImageView,
uri: String,
placeholder: Int = R.drawable.placeholder,
error: Int = R.drawable.error
) {
Glide.with(view.context)
.load(uri)
.placeholder(placeholder)
.error(error)
.fitCenter() // 居中裁剪,减少内存占用
.override(200, 200) // 指定目标尺寸,避免大图加载
.diskCacheStrategy(DiskCacheStrategy.ALL) // 同时缓存原始和转换后的图片
.into(view)
}
// 预加载图片
fun preloadImage(uri: String) {
Glide.with(applicationContext)
.load(uri)
.preload(200, 200)
}
// 清除缓存
fun clearMemoryCache() {
Glide.get(applicationContext).clearMemory()
}
fun clearDiskCache() {
Glide.get(applicationContext).clearDiskCache()
}
}
4.4 网络优化
// 网络层优化配置
object NetworkConfig {
// 1. 连接池优化
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
// 连接池:最大5个空闲连接,保持5分钟
.connectionPool(ConnectionPool(
maxIdleConnections = 5,
keepAliveDuration = 5,
TimeUnit.MINUTES
))
// DNS缓存:减少DNS查询时间
.dns(object : Dns {
private val cache = SimpleDnsCache()
override fun lookup(hostname: String): List<InetAddress> {
return cache.get(hostname) ?: run {
val addresses = Dns.getDefault().lookup(hostname)
cache.put(hostname, addresses)
return addresses
}
}
})
// HTTP/2支持:多路复用,减少连接开销
.protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
.build()
// 2. 请求合并:相同URL的请求合并发送
class RequestMerger {
private val pendingRequests = mutableMapOf<String, MutableList<Continuation<Any?>>>()
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun <T> merge(
url: String,
continuation: Continuation<T>
) {
val key = url
// 如果已有相同请求在等待,加入等待列表
if (pendingRequests.containsKey(key)) {
pendingRequests[key]?.add(continuation as Continuation<Any?>)
return
}
// 否则发起新请求
pendingRequests[key] = mutableListOf(continuation as Continuation<Any?>)
scope.launch {
try {
val result = apiService.getData(url)
// 通知所有等待的请求
pendingRequests.remove(key)?.forEach { cont ->
cont.resume(result)
}
} catch (e: Exception) {
pendingRequests.remove(key)?.forEach { cont ->
cont.resumeWithException(e)
}
}
}
}
}
// 3. 请求去重:相同参数在TTL时间内只发一次请求
class RequestDeduplicator {
private val recentRequests = mutableMapOf<String, Long>()
private val ttl = 5000L // 5秒去重窗口
fun shouldRequest(key: String): Boolean {
val now = System.currentTimeMillis()
return if (recentRequests.containsKey(key)) {
now - recentRequests[key]!! > ttl
} else {
recentRequests[key] = now
true
}
}
}
}
4.5 数据库优化
// Room数据库优化配置
@Database(
entities = [User::class, Order::class, Product::class],
version = 3,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun orderDao(): OrderDao
abstract fun productDao(): ProductDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
// 数据库优化配置
.addCallback(object : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
// 开启WAL模式,提升读写性能
db.execSQL("PRAGMA journal_mode=WAL")
// 提升写入性能
db.execSQL("PRAGMA synchronous=NORMAL")
}
})
// 查询优化:允许在主线程查询(不推荐,但便于调试)
.allowMainThreadQueries()
// 构建数据库
.build()
INSTANCE = instance
instance
}
}
}
}
// DAO优化:批量操作
@Dao
interface OrderDao {
// ❌ 错误做法:循环插入,每次插入都开启事务
@Insert
suspend fun insertOrder(order: Order)
// ✅ 正确做法:批量插入,一次性事务
@Insert
suspend fun insertOrders(orders: List<Order>)
// ❌ 错误做法:多次查询
@Query("SELECT * FROM orders WHERE userId = :userId")
fun getAllOrders(userId: String): Flow<List<Order>>
// ✅ 正确做法:使用分页,减少内存占用
@Query("SELECT * FROM orders WHERE userId = :userId")
fun getOrdersPaged(userId: String): PagingSource<Int, Order>
// ❌ 错误做法:不合适的索引
// 数据库默认索引
// ✅ 正确做法:添加复合索引
// CREATE INDEX idx_user_status ON orders(user_id, status)
}
// 数据库迁移
object DatabaseMigrations {
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// 添加新表
database.execSQL("""
CREATE TABLE IF NOT EXISTS `products` (
`id` TEXT NOT NULL,
`name` TEXT NOT NULL,
`price` REAL NOT NULL,
PRIMARY KEY(`id`)
)
""")
// 添加索引
database.execSQL("CREATE INDEX `index_products_name` ON `products`(`name`)")
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
// 添加新列
database.execSQL("ALTER TABLE orders ADD COLUMN `discount` REAL NOT NULL DEFAULT 0")
// 修改表结构(需要重建表)
database.execSQL("""
CREATE TABLE orders_new (
`id` TEXT NOT NULL,
`userId` TEXT NOT NULL,
`total` REAL NOT NULL,
`status` TEXT NOT NULL,
`discount` REAL NOT NULL DEFAULT 0,
PRIMARY KEY(`id`)
)
""")
database.execSQL("""
INSERT INTO orders_new (id, userId, total, status, discount)
SELECT id, userId, total, status, 0 FROM orders
""")
database.execSQL("DROP TABLE orders")
database.execSQL("ALTER TABLE orders_new RENAME TO orders")
}
}
}
五、性能监控与问题排查
优化不是一次性的工作,需要持续监控和迭代。
5.1 监控指标
// 性能监控工具类
object PerformanceMonitor {
// 1. 启动时间监控
private var coldStartStartTime = 0L
private var coldStartEndTime = 0L
fun onColdStartStart() {
coldStartStartTime = SystemClock.uptimeMillis()
}
fun onColdStartEnd() {
coldStartEndTime = SystemClock.uptimeMillis()
val duration = coldStartEndTime - coldStartStartTime
if (duration > 2000) { // 超过2秒就报警
reportSlowStart(duration)
}
}
// 2. 内存监控
fun checkMemoryLeaks() {
val runtime = Runtime.getRuntime()
val memoryInfo = Debug.MemoryInfo()
Debug.getMemoryInfo(memoryInfo)
val pss = memoryInfo.totalPss / 1024 // KB转MB
val privateDirty = memoryInfo.privateDirty / 1024
if (pss > 200) { // 超过200MB报警
reportHighMemory(pss, privateDirty)
}
}
// 3. 帧率监控
fun startFrameMonitor(view: View) {
view.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
val frameTime = SystemClock.uptimeMillis()
// 计算帧率,如果低于50fps就报警
}
}
// 4. 网络监控
fun monitorNetworkPerformance() {
// 监控请求耗时,超过1秒就报警
NetworkMonitor.startMonitoring()
}
private fun reportSlowStart(duration: Long) {
// 上报到监控平台
Analytics.reportEvent("slow_cold_start", mapOf("duration_ms" to duration))
}
private fun reportHighMemory(pss: Int, privateDirty: Int) {
Analytics.reportEvent("high_memory", mapOf(
"pss_mb" to pss,
"private_dirty_mb" to privateDirty
))
}
}
5.2 问题排查流程
当出现性能问题时,按照以下步骤排查:
1. 复现问题
↓
2. 收集数据(trace、memory dump、ANR日志)
↓
3. 分析数据
↓
4. 定位问题
↓
5. 修复问题
↓
6. 验证修复
常用工具:
| 工具 | 用途 |
|---|---|
| Android Studio Profiler | CPU、内存、网络、电量监控 |
| LeakCanary | 内存泄漏检测 |
| StrictMode | 主线程违规检测 |
| BlockCanary | 卡顿检测 |
| Perfetto | 系统级性能分析 |
| Android Studio Trace | 方法调用追踪 |
StrictMode配置:
// 在Application中开启StrictMode
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads() // 检测磁盘读取
.detectDiskWrites() // 检测磁盘写入
.detectNetwork() // 检测网络访问
.penaltyLog() // 打印日志
.penaltyDeath() // 崩溃(谨慎使用)
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects() // 检测SQL对象泄漏
.detectLeakedClosableObjects() // 检测Closeable泄漏
.penaltyLog()
.penaltyDeath()
.build()
)
}
}
}
六、团队协作与知识沉淀
最后,我想谈谈团队协作。架构再好,如果团队配合不好,也一样会出问题。
6.1 技术文档规范
# 文档命名规范
## API文档
- 格式:`docs/api/{模块名}-{版本}.md`
- 示例:`docs/api/user-api-v2.md`
## 架构文档
- 格式:`docs/architecture/{模块名}-design.md`
- 示例:`docs/architecture/payment-module-design.md`
## 技术方案
- 格式:`docs/design/{方案名}-{日期}.md`
- 示例:`docs/design/dark-mode-implementation-2024-01-15.md`
## 会议纪要
- 格式:`docs/meeting/{日期}-{主题}.md`
- 示例:`docs/meeting/2024-01-15-weekly-tech-review.md`
6.2 代码评审checklist
# Code Review Checklist
## 功能性
- [ ] 功能是否按需求实现?
- [ ] 边界条件是否处理?
- [ ] 异常情况是否处理?
## 代码质量
- [ ] 代码是否符合规范?
- [ ] 是否有冗余代码?
- [ ] 是否有更好的实现方式?
## 性能
- [ ] 是否有性能问题?
- [ ] 是否有内存泄漏风险?
- [ ] 是否有线程安全问题?
## 安全
- [ ] 是否有安全隐患?
- [ ] 敏感数据是否加密?
- [ ] 输入是否校验?
## 可维护性
- [ ] 代码是否易于理解?
- [ ] 是否有足够的注释?
- [ ] 是否易于测试?
写在最后
回想我们那段”崩盘”的经历,最深刻的教训是:架构不是越多层越好,规范不是越复杂越好,优化不是越多越好。
关键在于:
- 适合团队现状:小团队用重型架构会累死,大团队用草台班子会乱套
- 循序渐进:不要试图一次性重构所有代码,先解决最痛的问题
- 持续改进:没有完美的架构,只有不断优化的架构
如果你正在经历项目崩盘的痛苦,别灰心。我们当年也是从那个深渊爬出来的。只要找对方法,团队效率翻倍、项目起死回生是完全可能的。
有什么具体问题,欢迎随时交流。祝你的项目一切顺利!
