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

news2024/11/23 17:13:10

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.coloradmin.cn/o/1649529.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Django框架四-项目

一、项目准备 1.流程与人员 2.需求分析 项目主要页面 归纳项目主要模块 3.架构设计 项目开发模式 项目架构设计

资源管理器CPU占用太高

最近资源管理器经常飙到80%-100%&#xff0c;所以电脑很卡。比如下面的新打开一个文件目录就这样 工具 shexview下载地址 排除 排序 先点Microsoft排序&#xff0c;上面粉色的就是所谓的外部插件 全部禁用 粉色全选->右键->Disable Selected Items &#xff08;看其他…

ZIP压缩输出流(将ZIP文件解压)

文章目录 前言一、ZIP压缩输出流是什么&#xff1f;二、使用介绍 1.使用方法2.实操展示总结 前言 该篇文章相对应的介绍如何使用java代码将各种文件&#xff08;文件夹&#xff09;从ZIP压缩文件中取出到指定的文件夹中。解压流将ZIP文件中的文件以条目的形式逐一读取&#xff…

Java毕业设计 基于SpringBoot vue企业信息管理系统

Java毕业设计 基于SpringBoot vue企业信息管理系统 SpringBoot 企业信息管理系统 功能介绍 员工&#xff1a;登录 个人中心 修改密码 个人信息 会议管理 公告管理 个人计划管理 通讯录管理 外出登记管理 请假管理 上下班打卡管理 管理员&#xff1a;登录 个人中心 修改密码 …

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

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

不上班,我靠这5份赚钱副业养活了自己

在这个快节奏的社会里&#xff0c;很多人都在为生活奔波忙碌。今天&#xff0c;就让我来跟大家分享一下我的“躺平”秘籍吧&#xff01; 这一个月来&#xff0c;我没有上班&#xff0c;但好在有副业养活自己。有时候&#xff0c;我真的觉得有一份自己喜欢的自媒体副业挺好的。…

开源代码分享(28)-含分布式光伏的配电网集群划分和集群电压协调控制

参考文献&#xff1a; [1] Chai Y , Guo L , Wang C ,et al.Network Partition and Voltage Coordination Control for Distribution Networks With High Penetration of Distributed PV Units[J].IEEE Transactions on Power Systems, 2018:3396-3407.DOI:10.1109/TPWRS.2018…

1688工厂货源API接口:用于商品采集、商品搜索、商品详情数据抓取

item_get 获得1688商品详情item_search 按关键字搜索商品item_search_img 按图搜索1688商品&#xff08;拍立淘&#xff09;item_search_suggest 获得搜索词推荐item_fee 获得商品快递费用seller_info 获得店铺详情item_search_shop 获得店铺的所有商品item_password 获得淘口令…

独有病眼花,春风吹不落。 (二维坐标压缩成一个点,并查集)

本题链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 题目&#xff1a; 样例&#xff1a; 输入 3 8 1 1 D 1 1 R 1 2 D 2 1 D 2 2 R 3 1 R 3 2 R 2 3 D 输出 8 思路&#xff1a; 根据题意&#xff0c;要求连接线段后&#xff0c;操作多少次&#xff0c;连接的线段闭合&…

在QEMU上运行OpenSBI+Linux+Rootfs

在QEMU上运行OpenSBILinuxRootfs 1 编译QEMU2 安装交叉编译工具3 编译OpenSBI4 编译Linux5 创建根文件系统5.1 编译busybox5.2 创建目录结构5.3 制作文件系统镜像5.3.1 创建 ext2 文件5.3.2 将目录结构拷贝进 ext2 文件5.3.3 取消挂载 6 运行OpenSBILinuxRootfs 本文所使用的版…

第二证券|集合竞价915到920能成交吗?

不能&#xff0c;买卖日9点15分到9点20分是集合竞价申报时刻&#xff0c;还没有发生集合竞价。 在这个时刻段内&#xff0c;投资者能够申报&#xff0c;也能够撤单&#xff0c;但这些挂单并不会立即成交。9:25时&#xff0c;系统会对一切收到的挂单进行一次性促成处理&#xf…

外网禅道配置

exportfs -avrf 修改代码&#xff0c;避免启动太慢&#xff1a;vi /opt/zbox/bin/zbox.php 启动和停止 /opt/zbox/zbox start /opt/zbox/zbox stop

windows 双网卡同时接入内外网

在公司使用wifi接入使用桌面云&#xff0c;但是公司wifi不能上外网&#xff0c;查资料不方便&#xff0c;通过手机同时接入外网。 同一台电脑设置同时连接内外网&#xff08;wifi或共享的网络&#xff09;_win7电脑同时使用手机和usb网卡使用wifi-CSDN博客 route print查看当前…

SolidWorks进行热力学有限元分析二、模型装配

1.先打开软件&#xff0c;新建装配体 2.选中你要装配的零件&#xff0c;直接导入就行 3.鼠标点击左键直接先放进去 4.开始装配&#xff0c;点配合 5.选择你要接触的两个面&#xff0c;鼠标右键确定&#xff0c;然后把剩下的面对齐一下就行了 6.搞定

【C 数据结构-动态内存管理】4. 无用单元收集(垃圾回收机制)

文章目录 【 1. 问题描述与解决方法 】【 2. 中断回收机制 】 【 1. 问题描述与解决方法 】 问题描述 动态存储管理的运行机制可以概括为&#xff1a;当用户发出申请空间的请求后&#xff0c;系统向用户分配内存&#xff1b;用户运行结束释放存储空间后&#xff0c;系统回收内…

5月5日智渍洁对江苏某公司冷却塔清洗-智渍洁

简报&#xff1a;5月5日智渍洁对江苏某公司冷却塔清洗 5月5日智渍洁对江苏某公司冷却塔清洗 - 重庆智渍洁环保科技有限公司简报&#xff1a;5月5日智渍洁对江苏某公司冷却塔清洗https://www.zhizijie.com/hl/zixun/gongsi/236.html

Python Dash库:一个Web应用只需几行代码

大家好&#xff0c;在数据科学领域&#xff0c;数据可视化是将数据以图形化形式展示出来&#xff0c;帮助我们更直观地理解数据。Python中有一个非常流行的数据可视化库叫做Dash&#xff0c;Dash以其简洁、高效和强大的功能而闻名&#xff0c;它允许开发者快速构建交互式Web应用…

Vulnhub项目:NAPPING: 1.0.1

1、靶机介绍 靶机地址&#xff1a;Napping: 1.0.1 ~ VulnHub 2、渗透过程 老规矩&#xff0c;先探测&#xff0c;靶机ip&#xff1a;192.168.56.152 本机ip&#xff1a;192.168.56.146 来看一看靶机开放哪些端口&#xff0c;nmap一下 nmap -sS -sV -A -T5 192.168.56.152 开…

Zookeeper服务

一、什么是Zookeeper Zookeeper 是一个分布式应用程序的协调服务&#xff0c;它提供了一个高性能的分布式配置管理、分布式锁服务和分布式协调服务。它是 Apache 软件基金会的一个项目&#xff0c;被设计用来处理大规模的分布式系统中的一些关键问题。 Zookeeper的组成员关系&…

java10基础(this super关键字 重写 final关键字 多态 抽象类)

目录 一. this和super关键字 1. this关键字 2. super关键字 二. 重写 三. final关键字 四. 多态 五. 抽象类 1. 抽象方法 2. 抽象类 3. 面向抽象设计 一. this和super关键字 1. this关键字 this 当前对象的引用 this.属性 this.方法名() this() -- 调用构造函数 …