Diagnosing and Fixing Slow Rendering (Jank) in Android Apps

Diagnosing and Fixing Slow Rendering (Jank) in Android Apps

Slow rendering, also known as jank, occurs when the app's UI frames take too long to render, causing stuttering and a poor user experience. To ensure smooth interaction, an app must render frames within 16ms to achieve 60 frames per second (fps), with higher frame rates like 90 fps or 120 fps demanding even tighter windows of 11ms and 8ms. If these timings are not met, the Android system's Choreographer will drop the frame, causing visible jank.

Identifying Jank

  1. Visual Inspection - A simple but less detailed approach involving manual navigation to spot jank. Running a release version of the application and using tools like "Profile GPU Rendering" can visually indicate rendering times relative to the 16ms benchmark.
  2. Systrace - This tool provides deeper insights by capturing system traces during app use. It shows frame rendering times, alerts, and traces of specific methods or libraries like RecyclerView.
  3. Custom Performance Monitoring - For issues not reproducible locally, integrating monitoring tools like Firebase Performance Monitoring can help track jank in real-world scenarios.

val frameMetrics = FrameMetricsAggregator()
frameMetrics.add(frameMetricsCallback)        

Understanding Slow Frames, Frozen Frames, and ANRs

Understanding these different types of performance issues is crucial. While slow frames affect smooth scrolling or animations, frozen frames and ANRs (Application Not Responding) occur when rendering takes excessively long or when the app fails to respond to system events.

  • Slow Frames - Rendering takes between 16ms and 700ms, causing stuttering in common scenarios like scrolling and animations.
  • Frozen Frames - Rendering takes between 700ms and 5s, making the app appear stuck. These are particularly noticeable during app startup or screen transitions but should be minimized to keep the app responsive.
  • ANRs (Application Not Responding) - App does not respond to user input for more than 5 seconds.

Best Practices for Resolving Jank

  1. Prioritize ANRs - These are critical as they cause complete app unresponsiveness.
  2. Address Frozen Frames - Especially during app startup or screen transitions, where they are more common.
  3. Optimize for Smooth Scrolling - Particularly in components like RecyclerView, which are prone to jank due to complex layouts or inefficient data binding.

Common Jank Sources and Solutions

A. Scrollable Lists (RecyclerView Issues)

  • notifyDataSetChanged() - Use DiffUtil for minimal updates instead of rebinding all items.

// Bad practice
fun onNewDataArrived(news: List<News>) {
    myAdapter.news = news
    myAdapter.notifyDataSetChanged()
}


// Better practice using DiffUtil
fun onNewDataArrived(news: List<News>) {
    val oldNews = myAdapter.items
    val result = DiffUtil.calculateDiff(MyCallback(oldNews, news))
    myAdapter.news = news
    result.dispatchUpdatesTo(myAdapter)
}        

  • Nested RecyclerViews - Share RecyclerView.RecycledViewPool between nested RecyclerViews.

class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() {
    private val sharedPool = RecyclerView.RecycledViewPool()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val innerRv = LayoutInflater.from(parent.context).inflate(R.layout.inner_recycler_view, parent, false)
        val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false)
        innerRv.apply {
            layoutManager = innerLLM
            recycledViewPool = sharedPool
        }
        return ViewHolder(innerRv)
    }
}        

  • Inflation and Creation Delays - Optimize view inflation and creation by prefetching items.Minimize view types and use prefetching to reduce inflation on the UI thread.

fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply {
        // Bind content from position to convertView.
    }
}        

B. Drawable or View Customizations

  • Set Bitmap Optimization - Replace canvas.drawBitmap with canvas.drawRoundRect for better performance.
  • Shader and Painting Adjustments -Utilize BitmapShader and shaderPaint for efficient bitmap handling.

C. Rendering Performance

  • UI Thread Optimization - Avoid painting bitmaps on the UI thread. Utilize hardware-accelerated Canvas for complex rendering.

// Avoid this:
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawRoundRect(...)


// Instead, use:
fun setBitmap(bitmap: Bitmap) {
    shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    invalidate()
}        

  • RenderThread Considerations - Reduce computation-heavy Canvas operations. Minimize large path animations and avoid clipPath for clipping behavior.

D. Thread Scheduling Delays

  • Binder Transactions - Avoid frequent binder calls that cause the UI thread to pause. Use tracing to identify and resolve binder-related jank.

E. Object Allocation and Garbage Collection

  • Avoid Frequent Allocations - Minimize object creation in tight loops. Use the Android Memory Profiler to identify allocation hotspots.

fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply {
        // Bind content from position to convertView.
    }
}        

Concluscion

To ensure a smooth user experience, diagnose and fix jank by identifying slow frames, using tools like Systrace and Profile GPU Rendering, and adopting best practices for rendering optimization. By addressing common sources of jank and running long tasks asynchronously, you can significantly improve your app's performance.

要查看或添加评论,请登录

Denis Koome的更多文章

社区洞察

其他会员也浏览了