├── .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 | 
102 |
103 |
104 | 
--------------------------------------------------------------------------------
/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 | 
106 |
107 |
108 | 
--------------------------------------------------------------------------------
/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 |
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