在Android中可以通过 leakcanary来检测Activity/Fragment中是否存在内存泄漏,而Flutter中并无类似的开源工具。虽然也可以用Observatory来查看是否存在内存泄漏,但比较繁琐,所以需要一个可以快速检测内存泄漏的工具。
根据Flutter 上的内存泄漏监控一文,笔者实现了一个简单的内存泄漏检测功能来供大家参考。
1、弱引用及gc的实现
要很好的进行内存泄漏检测,主要在于以下两点。
- 弱引用
- 强制gc
1.1、弱引用
在Dart中虽不存在类似Java中WeakReference的类,但还是存在弱引用的。_WeakProperty就是Dart中的弱引用,它是以key-value来保存值。
@pragma("vm:entry-point")
class _WeakProperty {
factory _WeakProperty(key, value) => _new(key, value);
get key => _getKey();
get value => _getValue();
set value(value) => _setValue(value);
static _WeakProperty _new(key, value) native "WeakProperty_new";
_getKey() native "WeakProperty_getKey";
_getValue() native "WeakProperty_getValue";
_setValue(value) native "WeakProperty_setValue";
}
很明显,它是一个私有类,无法直接使用它。那么如何使用它尼?通过Expando即可。它的实现在expando_patch.dart中,如下。
@patch
class Expando<T> {
@patch
Expando([String name])
: name = name,
_data = new List(_minSize),
_used = 0;
//...
@patch
void operator []=(Object object, T value) {
//...
//根据object的哈希码及mask计算出idx
var idx = object._identityHashCode & mask;
//...
if (_used < _limit) {
// 创建一个_WeakProperty对象并添加到集合_data中
_data[idx] = new _WeakProperty(object, value);
_used++;
return;
}
// 对集合_data进行扩容
_rehash();
this[object] = value;
}
//...
}
再来看Expando的使用,很简单,直接创建一个Expando对象并按照key-value的使用方式即可,以源码MethodChannel中的Expando使用为例。
Expando<Object> _methodChannelHandlers = Expando<Object>();
class MethodChannel {
void setMethodCallHandler(Future<dynamic> Function(MethodCall call)? handler) {
// 当前MethodChannel对象为key,handler为value
_methodChannelHandlers[this] = handler;
//...
}
// 通过当前对象从Expando中拿到handler
bool checkMethodCallHandler(Future<dynamic> Function(MethodCall call)? handler) => _methodChannelHandlers[this] == handler;
//...
}
1.2、强制gc
先来了解一下Dart的gc机制。Dart中是采用可达性分析算法来判断对象是否存活的,也实现了标记清除算法、复制算法及标记整理算法来进行对象的回收。在新生代,from与to是相等的两块空间,采用复制算法来进行gc;在老年代,默认是采用标记清除算法来进行回收,当低内存时,采用的是标记整理算法来进行gc。
虽然强制gc在Dart中是无法实现的,但可以借助vm_service,通过其中的getAllocationProfile来进行强制gc。使用方式如下。
_vmService.getAllocationProfile(findCurrentIsolateId(), gc: true);
在Java中无法对gc完成进行监听。但借助vm_service,Dart可以来监听gc是否完成。
GcTrigger.init(VmService vmService) {
_vmService = vmService;
_vmService.streamListen("GC").then((event) {
debugPrint("GC监听注册完毕");
});
//监听gc事件流
_vmService.onGCEvent
.listen(onData, onDone: onDone, onError: onError, cancelOnError: false);
}
2、内存泄漏检测的实现
有了弱引用及主动gc的方法,那么就可以来实现内存泄漏的检测。由于需要借助vm_service。所以需要先导入该包。
2.1、与vm_service建立连接
根据类Service的getInfo方法,可以很快速的获取建立连接所需要的路径。由于是通过Socket来建立连接的,所以需要将路径转成符合socket格式,然后将路径传给vmServiceConnectUri方法即可成功的建立连接。代码如下。
void install() {
const bool inProduction = const bool.fromEnvironment("dart.vm.product");
//如果是生产环境,直接返回
if (inProduction) return;
//获取service协议信息
Service.getInfo().then((serviceProtocolInfo) {
//获取到连接路径
Uri uri = serviceProtocolInfo.serverUri;
//转换成ws//...格式
String url = convertToWebSocketUrl(serviceProtocolUrl: uri).toString();
vmServiceConnectUri(url).then((vmService) async {
//拿到vmService对象
//...
});
});
}
2.2、弱引用的使用
弱引用的使用就很简单了,创建一个对象。并要检测对象最为key传递给Expando。代码如下。
Expando _state = Expando("_state");
void addWatch(State state) {
if (_state == null) _state = Expando();
//state是要检测的对象,这里是页面的State对象
_state[state] = true;
//...
}
2.3、强制gc
强制gc就很简单了,直接调用vmService的getAllocationProfile方法即可。代码如下。
//第二个参数必须设为true,否则不会进行gc
_vmService.getAllocationProfile(findCurrentIsolateId(), gc: true);
2.4、泄漏路径的获取
要判断对象是否被回收,需要拿到_WeakProperty实例并判断其中的key是否为null。由于无法使用反射,所以无法通过反射来拿到_WeakProperty实例并判断其中的key。好在vm_service提供了getObject方法来获取一个实例,通过该方法就可以拿到Expando中的_WeakProperty实例。
void afterGc(Expando expando) async {
BoundField _dataField;
//根据expando来得到Instance
Instance instance = await getObject(expando);
//释放引用
expando = null;
for (BoundField field in instance.fields) {
//判断属性名称是否是_data
if (field.decl.name == "_data") {
_dataField = field;
break;
}
}
//拿到Expando中_data属性的引用
InstanceRef instanceRef = _dataField.value;
//拿到Expando中_data属性的实例
instance =
await _vmService.getObject(findCurrentIsolateId(), instanceRef.id);
List<dynamic> instanceRefs = instance.elements;
//由于_data是一个集合,所以可以遍历该集合从而拿到_WeakProperty的引用
for (InstanceRef instanceRef in instanceRefs) {
//_WeakProperty的引用
if (instanceRef != null) {
//...
}
}
}
//根据expando对象来拿到该对象的id
Future<String> getObjectId(Expando expando) async {
return await obj2Id(_vmService, _libraryId, expando);
}
//拿到expando对于的Instance
Future<Instance> getObject(Expando expando) async {
String objectId = await getObjectId(expando);
return await _vmService.getObject(findCurrentIsolateId(), objectId);
}
//获取当前isolate的id
String findCurrentIsolateId() {
return Service.getIsolateID(dart_isolate.Isolate.current);
}
obj2Id方法的实现来自于Flutter 上的内存泄漏监控中。
拿到_WeakProperty的引用后,又可以通过getObject方法来拿到_WeakProperty实例。由于Instance中的propertyKey属性就对应着_WeakProperty中的key。所以可以通过propertyKey来判断是否回收了对象。
void afterGc(Expando expando) async {
//...
for (InstanceRef instanceRef in instanceRefs) {
if (instanceRef != null) {
print("--------------------------------------------------------------");
//存在内存泄漏
_vmService
.getObject(findCurrentIsolateId(), instanceRef.id)
.then((obj) {
、 if (obj is Instance) {
Instance instance = obj;
InstanceRef instanceRef = instance.propertyKey;
//instanceRef不为null,表示存在内存泄漏
if (instanceRef != null) {
//获取泄漏路径,第三个参数是最大路径数量
_vmService
.getRetainingPath(
findCurrentIsolateId(), instanceRef.id, 1000)
.then((path) async {
//elements表示泄漏路径中的每个对象
for (RetainingObject retainingObject in path.elements) {
debugPrint("retainingObject:$retainingObject");
}
});
} else {
print("obj is not Instance,Instance:$Instance");
}
} else {...}
}).catchError((error) {
print("Gc回收后获取内存泄漏对象出错:$error");
});
}
}
}
如果存在内存泄漏,那么通过getRetainingPath就可以拿到泄漏的内存路径,然后进行分析是哪里产生了内存泄漏。
3、总结
通过上面代码就可以实现内存泄漏检测功能,虽简单,但核心功能还是在的。当然也存在很多的问题,也是笔者目前遇到的问题。如下。
- 由于没有监听每个页面
State的方法,所以需要在每个页面State的dispose的方法手动添加对当前State对象的监听,这样就太繁琐了。 - 泄漏的路径未进行二次处理,从而导致目前来分析泄漏路径是比较繁琐的。
待上面问题解决后,一个成熟的Flutter内存泄漏检测工具才算完成。
【参考资料】