内存抖动
内存抖动是由于短时间内有大量对象进出新生区导致的,内存忽高忽低,有短时间内快速上升和下落的趋势,分析图呈锯齿状。
它伴随着频繁的GC,GC会大量占用UI线程和CPU资源,会导致APP整体卡顿,甚至OOM可能。
GC过程中会暂停用户线程包括UI线程,Stop the World,频繁的创建,释放对象,GC频繁出现,就会导致App整体卡顿
内存抖动就是不停的在新生代创建对象,创建对象很多,会有空间分配担保,新生代空间比较小占1/3,老年代2/3,当新生代空间不够时,老年代会腾挪一部分空间给新生代,导致老年代空间下降,这个时候假设创建了大对象,从新生代经过了from到to,年龄足够到了老年代,这个时候空间不够,如果没有连续的空间,就会导致OOM。
内存泄漏
程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存浪费。
引用计数法和可达性分析(根可达)
引用计数法:一个对象身上没有引用,不可用,这个对象就会被回收。
可达性分析:静态变量,线程栈变量,常量池,JNI都可以被称为可达性根,GC的根,如果一个对象满足可达性分析,也就是根可达,如果可以通过一个静态变量往下找可以找到这个对象,这个对象继续往下找到下一个对象,比如Activity。那么Activity就不会被回收,根可达。如果根不可达就会被回收。
比如Activity已经执行了onDestory方法,但是这个Activity还被静态变量所引用(根可达),,这个时候就会发生内存泄漏
内存分析工具 Android Profiler
内存性能分析器是Android Profiler中的一个组件,可以帮助我们识别可能会导致应用卡顿甚至奔溃的内存泄漏和内存抖动。它显示一个应用内存使用量的实时图表,让我们可以
- 捕获堆转储
把堆内存上实时的数据给倒到一个文件(hprof)上去
捕获内容
-
强制执行垃圾回收
点击垃圾桶按钮,会执行gc,分析曲线也会有垃圾桶的标志
同时日志也会输出
-
跟踪内存分配
例子分析过程
自定义View实现一个仿Ios花瓣加载动画
package com.example.memoryoptimizing.view
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.animation.LinearInterpolator
import com.example.memoryoptimizing.utils.DPUtils
class IosLoadingView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val radius: Double = DPUtils.dpToPx(context, 9F).toDouble()
private val insideRadius: Double = DPUtils.dpToPx(context, 5F).toDouble()
// 定义起始点和结束点坐标
private var northwestXStart = 0f
private var northwestYStart = 0f
private var northXStart = 0f
private var northYStart = 0f
private var northeastXStart = 0f
private var northeastYStart = 0f
private var eastXStart = 0f
private var eastYStart = 0f
private var southeastXStart = 0f
private var southeastYStart = 0f
private var southXStart = 0f
private var southYStart = 0f
private var southwestXStart = 0f
private var southwestYStart = 0f
private var westXStart = 0f
private var westYStart = 0f
private var northwestXEnd = 0f
private var northwestYEnd = 0f
private var northXEnd = 0f
private var northYEnd = 0f
private var northeastXEnd = 0f
private var northeastYEnd = 0f
private var eastXEnd = 0f
private var eastYEnd = 0f
private var southeastXEnd = 0f
private var southeastYEnd = 0f
private var southXEnd = 0f
private var southYEnd = 0f
private var southwestXEnd = 0f
private var southwestYEnd = 0f
private var westXEnd = 0f
private var westYEnd = 0f
private var currentColor = 7
private val colors = arrayOf(
"#a5a5a5", "#b7b7b7", "#c0c0c0", "#c9c9c9",
"#d2d2d2", "#dbdbdb", "#e4e4e4", "#e4e4e4"
)
private lateinit var valueAnimator: ValueAnimator
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val paint = Paint().apply {
isAntiAlias = true
strokeWidth = DPUtils.dpToPx(context, 2F).toFloat()
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND
}
fun drawPath(startX: Float, startY: Float, endX: Float, endY: Float, colorIndex: Int) {
Path().apply {
moveTo(startX, startY)
lineTo(endX, endY)
}.also { path ->
paint.color = Color.parseColor(colors[colorIndex])
canvas.drawPath(path, paint)
}
}
drawPath(northwestXStart, northwestYStart, northwestXEnd, northwestYEnd, 0)
drawPath(northXStart, northYStart, northXEnd, northYEnd, 1)
drawPath(northeastXStart, northeastYStart, northeastXEnd, northeastYEnd, 2)
drawPath(eastXStart, eastYStart, eastXEnd, eastYEnd, 3)
drawPath(southeastXStart, southeastYStart, southeastXEnd, southeastYEnd, 4)
drawPath(southXStart, southYStart, southXEnd, southYEnd, 5)
drawPath(southwestXStart, southwestYStart, southwestXEnd, southwestYEnd, 6)
drawPath(westXStart, westYStart, westXEnd, westYEnd, 7)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val centerX = measuredWidth / 2.0
val centerY = measuredHeight / 2.0
val leg = radius * Math.cos(Math.PI / 4)
val insideLeg = insideRadius * Math.cos(Math.PI / 4)
northwestXStart = (centerX - leg).toFloat()
northwestYStart = (centerY - leg).toFloat()
northXStart = centerX.toFloat()
northYStart = (centerY - radius).toFloat()
northeastXStart = (centerX + leg).toFloat()
northeastYStart = (centerY - leg).toFloat()
eastXStart = (centerX + radius).toFloat()
eastYStart = centerY.toFloat()
southeastXStart = (centerX + leg).toFloat()
southeastYStart = (centerY + leg).toFloat()
southXStart = centerX.toFloat()
southYStart = (centerY + radius).toFloat()
southwestXStart = (centerX - leg).toFloat()
southwestYStart = (centerY + leg).toFloat()
westXStart = (centerX - radius).toFloat()
westYStart = centerY.toFloat()
northwestXEnd = (centerX - insideLeg).toFloat()
northwestYEnd = (centerY - insideLeg).toFloat()
northXEnd = centerX.toFloat()
northYEnd = (centerY - insideRadius).toFloat()
northeastXEnd = (centerX + insideLeg).toFloat()
northeastYEnd = (centerY - insideLeg).toFloat()
eastXEnd = (centerX + insideRadius).toFloat()
eastYEnd = centerY.toFloat()
southeastXEnd = (centerX + insideLeg).toFloat()
southeastYEnd = (centerY + insideLeg).toFloat()
southXEnd = centerX.toFloat()
southYEnd = (centerY + insideRadius).toFloat()
southwestXEnd = (centerX - insideLeg).toFloat()
southwestYEnd = (centerY + insideLeg).toFloat()
westXEnd = (centerX - insideRadius).toFloat()
westYEnd = centerY.toFloat()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
startAnimation()
}
private fun startAnimation() {
valueAnimator = ValueAnimator.ofInt(7, 0).apply {
duration = 400
repeatCount = ValueAnimator.INFINITE
interpolator = LinearInterpolator()
addUpdateListener { animation ->
val newColorIndex = animation.animatedValue as Int
if (newColorIndex != currentColor) {
colors.rotateRight()
invalidate()
currentColor = newColorIndex
}
}
}
valueAnimator.start()
}
private fun Array<String>.rotateRight() {
val last = this.last()
for (i in this.size - 1 downTo 1) {
this[i] = this[i - 1]
}
this[0] = last
}
}
使用AndroidStudio自带的内存分析工具profiler,发现total一直在增加
观察对象分配情况除去系统分配的,发现Path和Paint对象占大头,观察调用地方发现调用是在onDraw方法不停调用生成,因为原来代码中动画不停循环invalidate(),会不断调用onDraw方法产生对象。
这样优化第一步,重用Paint对象和Path对象,不让他在onDraw方法一直生成。
package com.example.memoryoptimizing.view
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.animation.LinearInterpolator
import com.example.memoryoptimizing.utils.DPUtils
class IosLoadingView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val radius: Double = DPUtils.dpToPx(context, 9F).toDouble()
private val insideRadius: Double = DPUtils.dpToPx(context, 5F).toDouble()
// 定义起始点和结束点坐标
private var northwestXStart = 0f
private var northwestYStart = 0f
private var northXStart = 0f
private var northYStart = 0f
private var northeastXStart = 0f
private var northeastYStart = 0f
private var eastXStart = 0f
private var eastYStart = 0f
private var southeastXStart = 0f
private var southeastYStart = 0f
private var southXStart = 0f
private var southYStart = 0f
private var southwestXStart = 0f
private var southwestYStart = 0f
private var westXStart = 0f
private var westYStart = 0f
private var northwestXEnd = 0f
private var northwestYEnd = 0f
private var northXEnd = 0f
private var northYEnd = 0f
private var northeastXEnd = 0f
private var northeastYEnd = 0f
private var eastXEnd = 0f
private var eastYEnd = 0f
private var southeastXEnd = 0f
private var southeastYEnd = 0f
private var southXEnd = 0f
private var southYEnd = 0f
private var southwestXEnd = 0f
private var southwestYEnd = 0f
private var westXEnd = 0f
private var westYEnd = 0f
private var currentColor = 7
private val colors = arrayOf(
"#a5a5a5", "#b7b7b7", "#c0c0c0", "#c9c9c9",
"#d2d2d2", "#dbdbdb", "#e4e4e4", "#e4e4e4"
)
private lateinit var valueAnimator: ValueAnimator
//重用Paint和Path
private val paint = Paint().apply {
isAntiAlias = true
strokeWidth = DPUtils.dpToPx(context, 2F).toFloat()
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND
}
private val path = Path()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
fun drawPath(startX: Float, startY: Float, endX: Float, endY: Float, colorIndex: Int) {
path.apply {
reset()
moveTo(startX, startY)
lineTo(endX, endY)
}.also { path ->
paint.color = Color.parseColor(colors[colorIndex])
canvas.drawPath(path, paint)
}
}
drawPath(northwestXStart, northwestYStart, northwestXEnd, northwestYEnd, 0)
drawPath(northXStart, northYStart, northXEnd, northYEnd, 1)
drawPath(northeastXStart, northeastYStart, northeastXEnd, northeastYEnd, 2)
drawPath(eastXStart, eastYStart, eastXEnd, eastYEnd, 3)
drawPath(southeastXStart, southeastYStart, southeastXEnd, southeastYEnd, 4)
drawPath(southXStart, southYStart, southXEnd, southYEnd, 5)
drawPath(southwestXStart, southwestYStart, southwestXEnd, southwestYEnd, 6)
drawPath(westXStart, westYStart, westXEnd, westYEnd, 7)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val centerX = measuredWidth / 2.0
val centerY = measuredHeight / 2.0
val leg = radius * Math.cos(Math.PI / 4)
val insideLeg = insideRadius * Math.cos(Math.PI / 4)
northwestXStart = (centerX - leg).toFloat()
northwestYStart = (centerY - leg).toFloat()
northXStart = centerX.toFloat()
northYStart = (centerY - radius).toFloat()
northeastXStart = (centerX + leg).toFloat()
northeastYStart = (centerY - leg).toFloat()
eastXStart = (centerX + radius).toFloat()
eastYStart = centerY.toFloat()
southeastXStart = (centerX + leg).toFloat()
southeastYStart = (centerY + leg).toFloat()
southXStart = centerX.toFloat()
southYStart = (centerY + radius).toFloat()
southwestXStart = (centerX - leg).toFloat()
southwestYStart = (centerY + leg).toFloat()
westXStart = (centerX - radius).toFloat()
westYStart = centerY.toFloat()
northwestXEnd = (centerX - insideLeg).toFloat()
northwestYEnd = (centerY - insideLeg).toFloat()
northXEnd = centerX.toFloat()
northYEnd = (centerY - insideRadius).toFloat()
northeastXEnd = (centerX + insideLeg).toFloat()
northeastYEnd = (centerY - insideLeg).toFloat()
eastXEnd = (centerX + insideRadius).toFloat()
eastYEnd = centerY.toFloat()
southeastXEnd = (centerX + insideLeg).toFloat()
southeastYEnd = (centerY + insideLeg).toFloat()
southXEnd = centerX.toFloat()
southYEnd = (centerY + insideRadius).toFloat()
southwestXEnd = (centerX - insideLeg).toFloat()
southwestYEnd = (centerY + insideLeg).toFloat()
westXEnd = (centerX - insideRadius).toFloat()
westYEnd = centerY.toFloat()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
startAnimation()
}
private fun startAnimation() {
valueAnimator = ValueAnimator.ofInt(7, 0).apply {
duration = 400
repeatCount = ValueAnimator.INFINITE
interpolator = LinearInterpolator()
addUpdateListener { animation ->
val newColorIndex = animation.animatedValue as Int
if (newColorIndex != currentColor) {
colors.rotateRight()
currentColor = newColorIndex
invalidate()
}
}
}
valueAnimator.start()
}
//不断变化数组
private fun Array<String>.rotateRight() {
val last = this.last()
for (i in this.size - 1 downTo 1) {
this[i] = this[i - 1]
}
this[0] = last
}
}
可以看到内存对象分配里没有Path和Paint了,但是String对象还是不断生成。观察代码发现,我们在更新每片花瓣颜色的时候,都是去更新String数组值的排序,然后onDraw方法的时候去依次取值,但是每次更新动画的时候,都会遍历String数组进行更新,还有onDraw方法Color.parseColor内部也会调用substring,会导致不断创建String对象,因此使用取模的方式去更新每个花瓣的颜色。
class IosLoadingView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val radius: Double = DPUtils.dpToPx(context, 9F).toDouble()
private val insideRadius: Double = DPUtils.dpToPx(context, 5F).toDouble()
// 定义起始点和结束点坐标
private var northwestXStart = 0f
private var northwestYStart = 0f
private var northXStart = 0f
private var northYStart = 0f
private var northeastXStart = 0f
private var northeastYStart = 0f
private var eastXStart = 0f
private var eastYStart = 0f
private var southeastXStart = 0f
private var southeastYStart = 0f
private var southXStart = 0f
private var southYStart = 0f
private var southwestXStart = 0f
private var southwestYStart = 0f
private var westXStart = 0f
private var westYStart = 0f
private var northwestXEnd = 0f
private var northwestYEnd = 0f
private var northXEnd = 0f
private var northYEnd = 0f
private var northeastXEnd = 0f
private var northeastYEnd = 0f
private var eastXEnd = 0f
private var eastYEnd = 0f
private var southeastXEnd = 0f
private var southeastYEnd = 0f
private var southXEnd = 0f
private var southYEnd = 0f
private var southwestXEnd = 0f
private var southwestYEnd = 0f
private var westXEnd = 0f
private var westYEnd = 0f
private var currentColor = 7
private val colors = arrayOf(
"#a5a5a5", "#b7b7b7", "#c0c0c0", "#c9c9c9",
"#d2d2d2", "#dbdbdb", "#e4e4e4", "#e4e4e4"
)
private var color = arrayOf<Int>()
private lateinit var valueAnimator: ValueAnimator
private val paint = Paint().apply {
isAntiAlias = true
strokeWidth = DPUtils.dpToPx(context, 2F).toFloat()
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND
}
init {
//提前调用Color.parseColor解析好数据,而不是在onDraw方法里频繁调用
color = Array(colors.size) { index ->
// 将每个颜色字符串解析为整数
Color.parseColor(colors[index])
}
}
private val path = Path()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
fun drawPath(startX: Float, startY: Float, endX: Float, endY: Float) {
path.apply {
reset()
moveTo(startX, startY)
lineTo(endX, endY)
}.also { path ->
paint.color =color[currentColor++ % color.size]
canvas.drawPath(path, paint)
}
}
drawPath(northwestXStart, northwestYStart, northwestXEnd, northwestYEnd)
drawPath(northXStart, northYStart, northXEnd, northYEnd)
drawPath(northeastXStart, northeastYStart, northeastXEnd, northeastYEnd)
drawPath(eastXStart, eastYStart, eastXEnd, eastYEnd)
drawPath(southeastXStart, southeastYStart, southeastXEnd, southeastYEnd)
drawPath(southXStart, southYStart, southXEnd, southYEnd)
drawPath(southwestXStart, southwestYStart, southwestXEnd, southwestYEnd)
drawPath(westXStart, westYStart, westXEnd, westYEnd)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val centerX = measuredWidth / 2.0
val centerY = measuredHeight / 2.0
val leg = radius * Math.cos(Math.PI / 4)
val insideLeg = insideRadius * Math.cos(Math.PI / 4)
northwestXStart = (centerX - leg).toFloat()
northwestYStart = (centerY - leg).toFloat()
northXStart = centerX.toFloat()
northYStart = (centerY - radius).toFloat()
northeastXStart = (centerX + leg).toFloat()
northeastYStart = (centerY - leg).toFloat()
eastXStart = (centerX + radius).toFloat()
eastYStart = centerY.toFloat()
southeastXStart = (centerX + leg).toFloat()
southeastYStart = (centerY + leg).toFloat()
southXStart = centerX.toFloat()
southYStart = (centerY + radius).toFloat()
southwestXStart = (centerX - leg).toFloat()
southwestYStart = (centerY + leg).toFloat()
westXStart = (centerX - radius).toFloat()
westYStart = centerY.toFloat()
northwestXEnd = (centerX - insideLeg).toFloat()
northwestYEnd = (centerY - insideLeg).toFloat()
northXEnd = centerX.toFloat()
northYEnd = (centerY - insideRadius).toFloat()
northeastXEnd = (centerX + insideLeg).toFloat()
northeastYEnd = (centerY - insideLeg).toFloat()
eastXEnd = (centerX + insideRadius).toFloat()
eastYEnd = centerY.toFloat()
southeastXEnd = (centerX + insideLeg).toFloat()
southeastYEnd = (centerY + insideLeg).toFloat()
southXEnd = centerX.toFloat()
southYEnd = (centerY + insideRadius).toFloat()
southwestXEnd = (centerX - insideLeg).toFloat()
southwestYEnd = (centerY + insideLeg).toFloat()
westXEnd = (centerX - insideRadius).toFloat()
westYEnd = centerY.toFloat()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
startAnimation()
}
private fun startAnimation() {
valueAnimator = ValueAnimator.ofInt(7, 0).apply {
duration = 400
repeatCount = ValueAnimator.INFINITE
interpolator = LinearInterpolator()
addUpdateListener { animation ->
val newColorIndex = animation.animatedValue as Int
if (newColorIndex != currentColor) {
currentColor = newColorIndex
postInvalidateOnAnimation()
}
}
}
valueAnimator.start()
}
}
可以看到内存变得稳定起来,并没有一直增加。
当退出当前Activity时后有内存泄漏的警告
可以看到退出MainActivity2的时候,动画是持有MainActivity2的引用的,由于我们设置了动画监听,当Mainctivity退出的时候,动画监听还持有MainActivity2的引用,导致内存泄漏。
这个时候我们应该在组件被移除窗口的时候,移除监听
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
if(valueAnimator != null){
valueAnimator.cancel()
valueAnimator.removeAllUpdateListeners()
}
}
再次捕获堆转储分析,发现不会内存泄漏了