剖析思路
内存走漏是指 Android 进程中,某些目标现已不再运用,但被一些生命周期更长的目标引证,导致其占用的内存资源无法被GC收回,内存占用不断添加的一种现象;内存走漏是导致咱们运用性能下降、卡顿的一种常见要素,解决此类问题最中心的思路能够总结为以下两步:
- 模拟内存走漏的操作路径,调查运用 Heap 内存变化,确定呈现问题的大概方位;
- 针对详细方位翻开剖析,找到走漏目标指向 GC Root 的完好引证链,从源头治理内存走漏。
剖析东西:Android Stuido Profiler
Profiler 中常用到的内存剖析的东西有两个:内存曲线图和 Heap Dump;内存曲线能够实时调查内存运用状况,协助咱们进行内存的动态剖析;
内存走漏呈现时,内存曲线典型的现象便是呈现阶梯状,一旦上升则难以下降;例如 Activity 走漏后,重复翻开、封闭页面内存占用会一路上升,而且点击垃圾桶图标手动GC后,占用量无法下降到翻开 Activity 之前的水平,这时大概率呈现内存走漏了。
这时,咱们能够手动 dump 此时间运用堆内存中的内存散布状况,用作静态剖析:
UI中的各项目标说明:
-
Allocations
:堆内存中该类的实例个数; -
Native Size
:该类一切实例引证到的Native目标所占内存 -
Shallow Size
:该类一切实例本身的实际内存占用大小,不包括其所引证到的目标的内存占用大小; -
Retained Size
:与Shallow Size
不同,这个数字代表该类一切实例及其一切引证到的目标的内存占用大小;
凭借一张图,能够对这几个特点有更直观的印象:
如上图,红点的内存大小代表 Shallow Size
,蓝点为 Native Size
,一切橙色点的内存大小则为 Retained Size
;当呈现内存走漏时,咱们更应该重视 Retained Size
这个数字,它的意义是,因内存走漏导致 Java 堆内存中所糟蹋的内存空间大小。 因为内存走漏往往会构成“链式效应”,从走漏的目标动身,该目标引证的一切目标和 Native 资源都无法收回,形成内存运用效率的下降。
别的 Leaks
代表或许的内存走漏实例数量;点击列表中的类能够查看该类的实例详情;Instance 列表中的 depth
代表该实例抵达 GC Root
的最短调用链深度,在图1右侧 Reference
一栏仓库中能够直观地看到完好调用链,这时就能够一路追溯找出最可疑的引证,结合代码剖析走漏原因,并对症下药,根治问题。
接下来剖析几个咱们在项目中遇到一部分典型内存走漏的事例:
事例剖析
事例1:BitmapBinder 内存走漏
在触及跨进程传输 Bitmap 的场景时,咱们采用了一种 BitmapBinder
的办法;因为 Intent 支持咱们传入自定义的 Binder,因而能够凭借 Binder 完成 Intent 传输 Bitmap 目标:
// IBitmapBinder AIDL文件
import android.graphics.Bitmap;
interface IBitmapInterface {
Bitmap getIntentBitmap();
}
但是,Activity1
在运用 BitmapBinder
向 Activity2
传递 Bitmap 后,呈现了两个严重的内存走漏问题:
- 跳转后再回来,
Activity1
finish 时无法收回; - 重复跳转时,
Bitmap
和Binder
目标会重复创立且无法收回;
先剖析 Heap Dump:
这是一个『多实例』内存走漏,即每次 finish Activity1
再翻开,都会添加一个 Activity 目标留在 Heap 中,无法毁掉;常见于内部类引证、静态数组引证(如监听器列表)等场景;根据 Profiler 供给的引证链,咱们找到了 BitmapExt
这个类:
suspend fun Activity.startActivity2WithBitmap() {
val screenShotBitmap = withContext(Dispatchers.IO) {
SDKDeviceHelper.screenShot()
} ?: return
startActivity(Intent().apply {
val bundle = Bundle()
bundle.putBinder(KEY_SCREENSHOT_BINDER, object : IBitmapInterface.Stub() {
override fun getIntentBitmap(): Bitmap {
return screenShotBitmap
}
})
putExtra (INTENT_QUESTION_SCREENSHOT_BITMAP, bundle)
})
}
BitmapExt
有一个 Activity 的大局扩展办法 startActivity2WithBitmap
,里边创立了一个 Binder,将获取到的屏幕截图 Bitmap 丢进去,并包在 Intent 中发送到 Activity2 ;明显这儿有个IBitmapInterface
的匿名内部类,看来走漏是从这儿产生的;
但有两个疑问,一是这个内部类是写在办法里的,办法结束时,不会把办法栈中的内部类引证清除掉吗?二是这个内部类也并没有引证到 Activity 吧?
要搞理解这两点,就要把 Kotlin 代码反编译成 Java 看看了:
@Nullable
public static final Object startActivity2WithBitmap(@NotNull Activity $this$startActivity2WithBitmap, boolean var1, @NotNull Continuation var2) {
...
Bitmap var14 = (Bitmap)var10000;
if (var14 == null) {
return Unit.INSTANCE;
} else {
Bitmap screenShotBitmap = var14;
Intent var4 = new Intent();
int var6 = false;
Bundle bundle = new Bundle();
// 内部类创立方位:
bundle.putBinder("screenShotBinder", (IBinder)(new BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1($this$startActivity2WithBitmap, screenShotBitmap)));
var4.putExtra("question_screenshot_bitmap", bundle);
Unit var9 = Unit.INSTANCE;
$this$startActivity2WithBitmap.startActivity(var4);
return Unit.INSTANCE;
}
}
// 这是kotlin compiler自动生成的一个一般类:
public final class BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1 extends IBitmapInterface.Stub {
// $FF: synthetic field
final Activity $this_startActivity2WithBitmap$inlined; // 引证了activity
// $FF: synthetic field
final Bitmap $screenShotBitmap$inlined;
BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1(Activity var1, Bitmap var2) {
this.$this_startActivity2WithBitmap$inlined = var1;
this.$screenShotBitmap$inlined = var2;
}
@NotNull
public Bitmap getIntentBitmap() {
return this.$screenShotBitmap$inlined;
}
}
在 Kotlin Compiler 编译生成的 Java 文件中,IBitmapInterface
匿名内部类被替换为一般类 BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1
,而且这个一般类持有了 Activity。呈现这个状况的原因是,Kotlin 为了在该类的内部能正常运用办法内的变量,把办法的入参以及内部类代码以上创立的一切变量都写进了该类的成员变量中;因而 Activity 被该类引证;别的 Binder 本身生命周期善于 Activity,因而产生内存走漏。
解决办法是,直接声明一个一般类,即可绕过 Kotlin Compiler 的“优化”,移除 Activity 的引证。
class BitmapBinder(private val bitmap: Bitmap): IBitmapInterface.Stub() {
override fun getIntentBitmap( ) = bitmap
}
// 运用:
bundle.putBinder(KEY_SCREENSHOT_BINDER, BitmapBinder(screenShotBitmap))
接下来,问题是 Bitmap 和 Binder 会重复创立且无法收回的问题,内存现象如图,每次跳转再封闭,内存都会上涨一点,如同阶梯;GC 后无法开释;
heap 中,经过 Bitmap 尺寸 2560x1600, 320density
能够推断,这些都是未能收回的截图 Bitmap 目标,被 Binder 持有;但查看 Binder 的引证链,却并没有发现任何被咱们运用相关的引证;
咱们推测 Binder 应该是被生命周期较长的 Native 层引证了,与 Binder 的完成有关,但没找到收回 Binder 的有效办法;
一种解决办法是,复用 Binder,确保每次翻开 Activity2 时,Binder 不会重复创立;别的将 BitmapBinder
的 Bitmap 改为弱引证,这样即使 Binder 不能收回,Bitmap 也能被及时收回,究竟 Bitmap 才是内存大户。
object BitmapBinderHolder {
private var mBinder: BitmapBinder? = null // 确保大局只有一个BitmapBinder
fun of(bitmap: Bitmap): BitmapBinder {
return mBinder ?: BitmapBinder(WeakReference(bitmap)).apply { mBinder = this }
}
}
class BitmapBinder(var bitmapRef: WeakReference<Bitmap>?): IBitmapInterface.Stub() {
override fun getIntentBitmap() = bitmapRef?.get()
}
// 运用:
bundle.putBinder(KEY_SCREENSHOT_BINDER, BitmapBinderHolder.of(screenShotBitmap))
验证:如内存图,一次 GC 后,创立的一切 Bitmap 都能够正常收回。
事例2:Flutter 多引擎场景 插件内存走漏
有不少项目运用了多引擎计划完成 Flutter 混合开发,在 Flutter 页面封闭时,为避免内存走漏,不光要将 FlutterView
、FlutterEngine
、MessageChannel
等相关组件及时解绑毁掉,同时也需求重视各个 Flutter 插件是否有正常的开释操作。
例如在咱们的一个多引擎项目中,经过重复翻开封闭一个页面,发现了一个内存走漏点:
这个activity是一个二级页面,运用多引擎计划,在上面跑了一个 FlutterView
;看样子是一个『单实例』的内存走漏,即无论开关多少次,Activity 只会保留一个实例在heap中无法开释,常见的场景是大局静态变量的引证。这种内存走漏对内存的影响比多实例走漏略轻一点,但假如这个 Activity 体量很大,持有较多的 Fragment、View,这些相关组件一起走漏的话,也是要侧重优化的。
从引证链来看,这是 FlutterEngine
内的一个通讯 Channel 引起的内存走漏;当 FlutterEngine
被创立时,引擎内的每个插件会创立出自己的MessageChannel
并注册到FlutterEngine.dartExecutor.binaryMessenger
中,以便每个插件都能独立和 Native 通讯。
例如一个一般插件的写法或许是这样:
class XXPlugin: FlutterPlugin {
private val mChannel: BasicMessageChannel<Any>? = null
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { // 引擎创立时回调
mChannel = BasicMessageChannel(flutterPluginBinding.flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NAME, JSONMessageCodec.INSTANCE)
mChannel?.setMessageHandler { message, reply ->
...
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { // 引擎毁掉时回调
mChannel?.setMessageHandler(null)
mChannel = null
}
}
能够看到其实 FlutterPlugin
其实是会持有 binaryMessenger
的引证的,而 binaryMessenger
又会有 FlutterJNI
的引证… 这一系列引证链最终会使 FlutterPlugin
持有 Context
,因而假如插件没有正确开释引证,就必然会呈现内存走漏。
咱们看下上图引证链中 loggerChannel
的写法是怎么样的:
class LoggerPlugin: FlutterPlugin {
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
LoggerChannelImpl.init(flutterPluginBinding.getFlutterEngine())
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
}
}
object LoggerChannelImpl { // 这是一个单例
private var loggerChannel: BasicMessageChannel<Any>?= null
fun init(flutterEngine: FlutterEngine) {
loggerChannel = BasicMessageChannel(flutterEngine.dartExecutor.binaryMessenger, LOGGER_CHANNEL, JSONMessageCodec.INSTANCE)
loggerChannel?.setMessageHandler { messageJO, reply ->
...
}
}
}
在 LoggerPlugin.onAttachedToEngine
中,将 FlutterEngine
传入到了单例 LoggerChannelImpl
里边,binaryMessenger
被单例持有,且 onDetachedFromEngine
办法未做毁掉操作,因而一向被单例引证,context无法开释。
这个插件或许在设计时,没有考虑到多引擎的场景;单引擎时,插件的 onAttachedToEngine
、onDetachedFromEngine
相当于是跟着运用的生命周期走的,因而不会呈现内存走漏;但在多引擎场景下,DartVM
会为每个引擎分配 isolate,和进程有些相似;isolate 的 dart 堆内存是彻底独立的,因而引擎之间任何目标(包括静态目标)都不互通;因而 FlutterEngine
会在自己的 isolate 中创立各自的 FlutterPlugin
实例,这使得每次创立引擎,插件的生命周期都会重走一遍。当毁掉一个引擎时,插件没有正常收回,没有及时开释 Context
、FlutterEngine
的相关引证,就会呈现内存走漏。
修改计划:
-
LoggerChannelImpl
无需运用单例写法,替换为一般类即可,确保每个引擎的MessageChannel
都是独立的; -
LoggerPlugin.onDetachedFromEngine
需求对MessageChannel
做毁掉和置空操作;
事例3:三方库 Native 引证 内存走漏
项目中接入了一个三方阅读器 SDK,在一次内存剖析时,发现每次翻开该阅读器,内存便会上升一截而且无法下降;从 heap dump 文件看,Profiler 并未指出项目中存在内存走漏,但能够看到 app heap 中有一个 Activity 未能收回的实例个数非常多,且内存占用较大。
查看 GCRoot References,发现这些 Activity 没有被任何已知的 GCRoot 引证:
毫无疑问这个 Activity 是存在内存走漏的,因为操作的时候现已把相关页面都 finish 掉而且手动 GC,因而原因只能是 Activity 被某个不行见的 GCRoot 引证了。
事实上,Profiler 的 Heap Dump 只会显现 Java 堆内存的 GCRoot,而在 Native 堆中的 GCRoot 并不会显现到这个引证列表中。所以,有没有或许是这个Activity被 Native 目标持有了?
咱们用动态剖析东西 Allocations Record
看一下 Java 类在 Native 堆的引证,果然发现了这个 Activity 的一些引证链:
但可惜引证链都是一些内存地址,没有显现类名,无法知道是何处引证到了 Activity;后面用 LeakCanary 试了一下,尽管也明确说明了是 Native 层 Global Variable
的引证形成的内存走漏,但仍是没有供给详细的调用方位;
咱们只好回到源码去剖析下或许的调用处了。这个是 DownloadActivity
是咱们为了适配阅读器SDK做的一个书籍下载的页面;当本地没有图书时,会先下载书籍文件,随后传入 SDK 中,翻开 SDK 自己的 Activity;因而,DownloadActivity
的功用便是下载、校验、解压书籍,并处理 SDK 阅读器的一些启动流程。
按惯例思路,先查看下载、校验、解压的代码,都没有发现疑点,listener 之类的都做了弱引证封装;因而推测是 SDK 本身的写法导致的内存走漏。
发现阅读器 SDK 启动时,有一个 context 入参:
class DownloadActivity {
...
private fun openBook() {
...
ReaderApi.getInstance().startReader(this, bookInfo)
}
}
因为这个 SDK 的源码都是混淆过的,只能硬啃了,从 startReader
办法点进去一路盯梢调用链:
class ReaderApi: void startReader(Activity context, BookInfo bookInfo)
↓
class AppExecutor: void a(Runnable var1)
↓
class ReaderUtils: static void a(Activity var0, BookViewerCallback var1, Bundle var2)
↓
class BookViewer: static void a(Context var0, AssetManager var1)
↓
class NativeCpp: static native void initJNI(Context var0, AssetManager var1);
最终到了 NativeCpp
这个类的 initJNI
办法,能够看到这个本地办法把咱们的 Activity 传进去了,后续处理不得而知,但基于上面的内存剖析咱们根本能够判定,正是因为这个办法,Activity 的引证被 Native 的长生命周期目标持有,导致 Activity 呈现内存走漏。
至于为什么 Native 需求用到 context 则无法剖析了,咱们只能将这个问题反馈给 SDK 供应商,让他们做进一步处理。解决办法也不难:
- 在毁掉阅读器时及时置空 Activity 引证;
-
startReader
办法不需求指定 Activity 目标,入参声明改为 Context 即可,外部就能够将Application Context
传进去。