Android 앱 최적화를 위한 팁 및 도구

게시 됨: 2022-03-11

Android 기기에는 코어가 많기 때문에 부드러운 앱을 작성하는 것은 누구에게나 간단한 작업이죠? 잘못된. Android의 모든 작업은 다양한 방식으로 수행될 수 있으므로 최상의 옵션을 선택하는 것이 어려울 수 있습니다. 가장 효율적인 방법을 선택하려면 내부에서 무슨 일이 일어나고 있는지 알아야 합니다. 다행스럽게도 현재 상황을 측정하고 설명하여 병목 현상을 찾는 데 도움이 되는 도구가 많이 있으므로 자신의 느낌이나 후각에 의존할 필요가 없습니다. 적절하게 최적화되고 부드러운 앱은 사용자 경험을 크게 개선하고 배터리를 덜 소모합니다.

최적화가 실제로 얼마나 중요한지 고려하기 위해 먼저 몇 가지 수치를 살펴보겠습니다. Nimbledroid 게시물에 따르면 사용자의 86%(저 포함)가 성능 저하로 인해 한 번만 사용한 앱을 제거했습니다. 일부 콘텐츠를 로드하는 경우 사용자에게 표시할 시간이 11초 미만입니다. 세 번째 사용자에게만 더 많은 시간이 주어집니다. 그로 인해 Google Play에서 많은 나쁜 리뷰를 받을 수도 있습니다.

더 나은 앱 빌드: Android 성능 패턴

사용자의 인내심을 테스트하는 것이 제거의 지름길입니다.
트위터

모든 사용자가 계속해서 가장 먼저 알아차리는 것은 앱의 시작 시간입니다. 다른 Nimbledroid 게시물에 따르면 상위 100개 앱 중 40개는 2초 이내에 시작하고 70개는 3초 이내에 시작합니다. 따라서 가능하면 일반적으로 가능한 한 빨리 일부 콘텐츠를 표시하고 백그라운드 확인 및 업데이트를 약간 지연해야 합니다.

섣부른 최적화는 모든 악의 근원이라는 것을 항상 기억하십시오. 또한 마이크로 최적화에 너무 많은 시간을 낭비해서는 안 됩니다. 자주 실행되는 코드를 최적화하면 가장 큰 이점을 얻을 수 있습니다. 예를 들어, 여기에는 모든 프레임(이상적으로는 초당 60회)을 실행하는 onDraw() 함수가 포함됩니다. 그리기는 가장 느린 작업이므로 필요한 만큼만 다시 그리십시오. 이에 대한 자세한 내용은 나중에 설명합니다.

성능 팁

이론으로 충분합니다. 여기에 성능이 중요한 경우 고려해야 할 몇 가지 목록이 있습니다.

1. 문자열 대 StringBuilder

문자열이 있고 어떤 이유로 더 많은 문자열을 10,000번 추가하고 싶다고 가정해 봅시다. 코드는 다음과 같을 수 있습니다.

 String string = "hello"; for (int i = 0; i < 10000; i++) { string += " world"; }

Android Studio Monitors에서 일부 문자열 연결이 얼마나 비효율적인지 확인할 수 있습니다. 수많은 가비지 컬렉션(GC)이 진행 중입니다.

문자열 대 StringBuilder

이 작업은 Android 5.1.1이 있는 상당히 좋은 기기에서 약 8초가 걸립니다. 동일한 목표를 달성하는 보다 효율적인 방법은 이와 같이 StringBuilder를 사용하는 것입니다.

 StringBuilder sb = new StringBuilder("hello"); for (int i = 0; i < 10000; i++) { sb.append(" world"); } String string = sb.toString();

동일한 장치에서 이는 5ms 이내에 거의 즉시 발생합니다. CPU 및 메모리 시각화는 거의 완전히 평평하므로 이 개선 사항이 얼마나 큰지 상상할 수 있습니다. 그러나 이 차이를 달성하기 위해 10,000개의 문자열을 추가해야 했으며, 이는 아마도 자주 하지 않을 것입니다. 따라서 한 두 개의 문자열만 추가하는 경우에는 개선 사항이 표시되지 않습니다. 그건 그렇고, 당신이 할 경우 :

 String string = "hello" + " world";

내부적으로 StringBuilder로 변환되므로 제대로 작동합니다.

문자열을 연결하는 첫 번째 방법이 왜 그렇게 느린지 궁금할 것입니다. String은 immutable이기 때문에 한번 생성되면 변경할 수 없기 때문입니다. 문자열 값을 변경한다고 생각하더라도 실제로는 새 값으로 새 문자열을 생성하는 것입니다. 다음과 같은 예에서:

 String myString = "hello"; myString += " world";

메모리에 저장되는 것은 "hello world" 문자열 1개가 아니라 실제로는 문자열 2개입니다. 문자열 myString에는 예상대로 "hello world"가 포함됩니다. 그러나 값이 "hello"인 원래 문자열은 참조 없이 여전히 살아 있으며 가비지 수집을 기다리고 있습니다. 이것이 문자열 대신 char 배열에 암호를 저장해야 하는 이유이기도 합니다. 암호를 문자열로 저장하면 예측할 수 없는 시간 동안 다음 GC까지 사람이 읽을 수 있는 형식으로 메모리에 유지됩니다. 위에서 설명한 불변성으로 돌아가서 문자열을 사용한 후 다른 값을 할당하더라도 문자열은 메모리에 남아 있습니다. 그러나 암호를 사용한 후 char 배열을 비우면 모든 곳에서 사라집니다.

2. 올바른 데이터 유형 선택

코드 작성을 시작하기 전에 컬렉션에 사용할 데이터 유형을 결정해야 합니다. 예를 들어 Vector 또는 ArrayList 를 사용해야 합니까? 글쎄, 그것은 당신의 사용 사례에 달려 있습니다. 한 번에 하나의 스레드만 작업하도록 허용하는 스레드로부터 안전한 컬렉션이 필요한 경우 동기화되므로 Vector 를 선택해야 합니다. 다른 경우에는 벡터를 사용해야 하는 특별한 이유가 없는 한 ArrayList 를 고수해야 합니다.

유니크한 오브제가 있는 컬렉션을 원하신다면 어떤가요? 글쎄, 당신은 아마 Set 를 선택해야합니다. 설계상 중복을 포함할 수 없으므로 스스로 처리할 필요가 없습니다. 여러 유형의 세트가 있으므로 사용 사례에 맞는 세트를 선택하십시오. 고유 항목의 간단한 그룹의 경우 HashSet 을 사용할 수 있습니다. 항목이 삽입된 순서를 유지하려면 LinkedHashSet 을 선택하십시오. TreeSet 은 항목을 자동으로 정렬하므로 정렬 방법을 호출할 필요가 없습니다. 또한 정렬 알고리즘을 생각할 필요 없이 항목을 효율적으로 정렬해야 합니다.

데이터가 지배적입니다. 올바른 데이터 구조를 선택하고 항목을 잘 구성했다면 알고리즘은 거의 항상 자명합니다. 알고리즘이 아니라 데이터 구조가 프로그래밍의 핵심입니다.
— Rob Pike의 5가지 프로그래밍 규칙

정수 또는 문자열을 정렬하는 것은 매우 간단합니다. 그러나 일부 속성을 기준으로 클래스를 정렬하려면 어떻게 해야 합니까? 당신이 먹는 음식의 목록을 작성하고 음식의 이름과 타임스탬프를 저장한다고 가정해 봅시다. 가장 낮은 것부터 가장 높은 것까지 타임스탬프를 기준으로 식사를 어떻게 정렬하시겠습니까? 다행히도 매우 간단합니다. Meal 클래스에서 Comparable 인터페이스를 구현하고 compareTo() 함수를 재정의하는 것으로 충분합니다. 가장 낮은 타임스탬프에서 가장 높은 순서로 식사를 정렬하려면 다음과 같이 작성할 수 있습니다.

 @Override public int compareTo(Object object) { Meal meal = (Meal) object; if (this.timestamp < meal.getTimestamp()) { return -1; } else if (this.timestamp > meal.getTimestamp()) { return 1; } return 0; }

3. 위치 업데이트

사용자의 위치를 ​​수집하는 많은 앱이 있습니다. 이를 위해서는 유용한 기능이 많이 포함된 Google 위치 서비스 API를 사용해야 합니다. 사용법에 대한 글이 따로 있으니 반복하지 않겠습니다.

성능 관점에서 몇 가지 중요한 점을 강조하고 싶습니다.

우선 가장 정확한 위치만 필요에 따라 사용하세요. 예를 들어 일기 예보를 수행하는 경우 가장 정확한 위치가 필요하지 않습니다. 네트워크를 기반으로 매우 거친 영역을 얻는 것이 더 빠르고 배터리 효율이 높습니다. 우선순위를 LocationRequest.PRIORITY_LOW_POWER 로 설정하여 이를 달성할 수 있습니다.

setSmallestDisplacement() 라는 LocationRequest 함수를 사용할 수도 있습니다. 미터로 설정하면 지정된 값보다 작은 경우 앱에서 위치 변경에 대한 알림을 받지 않습니다. 예를 들어 주변에 식당이 있는 지도가 있고 최소 변위를 20미터로 설정한 경우 사용자가 방에서 그냥 걸어다니는 경우 앱은 식당 확인을 요청하지 않습니다. 어쨌든 근처에 새로운 레스토랑이 없기 때문에 요청은 쓸모가 없습니다.

두 번째 규칙은 필요할 때만 위치 업데이트를 요청하는 것입니다. 이것은 아주 자명합니다. 일기 예보 앱을 실제로 구축하고 있다면 정확한 예보가 없을 수 있으므로 몇 초마다 위치를 요청할 필요가 없습니다(필요한 경우 저에게 연락). 기기가 위치에 대해 앱을 업데이트하는 데 필요한 간격을 설정하기 위해 setInterval() 함수를 사용할 수 있습니다. 여러 앱이 계속 사용자의 위치를 ​​요청하는 경우 더 높은 setInterval() 설정이 있더라도 모든 앱은 새로운 위치 업데이트마다 알림을 받습니다. 앱이 너무 자주 알림을 받는 것을 방지하려면 항상 setFastestInterval() 을 사용하여 가장 빠른 업데이트 간격을 설정해야 합니다.

마지막으로 세 번째 규칙은 필요한 경우에만 위치 업데이트를 요청하는 것입니다. x초마다 지도에 일부 주변 개체를 표시하고 앱이 백그라운드로 실행되는 경우 새 위치를 알 필요가 없습니다. 어쨌든 사용자가 지도를 볼 수 없다면 지도를 업데이트할 이유가 없습니다. 적절한 경우, 가급적이면 onPause() 에서 위치 업데이트 수신을 중지해야 합니다. 그런 다음 onResume() 에서 업데이트를 재개할 수 있습니다.

4. 네트워크 요청

앱이 데이터를 다운로드하거나 업로드하기 위해 인터넷을 사용할 가능성이 높습니다. 그렇다면 네트워크 요청 처리에 주의를 기울여야 하는 몇 가지 이유가 있습니다. 그 중 하나는 많은 사람들에게 매우 제한된 모바일 데이터이며 낭비해서는 안됩니다.

두 번째는 배터리입니다. WiFi와 모바일 네트워크는 둘 다 너무 많이 사용하면 상당히 많은 양을 소비할 수 있습니다. 1kb를 다운로드한다고 가정해 보겠습니다. 네트워크 요청을 하려면 셀룰러 또는 WiFi 라디오를 깨워야 데이터를 다운로드할 수 있습니다. 그러나 라디오는 수술 직후에 잠들지 않습니다. 장치 및 이동 통신사에 따라 약 20-40초 동안 상당히 활성 상태를 유지합니다.

네트워크 요청

그래서 당신은 그것에 대해 무엇을 할 수 있습니까? 일괄. 몇 초마다 라디오를 깨우지 않으려면 사용자가 앞으로 몇 분 동안 필요할 수 있는 항목을 미리 가져옵니다. 적절한 일괄 처리 방법은 앱에 따라 매우 역동적이지만, 가능하다면 사용자에게 필요할 수 있는 데이터를 다음 3-4분 안에 다운로드해야 합니다. 사용자의 인터넷 유형 또는 충전 상태에 따라 배치 매개변수를 편집할 수도 있습니다. 예를 들어 사용자가 충전하는 동안 Wi-Fi에 연결되어 있으면 사용자가 배터리가 부족한 모바일 인터넷에 연결되어 있는 경우보다 훨씬 더 많은 데이터를 미리 가져올 수 있습니다. 이러한 모든 변수를 고려하는 것은 극히 소수의 사람들만이 할 수 있는 힘든 일이 될 수 있습니다. 다행히도 GCM Network Manager가 구출해 줍니다!

GCM Network Manager는 사용자 정의 가능한 속성이 많이 포함된 정말 유용한 클래스입니다. 반복 작업과 일회성 작업을 모두 쉽게 예약할 수 있습니다. 반복 작업에서 가장 낮은 반복 간격과 가장 높은 반복 간격을 설정할 수 있습니다. 이렇게 하면 요청뿐만 아니라 다른 앱의 요청도 일괄 처리할 수 있습니다. 라디오는 일정 기간마다 한 번만 깨워야 하며, 켜져 있는 동안 대기열에 있는 모든 앱이 다운로드 및 업로드해야 하는 것을 업로드합니다. 이 관리자는 또한 장치의 네트워크 유형 및 충전 상태를 알고 있으므로 그에 따라 조정할 수 있습니다. 이 기사에서 자세한 내용과 샘플을 찾을 수 있으므로 확인하는 것이 좋습니다. 예제 작업은 다음과 같습니다.

 Task task = new OneoffTask.Builder() .setService(CustomService.class) .setExecutionWindow(0, 30) .setTag(LogService.TAG_TASK_ONEOFF_LOG) .setUpdateCurrent(false) .setRequiredNetwork(Task.NETWORK_STATE_CONNECTED) .setRequiresCharging(false) .build();

그건 그렇고, Android 3.0부터 메인 스레드에서 네트워크 요청을 하면 NetworkOnMainThreadException 이 발생합니다. 다시는 그렇게 하지 말라고 분명히 경고할 것입니다.

5. 반성

리플렉션은 클래스와 개체가 자체 생성자, 필드, 메서드 등을 검사하는 기능입니다. 특정 OS 버전에서 주어진 방법을 사용할 수 있는지 확인하기 위해 일반적으로 이전 버전과의 호환성을 위해 사용됩니다. 그 목적으로 리플렉션을 사용해야 하는 경우 리플렉션을 사용하는 것이 매우 느리기 때문에 응답을 캐시해야 합니다. 의존성 주입을 위한 Roboguice와 같이 널리 사용되는 일부 라이브러리도 Reflection을 사용합니다. 그렇기 때문에 Dagger 2를 선호해야 하는 이유입니다. 리플렉션에 대한 자세한 내용은 별도의 게시물을 확인하실 수 있습니다.

6. 오토복싱

Autoboxing 및 Unboxing은 기본 유형을 Object 유형으로 또는 그 반대로 변환하는 프로세스입니다. 실제로는 int를 Integer로 변환하는 것을 의미합니다. 이를 달성하기 위해 컴파일러는 내부적으로 Integer.valueOf() 함수를 사용합니다. 변환 속도가 느릴 뿐만 아니라 개체는 기본 요소보다 훨씬 더 많은 메모리를 사용합니다. 몇 가지 코드를 살펴보겠습니다.

 Integer total = 0; for (int i = 0; i < 1000000; i++) { total += i; }

평균적으로 500ms가 걸리지만 자동 박싱을 피하기 위해 다시 작성하면 속도가 크게 빨라집니다.

 int total = 0; for (int i = 0; i < 1000000; i++) { total += i; }

이 솔루션은 25배 빠른 약 2ms로 실행됩니다. 내 말을 못 믿겠다면 시험해 보십시오. 숫자는 장치마다 분명히 다르지만 여전히 훨씬 빠릅니다. 또한 최적화를 위한 매우 간단한 단계이기도 합니다.

좋아요, 아마도 이렇게 자주 Integer 유형의 변수를 생성하지 않을 것입니다. 그러나 피하기가 더 어려운 경우는 어떻습니까? 지도에서와 같이 Map<Integer, Integer> 와 같은 Objects를 사용해야 하는 곳은 어디입니까? 많은 사람들이 사용하는 솔루션을 보십시오.

 Map<Integer, Integer> myMap = new HashMap<>(); for (int i = 0; i < 100000; i++) { myMap.put(i, random.nextInt()); }

맵에 100k 임의의 int를 삽입하는 데 약 250ms가 걸립니다. 이제 SparseIntArray로 솔루션을 살펴보십시오.

 SparseIntArray myArray = new SparseIntArray(); for (int i = 0; i < 100000; i++) { myArray.put(i, random.nextInt()); }

이것은 훨씬 적게, 대략 50ms가 걸립니다. 복잡한 작업을 수행할 필요가 없고 코드도 읽을 수 있기 때문에 성능을 향상시키는 더 쉬운 방법 중 하나이기도 합니다. 첫 번째 솔루션으로 깨끗한 앱을 실행하는 동안 13MB의 메모리가 필요했지만 원시 int를 사용하면 7MB 미만이 사용되었으므로 절반만 사용했습니다.

SparseIntArray는 자동 박싱을 방지하는 데 도움이 되는 멋진 컬렉션 중 하나일 뿐입니다. Map<Integer, Long> 과 같은 지도는 지도 값이 Long 유형 SparseLongArray 로 대체될 수 있습니다. SparseLongArray 의 소스 코드를 보면 꽤 흥미로운 것을 볼 수 있습니다. 내부적으로는 기본적으로 한 쌍의 어레이일 뿐입니다. 유사하게 SparseBooleanArray 를 사용할 수도 있습니다.

소스 코드를 SparseIntArrayHashMap 보다 느릴 수 있다는 메모를 보았을 것입니다. 나는 많은 실험을 해왔지만 SparseIntArray 는 항상 메모리와 성능면에서 더 좋았습니다. 선택하는 것은 여전히 ​​귀하에게 달려 있다고 생각하고 사용 사례를 실험하고 가장 적합한 것을 확인하십시오. 지도를 사용할 때 머리에 SparseArrays 가 있어야 합니다.

7. 온드로

위에서 말했듯이 성능을 최적화할 때 자주 실행되는 코드를 최적화할 때 가장 큰 이점을 얻을 수 있습니다. 많이 실행되는 함수 중 하나는 onDraw() 입니다. 화면에 뷰를 그리는 역할을 하는 것은 놀라운 일이 아닙니다. 장치는 일반적으로 60fps로 실행되므로 기능은 초당 60번 실행됩니다. 모든 프레임에는 준비 및 그리기를 포함하여 완전히 처리되어야 하는 16ms가 있으므로 느린 기능은 정말 피해야 합니다. 메인 스레드만 화면에 그릴 수 있으므로 비용이 많이 드는 작업은 피해야 합니다. 몇 초 동안 기본 스레드를 정지하면 악명 높은 ANR(응용 프로그램 응답 없음) 대화 상자가 표시될 수 있습니다. 이미지 크기 조정, 데이터베이스 작업 등을 위해서는 백그라운드 스레드를 사용하십시오.

사용자가 프레임 속도가 떨어지는 것을 눈치채지 못할 거라고 생각한다면 오산입니다!
트위터

코드를 단축하는 것이 더 효율적일 거라고 생각하는 사람들을 본 적이 있습니다. 더 짧은 코드가 더 빠른 코드를 의미하는 것은 아니기 때문에 확실히 그렇게 할 수 있는 방법은 아닙니다. 어떤 경우에도 줄 수로 코드 품질을 측정해서는 안 됩니다.

onDraw() 에서 피해야 할 것 중 하나는 Paint와 같은 객체를 할당하는 것입니다. 생성자에서 모든 것을 준비하므로 그릴 때 준비됩니다. onDraw() 가 최적화되어 있더라도 필요한 만큼만 호출해야 합니다. 최적화된 함수를 호출하는 것보다 나은 점은 무엇입니까? 글쎄, 어떤 기능도 전혀 호출하지 않습니다. 텍스트를 그리고 싶은 경우에는 drawText() 라는 꽤 깔끔한 도우미 함수가 있습니다. 여기서 텍스트, 좌표, 텍스트 색상 등을 지정할 수 있습니다.

8. 뷰홀더

여러분은 아마 이것을 알고 있을 것입니다. 그러나 저는 그것을 건너뛸 수 없습니다. Viewholder 디자인 패턴은 스크롤 목록을 더 부드럽게 만드는 방법입니다. 이것은 일종의 뷰 캐싱으로, findViewById() 에 대한 호출을 심각하게 줄이고 뷰를 저장하여 뷰를 부풀릴 수 있습니다. 다음과 같이 보일 수 있습니다.

 static class ViewHolder { TextView title; TextView text; public ViewHolder(View view) { title = (TextView) view.findViewById(R.id.title); text = (TextView) view.findViewById(R.id.text); } }

그런 다음 어댑터의 getView() 함수 내에서 사용 가능한 보기가 있는지 확인할 수 있습니다. 그렇지 않은 경우 하나를 만듭니다.

 ViewHolder viewHolder; if (convertView == null) { convertView = inflater.inflate(R.layout.list_item, viewGroup, false); viewHolder = new ViewHolder(convertView); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.title.setText("Hello World");

인터넷에서 이 패턴에 대한 유용한 정보를 많이 찾을 수 있습니다. 목록 보기에 일부 섹션 헤더와 같은 여러 유형의 요소가 있는 경우에도 사용할 수 있습니다.

9. 이미지 크기 조정

귀하의 앱에 일부 이미지가 포함될 가능성이 있습니다. 웹에서 일부 JPG를 다운로드하는 경우 해상도가 정말 클 수 있습니다. 그러나 표시되는 장치는 훨씬 작습니다. 기기의 카메라로 사진을 찍어도 사진 해상도가 디스플레이의 해상도보다 훨씬 크기 때문에 표시하기 전에 크기를 줄여야 합니다. 이미지를 표시하기 전에 크기를 조정하는 것이 중요합니다. 전체 해상도로 표시하려고 하면 메모리가 매우 빨리 소진될 것입니다. Android 문서에 비트맵을 효율적으로 표시하는 방법에 대해 많이 쓰여 있습니다. 요약해 보겠습니다.

그래서 당신은 비트맵을 가지고 있지만 그것에 대해 아무것도 모릅니다. 서비스에 inJustDecodeBounds라는 유용한 Bitmap 플래그가 있어 비트맵의 해상도를 확인할 수 있습니다. 비트맵이 1024x768이고 이를 표시하는 데 사용되는 ImageView가 400x300이라고 가정해 보겠습니다. 주어진 ImageView보다 클 때까지 비트맵의 해상도를 2로 계속 나누어야 합니다. 그렇게 하면 비트맵을 2배로 다운샘플링하여 512x384의 비트맵을 제공합니다. 다운샘플링된 비트맵은 메모리를 4배 적게 사용하므로 유명한 OutOfMemory 오류를 방지하는 데 많은 도움이 됩니다.

이제 방법을 알았으니 하지 말아야 합니다. … 적어도 앱이 이미지에 크게 의존하는 경우는 아니며 1-2개의 이미지가 아닙니다. 수동으로 이미지 크기 조정 및 재활용과 같은 작업을 확실히 피하고 일부 타사 라이브러리를 사용하십시오. 가장 인기 있는 것은 Square의 Picasso, Universal Image Loader, Facebook의 Fresco 또는 내가 가장 좋아하는 Glide입니다. 주변에 활발한 개발자 커뮤니티가 있으므로 GitHub의 문제 섹션에서도 도움이 되는 많은 사람들을 찾을 수 있습니다.

10. 엄격한 모드

Strict Mode는 많은 사람들이 알지 못하는 매우 유용한 개발자 도구입니다. 일반적으로 메인 스레드에서 네트워크 요청이나 디스크 액세스를 감지하는 데 사용됩니다. Strict Mode가 찾아야 할 문제와 트리거해야 할 페널티를 설정할 수 있습니다. Google 샘플은 다음과 같습니다.

 public void onCreate() { if (DEVELOPER_MODE) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .penaltyDeath() .build()); } super.onCreate(); }

Strict Mode가 찾을 수 있는 모든 문제를 감지하려면 detectAll() 을 사용할 수도 있습니다. 많은 성능 팁과 마찬가지로 엄격 모드에서 보고하는 모든 것을 맹목적으로 수정해서는 안 됩니다. 조사하고 문제가 아니라고 확신하면 그대로 두십시오. 또한 엄격 모드는 디버깅에만 사용하고 프로덕션 빌드에서는 항상 비활성화해야 합니다.

디버깅 성능: Pro Way

이제 병목 현상을 찾는 데 도움이 되는 몇 가지 도구를 살펴보거나 적어도 뭔가 잘못되었음을 보여줍시다.

1. 안드로이드 모니터

Android Studio에 내장된 도구입니다. 기본적으로 왼쪽 하단 모서리에서 Android 모니터를 찾을 수 있으며 거기에서 2개의 탭 간에 전환할 수 있습니다. 로그캣과 모니터. 모니터 섹션에는 4개의 다른 그래프가 있습니다. 네트워크, CPU, GPU 및 메모리. 그것들은 꽤 자명하기 때문에 나는 그것들을 빠르게 살펴보겠다. 다음은 다운로드되는 일부 JSON을 구문 분석하는 동안 찍은 그래프의 스크린샷입니다.

안드로이드 모니터

네트워크 부분은 수신 및 발신 트래픽을 KB/s 단위로 표시합니다. CPU 부분은 CPU 사용량을 백분율로 표시합니다. GPU 모니터는 UI 창의 프레임을 렌더링하는 데 걸리는 시간을 표시합니다. 4가지 모니터 중 가장 디테일한 모니터이니 자세한 내용이 궁금하시면 읽어보세요.

마지막으로 가장 많이 사용하게 될 메모리 모니터가 있습니다. 기본적으로 현재 사용 가능한 메모리 및 할당된 메모리 양이 표시됩니다. 사용된 메모리 양이 감소하는지 테스트하기 위해 가비지 컬렉션도 강제 실행할 수 있습니다. HPROF 뷰어 및 분석기로 열 수 있는 HPROF 파일을 생성하는 Dump Java Heap이라는 유용한 기능이 있습니다. 이렇게 하면 할당한 개체의 수, 할당한 개체의 메모리 양, 메모리 누수를 일으키는 개체를 확인할 수 있습니다. 이 분석기를 사용하는 방법을 배우는 것은 가장 간단한 작업은 아니지만 그만한 가치가 있습니다. 메모리 모니터로 할 수 있는 다음 작업은 원하는 대로 시작 및 중지할 수 있는 시간 지정 할당 추적을 수행하는 것입니다. 예를 들어 장치를 스크롤하거나 회전할 때와 같이 많은 경우에 유용할 수 있습니다.

2. GPU 오버드로

이것은 개발자 모드를 활성화한 후 개발자 옵션에서 활성화할 수 있는 간단한 도우미 도구입니다. GPU 오버드로 디버그, "오버드로 영역 표시"를 선택하면 화면에 이상한 색상이 표시됩니다. 괜찮아, 그렇게 될거야. 색상은 특정 영역이 몇 번이나 과도하게 그려졌는지를 의미합니다. 트루 컬러는 오버드로가 없었음을 의미하며 이것이 목표로 해야 하는 것입니다. 파란색은 오버드로 1개, 녹색은 2개, 분홍색 3개, 빨간색 4개를 의미합니다.

GPU 오버드로

트루 컬러를 보는 것이 가장 좋지만 특히 텍스트, 탐색 창, 대화 상자 등 주변에서 항상 약간의 오버드로가 표시됩니다. 그러니 완전히 없애려고 하지 마세요. 앱이 파란색 또는 초록색이면 문제가 없을 것입니다. 그러나 일부 간단한 화면에 너무 많은 빨간색이 표시되면 무슨 일이 일어나고 있는지 조사해야 합니다. 교체하는 대신 계속 추가하면 너무 많은 조각이 서로 쌓일 수 있습니다. 위에서 말했듯이 그리기는 앱에서 가장 느린 부분이므로 3개 이상의 레이어가 그려지면 아무 의미가 없습니다. 즐겨찾는 앱을 마음껏 사용해 보세요. 다운로드가 10억 개 이상인 앱에도 빨간색 영역이 있는 것을 볼 수 있으므로 최적화를 시도할 때 여유를 가지세요.

3. GPU 렌더링

이것은 프로필 GPU 렌더링이라고 하는 개발자 옵션의 또 다른 도구입니다. 그것을 선택하면 "화면을 막대로"를 선택하십시오. 화면에 몇 가지 컬러 막대가 나타납니다. 모든 응용 프로그램에는 별도의 막대가 있기 때문에 이상하게도 상태 표시줄에는 자체 막대가 있으며 소프트웨어 탐색 버튼이 있는 경우 자체 막대도 있습니다. 어쨌든 막대는 화면과 상호 작용할 때 업데이트됩니다.

GPU 렌더링

막대는 3-4개의 색상으로 구성되며 Android 문서에 따르면 크기가 실제로 중요합니다. 작을수록 좋습니다. 맨 아래에는 보기의 표시 목록을 만들고 업데이트하는 데 사용된 시간을 나타내는 파란색이 있습니다. 이 부분이 너무 크다면 커스텀 뷰 드로잉이 많다는 의미이거나 onDraw() 함수에서 수행한 작업이 많다는 의미입니다. Android 4.0 이상이면 파란색 막대 위에 보라색 막대가 표시됩니다. 리소스를 렌더 스레드로 전송하는 데 소요된 시간을 나타냅니다. 그런 다음 Android의 2D 렌더러가 OpenGL에 명령을 실행하여 표시 목록을 그리고 다시 그리는 데 소요된 시간을 나타내는 빨간색 부분이 나타납니다. 상단에는 CPU가 GPU가 작업을 마칠 때까지 기다리는 시간을 나타내는 주황색 막대가 있습니다. 키가 너무 크면 앱이 GPU에서 너무 많은 작업을 수행하고 있는 것입니다.

당신이 충분히 좋다면 오렌지 위에 하나의 색상이 더 있습니다. 16ms 임계값을 나타내는 녹색 선입니다. 목표는 60fps에서 앱을 실행하는 것이므로 모든 프레임을 그리는 데 16ms가 있습니다. 그렇지 않으면 일부 프레임을 건너뛸 수 있고 앱이 불안정해질 수 있으며 사용자는 확실히 알아차릴 수 있습니다. 부드러움이 가장 중요한 부분인 애니메이션과 스크롤에 특별한 주의를 기울이십시오. 이 도구를 사용하여 일부 건너뛴 프레임을 감지할 수는 있지만 문제가 정확히 어디에 있는지 파악하는 데 실제로 도움이 되지는 않습니다.

4. 계층 뷰어

이것은 정말 강력하기 때문에 제가 가장 좋아하는 도구 중 하나입니다. 도구 -> Android -> Android Device Monitor를 통해 Android Studio에서 시작하거나 sdk/tools 폴더에 "모니터"로 있습니다. 거기에서 독립형 hierarachyviewer 실행 파일을 찾을 수도 있지만 더 이상 사용되지 않으므로 모니터를 열어야 합니다. 그러나 Android Device Monitor를 열고 Hierarchy Viewer 관점으로 전환합니다. 기기에 할당된 실행 중인 앱이 표시되지 않으면 문제를 해결하기 위해 할 수 있는 몇 가지 작업이 있습니다. 또한 이 문제 스레드를 확인하십시오. 모든 종류의 문제와 모든 종류의 솔루션을 가진 사람들이 있습니다. 당신에게도 효과가 있을 것입니다.

Hierarchy Viewer를 사용하면 보기 계층 구조에 대한 정말 깔끔한 개요를 얻을 수 있습니다(분명히). 모든 레이아웃을 별도의 XML로 보면 쓸모없는 보기를 쉽게 찾을 수 있습니다. 그러나 레이아웃을 계속 결합하면 쉽게 혼동될 수 있습니다. 이와 같은 도구를 사용하면 예를 들어, 자식이 1개 있고 다른 RelativeLayout이 있는 일부 RelativeLayout을 쉽게 찾을 수 있습니다. 그 중 하나를 제거할 수 있습니다.

각 보기가 얼마나 커야 하는지 알아보기 위해 전체 보기 계층 구조를 순회하게 하므로 requestLayout() 호출을 피하십시오. 측정값과 충돌이 있는 경우 계층이 여러 번 탐색될 수 있으며, 일부 애니메이션 중에 이러한 일이 발생하면 일부 프레임을 확실히 건너뜁니다. Android가 뷰를 그리는 방법에 대해 더 알고 싶다면 이 글을 읽어보세요. Hierarchy Viewer에서 볼 수 있는 한 가지 보기를 살펴보겠습니다.

계층 뷰어

오른쪽 상단 모서리에는 독립 실행형 창에서 특정 보기의 미리보기를 최대화하기 위한 버튼이 있습니다. 그 아래에서 앱에서 보기의 실제 미리보기를 볼 수도 있습니다. 다음 항목은 뷰 자체를 포함하여 주어진 뷰에 얼마나 많은 자식이 있는지 나타내는 숫자입니다. 노드(루트가 바람직함)를 선택하고 "레이아웃 시간 가져오기"(3개의 색상 원)를 누르면 측정값, 레이아웃 및 그리기라는 레이블이 지정된 색상 원과 함께 3개의 값이 더 채워집니다. 측정 단계가 주어진 보기를 측정하는 데 걸린 시간을 나타내는 것은 놀라운 일이 아닙니다. 레이아웃 단계는 렌더링 시간에 관한 것이고 드로잉은 실제 드로잉 작업입니다. 이러한 값과 색상은 서로 상대적입니다. 녹색은 뷰가 트리의 모든 뷰 중 상위 50%에서 렌더링됨을 의미합니다. 노란색은 트리에 있는 모든 보기의 느린 50%에서 렌더링됨을 의미하고 빨간색은 주어진 보기가 가장 느린 보기 중 하나임을 의미합니다. 이 값은 상대적이므로 항상 빨간색 값이 있습니다. 당신은 단순히 그들을 피할 수 없습니다.

값 아래에는 객체의 내부 뷰 ID인 "TextView"와 같은 클래스 이름과 XML 파일에서 설정한 뷰의 android:id가 있습니다. 코드에서 ID를 참조하지 않더라도 모든 보기에 ID를 추가하는 습관을 기를 것을 촉구합니다. Hierarchy Viewer에서 보기를 식별하는 것이 정말 간단해지며 프로젝트에 자동화된 테스트가 있는 경우 요소를 훨씬 빠르게 타겟팅할 수도 있습니다. 그러면 동료와 함께 작성하는 시간을 절약할 수 있습니다. XML 파일에 추가된 요소에 ID를 추가하는 것은 매우 간단합니다. 그러나 동적으로 추가된 요소는 어떻습니까? 글쎄, 그것은 정말 간단합니다. values ​​폴더 안에 ids.xml 파일을 만들고 필수 필드에 입력하기만 하면 됩니다. 다음과 같이 보일 수 있습니다.

 <resources> <item name="item_title" type="id"/> <item name="item_body" type="id"/> </resources>

그런 다음 코드에서 setId(R.id.item_title) 을 사용할 수 있습니다. 더 간단할 수 없습니다.

UI를 최적화할 때 주의해야 할 몇 가지 사항이 더 있습니다. 일반적으로 깊은 계층 구조는 피하고 얕고 넓은 계층 구조를 선호해야 합니다. 필요하지 않은 레이아웃은 사용하지 마십시오. 예를 들어 중첩된 LinearLayouts 그룹을 RelativeLayout 또는 TableLayout 으로 바꿀 수 있습니다. 항상 LinearLayoutRelativeLayout 을 사용하지 말고 다양한 레이아웃을 자유롭게 실험해 보십시오. 또한 필요할 때 몇 가지 사용자 정의 보기를 만들어 보십시오. 제대로 수행하면 성능이 크게 향상될 수 있습니다. 예를 들어 Instagram에서 댓글을 표시하는 데 TextView를 사용하지 않는다는 것을 알고 계셨습니까?

Pixel Perfect 도구 등을 사용하여 다양한 창에 대한 설명과 함께 Android 개발자 사이트에서 Hierarchy Viewer에 대한 추가 정보를 찾을 수 있습니다. 한 가지 더 지적하고 싶은 것은 .psd 파일에서 보기를 캡처하는 것입니다. 이 작업은 다음을 통해 수행할 수 있습니다. "창 레이어 캡처" 버튼을 클릭합니다. 모든 보기는 별도의 레이어에 있으므로 Photoshop 또는 GIMP에서 숨기거나 변경하는 것은 정말 간단합니다. 아, 이것이 가능한 모든 보기에 ID를 추가해야 하는 또 다른 이유입니다. 레이어에 실제로 의미가 있는 이름이 지정됩니다.

개발자 옵션에서 더 많은 디버깅 도구를 찾을 수 있으므로 활성화하고 어떤 작업을 수행하는지 확인하는 것이 좋습니다. 무엇이 잘못될 수 있습니까?

Android 개발자 사이트에는 성능에 대한 모범 사례 모음이 포함되어 있습니다. 그것들은 내가 실제로 이야기하지 않은 메모리 관리를 포함하여 다양한 영역을 다룹니다. 메모리를 처리하고 메모리 누수를 추적하는 것은 완전히 별개의 이야기이기 때문에 조용히 무시했습니다. 이미지를 효율적으로 표시하기 위해 타사 라이브러리를 사용하면 많은 도움이 되지만 여전히 메모리 문제가 있는 경우 Square에서 만든 Leak canary를 확인하거나 이것을 읽으십시오.

마무리

그래서 이것은 좋은 소식이었습니다. 나쁜 소식은 Android 앱을 최적화하는 것이 훨씬 더 복잡하다는 것입니다. 모든 일에는 여러 가지 방법이 있으므로 각 방법의 장단점을 잘 알고 있어야 합니다. 일반적으로 이점만 있는 은색 총알 솔루션은 없습니다. 배후에서 무슨 일이 일어나고 있는지 이해해야만 자신에게 가장 적합한 솔루션을 선택할 수 있습니다. 당신이 좋아하는 개발자가 어떤 것이 좋다고 해서 그것이 당신에게 최고의 솔루션이라는 의미는 아닙니다. 논의할 영역과 더 발전된 프로파일링 도구가 더 많이 있으므로 다음 시간에 다루도록 하겠습니다.

최고의 개발자와 최고의 회사로부터 배우십시오. 이 링크에서 수백 개의 엔지니어링 블로그를 찾을 수 있습니다. 분명히 Android 관련 항목만 있는 것은 아니므로 Android에만 관심이 있는 경우 특정 블로그를 필터링해야 합니다. 페이스북과 인스타그램 블로그를 적극 추천합니다. Android의 Instagram UI가 의심스럽긴 하지만 엔지니어링 블로그에는 정말 멋진 기사가 있습니다. 매일 수억 명의 사용자를 처리하는 회사에서 작업이 어떻게 수행되는지 쉽게 볼 수 있으므로 블로그를 읽지 않는 것이 미친 것처럼 보입니다. 세상은 정말 빠르게 변화하고 있습니다. 따라서 지속적으로 개선하려고 노력하지 않고 다른 사람에게서 배우고 새로운 도구를 사용하지 않는다면 뒤처지게 될 것입니다. 마크 트웨인(Mark Twain)이 말했듯이, 읽지 않는 사람은 읽을 수 없는 사람보다 유리하지 않습니다.