• [Android | 앱 성능 최적화] GPU 렌더링 속도 프로파일링 및 레이아웃 평탄화

    2023. 1. 12.

    by. 하루플스토리

    반응형

    안녕하세요, 하루플입니다.

     

    많이 아시다시피 안드로이드와 IOS의 앱 성능 차이는 눈에 띌 정도로 크게 납니다.

    IOS는 걱정을 덜해도 되는 반면 안드로이드는 개발할 때 성능면에서 더 깊은 고민과 함께 코드를 짜야합니다..😭

    곧 출시되는 갤럭시S23부터는 스냅드래곤8 Gen2 가 들어가는데 유출된 성능을 보면 싱글코어 1500점대, 멀티코어 4600점대로 애플의 A14칩셋 정도로 따라온 것을 볼 수 있습니다. (분발해라 엑시노스던 퀄컴이던..)

    이정도만 되도 성능 걱정을 조금 덜 할텐데.. 우리는 이전 세대의 스마트폰을 커버할 수 있도록 개발해야하기 때문에 성능 최적화에 계속 힘써야합니다. 물론 칩셋 성능이 좋아지더라도 계속 최적화된 소프트웨어를 개발 해야겠죠.

     

    회사에서 신규 앱을 개발한지 벌써 1년이 넘었는데요, 계속해서 기능이 추가되고 복잡한 커스텀 UI나 애니메이션을 넣다보니 슬슬 앱에 렉이 생기기 시작했습니다. 거기에 모자라서 BottomNavigation으로 여러 Fragment를 한번에 생성하고 그 BottomNavigation의 Fragment 내부에 ViewPager 화면이 11개가 있는 등 어마어마한 양의 뷰를 처리하다보니 렉이 안생길수가 없습니다ㅜㅜ 자연스럽게 메모리도 많이 먹게 되었고, 애니메이션을 실행할 때 심한 렉이 동반되기 시작했습니다.

    그렇게 뒤늦게나마 앱 최적화 작업에 들어가게 되었습니다.

     

     

    안드로이드의 성능 최적화를 검색하면 아마 GPU 렌더링 프로파일링과 오버드로 디버그를 통해 문제를 해결하는게 가장 많이 보일텐데요, 이번 글에서는 GPU 렌더링 속도 프로파일링으로 어떻게 성능을 향상했는지 알려드리겠습니다.


    프로파일러 사용 설정

    설정 - gpu 검색 - 프로필 HWUI 렌더링 - 화면에 막대로 표시

     

    프로파일러로 속도 저하 문제 확인

    이 기능은 프로파일러 말 그대로 속도 저하의 문제가 무엇인지 확인하는 용도로만 사용합니다. 시각적으로 구동되고 있는 앱이 어느정도의 프레임을 달성하고 있는지 보여줍니다. 요즘 출시되는 스마트폰은 초당 120 프레임까지도 표현하는데 사실 렉걸린다는거 부터 초당 10프레임까지도 떨어진다는걸 의미하기 때문에 속도 문제를 해결하기 전에는 하드웨어의 120프레임을 온전히 활용할 수가 없습니다😭

     

    토스와 배달의 민족 Android App GPU 렌더링 프로파일링

    GPU 렌더링 프로파일링을 켜두고 토스와 배달의 민족 앱을 구동해봤습니다.

     

    가로 선의 의미를 알아야 하는데요, 얇게 있는 녹색 가로선은 16.67ms를 의미합니다. 초당 60프레임을 달성하려면 맨 아래에 있는 녹색 가로선을 그래프가 넘지 않아야 합니다. 그런데 사진에서 토스나 배달의 민족 앱 모두 훌쩍 뛰어넘어 있는걸 볼 수 있습니다. 이는 앱을 최초 실행하였을 때 레이아웃을 계산해야하기 때문에 필연적으로 최초 1회동안만 발생하는 렉이라 괜찮습니다. 우리는 앱을 구동중에 렉이 발생하지 않도록 해야합니다. 이제 세로선의 의미를 보겠습니다. 제가 중점으로 봐야할 부분은 '측정, 레이아웃' 입니다.

     

    많은 뷰와 레이아웃 계층을 사용한 앱이라면 저 연두색 그래프가 길게 나와있을겁니다. 레이아웃을 계산하는데 시간이 너무 오래걸려 발생하는 문제인데 다행스럽게도 레이아웃 계층을 줄이는 몇가지 해결 방법이 있습니다!

     

    Constraint Latout을 하나만 사용하여 평탄화
    <androidx.constraintlayout.widget.ConstraintLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <androidx.constraintlayout.widget.ConstraintLayout
        	android:id="@+id/button"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
        </androidx.constraintlayout.widget.ConstraintLayout>
    
    </androidx.constraintlayout.widget.ConstraintLayout>

    사실 최적화를 진행하기 전에는 ConstraintLayout 내부에 ConstraintLayout을 하나 더 생성해서 조금 복잡한 UI의 버튼을 구현하곤 했었습니다😱 이게 개발자 입장에서는 레이아웃을 통째로 버튼으로 쓰면되니까 편한데 성능에서는 전혀 좋지 않습니다.

    레이아웃 계층이 하나씩 늘어날 때마다 안드로이드는 뷰 위치를 계산해야 하는 횟수가 증가하게 됩니다. 그래서 Constraint 레이아웃을 하나만 사용하도록 xml을 수정했고 같은 뷰를 구현하더라도 레이아웃 계산에 쓰이는 시간을 줄일수 있게 되었습니다.

     

    한번 이렇게 리팩토링을 거치고나면서 성능의 중요성을 깨닫고.. 이제는 처음부터 ConstraintLayout을 한번만 사용하려고 하고 있습니다ㅠㅠ

     

     

    정말 간단히 가로/세로로만 뷰가 배치되면 LinearLayout 사용하기

    보통 앱 화면을 개발할 때 가로, 세로로만 뷰가 배치되는 경우는 거의 없습니다. 그럼에도 화면 내부에서 일부 모듈만을 개발한다거나 정말 간단한 화면을 구현할 때는 가로, 세로로만 배치되는 경우가 간혹 있습니다. 이런 경우에는 LinearLayout을 써도 계층이 더 복잡해지지 않기 때문에 괜찮습니다. 

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        
        <TextView
            android:id="@+id/tv_test_1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    
        <TextView
            android:id="@+id/tv_test_2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    
    </LinearLayout>

     

     

    다른 레이아웃을 가져올 때 merge 태그 사용하기
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        
        <include
            layout="@layout/act_main"/>
    
    </LinearLayout>

    보통 반복되는 레이아웃을 재사용하기 위해 <include/> 태그를 사용하곤 합니다. 이때 include 내부에 들어있는 layout은 무조건 레이아웃이 감싸져있겠죠? 그래서 위의 코드와 아래 코드가 똑같을 겁니다.

     

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            
            ''' 중략 '''
            
        </androidx.constraintlayout.widget.ConstraintLayout>
    
    </LinearLayout>

    이러면 쓸모없이 레이아웃이 하나 더 생성되어 있는거고 마찬가지로 레이아웃 계층 계산에 시간이 더 소요되게 됩니다. 이를 해결하기 위해 안드로이드에서는 merge 태그를 제공합니다. 

     

    <?xml version="1.0" encoding="utf-8"?>
    <merge xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/layout_parent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
    
    </merge>

    여러 화면에 갖다붙힐 모듈의 xml에 merge로 감싸고 해당 xml이 어떤 레이아웃을 사용할지 tools:parentTag 로 정의해주면 쉽게 레이아웃 평탄화를 할 수 있습니다. merge 태그로 만든 레이아웃은 가장 바깥 레이아웃이 사라지고 include 등을 통해 넣게된 xml의 부모 레이아웃과 엮이게 됩니다.

     

    지금 회사에서 개발하는 앱의 각 기능들을 모듈화 해두었고, 각 모듈 xml을 화면으로 그대로 가져와 사용하는 경우가 잦습니다. 그래서 merge 태그를 많이 사용할 수 있었고 속도 향상도 많이 이룰수 있었습니다.

     

    앱 속도 최적화를 위한 다양한 방법이 있는데 가장 보편적으로 사용하는 레이아웃 평탄화를 소개했습니다. 다른 최적화 방법도 계속 고민하고 적용후 포스팅해보겠습니다.

    반응형

    댓글