ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Compose] LazyVerticalGrid 버벅거림 현상 해결해보기
    Android 2025. 4. 25. 18:12
    728x90
    반응형

     

     

    테스트 기기 : 갤럭시 s8(Android 9)

     

    tab layout과 compose의 LazyVerticalGrid를 사용해 ui를 만들었다.

    포스팅의 이미지가 여러 개 보이는데, 문제는 다음과 같았다

     

    1. 이미지 로드 속도가 느리다

    2. 스크롤을 할 때 버벅거리는 현상이 발생한다

     

    이미지의 경우 coil을 사용해 불러오고 있었고

    스크롤을 할 때 버벅거림이 생기는 것은 불러올 포스팅의 갯수가 많아지면 점점 더 심해졌다

    사용자들이 만약 이 앱을 사용한다면 버벅여서 불편함을 겪었을 것이다(나같으면 신고했음)

     

    처음에는 이러한 문제들 때문에 이미지를 업로드할 때 bitmap.compress를 사용해서 이미지 파일의 크기를 70% 수준으로 압축해서 줄였으나 소용이 없었다^^...

    왜냐하면 이미지 해상도(resolution)과 이미지 파일 크기(size), 이미지 압축(compress)는 다르기 때문이다

    이미지 해상도(Resolution)

    이미지를 이루는 pixel(RGB + Alpha(투명도))의 갯수

     

    이미지 파일 크기

    이미지 파일에 저장된 픽셀 정보들이 차지하는 용량 

     

    이미지 압축(compress)

    이미지 파일에 저장된 픽셀 정보들을 저장하는 방식을 변경해 파일의 크기를 줄임

    픽셀 정보들을 저장하는 방식을 변경하는 것이기 때문에 해상도는 그대로임(픽셀의 갯수는 그대로임) 

     

     

    메모리 덤프를 떠보고 네이티브 사이즈를 보았는데 뭔가 엄청 커보여서 이미지 파일들을 살펴보기 시작했다

    이미지 파일 크기는 괜찮은 거 같아서 storage에 업로드된 이미지를 다운로드 받아보았다

    근데 이상하게 해상도가 너무 컸다

     

    다른 이미지들도 살펴보니 이미지 크기가 3000 * 4000대였다

    이미지 파일의 용량과는 관계없이 이미지가 다 저렇게 컸다...

     

     

    근데 내가 여기서 의문이 드는 것은 이미지 파일이 화면에 보일 때는 작은데 왜 느리게 로드되는지였다

    내가 무언갈 놓치고 있나하는 생각이 들어 검색해보던 중 아주 도움이 되는 글을 발견했다

     

    https://istiakmorsalin.medium.com/practical-image-processing-on-android-a195054f6795

     

    PRACTICAL IMAGE PROCESSING ON ANDROID

    Hello, Today, I am going to write about image processing on Android. Obviously, This is not new to you But I am hoping the audience of my…

    istiakmorsalin.medium.com

     

    https://medium.com/@n20/title-how-images-are-stored-and-displayed-in-android-a-simple-easy-guide-png-url-vector-44bc172c11c8

     

    Title: How Images Are Stored and Displayed in Android: A Simple, Easy Guide : PNG / URL / Vector

    Ever wonder how Android turns image files like .png, .jpg, or .svg into something you see on your phone’s screen? It might seem like magic…

    medium.com

     

    - 로컬에 있는 이미지의 경우 

    기기내에 있는 이미지를 가져온 후, 이 이미지 파일(png, jpg등)을 bitmap으로 압축해제(decode)해서 메모리에 올림 -> 메모리에 올라간 비트맵을 GPU를 사용해 화면에 보여줌

     

    - url 등으로 받아오는 이미지(로컬에 있는 이미지가 아닌 경우)의 경우

    백그라운드로 이미지를 다운로드 함(coil이나 glide같은 라이브러리 사용) -> 다운로드 한 이미지 캐싱 -> bitmap으로 decode하고 메모리에 올려 GPU를 사용해 화면에 보여줌

     

    여기서 bitmap이 뭔지 알아야한다

     

    bitmap

    픽셀들이 2차원 배열 형태(width, height)로 있는 것을 말하며, 픽셀 하나당 4byte를 가지고 있다(포맷마다 다를 수있음(SRGB 포맷 기준) red, green, blue, alph(투명도))

    이러한 픽셀들이 모여서 이미지나 영상을 나타낼 수 있다.

    그리고 비트맵은 기본적으로 압축을 하지 않기 때문에 전체 크기(가로 x 세로)에 해당하는 픽셀 정보를 다 저장해야함 -> 메모리 용량을 많이 차지한다고 한다.

     

    그러니까 예를 들면 이미지의 크기가 3040 * 4080이라면 

    3,040 * 4,080 = 12,403,200

    12,403,200 * 4 = 49,612,800

    49,612,800 Bytes

    49,612,800 / 1024 = 48,450KB

    48,450kb / 1024 = 47.314MB

     

    그러면 대충 이미지 하나당 50MB를 메모리에서 차지하고 있다는 얘기가 된다

     

    비트맵으로 decode하고 나서 메모리에 저렇게 많은 공간을 차지하고 있는줄은 몰랐다

    어쩐지 맨 처음 올린 gif 파일처럼 해당 탭에만 가면 자꾸 메모리가 엄청 튀더라니만

     

    그러면 decode할 때 리사이징을 통해 이미지 크기 줄여줘야겠다는 생각이 들었다

    왜냐면 이미지 크기가 작아지면 이미지 파일에 저장될 픽셀들이 적어지기 때문이다

     

    나는 Coil을 통해 이미지를 가져오고 있었고 원본 사이즈대로 가져오고 있었기 때문에 아래의 코드처럼 수정해 리사이징해주었다

    // 수정 전
    class CoilImgRequest {
        companion object {
            fun getImgRequest(sourceImg: String) =
                ImageRequest.Builder(ChakSaiApplication.getInstance())
                    .data(if (sourceImg.isNotBlank()) sourceImg else R.drawable.err_img)
                    .size(Size.ORIGINAL)
                    .error(R.drawable.err_img)
                    .fallback(R.drawable.err_img)
                    .crossfade(true)
                    .build()
     }
    }
    
    
    // 수정 후 
    class CoilImgRequest {
        companion object {
            fun getImgRequest(sourceImg: String, size: Size = Size.ORIGINAL) =
                ImageRequest.Builder(ChakSaiApplication.getInstance())
                    .data(if (sourceImg.isNotBlank()) sourceImg else R.drawable.err_img)
                    .size(size)
                    .error(R.drawable.err_img)
                    .fallback(R.drawable.err_img)
                    .crossfade(true)
                    .build()
     }
    }

     

    그리고 이미지를 로드하는 쪽에서 width, height를 지정해주었다

    @Composable
    fun GridNoteList(
        posts: List<PostWithUser>,
        onNavigationToDetailPost: (postId: String) -> Unit,
    ) {
        LazyVerticalGrid(
            columns = GridCells.Fixed(2),
            contentPadding = PaddingValues(AppTheme.size.small)
        ) {
            items(
               items = posts,
                key = {post -> post.id}
            ) {post ->
                ListItem(post, onNavigationToDetailPost)
            }
        }
    }
    
    @Composable
    fun ListItem(data: PostWithUser,  onNavigationToDetailPost: (postId: String) -> Unit,) {
        Card(
            modifier = Modifier
                .width(dimensionResource(R.dimen.cardMedium))
                .height(dimensionResource(R.dimen.cardMedium))
                .padding(dimensionResource(R.dimen.cardPadding)),
            shape = RoundedCornerShape(AppTheme.size.normal),
            elevation = CardDefaults.cardElevation(
                defaultElevation = AppTheme.size.xs,
                pressedElevation = AppTheme.size.small
            ),
            onClick = { onNavigationToDetailPost(data.id) }
        ) {
            AsyncImage(
                modifier = Modifier.fillMaxSize(),
                model = CoilImgRequest.getImgRequest(data.image, size = Size(160, 160)),
                contentScale = ContentScale.Crop,
                contentDescription = stringResource(R.string.post_img_desc),
            )
        }
    }

     

     

     

     

    네이티브쪽 메모리 차지도 많이 줄었고 버벅거림도 사라졌다!

    그리고 메모리 덤프도 보니까 훨씬 안정적으로 바뀌었다

    (이전에는 1GB까지 메모리가 튀기도 했음;;)

     

     

    해결방법은 너무나도 간단했지만 어디서 메모리가 많이 나가고 그 원인을 찾아가는 것이 재미있었다

    그리고 단순히 이미지를 사용하는 것 뿐만 아니라 이미지가 어떻게 로드되는지 프로세스를 알고 있어야 이런 문제도 해결할 수 있겠다는 생각이 들었다

    기본을 더욱더 갈고 닦아야겠다ㅎ

    반응형
Designed by Tistory.