├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_zh-CN.md ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── ljk │ └── leak_detector │ └── LeakDetectorPlugin.kt ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── ljk │ │ │ │ │ └── leak_detector_example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ └── settings_aar.gradle ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ └── main.dart ├── macos │ ├── .gitignore │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── app_icon_1024.png │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ └── app_icon_64.png │ │ ├── Base.lproj │ │ └── MainMenu.xib │ │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements ├── pubspec.lock ├── pubspec.yaml └── test │ └── widget_test.dart ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── LeakDetectorPlugin.h │ ├── LeakDetectorPlugin.m │ └── SwiftLeakDetectorPlugin.swift └── leak_detector.podspec ├── leak_detector.iml ├── lib ├── leak_detector.dart └── src │ ├── leak_analyzer.dart │ ├── leak_data.dart │ ├── leak_data_store.dart │ ├── leak_detector.dart │ ├── leak_detector_task.dart │ ├── leak_navigator_observer.dart │ ├── leak_record_handler.dart │ ├── leak_sqlite_store.dart │ ├── leak_state_mixin.dart │ ├── view │ ├── bottom_popup_card.dart │ ├── leak_preview_page.dart │ └── popup_window.dart │ └── vm_service_utils.dart ├── macos ├── Classes │ └── LeakDetectorPlugin.swift └── leak_detector.podspec ├── pubspec.lock ├── pubspec.yaml └── test └── leak_detector_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | .idea/ 7 | 8 | build/ 9 | .idea/# 10 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.0 2 | 3 | * upgrade Gradle plugin and dependencies 4 | 5 | ## 1.0.1+4 6 | 7 | * fix bug 8 | 9 | ## 1.0.1+3 10 | 11 | * add `kotlin-android` to android 12 | 13 | ## 1.0.1+2 14 | 15 | * fix bug 16 | 17 | ## 1.0.1+1 18 | 19 | * reduce dart version to `2.12.0` 20 | 21 | ## 1.0.1 22 | 23 | * analyze leaked node type, `Widget`, `Element` 24 | * add `LeakNavigatorObserver`, Automatically check for memory leaks by `NavigatorObserver` 25 | * fix some bugs. 26 | 27 | ## 1.0.0+1 28 | 29 | * Change project configuration 30 | 31 | ## 1.0.0 32 | 33 | * Support `macos` platform 34 | 35 | ## 0.2.0 36 | 37 | * Migrate package to null-safety 38 | 39 | ## 0.1.2+1-beta 40 | 41 | * change `FlatButton` to `TextButton` 42 | 43 | ## 0.1.2-beta 44 | 45 | * Update README.md 46 | 47 | ## 0.1.1-beta 48 | 49 | * Add document description 50 | 51 | ## 0.1.0-beta 52 | 53 | * Increase the basic memory leak detection function, provide `State`, `Element` memory leak detection, provide preview page, leak record storage function. 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021, Jiakuo Liu 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [中文文档](README_zh-CN.md) 2 | 3 | # leak_detector 4 | 5 | flutter Memory leak detection tool 6 | 7 | ## Usage 8 | 9 | #### initialize 10 | 11 | In order to prevent the underlying library `vm service` from crashing, please call before adding the memory leak detection object: 12 | ```dart 13 | LeakDetector().init(maxRetainingPath: 300); //maxRetainingPath default is 300 14 | ``` 15 | Enabling leak detection will reduce performance, and Full GC may drop frames on the page. 16 | Initialized by `assert` in the plugin, so you don't need to turn it off when build on `release` mode. 17 | 18 | #### Detect 19 | 20 | Add `LeakNavigatorObserver` to `navigatorObservers` in `MaterialApp`, it will automatically detect whether there is a memory leak in the page's `Widget` and its corresponding `Element` object. If page's Widget is a `StatefulWidget`, it will also be automatically checked Its corresponding `State`. 21 | 22 | ```dart 23 | import 'package:leak_detector/leak_detector.dart'; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return MaterialApp( 28 | navigatorObservers: [ 29 | //used the LeakNavigatorObserver 30 | LeakNavigatorObserver( 31 | shouldCheck: (route) { 32 | return route.settings.name != null && route.settings.name != '/'; 33 | }, 34 | ), 35 | ], 36 | ); 37 | } 38 | ``` 39 | 40 | #### Get leaked information 41 | 42 | `LeakDetector().onLeakedStream` can register your listener, and notify the object's reference chain after detecting a memory leak. 43 | `LeakDetector().onEventStream` can monitor internal time notifications, such as `start Gc`, `end Gc`, etc. 44 | 45 | A preview page of the reference chain is provided. You only need to add the following code. Note that the `Bulid Context` must be able to obtain the`NavigatorState`: 46 | 47 | ```dart 48 | import 'package:leak_detector/leak_detector.dart'; 49 | 50 | //show preview page 51 | LeakDetector().onLeakedStream.listen((LeakedInfo info) { 52 | //print to console 53 | info.retainingPath.forEach((node) => print(node)); 54 | //show preview page 55 | showLeakedInfoPage(navigatorKey.currentContext, info); 56 | }); 57 | ``` 58 | 59 | Preview page display: 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | It contains the class information of the reference chain node, the referenced attribute information, the source code of the attribute declaration, and the location of the source code (line number: column number). 68 | 69 | #### Get memory leak recording 70 | 71 | ```dart 72 | import 'package:leak_detector/leak_detector.dart'; 73 | 74 | getLeakedRecording().then((List infoList) { 75 | showLeakedInfoListPage(navigatorKey.currentContext, infoList); 76 | }); 77 | ``` 78 | 79 | 80 | 81 | 82 | #### *Cannot connect to `vm_service` on real mobile devices 83 | 84 | The VM service allows for an extended feature set via the Dart Development Service (DDS) that forward all core VM service RPCs described in this document to the true VM service. 85 | 86 | So when we connect to the computer to run, the `DDS` on the computer will first connect to the `vm_service` on our mobile end, causing our `leak_detector` plugin to fail to connect to the `vm_service` again. 87 | 88 | There are two solutions: 89 | 90 | - After the `run` is complete, disconnect from the computer, and then it is best to restart the app. 91 | 92 | If the completed test package is installed on the mobile phone, the above problem does not exist, so this method is suitable for use by testers. 93 | 94 | - Add the `--disable-dds` parameter after `flutter run` to turn off the `DDS`. After testing, this will not cause any impact on debugging 95 | 96 | It can be configured as follows in `Android Studio`. 97 | 98 | After [Pull Request #80900](https://github.com/flutter/flutter/pull/80900) is merged, `--disable-dds` was renamed to `--no-dds` 99 | 100 | 101 | ![image](https://liujiakuoyx.github.io/images/leak_detector/peizhi1.png) 102 | 103 | 104 | ![image](https://liujiakuoyx.github.io/images/leak_detector/peizhi2.png) -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 | # leak_detector 2 | 3 | flutter内存泄漏检测工具 4 | 5 | ## 开始使用 6 | 7 | #### 初始化 8 | 9 | 为了避免底层库`vm_service`发生crash,请在添加内存泄漏检测对象之前调用: 10 | ```dart 11 | //maxRetainingPath:引用链的最大长度,设置越短性能越高,但是很有可能获取不到完整的泄漏路径 默认是 300 12 | LeakDetector().init(maxRetainingPath: 300); 13 | ``` 14 | 开启泄漏检测会降低性能,Full GC可能会使页面掉帧。 15 | 插件中通过`assert`语句初始化,所以您不用特意在`release`版本中关闭该插件。 16 | 17 | #### 检测 18 | 19 | 在`MaterialApp`增加路由的监听器`LeakNavigatorObserver`,这样将会自动检测页面的`Widget`和其对应的`Element`是否存在内存泄漏,如果页面的`Widget`是`StatefulWidget`,也会自动检查其对应的`State`对象。 20 | 21 | ```dart 22 | import 'package:leak_detector/leak_detector.dart'; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return MaterialApp( 27 | navigatorObservers: [ 28 | //used the LeakNavigatorObserver 29 | LeakNavigatorObserver( 30 | //返回false则不会校验这个页面. 31 | shouldCheck: (route) { 32 | return route.settings.name != null && route.settings.name != '/'; 33 | }, 34 | ), 35 | ], 36 | ); 37 | } 38 | ``` 39 | 40 | #### 获取泄漏信息 41 | 42 | `LeakDetector().onLeakedStream`可以注册自己的监听函数,在检测到内存泄漏之后会通知对象的引用链数据。 43 | `LeakDetector().onEventStream`可以监听内部时间的通知,如`startGc`,`endGc`等。 44 | 45 | 提供了一个引用链的预览页面,你只需要添加以下代码即可,注意其中的`BulidContext`必须能够获取`NavigatorState`: 46 | 47 | ```dart 48 | import 'package:leak_detector/leak_detector.dart'; 49 | 50 | //show preview page 51 | LeakDetector().onLeakedStream.listen((LeakedInfo info) { 52 | //print to console 53 | info.retainingPath.forEach((node) => print(node)); 54 | //show preview page 55 | showLeakedInfoPage(navigatorKey.currentContext, info); 56 | }); 57 | ``` 58 | 59 | 页面展示效果如下: 60 | 61 |
62 |          63 |
64 | 65 | 66 | 其中包含引用链节点的类信息、被引用属性信息、属性声明源码、源码位置(行号:列号)。 67 | 68 | #### 内存泄漏历史记录 69 | 70 | ```dart 71 | import 'package:leak_detector/leak_detector.dart'; 72 | 73 | getLeakedRecording().then((List infoList) { 74 | showLeakedInfoListPage(navigatorKey.currentContext, infoList); 75 | }); 76 | ``` 77 | 78 | 79 |
80 | 81 |
82 | 83 | 84 | #### *真机上无法连接vm_service问题 85 | 86 | `vm_service` 存在 [Single Client Mode](https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#single-client-mode)(单一客户端模式)。 87 | 88 | 当`DDS(Dart Development Service)`连接到`vm_service`时,`vm_service`进入单一客户端模式,之后不再接受其他的`WebSocket`连接,而是将`WebSocket`转发给`DDS`,直到`DDS`与`vm_service`断开连接,则`vm_service`才能再次开始接受`WebSocket`请求。 89 | 90 | 所以当我们连接电脑运行的时候,电脑端的`DDS`会首先连接到我们的移动端的`vm_service`的`WebSocket`服务,导致我们的`leak_detector`插件无法再次连接到`vm_service`。 91 | 92 | 有两种解决办法: 93 | 94 | - `run`完成之后,断开与电脑端的连接,然后最好重启app。 95 | 96 | 如果是打好的测试包安装在手机上,是不存在上面的问题的,所以这种方法适用于给测试人员使用的情况下。 97 | 98 | - 在`flutter run`后面加上`--disable-dds`参数关闭调试端的`DDS`服务,经过测试,这样做并不会造成调试端的功能问题。 99 | 100 | 要是使用`Android Studio`也可以像下面这样配置。 101 | 102 | **注意**:在 [Pull Request #80900](https://github.com/flutter/flutter/pull/80900) 合入之后,`--disable-dds`被改名为`--no-dds` 103 | 104 | 105 | ![image](https://liujiakuoyx.github.io/images/leak_detector/peizhi1.png) 106 | 107 | 108 | ![image](https://liujiakuoyx.github.io/images/leak_detector/peizhi2.png) -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.ljk.leak_detector' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.7.10' 6 | repositories { 7 | google() 8 | jcenter() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:7.3.0' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | rootProject.allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | } 22 | } 23 | 24 | allprojects { 25 | gradle.projectsEvaluated { 26 | tasks.withType(JavaCompile) { 27 | options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" 28 | } 29 | } 30 | } 31 | 32 | apply plugin: 'com.android.library' 33 | apply plugin: 'kotlin-android' 34 | 35 | android { 36 | compileSdkVersion 31 37 | 38 | sourceSets { 39 | main.java.srcDirs += 'src/main/kotlin' 40 | } 41 | defaultConfig { 42 | minSdkVersion 16 43 | } 44 | lintOptions { 45 | disable 'InvalidPackage' 46 | } 47 | } 48 | 49 | dependencies { 50 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 51 | } 52 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'leak_detector' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/ljk/leak_detector/LeakDetectorPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.ljk.leak_detector 2 | 3 | import androidx.annotation.NonNull 4 | 5 | import io.flutter.embedding.engine.plugins.FlutterPlugin 6 | import io.flutter.plugin.common.MethodCall 7 | import io.flutter.plugin.common.MethodChannel 8 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler 9 | import io.flutter.plugin.common.MethodChannel.Result 10 | import io.flutter.plugin.common.PluginRegistry.Registrar 11 | 12 | /** LeakDetectorPlugin */ 13 | class LeakDetectorPlugin: FlutterPlugin, MethodCallHandler { 14 | /// The MethodChannel that will the communication between Flutter and native Android 15 | /// 16 | /// This local reference serves to register the plugin with the Flutter Engine and unregister it 17 | /// when the Flutter Engine is detached from the Activity 18 | private lateinit var channel : MethodChannel 19 | 20 | override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 21 | channel = MethodChannel(flutterPluginBinding.binaryMessenger, "leak_detector") 22 | channel.setMethodCallHandler(this) 23 | } 24 | 25 | override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { 26 | if (call.method == "getPlatformVersion") { 27 | result.success("Android ${android.os.Build.VERSION.RELEASE}") 28 | } else { 29 | result.notImplemented() 30 | } 31 | } 32 | 33 | override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { 34 | channel.setMethodCallHandler(null) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # leak_detector_example 2 | 3 | Demonstrates how to use the leak_detector plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 31 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.ljk.leak_detector_example" 42 | minSdkVersion 16 43 | targetSdkVersion 29 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | } 47 | 48 | buildTypes { 49 | release { 50 | // TODO: Add your own signing config for the release build. 51 | // Signing with the debug keys for now, so `flutter run --release` works. 52 | signingConfig signingConfigs.debug 53 | } 54 | } 55 | } 56 | 57 | flutter { 58 | source '../..' 59 | } 60 | 61 | dependencies { 62 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 63 | } 64 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 12 | 19 | 23 | 27 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/ljk/leak_detector_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ljk.leak_detector_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.3.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - FMDB (2.7.5): 4 | - FMDB/standard (= 2.7.5) 5 | - FMDB/standard (2.7.5) 6 | - leak_detector (0.0.1): 7 | - Flutter 8 | - sqflite (0.0.2): 9 | - Flutter 10 | - FMDB (>= 2.7.5) 11 | 12 | DEPENDENCIES: 13 | - Flutter (from `Flutter`) 14 | - leak_detector (from `.symlinks/plugins/leak_detector/ios`) 15 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 16 | 17 | SPEC REPOS: 18 | trunk: 19 | - FMDB 20 | 21 | EXTERNAL SOURCES: 22 | Flutter: 23 | :path: Flutter 24 | leak_detector: 25 | :path: ".symlinks/plugins/leak_detector/ios" 26 | sqflite: 27 | :path: ".symlinks/plugins/sqflite/ios" 28 | 29 | SPEC CHECKSUMS: 30 | Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c 31 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 32 | leak_detector: 30775053068909f517f5ae3f82dffff4c9b450bc 33 | sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 34 | 35 | PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c 36 | 37 | COCOAPODS: 1.9.3 38 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | leak_detector_example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:leak_detector/leak_detector.dart'; 3 | 4 | void main() { 5 | runApp(MyApp()); 6 | } 7 | 8 | class MyApp extends StatefulWidget { 9 | @override 10 | _MyAppState createState() => _MyAppState(); 11 | } 12 | 13 | class _MyAppState extends State { 14 | GlobalKey navigatorKey = GlobalKey(); 15 | bool _checking = false; 16 | 17 | @override 18 | void initState() { 19 | super.initState(); 20 | LeakDetector().init(maxRetainingPath: 300); 21 | LeakDetector().onLeakedStream.listen((LeakedInfo info) { 22 | //print to console 23 | info.retainingPath.forEach((node) => print(node)); 24 | //show preview page 25 | showLeakedInfoPage(navigatorKey.currentContext!, info); 26 | }); 27 | LeakDetector().onEventStream.listen((DetectorEvent event) { 28 | print(event); 29 | if (event.type == DetectorEventType.startAnalyze) { 30 | setState(() { 31 | _checking = true; 32 | }); 33 | } else if (event.type == DetectorEventType.endAnalyze) { 34 | setState(() { 35 | _checking = false; 36 | }); 37 | } 38 | }); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return MaterialApp( 44 | navigatorKey: navigatorKey, 45 | routes: { 46 | '/p1': (_) => LeakPage1(), 47 | '/p2': (_) => LeakPage2(), 48 | '/p3': (_) => LeakPage3(), 49 | '/p4': (_) => LeakPage4(), 50 | }, 51 | navigatorObservers: [ 52 | //used the LeakNavigatorObserver. 53 | LeakNavigatorObserver( 54 | checkLeakDelay: 0, 55 | shouldCheck: (route) { 56 | //You can customize which `route` can be detected 57 | return route.settings.name != null && route.settings.name != '/'; 58 | }, 59 | ), 60 | ], 61 | home: Scaffold( 62 | floatingActionButton: FloatingActionButton( 63 | child: Icon( 64 | Icons.adjust, 65 | color: _checking ? Colors.white : null, 66 | ), 67 | backgroundColor: _checking ? Colors.red : null, 68 | onPressed: () {}, 69 | ), 70 | body: Container( 71 | child: Center( 72 | child: Column( 73 | mainAxisSize: MainAxisSize.min, 74 | children: [ 75 | TextButton( 76 | onPressed: () { 77 | Navigator.of(navigatorKey.currentContext!).pushNamed('/p1'); 78 | }, 79 | style: ButtonStyle( 80 | side: MaterialStateProperty.resolveWith( 81 | (states) => BorderSide(width: 1, color: Colors.blue), 82 | ), 83 | ), 84 | child: Padding( 85 | padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), 86 | child: Text('jump(Stateless,widget leaked)'), 87 | ), 88 | ), 89 | SizedBox( 90 | height: 20, 91 | ), 92 | TextButton( 93 | onPressed: () { 94 | Navigator.of(navigatorKey.currentContext!).pushNamed('/p2'); 95 | }, 96 | style: ButtonStyle( 97 | side: MaterialStateProperty.resolveWith( 98 | (states) => BorderSide(width: 1, color: Colors.blue), 99 | ), 100 | ), 101 | child: Padding( 102 | padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), 103 | child: Text('jump(Stateful,widget leaked)'), 104 | ), 105 | ), 106 | SizedBox( 107 | height: 20, 108 | ), 109 | TextButton( 110 | onPressed: () { 111 | Navigator.of(navigatorKey.currentContext!).pushNamed('/p3'); 112 | }, 113 | style: ButtonStyle( 114 | side: MaterialStateProperty.resolveWith( 115 | (states) => BorderSide(width: 1, color: Colors.blue), 116 | ), 117 | ), 118 | child: Padding( 119 | padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), 120 | child: Text('jump(Stateful,state leaked)'), 121 | ), 122 | ), 123 | SizedBox( 124 | height: 20, 125 | ), 126 | TextButton( 127 | onPressed: () { 128 | Navigator.of(navigatorKey.currentContext!).pushNamed('/p4'); 129 | }, 130 | style: ButtonStyle( 131 | side: MaterialStateProperty.resolveWith( 132 | (states) => BorderSide(width: 1, color: Colors.blue), 133 | ), 134 | ), 135 | child: Padding( 136 | padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), 137 | child: Text('jump(Stateful,element leaked)'), 138 | ), 139 | ), 140 | SizedBox( 141 | height: 20, 142 | ), 143 | TextButton( 144 | onPressed: () { 145 | getLeakedRecording().then((List infoList) { 146 | showLeakedInfoListPage( 147 | navigatorKey.currentContext!, infoList); 148 | }); 149 | }, 150 | style: ButtonStyle( 151 | side: MaterialStateProperty.resolveWith( 152 | (states) => BorderSide(width: 1, color: Colors.blue), 153 | ), 154 | ), 155 | child: Padding( 156 | padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), 157 | child: Text('read history'), 158 | ), 159 | ), 160 | ], 161 | ), 162 | ), 163 | ), 164 | ), 165 | ); 166 | } 167 | } 168 | 169 | class LeakPage1 extends StatelessWidget { 170 | @override 171 | Widget build(BuildContext context) { 172 | return Material( 173 | child: Container( 174 | child: Center( 175 | child: TextButton( 176 | onPressed: () { 177 | Navigator.of(context).pop(this); 178 | }, 179 | style: ButtonStyle( 180 | side: MaterialStateProperty.resolveWith( 181 | (states) => BorderSide(width: 1, color: Colors.blue), 182 | ), 183 | ), 184 | child: Text('back'), 185 | ), 186 | ), 187 | ), 188 | ); 189 | } 190 | } 191 | 192 | class LeakPage2 extends StatefulWidget { 193 | @override 194 | State createState() { 195 | return LeakPageState2(); 196 | } 197 | } 198 | 199 | class LeakPageState2 extends State { 200 | @override 201 | Widget build(BuildContext context) { 202 | return Material( 203 | child: Container( 204 | child: Center( 205 | child: TextButton( 206 | onPressed: () { 207 | Navigator.of(context).pop(widget); 208 | }, 209 | style: ButtonStyle( 210 | side: MaterialStateProperty.resolveWith( 211 | (states) => BorderSide(width: 1, color: Colors.blue), 212 | ), 213 | ), 214 | child: Text('back'), 215 | ), 216 | ), 217 | ), 218 | ); 219 | } 220 | } 221 | 222 | class LeakPage3 extends StatefulWidget { 223 | @override 224 | State createState() { 225 | return LeakPageState3(); 226 | } 227 | } 228 | 229 | class LeakPageState3 extends State { 230 | @override 231 | Widget build(BuildContext context) { 232 | return Material( 233 | child: Container( 234 | child: Center( 235 | child: TextButton( 236 | onPressed: () { 237 | Navigator.of(context).pop(this); 238 | }, 239 | style: ButtonStyle( 240 | side: MaterialStateProperty.resolveWith( 241 | (states) => BorderSide(width: 1, color: Colors.blue), 242 | ), 243 | ), 244 | child: Text('back'), 245 | ), 246 | ), 247 | ), 248 | ); 249 | } 250 | } 251 | 252 | class LeakPage4 extends StatefulWidget { 253 | @override 254 | State createState() { 255 | return LeakPageState4(); 256 | } 257 | } 258 | 259 | class LeakPageState4 extends State { 260 | TextEditingController _controller = TextEditingController(); 261 | 262 | @override 263 | Widget build(BuildContext context) { 264 | return Material( 265 | child: Container( 266 | child: Column( 267 | children: [ 268 | TextField( 269 | controller: _controller, 270 | ), 271 | TextButton( 272 | onPressed: () { 273 | Navigator.of(context).pop(context); 274 | }, 275 | style: ButtonStyle( 276 | side: MaterialStateProperty.resolveWith( 277 | (states) => BorderSide(width: 1, color: Colors.blue), 278 | ), 279 | ), 280 | child: Text('back'), 281 | ) 282 | ], 283 | ), 284 | ), 285 | ); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /example/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/xcuserdata/ 7 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import leak_detector 9 | import sqflite 10 | 11 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 12 | LeakDetectorPlugin.register(with: registry.registrar(forPlugin: "LeakDetectorPlugin")) 13 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 14 | } 15 | -------------------------------------------------------------------------------- /example/macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.11' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | end 35 | 36 | post_install do |installer| 37 | installer.pods_project.targets.each do |target| 38 | flutter_additional_macos_build_settings(target) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /example/macos/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - FlutterMacOS (1.0.0) 3 | - FMDB (2.7.5): 4 | - FMDB/standard (= 2.7.5) 5 | - FMDB/standard (2.7.5) 6 | - leak_detector (0.0.1): 7 | - FlutterMacOS 8 | - sqflite (0.0.2): 9 | - FlutterMacOS 10 | - FMDB (>= 2.7.5) 11 | 12 | DEPENDENCIES: 13 | - FlutterMacOS (from `Flutter/ephemeral`) 14 | - leak_detector (from `Flutter/ephemeral/.symlinks/plugins/leak_detector/macos`) 15 | - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) 16 | 17 | SPEC REPOS: 18 | trunk: 19 | - FMDB 20 | 21 | EXTERNAL SOURCES: 22 | FlutterMacOS: 23 | :path: Flutter/ephemeral 24 | leak_detector: 25 | :path: Flutter/ephemeral/.symlinks/plugins/leak_detector/macos 26 | sqflite: 27 | :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos 28 | 29 | SPEC CHECKSUMS: 30 | FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 31 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 32 | leak_detector: 1879f859d21e71b3da8c67f0b12bc571dcd9ba36 33 | sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea 34 | 35 | PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c 36 | 37 | COCOAPODS: 1.9.3 38 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 64 | 65 | 71 | 73 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /example/macos/Runner/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = leak_detector_example 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.ljk.leakDetectorExample 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2021 com.ljk. All rights reserved. 15 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /example/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /example/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController.init() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.9.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.2.1" 25 | clock: 26 | dependency: transitive 27 | description: 28 | name: clock 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.1.1" 32 | collection: 33 | dependency: transitive 34 | description: 35 | name: collection 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.16.0" 39 | cupertino_icons: 40 | dependency: "direct main" 41 | description: 42 | name: cupertino_icons 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.0.0" 46 | fake_async: 47 | dependency: transitive 48 | description: 49 | name: fake_async 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.3.1" 53 | flutter: 54 | dependency: "direct main" 55 | description: flutter 56 | source: sdk 57 | version: "0.0.0" 58 | flutter_test: 59 | dependency: "direct dev" 60 | description: flutter 61 | source: sdk 62 | version: "0.0.0" 63 | leak_detector: 64 | dependency: "direct main" 65 | description: 66 | path: ".." 67 | relative: true 68 | source: path 69 | version: "1.1.0" 70 | matcher: 71 | dependency: transitive 72 | description: 73 | name: matcher 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "0.12.12" 77 | material_color_utilities: 78 | dependency: transitive 79 | description: 80 | name: material_color_utilities 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "0.1.5" 84 | meta: 85 | dependency: transitive 86 | description: 87 | name: meta 88 | url: "https://pub.dartlang.org" 89 | source: hosted 90 | version: "1.8.0" 91 | path: 92 | dependency: transitive 93 | description: 94 | name: path 95 | url: "https://pub.dartlang.org" 96 | source: hosted 97 | version: "1.8.2" 98 | sky_engine: 99 | dependency: transitive 100 | description: flutter 101 | source: sdk 102 | version: "0.0.99" 103 | source_span: 104 | dependency: transitive 105 | description: 106 | name: source_span 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "1.9.0" 110 | sqflite: 111 | dependency: transitive 112 | description: 113 | name: sqflite 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "2.2.6" 117 | sqflite_common: 118 | dependency: transitive 119 | description: 120 | name: sqflite_common 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "2.4.3" 124 | stack_trace: 125 | dependency: transitive 126 | description: 127 | name: stack_trace 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.10.0" 131 | stream_channel: 132 | dependency: transitive 133 | description: 134 | name: stream_channel 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "2.1.0" 138 | string_scanner: 139 | dependency: transitive 140 | description: 141 | name: string_scanner 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "1.1.1" 145 | synchronized: 146 | dependency: transitive 147 | description: 148 | name: synchronized 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "3.0.0" 152 | term_glyph: 153 | dependency: transitive 154 | description: 155 | name: term_glyph 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "1.2.1" 159 | test_api: 160 | dependency: transitive 161 | description: 162 | name: test_api 163 | url: "https://pub.dartlang.org" 164 | source: hosted 165 | version: "0.4.12" 166 | vector_math: 167 | dependency: transitive 168 | description: 169 | name: vector_math 170 | url: "https://pub.dartlang.org" 171 | source: hosted 172 | version: "2.1.2" 173 | vm_service: 174 | dependency: transitive 175 | description: 176 | name: vm_service 177 | url: "https://pub.dartlang.org" 178 | source: hosted 179 | version: "11.2.0" 180 | sdks: 181 | dart: ">=2.18.0 <3.0.0" 182 | flutter: ">=3.3.0" 183 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: leak_detector_example 2 | description: Demonstrates how to use the leak_detector plugin. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | environment: 9 | sdk: ">=2.18.0 <4.0.0" 10 | flutter: ">=3.3.0" 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | 16 | leak_detector: 17 | # When depending on this package from a real application you should use: 18 | # leak_detector: ^x.y.z 19 | # See https://dart.dev/tools/pub/dependencies#version-constraints 20 | # The example app is bundled with the plugin so we use a path dependency on 21 | # the parent directory to use the current plugin's version. 22 | path: ../ 23 | 24 | # The following adds the Cupertino Icons font to your application. 25 | # Use with the CupertinoIcons class for iOS style icons. 26 | cupertino_icons: ^1.0.0 27 | 28 | dev_dependencies: 29 | flutter_test: 30 | sdk: flutter 31 | 32 | # For information on the generic Dart part of this file, see the 33 | # following page: https://dart.dev/tools/pub/pubspec 34 | 35 | # The following section is specific to Flutter. 36 | flutter: 37 | 38 | # The following line ensures that the Material Icons font is 39 | # included with your application, so that you can use the icons in 40 | # the material Icons class. 41 | uses-material-design: true 42 | 43 | # To add assets to your application, add an assets section, like this: 44 | # assets: 45 | # - images/a_dot_burr.jpeg 46 | # - images/a_dot_ham.jpeg 47 | 48 | # An image asset can refer to one or more resolution-specific "variants", see 49 | # https://flutter.dev/assets-and-images/#resolution-aware. 50 | 51 | # For details regarding adding assets from package dependencies, see 52 | # https://flutter.dev/assets-and-images/#from-packages 53 | 54 | # To add custom fonts to your application, add a fonts section here, 55 | # in this "flutter" section. Each entry in this list should have a 56 | # "family" key with the font family name, and a "fonts" key with a 57 | # list giving the asset and other descriptors for the font. For 58 | # example: 59 | # fonts: 60 | # - family: Schyler 61 | # fonts: 62 | # - asset: fonts/Schyler-Regular.ttf 63 | # - asset: fonts/Schyler-Italic.ttf 64 | # style: italic 65 | # - family: Trajan Pro 66 | # fonts: 67 | # - asset: fonts/TrajanPro.ttf 68 | # - asset: fonts/TrajanPro_Bold.ttf 69 | # weight: 700 70 | # 71 | # For details regarding fonts from package dependencies, 72 | # see https://flutter.dev/custom-fonts/#from-packages 73 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:leak_detector_example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Verify Platform version', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that platform version is retrieved. 19 | expect( 20 | find.byWidgetPredicate( 21 | (Widget widget) => 22 | widget is Text && widget.data!.startsWith('Running on:'), 23 | ), 24 | findsOneWidget, 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liujiakuoyx/leak_detector/7c68ab8ca205e747e32e1d047036541daa238a78/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Classes/LeakDetectorPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface LeakDetectorPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /ios/Classes/LeakDetectorPlugin.m: -------------------------------------------------------------------------------- 1 | #import "LeakDetectorPlugin.h" 2 | #if __has_include() 3 | #import 4 | #else 5 | // Support project import fallback if the generated compatibility header 6 | // is not copied when this plugin is created as a library. 7 | // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 8 | #import "leak_detector-Swift.h" 9 | #endif 10 | 11 | @implementation LeakDetectorPlugin 12 | + (void)registerWithRegistrar:(NSObject*)registrar { 13 | [SwiftLeakDetectorPlugin registerWithRegistrar:registrar]; 14 | } 15 | @end 16 | -------------------------------------------------------------------------------- /ios/Classes/SwiftLeakDetectorPlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | public class SwiftLeakDetectorPlugin: NSObject, FlutterPlugin { 5 | public static func register(with registrar: FlutterPluginRegistrar) { 6 | let channel = FlutterMethodChannel(name: "leak_detector", binaryMessenger: registrar.messenger()) 7 | let instance = SwiftLeakDetectorPlugin() 8 | registrar.addMethodCallDelegate(instance, channel: channel) 9 | } 10 | 11 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 12 | result("iOS " + UIDevice.current.systemVersion) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ios/leak_detector.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint leak_detector.podspec' to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'leak_detector' 7 | s.version = '0.0.1' 8 | s.summary = 'A new Flutter plugin.' 9 | s.description = <<-DESC 10 | A new Flutter plugin. 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*' 17 | s.dependency 'Flutter' 18 | s.platform = :ios, '8.0' 19 | 20 | # Flutter.framework does not contain a i386 slice. 21 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 22 | s.swift_version = '5.0' 23 | end 24 | -------------------------------------------------------------------------------- /leak_detector.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /lib/leak_detector.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Jiakuo Liu. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | library leak_detector; 5 | 6 | import 'src/leak_data.dart'; 7 | import 'src/leak_data_store.dart'; 8 | 9 | export 'src/leak_detector.dart'; 10 | export 'src/leak_state_mixin.dart'; 11 | export 'src/view/leak_preview_page.dart'; 12 | export 'src/leak_data.dart'; 13 | export 'src/leak_navigator_observer.dart'; 14 | 15 | ///read historical leaked data 16 | Future> getLeakedRecording() => LeakedRecordStore().getAll(); 17 | -------------------------------------------------------------------------------- /lib/src/leak_analyzer.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Jiakuo Liu. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'package:vm_service/vm_service.dart'; 5 | 6 | import 'leak_data.dart'; 7 | import 'vm_service_utils.dart'; 8 | 9 | ///analyze leaked path 10 | ///引用链分析 11 | class LeakAnalyzer { 12 | /// The type of GC root which is holding a reference to the specified object. 13 | /// Possible values include: * class table * local handle * persistent 14 | /// handle * stack * user global * weak persistent handle * unknown 15 | /// 16 | /// run on subIsolate 17 | static Future analyze(AnalyzeData analyzeData) async { 18 | final leakedInstance = analyzeData.leakedInstance; 19 | final maxRetainingPath = analyzeData.maxRetainingPath; 20 | if (leakedInstance?.id != null && maxRetainingPath != null) { 21 | final retainingPath = await VmServerUtils() 22 | .getRetainingPath(leakedInstance!.id!, maxRetainingPath); 23 | if (retainingPath?.elements != null && 24 | retainingPath!.elements!.isNotEmpty) { 25 | final retainingObjectList = retainingPath.elements!; 26 | final stream = Stream.fromIterable(retainingObjectList) 27 | .asyncMap(_defaultAnalyzeNode); 28 | List retainingPathList = []; 29 | (await stream.toList()).forEach((e) { 30 | if (e != null) { 31 | retainingPathList.add(e); 32 | } 33 | }); 34 | 35 | return LeakedInfo(retainingPathList, retainingPath.gcRootType); 36 | } 37 | } 38 | return null; 39 | } 40 | 41 | static Future _getObjectType(Class? clazz) async { 42 | if (clazz?.name == null) return LeakedNodeType.unknown; 43 | if (clazz!.name == 'Widget') { 44 | return LeakedNodeType.widget; 45 | } else if (clazz.name == 'Element') { 46 | return LeakedNodeType.element; 47 | } 48 | if (clazz.superClass?.id != null) { 49 | Class? superClass = (await VmServerUtils() 50 | .getObjectInstanceById(clazz.superClass!.id!)) as Class?; 51 | return _getObjectType(superClass); 52 | } else { 53 | return LeakedNodeType.unknown; 54 | } 55 | } 56 | 57 | ///0 FieldRef 58 | ///1 classRef 59 | static Future getFieldAndClassByName(Class? clazz, String name) async { 60 | if (clazz?.fields == null) return null; 61 | for (int i = 0; i < clazz!.fields!.length; i++) { 62 | var field = clazz.fields![i]; 63 | if (field.id != null && field.id!.endsWith(name)) { 64 | return [field, Class.parse(clazz.json)]; 65 | } 66 | } 67 | if (clazz.superClass?.id != null) { 68 | Class? superClass = (await VmServerUtils() 69 | .getObjectInstanceById(clazz.superClass!.id!)) as Class?; 70 | return getFieldAndClassByName(superClass, name); 71 | } else { 72 | return null; 73 | } 74 | } 75 | 76 | static Future _getKeyInfo(RetainingObject retainingObject) async { 77 | String? keyString; 78 | if (retainingObject.parentMapKey?.id != null) { 79 | Obj? keyObj = await VmServerUtils() 80 | .getObjectInstanceById(retainingObject.parentMapKey!.id!); 81 | if (keyObj?.json != null) { 82 | Instance? keyInstance = Instance.parse(keyObj!.json!); 83 | if (keyInstance != null && 84 | (keyInstance.kind == 'String' || 85 | keyInstance.kind == 'Int' || 86 | keyInstance.kind == 'Double' || 87 | keyInstance.kind == 'Bool')) { 88 | keyString = '${keyInstance.kind}: \'${keyInstance.valueAsString}\''; 89 | } else { 90 | if (keyInstance?.id != null) { 91 | keyString = 92 | 'Object: class=${keyInstance?.classRef?.name}, ${await VmServerUtils().invokeMethod(keyInstance!.id!, 'toString', [])}'; 93 | } 94 | } 95 | } 96 | } 97 | return keyString; 98 | } 99 | 100 | static Future _getSourceCodeLocation( 101 | String? parentField, Class clazz) async { 102 | SourceCodeLocation? sourceCodeLocation; 103 | if (parentField != null && clazz.name != '_Closure') { 104 | //get field and owner class 105 | List? fieldAndClass = await getFieldAndClassByName( 106 | clazz, Uri.encodeQueryComponent(parentField)); 107 | if (fieldAndClass != null) { 108 | FieldRef fieldRef = fieldAndClass[0]; 109 | Class fieldClass = fieldAndClass[1]; 110 | if (fieldRef.id != null) { 111 | Field? field = (await VmServerUtils() 112 | .getObjectInstanceById(fieldRef.id!)) as Field?; 113 | if (field != null && field.location?.script?.id != null) { 114 | //get field's Script info, source code, line number, clounm number 115 | Script? script = (await VmServerUtils() 116 | .getObjectInstanceById(field.location!.script!.id!)) as Script?; 117 | if (script != null && field.location?.tokenPos != null) { 118 | int? line = 119 | script.getLineNumberFromTokenPos(field.location!.tokenPos!); 120 | int? column = 121 | script.getColumnNumberFromTokenPos(field.location!.tokenPos!); 122 | String? codeLine; 123 | codeLine = script.source 124 | ?.substring( 125 | field.location!.tokenPos!, field.location!.endTokenPos) 126 | .split('\n') 127 | .first; 128 | sourceCodeLocation = SourceCodeLocation(codeLine, line, column, 129 | fieldClass.name, fieldClass.library?.uri); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | return sourceCodeLocation; 136 | } 137 | 138 | static Future _getClosureInfo(Instance? instance) async { 139 | if (instance != null && instance.kind == 'Closure') { 140 | final name = instance.closureFunction?.name; 141 | final owner = instance.closureFunction?.owner; 142 | final info = 143 | ClosureInfo(closureFunctionName: name, closureOwner: owner?.name); 144 | await _getClosureOwnerInfo(owner, info); 145 | return info; 146 | } 147 | return null; 148 | } 149 | 150 | static _getClosureOwnerInfo(dynamic ref, ClosureInfo info) async { 151 | if (ref?.id == null) return; 152 | if (ref is LibraryRef) { 153 | Library? library = 154 | (await VmServerUtils().getObjectInstanceById((ref).id!)) as Library?; 155 | info.libraries = library?.uri; 156 | } else if (ref is ClassRef) { 157 | Class? clazz = 158 | (await VmServerUtils().getObjectInstanceById(ref.id!)) as Class?; 159 | info.closureOwnerClass = clazz?.name; 160 | info.libraries = clazz?.library?.uri; 161 | } else if (ref is FuncRef) { 162 | if (info.funLine == null) { 163 | //if fun location is null, get the fun code location. 164 | Func? func = 165 | (await VmServerUtils().getObjectInstanceById(ref.id!)) as Func?; 166 | if (func?.location?.script?.id != null) { 167 | //get script info. 168 | Script? script = (await VmServerUtils() 169 | .getObjectInstanceById(func!.location!.script!.id!)) as Script?; 170 | if (script != null && func.location?.tokenPos != null) { 171 | info.funLine = 172 | script.getLineNumberFromTokenPos(func.location!.tokenPos!); 173 | info.funColumn = 174 | script.getColumnNumberFromTokenPos(func.location!.tokenPos!); 175 | } 176 | } 177 | } 178 | await _getClosureOwnerInfo(ref.owner, info); 179 | } 180 | } 181 | 182 | static Future _defaultAnalyzeNode( 183 | RetainingObject retainingObject) async { 184 | if (retainingObject.value is InstanceRef) { 185 | InstanceRef instanceRef = retainingObject.value as InstanceRef; 186 | final String name = instanceRef.classRef?.name ?? ''; 187 | 188 | Class? clazz; 189 | if (instanceRef.classRef?.id != null) { 190 | //get class info 191 | clazz = (await VmServerUtils() 192 | .getObjectInstanceById(instanceRef.classRef!.id!)) as Class?; 193 | } 194 | 195 | SourceCodeLocation? sourceCodeLocation; 196 | if (retainingObject.parentField != null && clazz != null) { 197 | //parentField source code location 198 | sourceCodeLocation = 199 | await _getSourceCodeLocation(retainingObject.parentField!, clazz); 200 | } 201 | 202 | String? toString; 203 | if (instanceRef.id != null) { 204 | //object toString 205 | toString = 206 | await VmServerUtils().invokeMethod(instanceRef.id!, 'toString', []); 207 | } 208 | 209 | //if is Map, get Key info. 210 | String? keyString = await _getKeyInfo(retainingObject); 211 | 212 | ClosureInfo? closureInfo; 213 | if (retainingObject.value?.json != null) { 214 | //if is Closure,get ClosureInfo 215 | closureInfo = 216 | await _getClosureInfo(Instance.parse(retainingObject.value!.json)); 217 | } 218 | return RetainingNode( 219 | name, 220 | parentField: retainingObject.parentField?.toString(), 221 | parentIndex: retainingObject.parentListIndex, 222 | parentKey: keyString, 223 | libraries: clazz?.library?.uri, 224 | sourceCodeLocation: sourceCodeLocation, 225 | string: toString, 226 | closureInfo: closureInfo, 227 | leakedNodeType: await _getObjectType(clazz), 228 | ); 229 | } else if (retainingObject.value?.type != '@Context') { 230 | return RetainingNode( 231 | retainingObject.value?.type ?? '', 232 | parentField: retainingObject.parentField?.toString(), 233 | ); 234 | } 235 | return null; 236 | } 237 | } 238 | 239 | class AnalyzeData { 240 | final ObjRef? leakedInstance; 241 | final int? maxRetainingPath; 242 | 243 | AnalyzeData(this.leakedInstance, this.maxRetainingPath); 244 | } 245 | -------------------------------------------------------------------------------- /lib/src/leak_data.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Jiakuo Liu. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:convert'; 5 | 6 | ///Leak the reference chain and other information of the object 7 | class LeakedInfo { 8 | ///Reference chain, if there are multiple reference chains, there is only one 9 | List retainingPath; 10 | 11 | /// The type of GC root which is holding a reference to the specified object. 12 | /// Possible values include: * class table * local handle * persistent 13 | /// handle * stack * user global * weak persistent handle * unknown 14 | String? gcRootType; 15 | 16 | ///Time to completion of leak detection 17 | int? timestamp; 18 | 19 | LeakedInfo(this.retainingPath, this.gcRootType, {this.timestamp}) { 20 | if (timestamp == null) { 21 | timestamp = DateTime.now().millisecondsSinceEpoch; 22 | } 23 | } 24 | 25 | bool get isNotEmpty => retainingPath.isNotEmpty; 26 | 27 | ///to json string 28 | String get retainingPathJson { 29 | if (isNotEmpty) { 30 | return jsonEncode(retainingPath.map((path) => path.toJson()).toList()); 31 | } 32 | return '[]'; 33 | } 34 | 35 | @override 36 | String toString() { 37 | return '$gcRootType, retainingPath: $retainingPathJson'; 38 | } 39 | } 40 | 41 | ///leaked node info 42 | class RetainingNode { 43 | String clazz = ''; //class name 44 | String? parentField; //parentField 45 | bool important = false; //进过分析是否为重要的节点 46 | String? libraries; //libraries name 47 | String? string; //object toString() 48 | String? parentKey; //if object in a Map,map's key 49 | int? parentIndex; //if object in a List,it is index in the List 50 | SourceCodeLocation? sourceCodeLocation; //source code, code location 51 | ClosureInfo? closureInfo; //if object is closure 52 | late LeakedNodeType leakedNodeType; //widget, element... 53 | 54 | RetainingNode( 55 | this.clazz, { 56 | this.parentKey, 57 | this.parentIndex, 58 | this.string, 59 | this.sourceCodeLocation, 60 | this.parentField, 61 | this.libraries, 62 | this.important = false, 63 | this.closureInfo, 64 | this.leakedNodeType = LeakedNodeType.unknown, 65 | }); 66 | 67 | @override 68 | String toString() { 69 | return jsonEncode(toJson()); 70 | } 71 | 72 | Map toJson() { 73 | return { 74 | 'clazz': clazz, 75 | 'parentKey': parentKey, 76 | 'string': string, 77 | 'parentIndex': parentIndex, 78 | 'sourceCodeLocation': sourceCodeLocation?.toJson(), 79 | 'parentField': parentField, 80 | 'libraries': libraries, 81 | 'important': important, 82 | 'leakedNodeType': leakedNodeType.index, 83 | 'closureInfo': closureInfo?.toJson(), 84 | }; 85 | } 86 | 87 | RetainingNode.fromJson(Map json) { 88 | clazz = json['clazz']; 89 | parentKey = json['parentKey']; 90 | parentIndex = json['parentIndex']; 91 | string = json['string']; 92 | leakedNodeType = 93 | LeakedNodeType.values[(json['leakedNodeType'] ?? 0) as int]; 94 | if (json['sourceCodeLocation'] is Map) { 95 | sourceCodeLocation = 96 | SourceCodeLocation.fromJson(json['sourceCodeLocation']); 97 | } 98 | parentField = json['parentField']; 99 | libraries = json['libraries']; 100 | important = json['important']; 101 | if (json['closureInfo'] is Map) { 102 | closureInfo = ClosureInfo.fromJson(json['closureInfo']); 103 | } 104 | } 105 | } 106 | 107 | ///leaked field source code location 108 | class SourceCodeLocation { 109 | String? code; 110 | int? lineNum; 111 | int? columnNum; 112 | String? className; 113 | String? uri; //lib uri 114 | 115 | SourceCodeLocation( 116 | this.code, this.lineNum, this.columnNum, this.className, this.uri); 117 | 118 | SourceCodeLocation.fromJson(Map json) { 119 | code = json['code']; 120 | lineNum = json['lineNum']; 121 | columnNum = json['columnNum']; 122 | className = json['className']; 123 | uri = json['uri']; 124 | } 125 | 126 | @override 127 | String toString() { 128 | return '$code($lineNum:$columnNum) $uri#$className'; 129 | } 130 | 131 | Map toJson() { 132 | return { 133 | 'code': code, 134 | 'lineNum': lineNum, 135 | 'columnNum': columnNum, 136 | 'className': className, 137 | 'uri': uri, 138 | }; 139 | } 140 | } 141 | 142 | /// if leaked node if Closure 143 | class ClosureInfo { 144 | String? closureFunctionName; 145 | String? closureOwner; //可能是 方法、类、包 146 | String? closureOwnerClass; //如果owner是类=owner,owner是方法所在类 147 | String? libraries; 148 | int? funLine; 149 | int? funColumn; 150 | 151 | ClosureInfo({ 152 | this.closureFunctionName, 153 | this.closureOwner, 154 | this.closureOwnerClass, 155 | this.libraries, 156 | this.funLine, 157 | this.funColumn, 158 | }); 159 | 160 | ClosureInfo.fromJson(Map json) { 161 | closureFunctionName = json['closureFunctionName']; 162 | closureOwner = json['closureOwner']; 163 | closureOwnerClass = json['closureOwnerClass']; 164 | libraries = json['libraries']; 165 | funLine = json['funLine']; 166 | funColumn = json['funColumn']; 167 | } 168 | 169 | Map toJson() { 170 | return { 171 | 'closureFunctionName': closureFunctionName, 172 | 'closureOwner': closureOwner, 173 | 'closureOwnerClass': closureOwnerClass, 174 | 'libraries': libraries, 175 | 'funLine': funLine, 176 | 'funColumn': funColumn, 177 | }; 178 | } 179 | 180 | @override 181 | String toString() { 182 | return '$libraries\nclosureFunName:$closureFunctionName($funLine:$funColumn)\nowner:$closureOwner\nownerClass:$closureOwnerClass'; 183 | } 184 | } 185 | 186 | enum LeakedNodeType { 187 | unknown, 188 | widget, 189 | element, 190 | } 191 | -------------------------------------------------------------------------------- /lib/src/leak_data_store.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Jiakuo Liu. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:io'; 5 | 6 | import 'package:leak_detector/src/leak_sqlite_store.dart'; 7 | 8 | import '../leak_detector.dart'; 9 | 10 | ///Leaked record store. 11 | abstract class LeakedRecordStore { 12 | static LeakedRecordStore? _instance; 13 | 14 | //TODO add windows, linux data store. 15 | factory LeakedRecordStore() { 16 | if (_instance == null) { 17 | if (Platform.isAndroid || Platform.isIOS || Platform.isMacOS) { 18 | _instance = LeakedRecordSQLiteStore(); 19 | } else if (Platform.isWindows) { 20 | //TODO windows store 21 | } else if (Platform.isLinux) { 22 | //TODO linux store 23 | } 24 | } 25 | return _instance!; 26 | } 27 | 28 | //get all data 29 | Future> getAll(); 30 | 31 | //clean the store 32 | void clear(); 33 | 34 | //delete by id 35 | void deleteById(int id); 36 | 37 | //insert a info list 38 | void addAll(List list); 39 | 40 | //add one 41 | void add(LeakedInfo info); 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/leak_detector.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Jiakuo Liu. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:async'; 5 | import 'dart:collection'; 6 | import 'dart:ffi'; 7 | 8 | import 'package:flutter/widgets.dart'; 9 | 10 | import 'leak_detector_task.dart'; 11 | import 'leak_data.dart'; 12 | import 'leak_record_handler.dart'; 13 | import 'vm_service_utils.dart'; 14 | 15 | typedef LeakEventListener = void Function(DetectorEvent event); 16 | 17 | ///泄漏检测主要工具类 18 | class LeakDetector { 19 | static LeakDetector? _instance; 20 | 21 | ///[VmService.getRetainingPath]limit 22 | static int? maxRetainingPath; 23 | 24 | ///detected object 25 | Map _watchGroup = {}; 26 | 27 | ///Queue to detect memory leaks, first in, first out 28 | Queue _checkTaskQueue = Queue(); 29 | 30 | ///Notify after a memory leak 31 | StreamController _onLeakedStreamController = 32 | StreamController.broadcast(); 33 | StreamController _onEventStreamController = 34 | StreamController.broadcast(); 35 | 36 | DetectorTask? _currentTask; 37 | 38 | Stream get onLeakedStream => _onLeakedStreamController.stream; 39 | 40 | Stream get onEventStream => _onEventStreamController.stream; 41 | 42 | factory LeakDetector() { 43 | _instance ??= LeakDetector._(); 44 | return _instance!; 45 | } 46 | 47 | void init({int maxRetainingPath = 300}) { 48 | LeakDetector.maxRetainingPath = maxRetainingPath; 49 | } 50 | 51 | LeakDetector._() { 52 | assert(() { 53 | VmServerUtils().getVmService(); //connect VmService 54 | onLeakedStream 55 | .listen(saveLeakedRecord); //add a listener, save leaked record 56 | return true; 57 | }()); 58 | } 59 | 60 | ///Start to detect whether there is a memory leak 61 | ensureReleaseAsync(String? group, {int delay = 0}) async { 62 | Expando? expando = _watchGroup[group]; 63 | _watchGroup.remove(group); 64 | if (expando != null) { 65 | //延时检测,有些state会在页面退出之后延迟释放,这并不表示就一定是内存泄漏。 66 | //比如runZone就会延时释放 67 | Timer(Duration(milliseconds: delay), () async { 68 | // add a check task 69 | _checkTaskQueue.add( 70 | DetectorTask( 71 | expando, 72 | sink: _onEventStreamController.sink, 73 | onStart: () => _onEventStreamController 74 | .add(DetectorEvent(DetectorEventType.check, data: group)), 75 | onResult: () { 76 | _currentTask = null; 77 | _checkStartTask(); 78 | }, 79 | onLeaked: (LeakedInfo? leakInfo) { 80 | //notify listeners 81 | if (leakInfo != null && leakInfo.isNotEmpty) { 82 | _onLeakedStreamController.add(leakInfo); 83 | } 84 | }, 85 | ), 86 | ); 87 | expando = null; 88 | _checkStartTask(); 89 | }); 90 | } 91 | } 92 | 93 | ///start check task if not empty 94 | void _checkStartTask() { 95 | if (_checkTaskQueue.isNotEmpty && _currentTask == null) { 96 | _currentTask = _checkTaskQueue.removeFirst(); 97 | _currentTask?.start(); 98 | } 99 | } 100 | 101 | ///[group] 认为可以在一块释放的对象组,一般在一个[State]中想监听的对象 102 | addWatchObject(Object obj, String group) { 103 | if (LeakDetector.maxRetainingPath == null) return; 104 | 105 | _onEventStreamController 106 | .add(DetectorEvent(DetectorEventType.addObject, data: group)); 107 | 108 | _checkType(obj); 109 | String key = group; 110 | Expando? expando = _watchGroup[key]; 111 | expando ??= Expando('LeakChecker$key'); 112 | expando[obj] = true; 113 | _watchGroup[key] = expando; 114 | } 115 | 116 | static _checkType(object) { 117 | if ((object == null) || 118 | (object is bool) || 119 | (object is num) || 120 | (object is String) || 121 | (object is Pointer) || 122 | (object is Struct)) { 123 | throw new ArgumentError.value(object, 124 | "Expandos are not allowed on strings, numbers, booleans, null, Pointers, Structs or Unions."); 125 | } 126 | } 127 | } 128 | 129 | ///Detector internal events 130 | class DetectorEvent { 131 | final DetectorEventType type; 132 | final dynamic data; 133 | 134 | @override 135 | String toString() { 136 | return '$type, $data'; 137 | } 138 | 139 | DetectorEvent(this.type, {this.data}); 140 | } 141 | 142 | enum DetectorEventType { 143 | addObject, //add a object 144 | check, 145 | startGC, 146 | endGc, 147 | startAnalyze, 148 | endAnalyze, 149 | } 150 | -------------------------------------------------------------------------------- /lib/src/leak_detector_task.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Jiakuo Liu. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:async'; 5 | 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:vm_service/vm_service.dart'; 8 | 9 | import 'leak_detector.dart'; 10 | import 'leak_analyzer.dart'; 11 | import 'leak_data.dart'; 12 | import 'vm_service_utils.dart'; 13 | 14 | ///check leak task 15 | abstract class _Task { 16 | void start() async { 17 | T? result; 18 | try { 19 | result = await run(); 20 | } catch (e) { 21 | print('_Task $e'); 22 | } finally { 23 | done(result); 24 | } 25 | } 26 | 27 | Future run(); 28 | 29 | ///make sure to call after run 30 | void done(T? result); 31 | } 32 | 33 | class DetectorTask extends _Task { 34 | Expando? expando; 35 | 36 | final VoidCallback? onStart; 37 | final Function()? onResult; 38 | final Function(LeakedInfo? leakInfo)? onLeaked; 39 | final StreamSink? sink; 40 | 41 | DetectorTask( 42 | this.expando, { 43 | required this.onResult, 44 | required this.onLeaked, 45 | this.onStart, 46 | this.sink, 47 | }); 48 | 49 | @override 50 | void done(Object? result) { 51 | onResult?.call(); 52 | } 53 | 54 | @override 55 | Future run() async { 56 | if (expando != null) { 57 | onStart?.call(); 58 | if (await _maybeHasLeaked()) { 59 | //run GC,ensure Object should release 60 | sink?.add(DetectorEvent(DetectorEventType.startGC)); 61 | await VmServerUtils().startGCAsync(); //GC 62 | sink?.add(DetectorEvent(DetectorEventType.endGc)); 63 | return await _analyzeLeakedPathAfterGC(); 64 | } 65 | } 66 | return null; 67 | } 68 | 69 | ///after Full GC, check whether there is a leak, 70 | ///if there is an analysis of the leaked reference chain 71 | Future _analyzeLeakedPathAfterGC() async { 72 | List weakPropertyList = 73 | await _getExpandoWeakPropertyList(expando!); 74 | expando = null; //一定要释放引用 75 | for (var weakProperty in weakPropertyList) { 76 | if (weakProperty != null) { 77 | final leakedInstance = await _getWeakPropertyKey(weakProperty.id); 78 | if (leakedInstance != null && leakedInstance.id != "objects/null") { 79 | final start = DateTime.now(); 80 | sink?.add(DetectorEvent(DetectorEventType.startAnalyze)); 81 | LeakedInfo? leakInfo = await compute( 82 | LeakAnalyzer.analyze, 83 | AnalyzeData(leakedInstance, LeakDetector.maxRetainingPath), 84 | debugLabel: 'analyze', 85 | ); 86 | sink?.add(DetectorEvent(DetectorEventType.endAnalyze, 87 | data: DateTime.now().difference(start))); 88 | onLeaked?.call(leakInfo); 89 | } 90 | } 91 | } 92 | return null; 93 | } 94 | 95 | ///some weak reference != null; 96 | Future _maybeHasLeaked() async { 97 | List weakPropertyList = 98 | await _getExpandoWeakPropertyList(expando!); 99 | for (var weakProperty in weakPropertyList) { 100 | if (weakProperty != null) { 101 | final leakedInstance = await _getWeakPropertyKey(weakProperty.id); 102 | if (leakedInstance != null) return true; 103 | } 104 | } 105 | return false; 106 | } 107 | 108 | ///List Item has id 109 | Future> _getExpandoWeakPropertyList(Expando expando) async { 110 | if (await VmServerUtils().hasVmService) { 111 | final data = (await VmServerUtils().getInstanceByObject(expando)) 112 | ?.getFieldValueInstance('_data'); 113 | if (data?.id != null) { 114 | final dataObj = await VmServerUtils().getObjectInstanceById(data.id); 115 | if (dataObj?.json != null) { 116 | Instance? weakListInstance = Instance.parse(dataObj!.json!); 117 | if (weakListInstance != null) { 118 | return weakListInstance.elements ?? []; 119 | } 120 | } 121 | } 122 | } 123 | return []; 124 | } 125 | 126 | ///get PropertyKey in [Expando] 127 | Future _getWeakPropertyKey(String weakPropertyId) async { 128 | final weakPropertyObj = 129 | await VmServerUtils().getObjectInstanceById(weakPropertyId); 130 | if (weakPropertyObj != null) { 131 | final weakPropertyInstance = Instance.parse(weakPropertyObj.json); 132 | return weakPropertyInstance?.propertyKey; 133 | } 134 | return null; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /lib/src/leak_navigator_observer.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Jiakuo Liu. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'package:flutter/widgets.dart'; 5 | 6 | import '../leak_detector.dart'; 7 | 8 | ///daley check leak 9 | ///Sometimes some pages refer to delayed callback functions 10 | ///Such as WebSocket is delay close connect. 11 | const int _defaultCheckLeakDelay = 500; 12 | 13 | typedef ShouldAddedRoute = bool Function(Route route); 14 | 15 | ///NavigatorObserver 16 | class LeakNavigatorObserver extends NavigatorObserver { 17 | final ShouldAddedRoute? shouldCheck; 18 | final int checkLeakDelay; 19 | 20 | ///[callback] if 'null',the all route can added to LeakDetector. 21 | ///if not 'null', returns ‘true’, then this route will be added to the LeakDetector. 22 | LeakNavigatorObserver( 23 | {this.checkLeakDelay = _defaultCheckLeakDelay, this.shouldCheck}); 24 | 25 | @override 26 | void didPop(Route route, Route? previousRoute) { 27 | _remove(route); 28 | } 29 | 30 | @override 31 | void didPush(Route route, Route? previousRoute) { 32 | _add(route); 33 | } 34 | 35 | @override 36 | void didRemove(Route route, Route? previousRoute) { 37 | _remove(route); 38 | } 39 | 40 | @override 41 | void didReplace({Route? newRoute, Route? oldRoute}) { 42 | if (newRoute != null) { 43 | _add(newRoute); 44 | } 45 | if (oldRoute != null) { 46 | _remove(oldRoute); 47 | } 48 | } 49 | 50 | ///add a object to LeakDetector 51 | void _add(Route route) { 52 | assert(() { 53 | if (route is ModalRoute && 54 | (shouldCheck == null || shouldCheck!.call(route))) { 55 | route.didPush().then((_) { 56 | final element = _getElementByRoute(route); 57 | if (element != null) { 58 | final key = _getRouteKey(route); 59 | watchObjectLeak(element, key); //Element 60 | watchObjectLeak(element.widget, key); //Widget 61 | if (element is StatefulElement) { 62 | watchObjectLeak(element.state, key); //State 63 | } 64 | } 65 | }); 66 | } 67 | 68 | return true; 69 | }()); 70 | } 71 | 72 | ///check and analyze the route 73 | void _remove(Route route) { 74 | assert(() { 75 | final element = _getElementByRoute(route); 76 | if (element != null) { 77 | final key = _getRouteKey(route); 78 | if (element is StatefulElement || element is StatelessElement) { 79 | //start check 80 | LeakDetector().ensureReleaseAsync(key, delay: checkLeakDelay); 81 | } 82 | } 83 | 84 | return true; 85 | }()); 86 | } 87 | 88 | ///add obj into the group 89 | watchObjectLeak(Object obj, String name) { 90 | assert(() { 91 | LeakDetector().addWatchObject(obj, name); 92 | return true; 93 | }()); 94 | } 95 | 96 | ///Get the ‘Element’ of our custom page 97 | Element? _getElementByRoute(Route route) { 98 | Element? element; 99 | if (route is ModalRoute && 100 | (shouldCheck == null || shouldCheck!.call(route))) { 101 | //RepaintBoundary 102 | route.subtreeContext?.visitChildElements((child) { 103 | //Builder 104 | child.visitChildElements((child) { 105 | if (child.widget is Semantics) { 106 | //Semantics 107 | child.visitChildElements((child) { 108 | //My Page 109 | element = child; 110 | }); 111 | } else { 112 | element = child; 113 | } 114 | }); 115 | }); 116 | } 117 | return element; 118 | } 119 | 120 | ///generate key by [Route] 121 | String _getRouteKey(Route route) { 122 | final hasCode = route.hashCode.toString(); 123 | String? key = route.settings.name; 124 | if (key == null || key.isEmpty) { 125 | key = route.hashCode.toString(); 126 | } else { 127 | key = '$key($hasCode)'; 128 | } 129 | return key; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/src/leak_record_handler.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Jiakuo Liu. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'leak_data.dart'; 5 | import 'leak_data_store.dart'; 6 | 7 | ///save leak info to database 8 | Function(LeakedInfo) saveLeakedRecord = 9 | (LeakedInfo leakInfo) => LeakedRecordStore().add(leakInfo); 10 | -------------------------------------------------------------------------------- /lib/src/leak_sqlite_store.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Jiakuo Liu. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:convert'; 5 | 6 | import 'package:leak_detector/src/leak_data_store.dart'; 7 | import 'package:sqflite/sqflite.dart'; 8 | import 'package:path/path.dart'; 9 | 10 | import 'leak_data.dart'; 11 | 12 | ///数据库升级表[版本号 | 数据库版本 | 备注 13 | /// 1.0.0 | 1 | 创建数据库 14 | const int _kLeakDatabaseVersion = 1; 15 | 16 | ///database 17 | class _LeakDataBase { 18 | static Future _openDatabase() async { 19 | return openDatabase( 20 | join(await getDatabasesPath(), 'leak_recording.db'), 21 | version: _kLeakDatabaseVersion, 22 | onCreate: (Database db, int version) { 23 | // Run the CREATE TABLE statement on the database. 24 | return db.execute( 25 | "CREATE TABLE IF NOT EXISTS ${_LeakRecordingTable._kTableName}(" 26 | "${_LeakRecordingTable._kId} TEXT NOT NULL PRIMARY KEY, " 27 | "${_LeakRecordingTable._kGCRootType} TEXT, " 28 | "${_LeakRecordingTable._kLeakPathJson} TEXT)", 29 | ); 30 | }, 31 | onUpgrade: (Database db, int oldVersion, int newVersion) async {}, 32 | ); 33 | } 34 | } 35 | 36 | /// table 37 | class _LeakRecordingTable { 38 | static const String _kTableName = 'leak_recording_table'; 39 | static const String _kGCRootType = 'gcType'; 40 | static const String _kLeakPathJson = 'leakPath'; //leaked path to json 41 | static const String _kId = '_id'; //time 42 | } 43 | 44 | ///[_LeakRecordingTable] Helper 45 | class LeakedRecordSQLiteStore implements LeakedRecordStore { 46 | static LeakedRecordSQLiteStore? _instance; 47 | 48 | Future get database => _LeakDataBase._openDatabase(); 49 | 50 | factory LeakedRecordSQLiteStore() { 51 | _instance ??= LeakedRecordSQLiteStore._(); 52 | return _instance!; 53 | } 54 | 55 | LeakedRecordSQLiteStore._(); 56 | 57 | Future> _queryAll() async { 58 | // Get a reference to the database. 59 | final Database db = await database; 60 | final List> maps = await db.query( 61 | _LeakRecordingTable._kTableName, 62 | ); 63 | if (maps.isNotEmpty) { 64 | return maps.map((dataMap) => _toData(dataMap)).toList(); 65 | } else { 66 | return []; 67 | } 68 | } 69 | 70 | Future _insert(LeakedInfo data) async { 71 | final Database db = await database; 72 | await db.insert( 73 | _LeakRecordingTable._kTableName, 74 | _toDatabaseMap(data), 75 | conflictAlgorithm: ConflictAlgorithm.replace, //冲突替换 76 | ); 77 | } 78 | 79 | Future _insertAll(List data) async { 80 | final Database db = await database; 81 | data.forEach((info) async { 82 | await db.insert( 83 | _LeakRecordingTable._kTableName, 84 | _toDatabaseMap(info), 85 | conflictAlgorithm: ConflictAlgorithm.replace, 86 | ); 87 | }); 88 | } 89 | 90 | Future _deleteById(int id) async { 91 | // Get a reference to the database (获得数据库引用) 92 | final db = await database; 93 | // Remove the Data from the Database. 94 | await db.delete( 95 | _LeakRecordingTable._kTableName, 96 | // Use a `where` clause to delete a specific meeting (使用 `where` 语句删除指定的id). 97 | where: "${_LeakRecordingTable._kId} = ?", 98 | // Pass the Data's id as a whereArg to prevent SQL injection (通过 `whereArg` 将 id 传递给 `delete` 方法,以防止 SQL 注入) 99 | whereArgs: [id.toString()], 100 | ); 101 | } 102 | 103 | Future _deleteAll() async { 104 | // Get a reference to the database (获得数据库引用) 105 | final db = await database; 106 | // Remove the Data from the Database. 107 | await db.delete(_LeakRecordingTable._kTableName); 108 | } 109 | 110 | Map _toDatabaseMap(LeakedInfo data) { 111 | return { 112 | _LeakRecordingTable._kId: data.timestamp.toString(), 113 | _LeakRecordingTable._kGCRootType: data.gcRootType, 114 | _LeakRecordingTable._kLeakPathJson: data.retainingPathJson, 115 | }; 116 | } 117 | 118 | LeakedInfo _toData(Map dataMap) { 119 | String gcRootType = dataMap[_LeakRecordingTable._kGCRootType]; 120 | String leakPathJson = dataMap[_LeakRecordingTable._kLeakPathJson]; 121 | String timestamp = dataMap[_LeakRecordingTable._kId]; 122 | List dataList = jsonDecode(leakPathJson); 123 | return LeakedInfo( 124 | dataList.map((map) => RetainingNode.fromJson(map)).toList(), 125 | gcRootType, 126 | timestamp: int.tryParse(timestamp), 127 | ); 128 | } 129 | 130 | @override 131 | void add(LeakedInfo info) => _insert(info); 132 | 133 | @override 134 | void addAll(List list) => _insertAll(list); 135 | 136 | @override 137 | void clear() => _deleteAll(); 138 | 139 | @override 140 | Future> getAll() => _queryAll(); 141 | 142 | @override 143 | void deleteById(int id) => _deleteById(id); 144 | } 145 | -------------------------------------------------------------------------------- /lib/src/leak_state_mixin.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Jiakuo Liu. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'leak_detector.dart'; 6 | 7 | const int _defaultCheckLeakDelay = 500; 8 | 9 | ///Used on [State], it can automatically detect whether 10 | ///[State] and its corresponding [Stateful Element] will leak memory 11 | @Deprecated('used [LeakNavigatorObserver]') 12 | mixin StateLeakMixin on State { 13 | ///daley check leak 14 | ///Sometimes some pages refer to delayed callback functions 15 | ///Such as WebSocket is delay close connect. 16 | int get checkLeakDelayMill => _defaultCheckLeakDelay; 17 | 18 | ///watch Group 19 | String get watchGroup => hashCode.toString(); 20 | 21 | @override 22 | @mustCallSuper 23 | void initState() { 24 | super.initState(); 25 | assert(() { 26 | watchObjectLeak(this); //State 27 | watchObjectLeak(context); //Element 28 | return true; 29 | }()); 30 | } 31 | 32 | @override 33 | @mustCallSuper 34 | void dispose() { 35 | super.dispose(); 36 | assert(() { 37 | //start check 38 | LeakDetector().ensureReleaseAsync(watchGroup, delay: checkLeakDelayMill); 39 | return true; 40 | }()); 41 | } 42 | 43 | //add obj into the group 44 | watchObjectLeak(Object obj) { 45 | assert(() { 46 | LeakDetector() 47 | .addWatchObject(obj, watchGroup); //'hashCode' is check group key 48 | return true; 49 | }()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/view/bottom_popup_card.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Jiakuo Liu. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'package:flutter/material.dart'; 5 | 6 | import 'popup_window.dart'; 7 | 8 | const double MAX_CLOSE_HEIGHT = 160; //向下滑动关闭的最大高度 9 | const double MAX_CLOSE_VELOCITY = 700.0; //向下滑动关闭的最小速度 10 | 11 | class BottomPopupCard { 12 | static show( 13 | BuildContext context, 14 | Widget child, 15 | ) async { 16 | await PopupWindow.showBottom( 17 | context, 18 | _CardWidget(child), 19 | barrierColor: Colors.black.withOpacity(0.6), 20 | ); 21 | } 22 | } 23 | 24 | class _CardWidget extends StatefulWidget { 25 | final Widget child; 26 | 27 | const _CardWidget(this.child, {Key? key}) : super(key: key); 28 | 29 | @override 30 | _CardWidgetState createState() { 31 | return _CardWidgetState(); 32 | } 33 | } 34 | 35 | class _CardWidgetState extends State<_CardWidget> 36 | with TickerProviderStateMixin { 37 | //手指滑动的高度 38 | double moveHeight = 0; 39 | 40 | //是否在进行动画 41 | bool isAnimForward = false; 42 | 43 | @override 44 | void initState() { 45 | super.initState(); 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | return CustomSingleChildLayout( 51 | delegate: _BottomWindowLayout(moveHeight), 52 | child: ClipRRect( 53 | borderRadius: BorderRadius.only( 54 | topLeft: Radius.circular(18), 55 | topRight: Radius.circular(18), 56 | ), 57 | child: Container( 58 | child: Material( 59 | color: Color(0xFF353535), 60 | child: Column( 61 | mainAxisSize: MainAxisSize.min, 62 | mainAxisAlignment: MainAxisAlignment.end, 63 | crossAxisAlignment: CrossAxisAlignment.center, 64 | children: [ 65 | //弹窗顶部可以拖动区域 66 | GestureDetector( 67 | onVerticalDragUpdate: (DragUpdateDetails details) { 68 | if (isAnimForward) return; //执行动画时,直接返回 69 | setState(() { 70 | //拖动的时候更新布局 71 | moveHeight += details.delta.dy; 72 | if (moveHeight < 0) moveHeight = 0; //最小值为0 73 | }); 74 | }, 75 | onVerticalDragEnd: (DragEndDetails details) => isAnimForward 76 | ? {} 77 | : _popIfCan(details.primaryVelocity ?? 0.0), 78 | onVerticalDragCancel: () => isAnimForward ? {} : _popIfCan(), 79 | child: Container( 80 | color: Color(0xFF353535), 81 | height: 40, 82 | child: Center( 83 | //上下拖动的横线 84 | child: Container( 85 | height: 4, 86 | width: 35, 87 | decoration: BoxDecoration( 88 | color: Color.fromRGBO(255, 225, 225, 1), 89 | borderRadius: BorderRadius.all(Radius.circular(6)), 90 | ), 91 | ), 92 | ), 93 | ), 94 | ), 95 | Container( 96 | height: 0.5, 97 | color: Colors.white12, 98 | ), 99 | widget.child, 100 | ], 101 | ), 102 | ), 103 | ), 104 | ), 105 | ); 106 | } 107 | 108 | //判断是否要关闭弹窗 109 | _popIfCan([double velocity = 0]) { 110 | //滑动距离阀值,或者速度阀值 111 | if (moveHeight > MAX_CLOSE_HEIGHT || velocity > MAX_CLOSE_VELOCITY) { 112 | //防止点击弹窗以外与cancel重复执行pop 113 | if (ModalRoute.of(context)?.isCurrent ?? false) 114 | Navigator.of(context).pop(); 115 | } else { 116 | //没有滑动到关闭弹窗阀值,执行归位动画。 117 | isAnimForward = true; //动画执行状态 118 | AnimationController controller = AnimationController( 119 | vsync: this, duration: Duration(milliseconds: 200)); 120 | CurvedAnimation curvedAnimation = 121 | CurvedAnimation(parent: controller, curve: Curves.easeOut); 122 | Animation animation = 123 | Tween(begin: moveHeight, end: 0).animate(curvedAnimation); 124 | controller.forward(); //执行动画 125 | controller.addListener(() { 126 | setState(() { 127 | //刷新最后一帧布局 128 | moveHeight = animation.value; 129 | }); 130 | }); 131 | controller.addStatusListener((AnimationStatus status) { 132 | if (status == AnimationStatus.completed) { 133 | setState(() { 134 | //有些时候最后一帧不是0,故将最后一帧归位 135 | moveHeight = 0; 136 | }); 137 | isAnimForward = false; //重置动画状态 138 | controller.dispose(); //释放动画 139 | } 140 | }); 141 | } 142 | } 143 | } 144 | 145 | /// 底部弹窗布局 146 | /// 主要用作约束弹窗布局,以及拖动高度变化 147 | class _BottomWindowLayout extends SingleChildLayoutDelegate { 148 | _BottomWindowLayout(this.moveHeight); 149 | 150 | final double moveHeight; 151 | 152 | @override 153 | BoxConstraints getConstraintsForChild(BoxConstraints constraints) { 154 | return new BoxConstraints( 155 | minWidth: constraints.maxWidth, 156 | maxWidth: constraints.maxWidth, 157 | minHeight: 0.0, 158 | maxHeight: constraints.maxHeight, 159 | ); 160 | } 161 | 162 | @override 163 | Offset getPositionForChild(Size size, Size childSize) { 164 | double height = size.height - childSize.height + moveHeight; 165 | return new Offset(0.0, height); 166 | } 167 | 168 | @override 169 | bool shouldRelayout(_BottomWindowLayout oldDelegate) { 170 | return moveHeight != oldDelegate.moveHeight; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /lib/src/view/popup_window.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Jiakuo Liu. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'package:flutter/material.dart'; 5 | 6 | const int _windowPopupDuration = 200; // 默认启动动画时间 7 | const Duration _kWindowDuration = Duration(milliseconds: _windowPopupDuration); 8 | 9 | /// [show]可以显示一个根据目标widget摆放的弹窗,带有透明度淡出动画 10 | /// [showBottom]可以显示一个底部弹窗,带有高度动画 11 | class PopupWindow { 12 | /// 展示一个根据参考Widget相对位置的popupWindow 13 | static show(BuildContext target, Widget window, 14 | {double elevation = 20, //高度,阴影 15 | int? duration, //启动动画时间 16 | PopupWindowAlign? alignment, //相对目标widget位置 17 | Offset offset = Offset.zero, //偏移量,为了更灵活定位 18 | Function(Object? result)? onResult, //返回值,弹窗页面pop时回传参数 19 | bool barrierDismissible = true, //点击外部区域是否可以消失 20 | Color? barrierColor //window背景颜色,一般是半透明的 21 | }) { 22 | // 参考控件的Render 23 | final RenderBox? targetRender = target.findRenderObject() as RenderBox?; 24 | // overlay管理一层层的Widget,储存了所有需要绘制的Widget 25 | // 这里可以理解为整个屏幕绘制的Box,即当前整个屏幕 26 | final RenderBox? overlay = 27 | Overlay.of(target)?.context.findRenderObject() as RenderBox?; 28 | if (targetRender != null && overlay != null) { 29 | // 获取参考widget在overlay(屏幕)中相对位置 30 | final RelativeRect position = RelativeRect.fromRect( 31 | Rect.fromPoints( 32 | targetRender.localToGlobal(Offset.zero, ancestor: overlay), 33 | targetRender.localToGlobal(targetRender.size.bottomRight(Offset.zero), 34 | ancestor: overlay), 35 | ), 36 | Offset.zero & overlay.size, 37 | ); 38 | 39 | /// 显示弹窗 40 | _showWindow( 41 | position, 42 | target, 43 | window, 44 | alignment: alignment, 45 | offset: offset, 46 | duration: duration, 47 | elevation: elevation, 48 | onResult: onResult, 49 | barrierDismissible: barrierDismissible, 50 | barrierColor: barrierColor, 51 | ); 52 | } 53 | } 54 | 55 | /// 显示底部Widget 56 | static showBottom( 57 | BuildContext context, 58 | Widget window, { 59 | double? windowHeight, 60 | Color barrierColor = Colors.black54, 61 | bool barrierDismissible = true, 62 | Function(Object? result)? onResult, 63 | }) async { 64 | Object? result = await Navigator.of(context).push(_BottomPopupWindowRoute( 65 | context, 66 | window, 67 | windowHeight, // 弹窗高度,某些情况下设置指定高度可以约束子布局的大小 68 | barrierColor, 69 | barrierDismissible)); 70 | // 页面传回数据 71 | if (onResult != null) { 72 | onResult(result); 73 | } 74 | } 75 | 76 | /// 显示底部Widget 77 | static showPopupWindowLeft( 78 | BuildContext context, 79 | WidgetBuilder windowBuilder, { 80 | double? windowWidth, 81 | Color barrierColor = Colors.black54, 82 | bool barrierDismissible = true, 83 | Function(Object? result)? onResult, 84 | }) async { 85 | Object? result = await Navigator.of(context).push(_LeftPopupWindowRoute( 86 | context, 87 | windowBuilder, 88 | windowWidth, // 弹窗高度,某些情况下设置指定高度可以约束子布局的大小 89 | barrierColor, 90 | barrierDismissible)); 91 | // 页面传回数据 92 | if (onResult != null) { 93 | onResult(result); 94 | } 95 | } 96 | 97 | /// 展示弹窗 98 | static _showWindow(RelativeRect position, BuildContext context, Widget window, 99 | {double elevation = 10, 100 | int? duration, 101 | PopupWindowAlign? alignment, 102 | Offset offset = Offset.zero, 103 | bool barrierDismissible = false, 104 | Function(Object? result)? onResult, 105 | Color? barrierColor}) async { 106 | // 启动弹窗 107 | Object? result = await Navigator.of(context).push(_PopupWindowRoute( 108 | position, 109 | window, 110 | elevation, 111 | MaterialLocalizations.of(context).modalBarrierDismissLabel, 112 | duration, 113 | alignment, 114 | offset, 115 | barrierDismissible, 116 | barrierColor)); 117 | // 返回数据 118 | if (onResult != null) { 119 | onResult(result); 120 | } 121 | } 122 | } 123 | 124 | /// PopupWindow的Route 125 | class _PopupWindowRoute extends PopupRoute { 126 | final RelativeRect position; 127 | final PopupWindowAlign? alignment; 128 | final Widget child; 129 | final double elevation; 130 | final int? duration; 131 | final Offset offset; 132 | final bool _barrierDismissible; 133 | final Color? _barrierColor; 134 | 135 | @override 136 | final String barrierLabel; 137 | 138 | _PopupWindowRoute( 139 | this.position, 140 | this.child, 141 | this.elevation, 142 | this.barrierLabel, 143 | this.duration, 144 | this.alignment, 145 | this.offset, 146 | this._barrierDismissible, 147 | this._barrierColor); 148 | 149 | // 背景颜色,默认为空 150 | // 这里可以支持使用半透明背景 151 | @override 152 | Color? get barrierColor => _barrierColor; 153 | 154 | // 点击外部区域是否可以关闭 155 | @override 156 | bool get barrierDismissible => _barrierDismissible; 157 | 158 | @override 159 | Widget buildPage(BuildContext context, Animation animation, 160 | Animation secondaryAnimation) { 161 | final CurveTween opacity = 162 | CurveTween(curve: const Interval(0.0, 1.0 / 3.0)); 163 | // CustomSingleChildLayout提供一个delegate来约束child 164 | return CustomSingleChildLayout( 165 | delegate: _PopupMenuLayout(position, alignment, offset), 166 | // 弹窗显示动画 167 | child: AnimatedBuilder( 168 | animation: animation, 169 | child: child, 170 | builder: (BuildContext context, Widget? child) { 171 | return Opacity( 172 | opacity: opacity.evaluate(animation), 173 | child: Material( 174 | // 高度,阴影效果 175 | elevation: elevation, 176 | color: Colors.transparent, 177 | child: child, 178 | ), 179 | ); 180 | }), 181 | ); 182 | } 183 | 184 | // 显示动画时长 185 | @override 186 | Duration get transitionDuration => duration == null || duration == 0 187 | ? _kWindowDuration 188 | : Duration(milliseconds: duration!); 189 | } 190 | 191 | class _PopupMenuLayout extends SingleChildLayoutDelegate { 192 | // 参考Widget的位置,一个矩形位置 193 | final RelativeRect position; 194 | 195 | // 相对参考Widget的摆放位置 196 | final PopupWindowAlign? align; 197 | 198 | // 为了更加灵活摆放,增加一个偏移量 199 | final Offset offset; 200 | 201 | _PopupMenuLayout(this.position, this.align, this.offset); 202 | 203 | @override 204 | bool shouldRelayout(_PopupMenuLayout oldDelegate) { 205 | return position != oldDelegate.position; 206 | } 207 | 208 | @override 209 | // 获取对child的盒约束 210 | BoxConstraints getConstraintsForChild(BoxConstraints constraints) { 211 | // child的布局约束条件 212 | // loose() 宽松盒约束 大小限制在给定大小范围内 213 | return BoxConstraints.loose(constraints.biggest); 214 | } 215 | 216 | @override 217 | // 获取child的位置,size是layout自己的大小,childSize是child大小 218 | Offset getPositionForChild(Size size, Size childSize) { 219 | double x = 0, y = 0; 220 | // 计算位置,提供四个方向居中,其他位置后续加 221 | if (align == null) { 222 | //默认 在控件底部左对齐 223 | x = position.left; 224 | y = size.height - position.bottom; 225 | } else { 226 | switch (align) { 227 | case PopupWindowAlign.centerRight: 228 | //centerRight 229 | x = size.width - position.right; 230 | y = (position.top + size.height - position.bottom) / 2 - 231 | childSize.height / 2; 232 | break; 233 | case PopupWindowAlign.topCenter: 234 | //topCenter 235 | x = (position.left + size.width - position.right) / 2 - 236 | childSize.width / 2; 237 | y = position.top - childSize.height; 238 | break; 239 | case PopupWindowAlign.centerLeft: 240 | //centerLeft 241 | x = position.left - childSize.width; 242 | y = (position.top + size.height - position.bottom) / 2 - 243 | childSize.height / 2; 244 | break; 245 | case PopupWindowAlign.bottomCenter: 246 | //bottomCenter 247 | x = (position.left + size.width - position.right) / 2 - 248 | childSize.width / 2; 249 | y = size.height - position.bottom; 250 | break; 251 | } 252 | } 253 | 254 | /// 偏移量,以便于更灵活定位 255 | x += offset.dx; 256 | y += offset.dy; 257 | if (x + childSize.width > size.width) { 258 | x = size.width - childSize.width; 259 | } 260 | if (y + childSize.height > size.height) { 261 | y = size.height - childSize.height; 262 | } 263 | // child左上角相对 264 | return Offset(x, y); 265 | } 266 | } 267 | 268 | /// 弹窗和目标控件的位置关系 269 | class PopupWindowAlign { 270 | final int type; 271 | 272 | const PopupWindowAlign(this.type); 273 | 274 | static const PopupWindowAlign centerRight = const PopupWindowAlign(0); 275 | static const PopupWindowAlign topCenter = const PopupWindowAlign(1); 276 | static const PopupWindowAlign centerLeft = const PopupWindowAlign(2); 277 | static const PopupWindowAlign bottomCenter = const PopupWindowAlign(3); 278 | } 279 | 280 | /// 底部popupWindow的Route 281 | class _BottomPopupWindowRoute extends PopupRoute { 282 | final BuildContext context; 283 | final Widget window; 284 | final double? windowHeight; 285 | final Color _barrierColor; 286 | final bool _barrierDismissible; 287 | 288 | _BottomPopupWindowRoute(this.context, this.window, this.windowHeight, 289 | this._barrierColor, this._barrierDismissible); 290 | 291 | @override 292 | Duration get transitionDuration => const Duration(milliseconds: 200); 293 | 294 | @override 295 | bool get barrierDismissible => _barrierDismissible; 296 | 297 | @override 298 | Color get barrierColor => _barrierColor; 299 | 300 | @override 301 | Widget buildPage(BuildContext context, Animation animation, 302 | Animation secondaryAnimation) { 303 | Widget bottomWindow = new MediaQuery.removePadding( 304 | context: context, 305 | removeTop: true, 306 | child: AnimatedBuilder( 307 | animation: animation, 308 | builder: (BuildContext context, Widget? child) { 309 | return ClipRect( 310 | child: CustomSingleChildLayout( 311 | delegate: _BottomPopupWindowLayout(animation.value, 312 | contentHeight: windowHeight), 313 | child: window, 314 | ), 315 | ); 316 | }, 317 | ), 318 | ); 319 | 320 | return bottomWindow; 321 | } 322 | 323 | @override 324 | String get barrierLabel => 325 | MaterialLocalizations.of(context).modalBarrierDismissLabel; 326 | } 327 | 328 | /// 左侧popupWindow的Route 329 | class _LeftPopupWindowRoute extends PopupRoute { 330 | final BuildContext context; 331 | final WidgetBuilder windowBuilder; 332 | final double? windowWidth; 333 | final Color _barrierColor; 334 | final bool _barrierDismissible; 335 | 336 | _LeftPopupWindowRoute(this.context, this.windowBuilder, this.windowWidth, 337 | this._barrierColor, this._barrierDismissible); 338 | 339 | @override 340 | Duration get transitionDuration => const Duration(milliseconds: 200); 341 | 342 | @override 343 | bool get barrierDismissible => _barrierDismissible; 344 | 345 | @override 346 | Color get barrierColor => _barrierColor; 347 | 348 | @override 349 | Widget buildPage(BuildContext context, Animation animation, 350 | Animation secondaryAnimation) { 351 | Widget bottomWindow = new MediaQuery.removePadding( 352 | context: context, 353 | removeTop: true, 354 | child: AnimatedBuilder( 355 | animation: animation, 356 | builder: (BuildContext context, Widget? child) { 357 | return ClipRect( 358 | child: CustomSingleChildLayout( 359 | delegate: _LeftPopupWindowLayout(animation.value, 360 | contentWidth: windowWidth), 361 | child: windowBuilder(context), 362 | ), 363 | ); 364 | }, 365 | ), 366 | ); 367 | 368 | return bottomWindow; 369 | } 370 | 371 | @override 372 | String get barrierLabel => 373 | MaterialLocalizations.of(context).modalBarrierDismissLabel; 374 | } 375 | 376 | abstract class _PopupWindowLayout extends SingleChildLayoutDelegate { 377 | final double progress; 378 | 379 | _PopupWindowLayout(this.progress); 380 | 381 | @override 382 | bool shouldRelayout(_PopupWindowLayout oldDelegate) { 383 | return progress != oldDelegate.progress; 384 | } 385 | } 386 | 387 | /// 底部弹窗布局 388 | class _BottomPopupWindowLayout extends _PopupWindowLayout { 389 | _BottomPopupWindowLayout(double progress, {this.contentHeight}) 390 | : super(progress); 391 | 392 | final double? contentHeight; 393 | 394 | @override 395 | BoxConstraints getConstraintsForChild(BoxConstraints constraints) { 396 | return new BoxConstraints( 397 | minWidth: constraints.maxWidth, 398 | maxWidth: constraints.maxWidth, 399 | minHeight: 0.0, 400 | // 当指定高度时设置指定高度,没有指定高度则对最大高度不加限制 401 | maxHeight: contentHeight ?? constraints.maxHeight, 402 | ); 403 | } 404 | 405 | @override 406 | Offset getPositionForChild(Size size, Size childSize) { 407 | // 计算动画过程中的高度 408 | double height = size.height - childSize.height * progress; 409 | return new Offset(0.0, height); 410 | } 411 | } 412 | 413 | /// 底部弹窗布局 414 | class _LeftPopupWindowLayout extends _PopupWindowLayout { 415 | _LeftPopupWindowLayout(double progress, {this.contentWidth}) 416 | : super(progress); 417 | 418 | final double? contentWidth; 419 | 420 | @override 421 | BoxConstraints getConstraintsForChild(BoxConstraints constraints) { 422 | return new BoxConstraints( 423 | minWidth: 0, 424 | maxWidth: contentWidth ?? constraints.maxWidth, 425 | minHeight: constraints.maxHeight, 426 | // 当指定高度时设置指定高度,没有指定高度则对最大高度不加限制 427 | maxHeight: constraints.maxHeight, 428 | ); 429 | } 430 | 431 | @override 432 | Offset getPositionForChild(Size size, Size childSize) { 433 | // 计算动画过程中的宽 434 | double width = childSize.width * (progress - 1); 435 | return new Offset(width, 0.0); 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /lib/src/vm_service_utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Jiakuo Liu. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:developer'; 5 | import 'dart:io'; 6 | 7 | import 'package:vm_service/vm_service.dart'; 8 | import 'package:vm_service/vm_service_io.dart'; 9 | import 'package:vm_service/utils.dart'; 10 | 11 | const String _findLibrary = 'package:leak_detector/src/vm_service_utils.dart'; 12 | 13 | ///VmServer api tools 14 | class VmServerUtils { 15 | static VmServerUtils? _instance; 16 | bool _enable = false; 17 | Uri? _observatoryUri; 18 | VmService? _vmService; 19 | VM? _vm; 20 | 21 | Future get hasVmService async => (await getVmService()) != null; 22 | 23 | factory VmServerUtils() { 24 | _instance ??= VmServerUtils._(); 25 | return _instance!; 26 | } 27 | 28 | bool get isEnable => _enable; 29 | 30 | VmServerUtils._() { 31 | //init 32 | assert(() { 33 | _enable = true; 34 | return true; 35 | }()); 36 | } 37 | 38 | ///get VmService's WebSocket uri 39 | Future getObservatoryUri() async { 40 | if (_enable) { 41 | // _observatoryUri = await _channel.invokeMethod('getObservatoryUri'); 42 | ServiceProtocolInfo serviceProtocolInfo = await Service.getInfo(); 43 | _observatoryUri = serviceProtocolInfo.serverUri; 44 | } 45 | return _observatoryUri; 46 | } 47 | 48 | ///VmService 49 | Future getVmService() async { 50 | if (_vmService == null) { 51 | final uri = await getObservatoryUri(); 52 | if (uri != null) { 53 | Uri url = convertToWebSocketUrl(serviceProtocolUrl: uri); 54 | try { 55 | _vmService = await vmServiceConnectUri(url.toString()); 56 | } catch (error) { 57 | if (error is SocketException) { 58 | //dds is enable 59 | print('vm_service connection refused, Try:'); 60 | print('run \'flutter run\' with --disable-dds to disable dds.'); 61 | } 62 | } 63 | } 64 | } 65 | return _vmService; 66 | } 67 | 68 | Future getVM() async { 69 | if (_vm == null) { 70 | _vm = await (await getVmService())?.getVM(); 71 | } 72 | return _vm; 73 | } 74 | 75 | ///find a [Library] on [Isolate] 76 | Future findLibrary(String uri) async { 77 | Isolate? mainIsolate = await findMainIsolate(); 78 | if (mainIsolate != null) { 79 | final libraries = mainIsolate.libraries; 80 | if (libraries != null) { 81 | for (int i = 0; i < libraries.length; i++) { 82 | var lib = libraries[i]; 83 | if (lib.uri == uri) { 84 | return lib; 85 | } 86 | } 87 | } 88 | } 89 | return null; 90 | } 91 | 92 | ///find main Isolate in VM 93 | Future findMainIsolate() async { 94 | IsolateRef? ref; 95 | final vm = await getVM(); 96 | if (vm == null) return null; 97 | vm.isolates?.forEach((isolate) { 98 | if (isolate.name == 'main') { 99 | ref = isolate; 100 | } 101 | }); 102 | final vms = await getVmService(); 103 | if (ref?.id != null) { 104 | return vms?.getIsolate(ref!.id!); 105 | } 106 | return null; 107 | } 108 | 109 | ///get ObjectId in VM by Object 110 | Future getObjectId(dynamic obj) async { 111 | final library = await findLibrary(_findLibrary); 112 | if (library == null || library.id == null) return null; 113 | final vms = await getVmService(); 114 | if (vms == null) return null; 115 | final mainIsolate = await findMainIsolate(); 116 | if (mainIsolate == null || mainIsolate.id == null) return null; 117 | Response keyResponse = 118 | await vms.invoke(mainIsolate.id!, library.id!, 'generateNewKey', []); 119 | final keyRef = InstanceRef.parse(keyResponse.json); 120 | String? key = keyRef?.valueAsString; 121 | if (key == null) return null; 122 | _objCache[key] = obj; 123 | 124 | try { 125 | Response valueResponse = await vms 126 | .invoke(mainIsolate.id!, library.id!, "keyToObj", [keyRef!.id!]); 127 | final valueRef = InstanceRef.parse(valueResponse.json); 128 | return valueRef?.id; 129 | } catch (e) { 130 | print('getObjectId $e'); 131 | } finally { 132 | _objCache.remove(key); 133 | } 134 | return null; 135 | } 136 | 137 | ///[VmService.invokeMethod] 138 | Future invokeMethod( 139 | String targetId, String method, List argumentIds) async { 140 | final vms = await getVmService(); 141 | if (vms == null) return null; 142 | final mainIsolate = await findMainIsolate(); 143 | if (mainIsolate != null && mainIsolate.id != null) { 144 | try { 145 | Response valueResponse = 146 | await vms.invoke(mainIsolate.id!, targetId, method, argumentIds); 147 | final valueRef = InstanceRef.parse(valueResponse.json); 148 | return valueRef?.valueAsString; 149 | } catch (e) {} 150 | } 151 | return null; 152 | } 153 | 154 | ///通过ObjectId获取Instance 155 | Future getObjectInstanceById(String objId) async { 156 | final vms = await getVmService(); 157 | if (vms == null) return null; 158 | final mainIsolate = await findMainIsolate(); 159 | if (mainIsolate != null && mainIsolate.id != null) { 160 | try { 161 | Obj object = await vms.getObject(mainIsolate.id!, objId); 162 | return object; 163 | } catch (e) { 164 | print('getObjectInstanceById error:$e'); 165 | } 166 | } 167 | return null; 168 | } 169 | 170 | ///通过Object获取Instance 171 | Future getInstanceByObject(dynamic obj) async { 172 | final vms = await getVmService(); 173 | if (vms == null) return null; 174 | final mainIsolate = await findMainIsolate(); 175 | if (mainIsolate != null && mainIsolate.id != null) { 176 | try { 177 | final objId = await getObjectId(obj); 178 | if (objId != null) { 179 | Obj object = await vms.getObject(mainIsolate.id!, objId); 180 | final instance = Instance.parse(object.json); 181 | return instance; 182 | } 183 | } catch (e) { 184 | print('getInstanceByObject error:$e'); 185 | } 186 | } 187 | return null; 188 | } 189 | 190 | ///[VmService.getRetainingPath] 191 | Future getRetainingPath(String objId, int limit) async { 192 | final vms = await getVmService(); 193 | if (vms == null) return null; 194 | final mainIsolate = await findMainIsolate(); 195 | if (mainIsolate != null && mainIsolate.id != null) { 196 | return vms.getRetainingPath(mainIsolate.id!, objId, limit); 197 | } 198 | return null; 199 | } 200 | 201 | ///start full gc 202 | Future startGCAsync() async { 203 | final vms = await getVmService(); 204 | if (vms == null) return null; 205 | final isolate = await findMainIsolate(); 206 | if (isolate != null && isolate.id != null) { 207 | await vms.getAllocationProfile(isolate.id!, gc: true); 208 | } 209 | } 210 | } 211 | 212 | int _key = 0; 213 | 214 | /// 顶级函数,必须常规方法,生成 key 用 215 | String generateNewKey() { 216 | return "${++_key}"; 217 | } 218 | 219 | Map _objCache = Map(); 220 | 221 | /// 顶级函数,根据 key 返回指定对象 222 | dynamic keyToObj(String key) { 223 | return _objCache[key]; 224 | } 225 | 226 | extension MyInstance on Instance { 227 | BoundField? getField(String name) { 228 | if (fields == null) return null; 229 | for (int i = 0; i < fields!.length; i++) { 230 | var field = fields![i]; 231 | if (field.decl?.name == name) { 232 | return field; 233 | } 234 | } 235 | return null; 236 | } 237 | 238 | dynamic getFieldValueInstance(String name) { 239 | final field = getField(name); 240 | if (field != null) { 241 | return field.value; 242 | } 243 | return null; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /macos/Classes/LeakDetectorPlugin.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | public class LeakDetectorPlugin: NSObject, FlutterPlugin { 5 | public static func register(with registrar: FlutterPluginRegistrar) { 6 | let channel = FlutterMethodChannel(name: "leak_detector", binaryMessenger: registrar.messenger) 7 | let instance = LeakDetectorPlugin() 8 | registrar.addMethodCallDelegate(instance, channel: channel) 9 | } 10 | 11 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 12 | switch call.method { 13 | case "getPlatformVersion": 14 | result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString) 15 | default: 16 | result(FlutterMethodNotImplemented) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /macos/leak_detector.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint leak_detector.podspec` to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'leak_detector' 7 | s.version = '0.0.1' 8 | s.summary = 'A new flutter plugin project.' 9 | s.description = <<-DESC 10 | A new flutter plugin project. 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*' 17 | s.dependency 'FlutterMacOS' 18 | 19 | s.platform = :osx, '10.11' 20 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } 21 | s.swift_version = '5.0' 22 | end 23 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.9.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.2.1" 25 | clock: 26 | dependency: transitive 27 | description: 28 | name: clock 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.1.1" 32 | collection: 33 | dependency: transitive 34 | description: 35 | name: collection 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.16.0" 39 | fake_async: 40 | dependency: transitive 41 | description: 42 | name: fake_async 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.3.1" 46 | flutter: 47 | dependency: "direct main" 48 | description: flutter 49 | source: sdk 50 | version: "0.0.0" 51 | flutter_test: 52 | dependency: "direct dev" 53 | description: flutter 54 | source: sdk 55 | version: "0.0.0" 56 | matcher: 57 | dependency: transitive 58 | description: 59 | name: matcher 60 | url: "https://pub.dartlang.org" 61 | source: hosted 62 | version: "0.12.12" 63 | material_color_utilities: 64 | dependency: transitive 65 | description: 66 | name: material_color_utilities 67 | url: "https://pub.dartlang.org" 68 | source: hosted 69 | version: "0.1.5" 70 | meta: 71 | dependency: transitive 72 | description: 73 | name: meta 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "1.8.0" 77 | path: 78 | dependency: "direct main" 79 | description: 80 | name: path 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "1.8.2" 84 | sky_engine: 85 | dependency: transitive 86 | description: flutter 87 | source: sdk 88 | version: "0.0.99" 89 | source_span: 90 | dependency: transitive 91 | description: 92 | name: source_span 93 | url: "https://pub.dartlang.org" 94 | source: hosted 95 | version: "1.9.0" 96 | sqflite: 97 | dependency: "direct main" 98 | description: 99 | name: sqflite 100 | url: "https://pub.dartlang.org" 101 | source: hosted 102 | version: "2.2.6" 103 | sqflite_common: 104 | dependency: transitive 105 | description: 106 | name: sqflite_common 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "2.4.3" 110 | stack_trace: 111 | dependency: transitive 112 | description: 113 | name: stack_trace 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "1.10.0" 117 | stream_channel: 118 | dependency: transitive 119 | description: 120 | name: stream_channel 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "2.1.0" 124 | string_scanner: 125 | dependency: transitive 126 | description: 127 | name: string_scanner 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.1.1" 131 | synchronized: 132 | dependency: transitive 133 | description: 134 | name: synchronized 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "3.0.1" 138 | term_glyph: 139 | dependency: transitive 140 | description: 141 | name: term_glyph 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "1.2.1" 145 | test_api: 146 | dependency: transitive 147 | description: 148 | name: test_api 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "0.4.12" 152 | vector_math: 153 | dependency: transitive 154 | description: 155 | name: vector_math 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "2.1.2" 159 | vm_service: 160 | dependency: "direct main" 161 | description: 162 | name: vm_service 163 | url: "https://pub.dartlang.org" 164 | source: hosted 165 | version: "11.2.0" 166 | sdks: 167 | dart: ">=2.18.0 <3.0.0" 168 | flutter: ">=3.3.0" 169 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: leak_detector 2 | description: Used to detect page memory leaks. You can use it to detect whether there is a memory leak in `Widget`, `Element` and `State`. 3 | version: 1.1.0 4 | author: Liu 5 | repository: https://github.com/liujiakuoyx/leak_detector.git 6 | 7 | environment: 8 | sdk: ">=2.18.0 <4.0.0" 9 | flutter: ">=3.3.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | sqflite: ^2.2.6 15 | path: ^1.8.2 16 | vm_service: ^11.2.0 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | 22 | # For information on the generic Dart part of this file, see the 23 | # following page: https://dart.dev/tools/pub/pubspec 24 | 25 | # The following section is specific to Flutter. 26 | flutter: 27 | # This section identifies this Flutter project as a plugin project. 28 | # The 'pluginClass' and Android 'package' identifiers should not ordinarily 29 | # be modified. They are used by the tooling to maintain consistency when 30 | # adding or updating assets for this project. 31 | plugin: 32 | platforms: 33 | android: 34 | package: com.ljk.leak_detector 35 | pluginClass: LeakDetectorPlugin 36 | ios: 37 | pluginClass: LeakDetectorPlugin 38 | macos: 39 | pluginClass: LeakDetectorPlugin 40 | 41 | # To add assets to your plugin package, add an assets section, like this: 42 | # assets: 43 | # - images/a_dot_burr.jpeg 44 | # - images/a_dot_ham.jpeg 45 | # 46 | # For details regarding assets in packages, see 47 | # https://flutter.dev/assets-and-images/#from-packages 48 | # 49 | # An image asset can refer to one or more resolution-specific "variants", see 50 | # https://flutter.dev/assets-and-images/#resolution-aware. 51 | 52 | # To add custom fonts to your plugin package, add a fonts section here, 53 | # in this "flutter" section. Each entry in this list should have a 54 | # "family" key with the font family name, and a "fonts" key with a 55 | # list giving the asset and other descriptors for the font. For 56 | # example: 57 | # fonts: 58 | # - family: Schyler 59 | # fonts: 60 | # - asset: fonts/Schyler-Regular.ttf 61 | # - asset: fonts/Schyler-Italic.ttf 62 | # style: italic 63 | # - family: Trajan Pro 64 | # fonts: 65 | # - asset: fonts/TrajanPro.ttf 66 | # - asset: fonts/TrajanPro_Bold.ttf 67 | # weight: 700 68 | # 69 | # For details regarding fonts in packages, see 70 | # https://flutter.dev/custom-fonts/#from-packages 71 | -------------------------------------------------------------------------------- /test/leak_detector_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:vm_service/utils.dart'; 5 | import 'package:vm_service/vm_service.dart'; 6 | import 'package:vm_service/vm_service_io.dart'; 7 | 8 | void main() async { 9 | TestWidgetsFlutterBinding.ensureInitialized(); 10 | ServiceProtocolInfo info = await Service.getInfo(); 11 | var serverUri = info.serverUri; 12 | if (serverUri != null) { 13 | VmService vmService = await vmServiceConnectUri( 14 | convertToWebSocketUrl(serviceProtocolUrl: serverUri).toString()); 15 | print('success ${(await vmService.getVM()).version}'); 16 | } 17 | } 18 | --------------------------------------------------------------------------------