Android/Today I Learned

Android Font 고군분투기

Nanamare 2021. 12. 6. 00:34
728x90

몇일전 기능 구현후 테스트 하는 과정에서 Font 관련 함수에서 특정한 상황에 크래시가 나는 상황을 경험했습니다.

 

멋진 팀원들의 도움을 받아 어떤식으로 해결하였는지 공유합니다.

 

크게

1. 왜 크래시가 났는지?

2. 왜 간헐적으로 났는지?

3. 크래시가 나지 않았을 때는 어떤 클래스에서 이를 해소해주고 있었는지?

에 대한 내용으로 구성됩니다.

 

문제가 생긴 함수는 아래와 비슷한 식의 Font 의 id 를 가져와서 적용해주는 코드였습니다.

// 예시 함수
fun TextView.setFont(@FontRes fontRes: Int) = apply {
    typeface = ResourcesCompat.getFont(context, fontRes)
}

 

크래시 재현 상황은 앱 시작하자마자 특정 화면으로  이동하거나, 딥링크 등을 통해 다이렉트로 이동하면 ResourceCompat 의 loadFont 함수에서 Font resource ID 를 찾을 수 없다는 에러를 던지고 있었습니다.

ResoucesCompat loadFont 함수

 

회사에서는 직접 만든 Qanda Font(줄여서 "콴폰"이라고 하겠습니다) 를 서버에서 다운 받아 폴더에 저장하고 사용하고 있기 때문에, CustomFontProvider 를 통해 Font 를 다운받고 완료되었을 때 Font 를 적용하도록 처리 되어야 합니다. 

 

 해야하는 프로세스를 간략하게 정리하면,

1. "콴폰" xml 정의

2. CustomFontProvider 를 통해 "콴폰"이 필요하다고 Query 시작

3. Query 가 시작되면 서버를 통해 "콴폰" 다운로드 진행

4. "콴폰" 다운로드가 완료되면 원하는 TextView 에 Font 를 적용해준다.

정도가 될 것 같습니다.

 

그리고 "콴폰" 을 다운 받는 부분까지는 문제 없이 진행되었다고 가정합니다. 3번까지의 과정이 궁금하다면 Github 에서 제가 다니는 회사를 검색하여 잘 찾아보시면 제공하고 있습니다 :)

 

하지만 위의 문제가 되는 코드에서 사용하고 있는 ResourcesCompat getFont 함수 내부를 보면, fontCallback 을 null 로 보내주고 있는데, ResourceCompat loadFont 를 보면 type == null 이면서 fontCallback == null 이면서 isCachedOnly(Cached 된 font 에서만 가져올 것인지 체크) 가 false 인 경우 에 NotFoundException 을 던지게 되어있는데 fontCallback 에는 null isCachedOnly 는 false 를 넘기고 있기 때문에 typeface == null 해당 조건으로만 판단하게 되어있습니다. 

 

따라서 Font 가 없다면 비동기적으로 다운받아 적용해주는 것이 아닌, Resource.getFont 함수를 호출할 때, 당시에 Font 가 저장되어 있지 않으면 결과적으로 NotFoundException 을 던지게 됩니다.

 

 

원인을 찾음으로써 왜 간헐적으로 크래시가 났는지도 알게되었습니다.

 해당 크래시가 나는 화면을 처음으로 가지 않고, 다른 화면(크래시가 나는 화면에서 사용하는 Font 가 있는 화면)들을 이용하다가 오게 되면 이미 Font 를 다운 받아져 있기 때문에 Memory Cache(LRU) 에서 찾아서 사용하고 있음을 찾을 수 있었습니다.

ResourceCompat loadFont 내부
TypefaceCompat findFromCache 함수

 

sTypeFaceCache 기본 값으로는 최대 16개로 되어 있었고 따로 maxSize 를 변경하지 않았기 때문에 16개 이상이 넘으면서 최근에 사용된 적이 없게 된다면 문제가 생길 수 있다는 것도 알게 되었습니다.

 

그러면서 문득 생각이 들었습니다.

 

결국은 다른 화면도 첫번째로 들어가는 순간이 있고 그때마다 크래시가 나야하는데, 나지 않는 이유가 있을텐데 어떤 클래스가 그런 작업을 해주고 있는 것인가 ?

 

이미 경험적으로 알고 계신 팀원도 계셨고, 그 이유에 대해 명확하게 알고 계신 분도 계셨습니다

핵심은 TextAppearance 클래스에 있었습니다. 회사에서 Xml 에서 TextView 를 사용할 때는 대부분 

android:textAppearance="?textAppearanceBody..."
style=@style/CustomStyle....

와 같은 방법으로 사용하거나, 직접 코드에서 사용하는 경우 

val type = TypedValue()
context.theme.resolveAttribute(R.attr.textAppearanceBody..., type, true)
TextViewCompat.setTextAppearance(this, type.resourceId)

 와 같은 식으로 많이 사용하고 있습니다.

 

그리고 추가적으로 Xml 에서 TextView 를 사용하게 되면 테마가 무엇이냐에 따라 MaterialComponentsViewInflater(AppCompatViewInflater) 가 호출되고 해당 클래스의 구현체인 AppCompatDelegateImpl 의 createView 가 호출되면서 그 안에서 TextView 를 AppCompatTextView 로 변경 하고 있습니다. (이부분은 드로이드 나이츠 2021 Pluu 님의 영상 강추 합니다)

AppCompatViewInflater createView 함수
TextView -> AppCompatTextView 로 변경

 

 

이제 거의 다 왔습니다.

결과적으로 AppCompatTextView 의 어디에서 Font 를 비동기적으로 설정해주고 있는지만 알아내면 됩니다.

AppCompatTextView  setTextAppearance 함수

 

 

내부에 mTextelper(AppCompatTextHelper Class) 라는 것을 가지고 있습니다. 

 

AppCompatTextHelper onSetTextAppearance 함수

 

updateTypeFaceAndStyle 함수 이름에서 왠지 해주고 있지 않을까 라는 느낌이 강력하게 느껴집니다.

 

 

AppCompatTextHelper updateTypeFaceAndStyle 함수

replayCallback 이라는 FontCallback 을 만들고 있습니다. 거의 이제 확신으로 다가 옵니다..!

만들어진 replayCallback 을 getFont 함수에 넘겨주고 있습니다.

 

 

TintTypedArray getFont 함수

내부에서 ResourcesCompat.getFont 를 호출하면서 fontCallback 을 다시 넘겨주고 있네요.

 

 

내부에서 loadFont 함수를 통해 typeface 를 반환해주고 있습니다. 아니라면 NotFoundException 처리를 해주는 곳도 보이네요.

함수 하나만 더 보면 확실한 결론을 알 수 있을 것 같습니다.

ResourcesCompat loadFont

 

아래의 함수가 마지막 입니다.

저희가 생각했던 기능들이 모두 들어있습니다.

findFromCache 를 통해 캐시가 히트 하면 fontCallBack.callbackSuccessAsync 메서드를 통해 알려주고 있습니다.

xml 에서 호출되었다면, FontResourceParserCompat 을 통해 familyEntry 를 만들고 TypeCompat.createFromResourcesFamilyXml 메서드를 사용하여 바로 typeface 를 반환하고 있습니다.

이제 정말로 기대하고 있던 코드가 나오는 부분입니다

위의 조건들도 아니라면, TypefaceCompat.createFromResourcesFontFile 즉 FontFile 을 통해 typeface 를 가져오고, fontCallBack.callbackSuccessAsync 를 통해 결과를 알려주고 있습니다.

 

비로소 확인할 부분은 모두 확인했고, 이번 고군분투를 하며 느낀점이 있다면, 

 

ResourcesCompat 의 getFont 함수에는 동기적으로, 비동기적으로 사용할 수 있도록 제공하고 있습니다. 따라서 setTextAppearance 를 사용하기 어려운 상황이나 혹은 사용하지 않고, getFont 메서드를 직접 사용하여 Font 를 적용할 수도 있습니다.

ResourcesCompat getFont 비동기 방식
ResourceCompat getFont 의 동기 방식 

 

 

이것으로 글을 마치고 도움을 주신 팀원분들에게 감사함을 전합니다.

 

이번 사건이 앞으로 내부 코드를 더욱 자주 보게 되는 계기가 되었으면 좋겠습니다.

728x90