性能优化里有两条线:一条是内存抖动导致的卡顿,一条是过度绘制引发的掉帧。前者让用户在列表滑动时感到滑雪场,后者让动画看起来像PPT。
本文从线上真实场景出发,展示如何通过 Profiler、Layout Inspector 和 Systrace 定位并解决这两类问题。
GC_FOR_ALLOC freed 2MB, 10% freeAndroid Studio → Profiler → Memory → Record allocations → 滑动列表 → Stop
观察 Allocations 面板,发现大量 Bitmap 和 String 对象频繁创建和回收。
点击某个高频分配点,查看 Call Stack:
android.graphics.Bitmap.<init>
com.example.ImageLoader.loadBitmap
com.example.MyAdapter.onBindViewHolder原来是每次 onBindViewHolder 都重新解码同一张图片。
overridefunonBindViewHolder(holder:ViewHolder,position:Int){
valbitmap=BitmapFactory.decodeResource(resources,R.drawable.icon)
holder.icon.setImageBitmap(bitmap)
}classImageCache{
privatevalcache=LruCache<Int,Bitmap>(4*1024*1024)
funget(resId:Int):Bitmap? =cache.get(resId)
funput(resId:Int,bitmap:Bitmap){
cache.put(resId,bitmap)
}
}
overridefunonBindViewHolder(holder:ViewHolder,position:Int){
valcached=imageCache.get(R.drawable.icon)
if(cached!=null){
holder.icon.setImageBitmap(cached)
}else{
// 异步解码并缓存
loadAsync(R.drawable.icon){bitmap->
imageCache.put(R.drawable.icon,bitmap)
holder.icon.setImageBitmap(bitmap)
}
}
}Tools → Layout Inspector → 选择当前 Activity
发现布局嵌套深度达到10层,且多处使用不透明背景叠加。
<LinearLayout
android:background="@color/white"><!-- 第1层背景 -->
<FrameLayout
android:background="@color/white"><!-- 第2层背景 -->
<ImageView
android:background="@color/white"<!--第3层背景-->
android:src="@drawable/icon"/>
</FrameLayout>
</LinearLayout>三层白色背景完全重叠,GPU 需要绘制3次。
只在最外层设置背景,内层全部移除:
<LinearLayout
android:background="@color/white">
<FrameLayout><!-- 移除背景 -->
<ImageView
android:src="@drawable/icon"/><!-- 移除背景 -->
</FrameLayout>
</LinearLayout><androidx.constraintlayout.widget.ConstraintLayout
android:background="@color/white">
<ImageView
android:id="@+id/icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
app:layout_constraintTop_toBottomOf="@id/icon"/>
</androidx.constraintlayout.widget.ConstraintLayout>层级从10层降为3层。
对于 RecyclerView 的分割线,使用 clipToPadding="false" + ItemDecoration 代替重复绘制背景。
OutOfMemoryError原图尺寸 4000x3000,但 ImageView 实际显示区域只有 800x600。
valbitmap=BitmapFactory.decodeFile(imagePath)// 原图:4000x3000,占用约46MB
imageView.setImageBitmap(bitmap)使用 inSampleSize 按需采样:
fundecodeSampledBitmap(path:String,reqWidth:Int,reqHeight:Int):Bitmap{
returnBitmapFactory.Options().run{
inJustDecodeBounds=true
BitmapFactory.decodeFile(path,this)
inSampleSize=calculateInSampleSize(this,reqWidth,reqHeight)
inJustDecodeBounds=false
BitmapFactory.decodeFile(path,this)
}
}
funcalculateInSampleSize(options:BitmapFactory.Options,reqWidth:Int,reqHeight:Int):Int{
val(height,width)=options.run{outHeighttooutWidth}
varinSampleSize=1
if(height>reqHeight||width>reqWidth){
valhalfHeight=height/2
valhalfWidth=width/2
while(halfHeight/inSampleSize>=reqHeight&&halfWidth/inSampleSize>=reqWidth){
inSampleSize*=2
}
}
returninSampleSize
}
// 使用
valbitmap=decodeSampledBitmap(imagePath,800,600)// 采样后:800x600,占用约1.8MBpythonsystrace.py-t10-otrace.htmlschedgfxviewwmamapp在 Chrome 中打开 trace.html,查找红色/黄色帧标记,定位耗时操作。
if(BuildConfig.DEBUG){
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectNetwork()
.penaltyLog()
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build()
)
}debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'运行后自动检测并在通知栏报告泄漏路径。
问题 | 排查工具 | 治理手段 |
|---|---|---|
内存抖动 | Memory Profiler | LruCache 缓存 + 对象池 |
过度绘制 | GPU调试 + Layout Inspector | 移除冗余背景 + ConstraintLayout |
Bitmap OOM | Memory Profiler | inSampleSize 采样 + 图片压缩 |
主线程卡顿 | Systrace | 异步加载 + 懒加载 |
性能优化不是一次性任务,而是持续监控 + 定向治理的过程。工具链 + 经验 = 流畅体验。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。