使用Android Studio Profiler时应用变慢,批量存游戏英雄数据遇性能问题
解决批量游戏数据采集与持久化的性能瓶颈问题
听起来你遇到了批量数据处理中典型的性能衰减问题,我来帮你拆解下核心原因和可落地的优化方案:
1. 资源复用缺失:OkHttpClient/Gson 实例重复创建
你大概率是在循环里每次请求都新建 OkHttpClient 或 Gson 实例,这会引发两个关键问题:
- OkHttpClient 每次初始化都会创建独立连接池,无法复用TCP连接,频繁的握手/断开会急剧消耗系统资源,后期请求延迟会飙升。
- Gson 实例重复创建会产生大量临时对象,加重GC负担,导致程序频繁卡顿。
优化方案:
把这两个实例做成全局单例,整个程序生命周期只初始化一次:
// 全局复用的OkHttpClient,可根据需求调整连接池参数 val okHttpClient = OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES)) .build() // 全局复用的Gson实例,可按需配置序列化规则 val gson = GsonBuilder() .setDateFormat("yyyy-MM-dd HH:mm:ss") .create()
2. 数据库写入效率低下:单条插入引发频繁IO
115个英雄 × 500条记录 = 57500条数据,如果每条记录都单独开启事务插入,会产生大量磁盘IO操作,数据库连接池也会被频繁占用,后期写入速度会越来越慢。
优化方案:
使用Exposed的 batchInsert 方法批量写入,把多条记录打包成一个事务提交:
// 假设HeroMatch是你的Exposed实体类 transaction { // 先将当前英雄的500条记录收集到列表,再批量插入 batchInsert(currentHeroMatches) { match -> this[HeroMatch.heroId] = match.heroId this[HeroMatch.matchTime] = match.matchTime this[HeroMatch.matchDetails] = match.matchDetails // 填充其他字段... } }
建议每收集50-100条记录就执行一次批量插入,既避免内存占用过高,又能大幅提升写入速度。
3. 线程模型不合理:单线程阻塞+主线程执行
如果你的整个操作都在Android主线程执行,批量请求和数据库写入会阻塞UI线程,导致Profiler卡顿、控制台输出缓慢;即使是后台单线程,长时间循环也会因为资源堆积(比如未释放的响应体、临时对象)导致性能衰减。
优化方案:
- 改用线程池处理并发请求,控制并发数(比如3-5个线程),既提升采集效率,又避免触发服务器限流:
val executor = Executors.newFixedThreadPool(3) // 遍历英雄列表,将每个英雄的采集任务提交到线程池 heroIds.forEach { heroId -> executor.submit { repeat(500) { // 发起请求、解析数据、收集到临时列表 Thread.sleep(1000) // 保留休眠逻辑避免服务器限流 } // 批量写入当前英雄的500条记录 } }
- 如果是Kotlin项目,更推荐用协程配合
CoroutineScope管理并发,代码更简洁且易于控制:
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) heroIds.forEach { heroId -> scope.launch { repeat(500) { // 请求+解析逻辑 delay(1000) // 协程的非阻塞休眠,不占用线程资源 } // 批量写入数据库 } }
4. 内存堆积与GC频繁触发
大量的JSON解析对象、未关闭的响应体如果没有及时回收,会导致内存占用持续升高,GC频繁触发(尤其是Full GC),这会让程序出现明显卡顿,甚至控制台输出都因为GC停顿而变慢。
优化方案:
- 手动关闭OkHttp的响应体:用
use语法自动关闭资源,避免内存泄漏:
okHttpClient.newCall(request).execute().use { response -> if (response.isSuccessful) { // 解析响应数据 } }
- 用Gson的流式解析处理大JSON数组,避免一次性把整个数组加载到内存:
response.body?.byteStream()?.use { inputStream -> JsonReader(InputStreamReader(inputStream)).use { reader -> reader.beginArray() while (reader.hasNext()) { val match = gson.fromJson<HeroMatch>(reader) // 收集到临时列表中 } reader.endArray() } }
- 定期清理临时数据列表:每次批量写入后,清空临时列表,让GC能及时回收这些对象。
5. 控制台输出拖慢程序
如果你的代码中每条请求或每条记录都打印日志,System.out.println 是同步IO操作,大量输出会严重阻塞线程,导致程序看起来变慢。
优化方案:
- 减少日志输出频率,比如每处理100条记录打印一次进度:
if (count % 100 == 0) { println("已处理英雄 $heroId 的 $count 条记录") }
- 改用更高效的日志框架(如SLF4J+Logback),它们会异步处理日志输出,不会阻塞业务线程。
内容的提问来源于stack exchange,提问作者Dinu Nicolae
相关产品推荐
相关产品推荐

