안드로이드 스튜디오에서는 xml 파일을 이용해 뷰 리소스를 미리 저장해두는 것이 일반적이다. 반면 Java나 Kotlin 코드에서 직접 객체를 만들고 크기를 지정해서 뷰를 그리는 방법 또한 존재한다. StackOverFlow에서는 xml이 아닌 코드에서 직접 동작시킬 때 Programatically 라는 단어를 주로 쓰는데, 한국어로 프로그래머티컬리하게 어떻게 번역해야 할지 도저히 모르겠다. 일단 ‘코드로 그린다’라고 표현하겠다.

최근에 동적으로 뷰를 그릴 일이 많아서 코드로 뷰를 생성하는 일이 잦았는데, 문득 성능 걱정이 되었다. 구글링해보니 성능 차이는 없다고 하는데 정말 그럴지 궁금했다. 정말 간단한 뷰를 그려서 속도가 얼마나 차이날지 테스트 해보았다.



xml과 코드의 장단점

두 방법의 장점을 간단히 훑고 가자면 다음과 같다.

  • xml의 장점

    1. 뷰를 재사용하거나 다른 레이아웃에서 가져다 쓰기 쉽다.
    2. 뷰가 로직과 분리되어 있어서 장기적으로 관리하기 편리하다.
    3. 미리보기를 통해 어떤 뷰를 나타내는지 미리 파악할 수 있다.
  • 코드로 그릴 때의 장점

    1. 로직의 상태에 따라 유동적으로 뷰를 조정할 수 있다.
    2. 별도로 리소스 파일을 생성할 필요가 없다.

나는 코드로는 비교적 간단한 뷰만 그리는 편이다. 안드로이드 버전 별로 처리가 다르거나, 뷰가 비슷하지만 약간씩만 다른 리소스의 xml 파일을 모두 생성하기 번거로울 때에 코드로 뷰를 그린다. 하지만 다른 사람들과 협업을 하며 유지보수를 하다보면, 빌드하기 전에 뷰를 미리 볼 수 있다는 것이 얼마나 큰 편의인지 알 수 있다.


테스트 예제

리사이클러뷰를 이용해 여러 개의 개체를 만들었다. 뷰가 맨 처음 만들어지는 시점에 시간을 기록해놓고, 각각의 뷰를 그리는 데까지 필요한 누적 시간을 밀리세컨드로 표기했다.


  • 실험기기 : 삼성 SM-G950N (갤럭시S8)
  • OS : 안드로이드 9.0
  • 실험방법 : 앱 실행 후 모든 프로세스 종료를 하고나서 재실행


<!-- activity_main.xml -->

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tvItemSquare"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="8dp"
        android:gravity="center"
        android:textColor="#ffffff"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
<!-- item_square.xml -->

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tvItemSquare"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="8dp"
        android:gravity="center"
        android:textColor="#ffffff"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val lm = GridLayoutManager(this, 3)
        val adapter = MyAdapter()

        rvMain.layoutManager = lm
        rvMain.adapter = adapter
    }
}

메인 뷰와 아이템뷰 레이아웃, 그리고 메인 액티비티는 두 테스트 모두 동일한 코드를 사용했다. background 리소스와 어댑터에서 테스트 과정을 구분했다.

xml에서 뷰를 만드는 과정에서는 별도의 drawable resource 파일을 background로 지정해주었다. 그리고 코드로 직접 그릴 때에는 drawable을 반환받는 function을 사용했다.


테스트1 - xml 파일로 그릴 때

<!-- background.xml -->
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
    <solid android:color="#333399" />
</shape>
class MyAdapter : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
    private val startTime: Date = Calendar.getInstance().time
    private var list: ArrayList<Int> = arrayListOf()
    private lateinit var context: Context

    init {
        for (i in 0..11) { list.add(i) }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        context = parent.context
        return MyViewHolder(LayoutInflater.from(context).inflate(R.layout.item_square, parent, false))
    }

    override fun getItemCount(): Int {
        return list.size
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val gapTime = (Calendar.getInstance().time.time - startTime.time)
        holder.bind(list[position], gapTime)
    }

    inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(i: Int, gapTime: Long) {
            itemView.tvItemSquare.apply{
                background = context.getDrawable(R.drawable.background)
                text = gapTime.toString()
            }
        }
    }
}


테스트2 - 코드 그릴 때

class MyAdapter : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
    private val startTime: Date = Calendar.getInstance().time
    private var list: ArrayList<Int> = arrayListOf()

    init {
        for (i in 0..11) { list.add(i) }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        return MyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_square, parent, false))
    }

    override fun getItemCount(): Int {
        return list.size
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val gapTime = (Calendar.getInstance().time.time - startTime.time)
        holder.bind(list[position], gapTime)
    }

    // drawable 반환받는 함수 추가
    fun getDrawable(): GradientDrawable {
        val shape = GradientDrawable()
        shape.shape = GradientDrawable.RECTANGLE
        shape.setColor(Color.parseColor("#333399"))
        return shape
    }

    inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(i: Int, gapTime: Long) {
            itemView.tvItemSquare.apply{
                background = getDrawable()
                text = gapTime.toString()
            }
        }
    }
}


테스트 결과

테스트1 - xml 파일로 그릴 때


테스트2 - 코드로 그릴 때


두 실험에서 모두 Adapter가 초기화 된 이후부터 마지막 사각형이 그려질 때까지 200ms 정도의 시간이 걸렸다. 간단한 구조의 뷰에서는 이 정도면 차이가 없다고 봐도 무방한 것 같다.

xml로 그린 뷰를 코드에서 동적으로 변환하는 것은 상당히 번거롭다. 색상 하나를 바꾸는 것에도 몇 줄의 코드가 필요하다. 혹은 사각형이 아닌 더 복잡한 뷰는 코드로만 구현할 수도 있다. 그렇지 않다면 각 뷰 마다, 각 수치마다 xml 파일을 만들어내야 할 것이다. 뷰에 계산을 많이 해야 할 것 같다 싶으면, 애초에 코드로 그릴 생각을 하는 게 좋아 보인다.

위의 예제를 조금 변경해서 각 뷰에 그라데이션 색상을 입히는 응용 예제도 만들어보았다.


/* MyAdapter.kt */
private var list: ArrayList<Int> = arrayListOf()

init {
    var red = 255
    var green = 255
    var blue = 255

    for (i in 0..11) {
        red -= 20
        green -= 20
        blue -= 10
        val c = Color.rgb(red, green, blue)

        list.add(c)
    }
}

...

fun getDrawable(color: Int): GradientDrawable {
    val shape = GradientDrawable()
    shape.shape = GradientDrawable.RECTANGLE
    shape.setColor(color) // 입력받은 색상으로 지정
    return shape
}


물론 xml 파일에서도 충분히 구현할 수 있지만, 로직 상태값에 따라 뷰를 바꾸어야 한다거나 드로어블을 그리는 노력까지 미리 계산하여 상황에 맞게 활용할 수 있어야겠다.



결론

검색해서 알게되는 정보랑, 그걸 굳이 굳이 직접 확인해서 얻는 정보는 체감이 확실히 다른 것 같다. 그냥 내 삽질에 대한 정신승리일 수도 있지만, 양쪽에서 모두 ‘차이없다’라는 결론이 나왔으니 내 궁금증 하나는 시원하게 풀려서 뿌듯하다.

xml은 파일이 생성되어 있으니까 더 빠를거라는 (뇌피셜)예상과는 달리, 성능 차이가 거의 없는 점을 깨달았지만, 뷰가 단순해서 그럴 거라는 또다른 명제가 생겼다. 코드로 xml을 제어하기 까다로울 정도의 복잡한 뷰를 다시 그려서 재실험 해봐야 더 명확한 결과를 얻을 수 있을 것 같다.

오늘의 영단어
gradation : 1. 단계 2. 그러데이션 3. 등급
gradient : 1. 경사도 2. 비탈 3. 기울기


References

  • https://stackoverflow.com/questions/54032921/performance-android-views-generated-programmatically-vs-xml-views