30分钟打造属于自己的Flutter内存泄漏检测工具---FlutterLeakCanary

30分钟打造属于自己的Flutter内存泄漏检测工具

  • 思路
    • 检测
    • Dart 也有弱引用-----WeakReference
    • 如何执行Full GC?
    • 如何知道一个引用他的文件路径以及类名?
  • 代码实践
    • 第一步,实现Full GC
    • 第二步,如何根据对象引用,获取出他的类名,路径等信息。
    • 第三步,定义工具接口
    • 第四步,添加代理类,隔离实现类
    • 第五步, 提供State的mixin监听类
    • 第六步,提供其他类的mixin监听类
    • 第七步,实现具体的管理类
  • 运行测试
    • 环境配置 --disable-dds
    • 检验成果

思路

检测

通过借鉴Android的内存泄漏检测工具LeakCanary的原理,使用弱引用持有引用,当这个引用执行释放动作的时候,执行Full GC后,如果弱引用的持有还在,那么就代表这个引用泄漏了。

Dart 也有弱引用-----WeakReference

关于Dart弱引用WeakReference怎么使用,我的这篇文章2分钟教你Flutter怎么避免引用内存泄漏>>会对你有帮助.

如何执行Full GC?

通过使用vm_service这个插件,在Dev可以执行Full GC请求,通过获取VmService的引用后,调用执行

vms.getAllocationProfile(isolate!.id!, gc: true)

就可以请求Full GC

如何知道一个引用他的文件路径以及类名?

vm_service这个插件里面有Api支持反射获取ClassRef读取引用里面的属性名,类名,以及路径等。

代码实践

有了以上的思路,我们就可以通过代码方式来实现检测内存泄漏,然后把泄漏的引用通知到UI展示出来。
代码我已经写好在 flutter_leak_canary: ^1.0.1,可做参考修改

第一步,实现Full GC

  1. 添加vm_service插件,获取VmService引用
 Future<VmService?> getVmService() async {
   if (_vmService == null && debug) {
     ServiceProtocolInfo serviceProtocolInfo = await Service.getInfo();
     _observatoryUri = serviceProtocolInfo.serverUri;
     if (_observatoryUri != null) {
       Uri url = convertToWebSocketUrl(serviceProtocolUrl: _observatoryUri!);
       try {
         _vmService = await vmServiceConnectUri(url.toString());
       } catch (error, stack) {
         print(stack);
       }
     }
   }
   return _vmService;
 }

  1. 执行GC的时候,flutter的无效引用回收是每个Isolate线程独立的,因为内存独立,相互不受影响。由于我们几乎所有代码都在UI线程执行的,所以我们需要进行筛选出UI线程,也就是’main’线程。
Future<VM?> getVM() async {
  if (!debug) {
    return null;
  }
  return _vm ??= await (await getVmService())?.getVM();
}

//获取ui线程
Future<Isolate?> getMainIsolate() async {
  if (!debug) {
    return null;
  }
  IsolateRef? ref;
  final vm = await getVM();
  if (vm == null) return null;
  //筛选出ui线程的索引
  var index = vm.isolates?.indexWhere((element) => element.name == 'main');
  if (index != -1) {
    ref = vm.isolates![index!];
  }
  final vms = await getVmService();
  if (ref?.id != null) {
    return vms?.getIsolate(ref!.id!);
  }
  return null;
}

3.根据上面方法,落实Full GC

//请求执行Full GC
Future try2GC() async {
  if (!debug) {
    return;
  }
  final vms = await getVmService();
  if (vms == null) return null;
  final isolate = await getMainIsolate();
  if (isolate?.id != null) {
    await vms.getAllocationProfile(isolate!.id!, gc: true);
  }
}

第二步,如何根据对象引用,获取出他的类名,路径等信息。

  1. 思路大概是这样,通过一个文件的路径能获取当前LibraryRef对象,通过这个LibraryRef对象可以调用这个文件里面的顶级函数,返回值可以加工得到刚才提过的ClassRef。
  2. 利用这个特性,我们可以先把需要检测的对象,丢到一个Map里面,然后写一个高级函数返回这个map保存的对象。然后通过api获取这个对象id后,可以得到Obj, 根据Obj可以得到对应Instance,这个Instance里面就有ClassRef

具体实现如下:

const String vmServiceHelperLiraryPath =
    'package:flutter_leak_canary/vm_service_helper.dart';
//dont remove this method, it's invoked by getObjectId
String getLiraryResponse() {
  return "Hello LeakCanary";
}
//dont remove this method, it's invoked by getObjectId
dynamic popSnapObject(String objectKey) {
  final object = _snapWeakReferenceMap[objectKey];
  return object?.target;
}

//
class VmServiceHelper {
//....    

//根据文件获取getLiraryByPath
Future<LibraryRef?> getLiraryByPath(String libraryPath) async {
  if (!debug) {
    return null;
  }
  Isolate? mainIsolate = await getMainIsolate();
  if (mainIsolate != null) {
    final libraries = mainIsolate.libraries;
    if (libraries != null) {
      final index =
          libraries.indexWhere((element) => element.uri == libraryPath);
      if (index != -1) {
        return libraries[index];
      }
    }
  }
  return null;
}

//通过顶部函数间接获取这个对象的objectId
Future<String?> getObjectId(WeakReference obj) async {
  if (!debug) {
    return null;
  }
  final library = await getLiraryByPath(vmServiceHelperLiraryPath);
  if (library == null || library.id == null) return null;
  final vms = await getVmService();
  if (vms == null) return null;
  final mainIsolate = await getMainIsolate();
  if (mainIsolate == null || mainIsolate.id == null) return null;
  Response libRsp =
      await vms.invoke(mainIsolate.id!, library.id!, 'getLiraryResponse', []);
  final libRspRef = InstanceRef.parse(libRsp.json);
  String? libRspRefVs = libRspRef?.valueAsString;
  if (libRspRefVs == null) return null;
  _snapWeakReferenceMap[libRspRefVs] = obj;
  try {
    Response popSnapObjectRsp = await vms.invoke(
        mainIsolate.id!, library.id!, "popSnapObject", [libRspRef!.id!]);
    final instanceRef = InstanceRef.parse(popSnapObjectRsp.json);
    return instanceRef?.id;
  } catch (e, stack) {
    print('getObjectId $stack');
  } finally {
    _snapWeakReferenceMap.remove(libRspRefVs);
  }
  return null;
}


//根据objectId获取Obj
Future<Obj?> getObjById(String objectId) async 
  if (!debug) {
    return null;
  }
  final vms = await getVmService();
  if (vms == null) return null;
  final mainIsolate = await getMainIsolate();
  if (mainIsolate?.id != null) {
    try {
      Obj obj = await vms.getObject(mainIsolate
      return obj;
    } catch (e, stack) {
      print('getObjById>>$stack');
    }
  }
  return null;
}


//根据objectId获取Instance.  
Future<Instance?> getInstanceByObjectId(String objectId) async {
  if (!debug) {
    return null;
  }
  Obj? obj = await getObjById(objectId);
  if (obj != null) {
    var instance = Instance.parse(obj.json);
    return instance;
  }
  return null;
}

//根据objectId获取出具体的类名,文件名,类在文件的第几行,第几列
//顶级函数>objectId>Obj>Instance
Future<LeakCanaryWeakModel?> _runQuery(objectId) async {
  final vmsh = VmServiceHelper();
  Instance? instance = await vmsh.getInstanceByObjectId(objectId!);
  if (instance != null &&
      instance.id != 'objects/null' &&
      instance.classRef is ClassRef) {
    ClassRef? targetClassRef = instance.classRef;
    final wm = LeakCanaryWeakModel(
        className: targetClassRef!.name,
        line: targetClassRef.location?.line,
        column: targetClassRef.location?.column,
        classFileName: targetClassRef.library?.uri);
    print(wm.className);
    return wm;
  }
  return null;
}

}

//泄漏信息模型
class LeakCanaryWeakModel {
  //泄漏时间
  late int createTime;
  //类名
  final String? className;
//所在文件名
  final String? classFileName;
  //所在列
  final int? line;
  //所在行数
  final int? column;

  LeakCanaryWeakModel({required this.className,required this.classFileName,required this.column,required this.line,}) {
    createTime = DateTime.now().millisecondsSinceEpoch;
  }
}


第三步,定义工具接口

定义一个接口,里面有添加监听,检测是否泄漏,获取当前泄漏的引用列表,通知当前有泄漏的引用

abstract class LeakCanaryMananger {
  //具体实现管理类,这个后面会介绍
  factory LeakCanaryMananger() => _LeakCanaryMananger();
  //监听当前引用,初始化时候调用
  void watch(WeakReference obj);
  //生命周期结束的以后,检测引用有没有泄漏
  void try2Check(WeakReference wr);
  //当前的泄漏列表
  List<LeakCanaryWeakModel> get canaryModels;
  //当前内存有新泄漏引用通知
  ValueNotifier get leakCanaryModelNotifier;
}

第四步,添加代理类,隔离实现类


class FlutterLeakCanary implements LeakCanaryMananger {
  final _helper = LeakCanaryMananger();
  static final _instance = FlutterLeakCanary._();
  FlutterLeakCanary._();
  factory() => _instance;

  static FlutterLeakCanary get() {
    return _instance;
  }

  
  void watch(obj) {
     _helper.watch(obj);
  }

  
  void try2Check(WeakReference wr) {
    _helper.try2Check(wr);
  }

  void addListener(VoidCallback listener) {
    _helper.leakCanaryModelNotifier.addListener(listener);
  }

  void removeListener(VoidCallback listener) {
    _helper.leakCanaryModelNotifier.removeListener(listener);
  }
  
  
  
  List<LeakCanaryWeakModel> get canaryModels => List.unmodifiable(_helper.canaryModels);
  
  
  ValueNotifier get leakCanaryModelNotifier => _helper.leakCanaryModelNotifier;
}



第五步, 提供State的mixin监听类

我们最不希望看到的泄漏类,一定是state。他泄漏后,他的context,也就是element无法回收,然后它里面持有所有的渲染相关的引用都无法回收,这个泄漏非常严重。
通过WeakReference来持有这个对象以来可以用来检测,二来避免自己写的工具导致内存泄漏。
initState的时候,把它放到检测队列,dispose以后进行检测

mixin LeakCanaryStateMixin<T extends StatefulWidget> on State<T> {
  late WeakReference _wr;
  String? objId;
  
  
  void initState() {
    super.initState();
    _wr = WeakReference(this);
    FlutterLeakCanary.get().watch(_wr);
  }

  
  
  void dispose() {
    super.dispose();
    FlutterLeakCanary.get().try2Check(_wr);
  }
}

第六步,提供其他类的mixin监听类


mixin LeakCanarySimpleMixin {
 late WeakReference _wr;
 String? objId;
 void watch()  {
   _wr = WeakReference(this);
  FlutterLeakCanary.get().watch(_wr);
 }
 void try2Check() {
   FlutterLeakCanary.get().try2Check(_wr);
 }
}

第七步,实现具体的管理类

对于引用的检测,是把引用包装到GCRunnable,使用消费者设计模型来做,3秒轮询检测一次。尽量用线程去分担检测,避免影响UI线程性能开销的统计。



class _LeakCanaryMananger implements LeakCanaryMananger {
  static final vmsh = VmServiceHelper();
  //objId:instance
  final _objectWeakReferenceMap = HashMap<int, WeakReference?>();
  List<GCRunnable> runnables = [];
  Timer? timer;
  bool isDetecting = false;
  //3秒轮训
  loopRunnables() {
    timer ??= Timer.periodic(Duration(seconds: 3), (timer) {
      if (isDetecting) {
        return;
      }
      if (runnables.isNotEmpty) {
        isDetecting = true;
        final trunnables = List<GCRunnable>.unmodifiable(runnables);
        runnables.clear();
        //使用线程去GC
        compute(runGc, null).then((value) async {
          await Future.forEach<GCRunnable>(trunnables, (runnable) async {
            if (runnable.objectId == "objects/null") {
              return;
            }
            try {
              
              final LeakCanaryWeakModel? wm = await runnable.run();
              //如果非空,就是泄漏了,然后对泄漏的进行class信息获取,发送到订阅的地方,一般是ui,进行刷新
              if (wm != null) {
                canaryModels.add(wm);
                leakCanaryModelNotifier.value = wm;
              }
            } catch (e, s) {
              print(s);
            } finally {
              _objectWeakReferenceMap.remove(runnable.wkObj.hashCode);
            }
          });
          isDetecting = false;
        });
      }
    });
  }

  
  void watch(WeakReference wr) async {
    bool isDebug = false;
    assert(() {
      isDebug = true;
      return true;
    }());
    if (!isDebug) {
      return;
    }
    _objectWeakReferenceMap[wr.hashCode] = wr;
    loopRunnables();
  }

  
  ValueNotifier leakCanaryModelNotifier = ValueNotifier(null);


	 //添加到待检测执行队列里,轮询扫描的时候执行,这样可以避免检测瓶颈
  void _check(WeakReference? wr) {
    assert(() {
      WeakReference? wkObj = _objectWeakReferenceMap[wr.hashCode];
      runnables.add(GCRunnable(wkObj: wkObj));
      return true;
    }());
  }

  
  void try2Check(WeakReference wr) async {
    bool isDebug = false;
    assert(() {
      isDebug = true;
      return true;
    }());
    if (!isDebug) {
      return;
    }
    if (wr.target != null) {
      _check(wr);
    }
  }

  
  List<LeakCanaryWeakModel> canaryModels = [];
}


class GCRunnable {
  String? objectId;
  final WeakReference? wkObj;

  GCRunnable({required this.wkObj});
  Future<LeakCanaryWeakModel?> run() async {
    if (wkObj?.target != null) {
      final vmsh = VmServiceHelper();
      //cant quary objectId with isolate, but quary instance
      objectId = await vmsh.getObjectId(wkObj!);
      LeakCanaryWeakModel? weakModel = await compute(_runQuery, object
      return weakModel;
    }
  }
}


运行测试

环境配置 --disable-dds

VsCode需要配置.vscode

“configurations”: [
{

“args”: [
“–disable-dds”
],
“type”: “dart”
},

]

Android Studio

在这里插入图片描述

检验成果

读下面的代码,看看那些会泄漏,然后在看看结果。

class WeakPage extends StatefulWidget {
  const WeakPage({super.key});

  
  State<WeakPage> createState() => _WeakPageState();
}

class TestModel with LeakCanarySimpleMixin {
  Timer? timer;
  int count = 0;
  init() {
    watch();
    timer = Timer.periodic(Duration(seconds: 1), (timer) {
      count++;
      print("TestModel $count");
    });
  }

  void dispose() {
    // timer?.cancel();
    try2Check();
  }
}

class TestModel2 with LeakCanarySimpleMixin {
  Timer? timer;
  int count = 0;
  init() {
    watch();
  }

  void dispose() {
    timer?.cancel();
    timer = null;
    try2Check();
  }
}

class _WeakPageState extends State<WeakPage> with LeakCanarySta
  TestModel? test = TestModel();
  TestModel2? test2 = TestModel2();
  Timer? timer;
  int count = 0;
  
  void initState() {
    super.initState();
    test?.init();
    test2?.init();
    timer = Timer.periodic(Duration(seconds: 1), (timer) {
      count++;
       print("_WeakPageState ${count}");
    });
  }

  
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    //timer.cancel();
    test?.dispose();
    test2?.dispose();
    test = null;
    test2 = null;
  }

  
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: Container(
          child: InkWell(
              onTap: () {
                Navigator.of(context).pop();
              },
              child: Text('back')),
        ),
      ),
    );
  }


泄漏结果:

在这里插入图片描述

需要获取源码的同学,到这里获取,点击>>flutter_leak_canary: ^1.0.1<<

是不是很赞?如果这篇文章对你有帮助,请关注🙏,点赞👍,收藏😋三连哦

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/599965.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python运维-日志记录、FTP、邮件提醒

本章目录如下&#xff1a; 五、日志记录 5.1、日志模块简介 5.2、logging模块的配置与使用 六、搭建FTP服务器与客户端 6.1、FTP服务器模式 6.2、搭建服务器 6.3、编写FTP客户端程序 七、邮件提醒 7.1、发送邮件 7.2、接收邮件 7.3、实例&#xff1a;将报警信息实时…

【系统架构师】-选择题(十四)

1、某企业开发信息管理系统平台进行 E-R 图设计&#xff0c;人力部门定义的是员工实体具有属性&#xff1a;员工号、姓名、性别、出生日期、联系方式和部门,培训部门定义的培训师实体具有属性:培训师号&#xff0c;姓名和职称&#xff0c;其中职称{初级培训师&#xff0c;中级培…

【每日刷题】Day33

【每日刷题】Day33 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 20. 有效的括号 - 力扣&#xff08;LeetCode&#xff09; 2. 445. 两数相加 II - 力扣&#xff08;…

pytest教程-38-钩子函数-pytest_runtest_protocol

领取资料&#xff0c;咨询答疑&#xff0c;请➕wei: June__Go 上一小节我们学习了pytest_collection_finish钩子函数的使用方法&#xff0c;本小节我们讲解一下pytest_runtest_protocol钩子函数的使用方法。 pytest_runtest_protocol 钩子函数在 pytest 运行单个测试用例之前…

uniapp picker组件的样式更改

不知道有没有小伙伴遇到过这个问题 我是各种穿透和层级都尝试了更改不了其样式 梳理一下 H5端 在全局app.vue下添加如下代码 .uni-picker-container .uni-picker-header{ background-color: $uni-color-pink; //picker头部背景色}.uni-picker-container .…

【busybox记录】【shell指令】uniq

目录 内容来源&#xff1a; 【GUN】【uniq】指令介绍 【busybox】【uniq】指令介绍 【linux】【uniq】指令介绍 使用示例&#xff1a; 去除重复行 - 默认输出 去除重复行 - 跳过第n段&#xff08;空格隔开&#xff09;&#xff0c;比较n1以后的内容&#xff0c;去重 去…

数组折半法查找数据(C语言)

一、N-S流程图&#xff1b; 二、运行结果&#xff1b; 三、源代码&#xff1b; # define _CRT_SECURE_NO_WARNINGS # include <stdio.h> //定义数据&#xff1b; #define N 15int main() {//初始化变量值&#xff1b;int a[N], i, top, bott, loca, flag 1, sign, numb…

使用macof发起MAC地址泛洪攻击

使用macof发起MAC地址泛洪攻击 MAC地址泛洪攻击原理&#xff1a; MAC地址泛洪攻击是一种针对交换机的攻击方式&#xff0c;目的是监听同一局域网中用户的通信数据。交换机的工作核心&#xff1a;端口- MAC地址映射表。这张表记录了交换机每个端口和与之相连的主机MAC地址之间…

Map集合的实现类~HashMap

存储结构&#xff1a;哈希表 键重复依据是hashCode和equals方法&#xff08;键不能重复&#xff09; 添加&#xff1a; 先创建Student类&#xff0c;那么往HashSet添加的就是Student对象作为键值&#xff0c;后面的作为值 删除&#xff1a; 判断&#xff1a; 遍历&#xff1a…

Parts2Whole革新:多参照图定制人像,创新自定义肖像生成框架!

DeepVisionary 每日深度学习前沿科技推送&顶会论文分享&#xff0c;与你一起了解前沿深度学习信息&#xff01; Parts2Whole革新&#xff1a;多参照图定制人像&#xff0c;创新自定义肖像生成框架&#xff01; 引言&#xff1a;探索多条件人像生成的新篇章 在数字内容创作…

【MATLAB源码-第204期】基于matlab的语音降噪算法对比仿真,谱减法、维纳滤波法、自适应滤波法;参数可调。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 语音降噪技术的目的是改善语音信号的质量&#xff0c;通过减少或消除背景噪声&#xff0c;使得语音更清晰&#xff0c;便于听者理解或进一步的语音处理任务&#xff0c;如语音识别和语音通讯。在许多实际应用中&#xff0c;如…

深度学习之基于YOLOv5智慧交通拥挤预警检测系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景 随着城市化进程的加速和人口规模的不断增长&#xff0c;交通拥挤问题日益严重。传统的交通拥挤预警方…

C++笔记-makefile添加第三方.h和.cpp及添加.h和lib库模板

目文件结构如下所示时&#xff1a; project/├── main.cpp├── test.cpp├── DIRA/│ ├── A.cpp│ └── A.h├── DIRBLIB/│ └── libB.so└── include/└── B.h Makefile如下所示&#xff1a; # 编译器设置 CXX g CXXFLAGS -stdc11 -Wall# 目录…

互联网十万个为什么之什么是云计算

云计算是一种通过互联网提供计算资源和服务的技术。它允许用户随时随地访问和使用云平台上的数据、软件和硬件资源。在数字化时代&#xff0c;互联网已经成为基础设施。云计算使得数据中心能够像一台计算机一样去工作。通过互联网将算力以按需使用、按量付费的形式提供给用户&a…

2024年Q1脱毛膏线上市场(京东天猫淘宝)销量销额排行榜

鲸参谋监测的2024年Q1季度线上电商平台&#xff08;天猫淘宝京东&#xff09;脱毛膏行业销售数据已出炉&#xff01; 根据鲸参谋数据显示&#xff0c;今年Q1季度在线上电商平台&#xff08;天猫淘宝京东&#xff09;&#xff0c;脱毛膏的销量累计接近220万件&#xff0c;环比增…

基于51单片机的ADC0804的电压表设计(仿真+源码+设计资料)

目录 1、前言 2、资料内容 3、仿真图 4、程序 资料下载地址&#xff1a;基于51单片机的ADC0804的电压表设计&#xff08;仿真源码设计资料&#xff09; 1、前言 最近看网上有很少的ADC0804的设计了&#xff0c;都由0809代替&#xff0c;但是有个别因为成本原因和学校课…

使用Express+Node.js搭建网站

Express是一个基于Node.js平台的快速、开放、极简的Web开发框架。它的作用是专门用来创建Web服务器&#xff0c;与Node.js内置的http模块功能相似&#xff0c;但更为简便和高效。 Express中文官网&#xff1a;Express - 基于 Node.js 平台的 web 应用开发框架 - Express中文文…

25考研英语长难句Day02

25考研英语长难句Day02 【a.词组】【b.断句】 如果你是你讲话对象中的一员&#xff0c;你就能了解你们大家共同的经历和问题&#xff0c;你也可以顺便评论一下食堂里难吃的食物或董事长臭名昭著的领带品味。 【a.词组】 单词解释addressv. 演说&#xff0c; 演讲&#xff1b;…

一堆自定义C#代码片段,让你开发效率飞涨

SharpBoxes 是一款用于 Visual Studio 的扩展&#xff0c;作者为本人&#xff1b; 该扩展旨在提高开发效率。它为开发人员提供了一组日常使用频率较高的代码片段&#xff0c;让你在编写代码时能够更快地插入常用的代码段。通过安装这个扩展&#xff0c;你可以使用快捷键轻松插…

Django 4.x 智能分页get_elided_page_range

Django智能分页 分页效果 第1页的效果 第10页的效果 带输入框的效果 主要函数 # 参数解释 # number: 当前页码&#xff0c;默认&#xff1a;1 # on_each_side&#xff1a;当前页码前后显示几页&#xff0c;默认&#xff1a;3 # on_ends&#xff1a;首尾固定显示几页&#…
最新文章