百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

Flutter内存泄漏检测

toyiye 2024-06-21 11:56 8 浏览 0 评论

背景

Flutter技术在最近两年可谓是非常火热,本想着经过这几年的快速发展其生态链也越来越成熟。在前一段时间想去Dart packages 上找一个类似于Android中的leakcanary的库居然没有找到,于是自己找了一些资料探索了一番。

内存泄漏对App的影响

内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。

怎么检测Flutter内存泄漏

Flutter程序运行在Dart VM中,Dart 又是以Isolate为单元管理自己的任务以及数据的,每个Isolate都有自己的独立Heap。Dart的内存管理和Java非常相似,这里借鉴Android平台leakcanary原理对Flutter内存泄漏进行探索,主要需要掌握以下三点前置知识点:

  • Dart中内管理
  • 内存泄漏检测时机
  • 聚合内存泄漏信息

Dart内存管理

Flutter程序运行在Dart VM中,在VM中Dart 又是以Isolate为单元管理自己的任务以及数据的,每个Isolate都有自己的独立Heap。Heap中有两个内存管理器分别是新生代和老年代,大量的小对象会在新生代创建和回收它采用的是复制算法效率非常高。Dart采用和Java一样的可达性作为垃圾回收的依据,当对象存活很长时间都没新生代回收器回收后就会进入来老年代,老年代采用的是标记清除法,它相对于复制算法更省空间。Flutter对垃圾回收的时机做了一些优化。

可参考: Flutter垃圾回收。

【https://medium.com/flutter/flutter-dont-fear-the-garbage-collector-d69b3ff1ca30】.

GC可达性

当GC根节点持有对象的引用则对象就是存活的,静态变量,单例等全局变量都可作为GC根节点,除此之外当前方法栈中的变量也是GC的根节点:

内存泄漏检测原理

参考Android中的内存泄漏检测流程,主要步骤分为:

  • 使用弱引用引用待观测对象
  • 并在合适的时机,触发 GC
  • 然后检查弱引用的对象是否为 null。如果不为 null,说明发生了内存泄漏(java中是用的引用队列判断)

生命周期结束

Flutter中可以Navigator作为切入点去找到对标Android中Activity的onDestory方法,可以自定义一个NavigatorObserver重写didPush/didRemove方法来监听Route中Widget的创建和移除,通过Route可以获取到对应的Element和Widget

Dart中的弱引用

Dart中也提供了弱引用 WeakProperty 在GC的时候如果对象不可达就会被回收,Dart层源码位置在/engine/src/third_party/dart/sdk/lib/internal/vm/lib/weak_property.dart WeakProperty不直接提供给开发者,只能间接通过Expand 来间接使用弱引用,在Expand中有一个data数组

Expando具体的实现源Expando具体的实现在expando_patch.dart 中,它内部重载了两个操作符set/get,这里需要注意Expand中的Key值不支持String,bool,num以及null类型的数据。

Expando的使用示例

var text = Text('expando');
Expando expando = Expando();
expando[text] = true;

set方法

  void operator []=(Object object, T? value) {
  ...
    // 创建_WeakProperty弱引用
      var ephemeron = new _WeakProperty();
      ephemeron.key = object;
      ephemeron.value = value;
      _data[idx] = ephemeron;
      
    ...
  }

get方法

 T? operator [](Object object) {

    var idx = object._identityHashCode & mask;
    var wp = _data[idx];

    while (wp != null) {
      if (identical(wp.key, object)) {
        // 获取_WeakProperty弱引用中的value
        return unsafeCast<T?>(wp.value);
      } else if (wp.key == null) {
        // This entry has been cleared by the GC.
        _data[idx] = _deletedEntry;
      }
      idx = (idx + 1) & mask;
      wp = _data[idx];
    }

    return null;
  }

主动触发VM GC

如何主动触发Dart GC,这里参考的devtools中的实现借助于vm_service(见下文介绍), 通过getAllocationProfile方法可以让VM进行Full GC,当DevTools Memory中出现蓝色小圆点就是VM GC了

vmService.getAllocationProfile(isolateId, gc: true);

Engine层的代码

void Heap::CollectAllGarbage(GCReason reason) {
  Thread* thread = Thread::Current();

  // New space is evacuated so this GC will collect all dead objects
  // kept alive by a cross-generational pointer.
  EvacuateNewSpace(thread, reason);
  if (thread->is_marking()) {
    // If incremental marking is happening, we need to finish the GC cycle
    // and perform a follow-up GC to purge any "floating garbage" that may be
    // retained by the incremental barrier.
    CollectOldSpaceGarbage(thread, kMarkSweep, reason);
  }
  CollectOldSpaceGarbage(
      thread, reason == kLowMemory ? kMarkCompact : kMarkSweep, reason);
  WaitForSweeperTasks(thread);
}

vmService

vmService的使用可以参考 vm_service,这里简单介绍一下Vm_Service的工作原理,VM_Service是Dart Developer Service简称DDS的通信协议,在Debug环境下Dart VM会创建一个vm-service的Dart Service Isolate监控VM中的各种状态,有点类似于JVMTI。我们可以借助于VM_Service去获取DartVM运行时的数据,Debug模式下DartVM在初始化Root Isolate的时候会启动一个Dart Service Isolate,这个Service Isolate可以获取VM运行时数据 dev tools中的调试信息也是用vmService获取到VM中的各种信息,Service Isolate中会启动一个WebSocket提供外部服务。

vmService协议的工作原理

通过VM_Service获取运行时数据

VM_Service中的数据交互用的json协议 ,具体可参考接口说明。VM中会给所有的东西(对象,方法,类)都分配一个id(这些id有些是动态的回收利用)VM_Service中都是通过id去获取类,对象的信息。首先我们要想办法获取对象运行时id,这里需要借助两个额外的顶级函数通过vmService.invoke获取对象的id

VM_Service中提供的数据结构有ObjRef, Obj两种类型ObjRef 指的是引用类型,ObjRef的数据结构比较简单。而Obj数据结构很详细。

获取对象在VM中的id

参考Flutter内存泄漏监控,通过定义两个顶级函数,用vm_service去调用generateNewKey生成key的id,再用vm_service调用key2Obj函数获取对象的id,vm_service操作的也只能是id

获取对象id示例代码

int _key = 0;

/// 顶级函数,通过invoke调用获取到key的id
String generateNewKey() {
  return "${++_key}";
}

Map<String, dynamic> _objCache = Map();

/// 顶级函数,通过invoke调用获取到object的id
dynamic key2Obj(String key) {
  return _objCache[key];
}


// dart对象转vm中的id
Future<String> obj2Id(dynamic obj, {sdk.Isolate? sdkIsolate}) async {
  String isolateId = _getIsolateId(sdkIsolate: sdkIsolate);
  VmService vmService = await getVmService();
  Isolate isolate = await vmService.getIsolate(isolateId);

  LibraryRef libraryRef = isolate.libraries!
      .where(
          (element) => element.uri == 'package:flutter_leaks/object_util.dart')
      .first;

  String libraryId = libraryRef.id!;

  // 用 vm service 执行 generateNewKey 函数生成 一个key
  Response keyRef =
      await vmService.invoke(isolateId, libraryId, "generateNewKey", []);
  //获取 generateNewKey 生成的key
  String key = keyRef.json!['valueAsString'];
  //把obj存到map
  _objCache[key] = obj;

  //key在vm中对应的id
  String vmkeyId = keyRef.json!['id'];
  try {
    // 调用 key2Obj 顶级函数,获取obj的在vm中的信息 (ps:使用vmService调用有参数的函数不能直接传参数的值,需要传参数在VM中对应的id)
    Response objRef =
        await vmService.invoke(isolateId, libraryId, "key2Obj", [vmkeyId]);
    // 获取obj在vm中的id
    return objRef.json!['id'];
  } finally {
    //移除map中的值
    _objCache.remove(key);
  }
}

获取调用链信息

vmService也给我们提供了方法获取引用链的方法,通过getRetainingPath可以获取泄漏的引用链路,它返回的数据有一个数组就是到GC Root的链路

vmService.getRetainingPath(isolateId, objId, limit);

实现内存泄漏检测

基于以上原理写了一个内存泄漏检测示例,需要注意的是页面退出后不会立马触发GC。这里我们是延时一段时间手动GC一次,当发现对象还未被回收再次手动GC一次。

处理const 对象

const是编译是常量,下面的代码c1,c2都是同一个对象而且不会被GC回收,c3会重新分配堆内存。所以我们在做内存泄露统计时候应该要排除const对象的统计

main() {

  final c1 =  const ConstClosure();
  final c2 =  const ConstClosure();
  final c3 =   ConstClosure();

  print('c1 == c2 ---->  ${c1 == c2}');
  print('c1 == c3 ---->  ${c1 == c3}');

}

class ConstClosure {
  const ConstClosure();
}

上述代码会输出true,false

在getRetainingPath中对于const常量对象在创建它的地方引用是CodeRef,这里我们需要对const进行排除

聚合引用链

这里因为用到了Expando,我们在拿到检测对象后要立马将Expando置为null消除Expando对检测对象的引用避免我们通过getRetainingPath拿到的不是我们想要的泄漏链路,当对象泄漏了这条链路回到GC Root结束。

这里通过简单的聚合我们找到泄漏的链路,在实际项目中的链路可能会特别的长,需要做一些链路的删减才会直观明了

总结

通过以上实践完成了一个简单的flutter_leakcanary,在项目使用中能够准确地帮我们找到内存泄漏的点。

参考

vm_service

Flutter垃圾收集器

Flutter内存泄漏监控

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码