分析步骤
分析内存泄漏问题步骤如下:
- 在内存泄漏前拍摄快照;
- 触发内存泄漏操作后,再次拍摄快照;
- 对比两次快照的数据,可快速找到泄漏对象并做进一步分析;
- 当有多个对象在比较视图都存在时,可以重复多次步骤2的操作,分别和未进行操作时对比,观察是否有对象出现明显的线性变化趋势,进一步缩小泄漏对象的范围。
录制Snapshot模板数据
-
连接好设备后启动应用,点击应用选择框(下图中①处)选择需要录制的应用,选择Snapshot模板(下图中②处),点击Create Session或双击Snapshot图标即可创建一个Snapshot的录制模板。
-
创建好模板后,点击三角按钮即开始录制。
-
待右侧泳道全部显示recording后则表明正在录制中,此时点击下图中方块按钮或者左侧暂停按钮都可结束录制。
-
拍摄快照:开始录制后,待右侧泳道全部显示recording后点击图中①处拍摄按钮,待②处显示出紫色条块表示快照拍摄完成。
-
录制完成后可点击下图①处按钮将录制文件导出,而点击下图②处的按钮即可导入之前录制好的导出件。
分析Snapshot数据
常见对象介绍
JSArray
目前所有JSArray展开后为数组里的各个元素:
其中_proto_:原型对象,所有数组的_proto_应该是一致的;length:内置属性访问器,可以访问数组长度。
TaggedDict
位于(array)标签中,一般为虚拟机内部创建的字典,ArkTS代码层面不可见。
TaggedArray
位于(array)标签中,一般为虚拟机内部创建的数组,ArkTS代码层面不可见。
COWArray
位于(array)标签中,一般为虚拟机内部创建的数组,ArkTS代码层面不可见。
JSObject
JSObject展开后为内部的各个属性如下:
以下通过具体代码来介绍下实例化对象、声明对象、构造函数间的关系:
class People {
old: number
name: string
constructor(old: number, name: string) {
this.old = old;
this.name = name;
}
printOld() {
console.log("old = ", this.old);
}
printName() {
console.log("name = ", this.name);
}
};
let p = new People(20, "Tom");
采集到的snapshot数据如下:
92729对象对应的是People,其主要声明了对象的属性和方法。
实例化对象的_proto_属性指向声明时的对象,声明对象里则会有constructor构造函数。当实例化多个对象时,实例化对象会有多个,但是声明对象和构造函数只有一个。
JSFunction
目前所有JSFunction都在(closure)标签中,展开即可看到所有JSFunction:
每个函数展开后为函数内的各个属性:
其中HomeObject表示父类对象,即该方法属于哪个对象;_proto_表示原型对象;LexicalEnv表示该函数的闭包上下文;name是内置属性访问器,可获取函数名;FunctionExtraInfo表示额外信息,比如一些napi接口会在这里记录函数地址;ProtoOrHClass表示原型或者隐藏类。
如果函数显示为anonymous(),则表示为匿名函数;如果函数显示为JSFunction(),则表示该函数可能为框架层函数,创建函数的时候未设置函数名。对于这两种函数名不可见的情况,可以通过查看其引用来间接确认其名称:
ArkInternalConstantPool
虚拟机创建的常量池,ArkTS代码层面不可见,涉及到的字符串常量会在(array)标签中展示:
LexicalEnv
闭包变量上下文;闭包是一个链状结构,如下所示:
733这个节点本身是一个闭包数组,其中0号元素是调用者(或者再往上的调用者,以此类推)的闭包;1号元素存储的是调试信息;2号及以后的元素存储的就是闭包传递的变量,上例传递了一个变量。
InternalAccessor
内置属性访问器,会有getter和setter方法,通过getter、setter可以获取、设置该属性。
分析方法
查看对象名称
对于声明对象,可以通过constructor属性来确定对象名称。
对于实例化对象,一般没有constructor,则需要展开_proto_属性后查找constructor;
若对象里有一些标志性属性,可以通过在代码里搜索属性名称来找到具体是哪个对象。
如果对象间有继承关系,则可以继续展开_proto_:
如上图则表明Man对象继承自People对象。