Xamarin批量插入时触发阻塞式GC Explicit问题求助
我来帮你分析下这个问题——从GC日志和代码来看,核心问题是批量插入过程中内存占用过高,导致频繁GC最终卡住。咱们一步步拆解问题,再给出针对性的优化方案:
从GC日志看本质
11-19 11:52:17.463 D/Mono (32524): GC_MAJOR: (LOS overflow) time 50.76ms, stw 60.27ms los size: 4096K in use: 1369K
11-19 11:52:17.693 D/Mono (32524): GC_MINOR: (Nursery full) time 2.45ms, stw 3.05ms promoted 10K major size: 8240K in use: 6634K
这些日志说明:
- 小对象堆(Nursery)频繁被占满:意味着大量临时小对象(比如JSON序列化产生的中间对象、字符串片段)没有及时回收
- 大对象堆(LOS)溢出:你的代码中生成了超大的SQL字符串(1000条INSERT拼接在一起),直接撑爆了大对象堆,触发阻塞式GC,最终导致程序卡住
现有代码的核心问题
- 超大SQL字符串拼接:第一个
BulkInsert方法把1000条INSERT语句拼成一个巨型字符串,拼接过程中还会产生多个中间字符串,内存占用呈指数级增长 - 冗余的JSON序列化:
GenerateInsertStatement里把JToken转字符串再反序列化为Dictionary,平白产生大量临时对象 - 未优化的事务与Command复用:第二个
BulkInsert虽然用了事务,但循环中每次创建新的SQLiteCommand,既浪费资源又增加内存开销 - 内存未及时释放:分页循环中,
JArray等大对象在使用后没有主动释放,内存累积到最后一页彻底耗尽
针对性优化方案
1. 改用参数化批量插入+事务(核心优化)
优先推荐用SQLite-net ORM的批量插入API,它内部做了性能优化;如果必须用原生SQL,就用参数化查询避免超大字符串拼接:
方案一:SQLite-net ORM批量插入(最简洁高效)
直接把JArray反序列化为实体列表,用InsertAll批量插入:
public void BulkInsert(JArray array, string tableName = "") { try { // 直接反序列化为实体,跳过冗余的Dictionary转换 var ordensList = array.ToObject<List<OrdemServico>>(); using (var connection = new SQLiteConnection(DataBaseUtil.GetDataBasePath())) { var transaction = connection.BeginTransaction(); try { connection.InsertAll(ordensList); transaction.Commit(); } catch { transaction.Rollback(); throw; } } } catch (Exception e) { LogUtil.WriteLog(e); } }
方案二:原生SQL参数化批量插入
如果必须用原生SQL,复用SQLiteCommand并使用参数化查询,避免字符串拼接:
public void BulkInsert(JArray array, string tableName = "") { try { if (string.IsNullOrEmpty(tableName)) { tableName = typeof(OrdemServico).Name; } using (var connection = new SQLiteConnection(DataBaseUtil.GetDataBasePath())) { var transaction = connection.BeginTransaction(); try { // 生成带参数占位符的INSERT模板 var firstOrdem = array[0].ToObject<OrdemServico>(); var properties = typeof(OrdemServico).GetProperties() .Where(p => p.Name != "Id"); // 排除主键字段 var columns = string.Join(",", properties.Select(p => p.Name)); var placeholders = string.Join(",", properties.Select((p, idx) => $"@param{idx}")); var sql = $"INSERT INTO {tableName} ({columns}) VALUES ({placeholders});"; using (var command = connection.CreateCommand(sql)) { foreach (var item in array) { var ordem = item.ToObject<OrdemServico>(); // 绑定参数 for (int i = 0; i < properties.Count(); i++) { var prop = properties.ElementAt(i); command.Parameters.AddWithValue($"@param{i}", prop.GetValue(ordem)); } command.ExecuteNonQuery(); command.Parameters.Clear(); // 复用Command,清空参数 } } transaction.Commit(); } catch { transaction.Rollback(); throw; } } } catch (Exception e) { LogUtil.WriteLog(e); } }
2. 优化JSON处理,减少临时对象
修改HTTP请求方法,直接返回JArray而非Object,避免后续类型转换的开销:
protected async Task<JArray> GetJArray(string metodo = "", string parametros = "") { try { var urlBase = new Uri(GetStringConnectionParameters(metodo) + parametros); using (HttpClient client = new HttpClient()) using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, urlBase)) using (HttpResponseMessage response = await client.SendAsync(request)) { if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); return JArray.Parse(content); // 直接返回JArray,跳过不必要的Object转换 } } } catch (Exception e) { LogUtil.WriteLog(e); } return null; }
3. 及时释放内存,缓解GC压力
在分页循环中,处理完当前页后主动释放大对象引用,并按需触发GC:
private async Task GenerateOrdensServico() { try { _logs.Add("ORDENS DE SERVIÇO"); double increment = ((1 - Progresso) / 2); int records = await _ordemServicoRest.GetCount(); int limit = _sistemaParametroRepository.GetTamanhoPaginaSincMobile(); int pages = (records / limit); for (int i = 0; i <= pages; i++) { JArray ordensServico = await _ordemServicoRest.GetAllInJsonFormatPaginated(DataBaseUtil.GetPagination(i, limit)); if (ordensServico == null) { _logs.Add("Não Contem O.S de Corte para importar!"); await App.Current.MainPage.DisplayAlert("Atenção", "Não tem O.S para importar!", "OK"); continue; } _ordemServicoRepository.BulkInsert(ordensServico); // 主动释放大对象引用,让GC可以回收内存 ordensServico = null; // 仅在内存压力大时触发GC,避免频繁调用影响性能 GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized); GC.WaitForPendingFinalizers(); AlterProgress(Progresso += ((Progresso * 100) + (increment / pages))); } } catch (Exception e) { LogUtil.WriteLog(e); } }
4. 完善事务回滚逻辑
确保异常时事务能正确回滚,避免数据库数据不一致:
// 针对你之前的第二个BulkInsert方法补充回滚逻辑 public void BulkInsert(JArray array, string tableName = "") { SQLiteTransaction transaction = null; try { if (string.IsNullOrEmpty(tableName)) { tableName = typeof(T).Name; } using (SQLiteConnection connection = new SQLiteConnection(DataBaseUtil.GetDataBasePath())) { transaction = connection.BeginTransaction(); foreach (var register in array) { string sql = DataBaseUtil.GenerateInsertStatement(register, tableName); using (var command = connection.CreateCommand(sql)) { command.ExecuteNonQuery(); } } transaction.Commit(); } } catch (Exception e) { transaction?.Rollback(); // 异常时回滚事务 LogUtil.WriteLog(e); } }
总结
这些优化的核心是减少临时对象创建、降低内存占用,同时提升SQLite插入效率。优先尝试SQLite-net的InsertAll方法,这是最简洁高效的方案;如果必须用原生SQL,参数化查询是关键。加上及时释放内存和优化JSON处理,就能解决最后一页卡住的问题。
内容的提问来源于stack exchange,提问作者Diego Macario

