기술나눔

Android 성능 최적화 메모리 최적화

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Android 성능 최적화 메모리 최적화

기억력 문제

  • 메모리 스래싱
  • 메모리 누수
  • 메모리 오버플로

메모리 스래싱

메모리 스래싱(Memory Thrashing)은 짧은 시간 내에 많은 수의 객체가 생성되고 소멸되어 가비지 컬렉션(Garbage Collection, GC) 활동이 자주 발생하는 것을 말합니다. 이렇게 빈번한 GC 활동은 많은 CPU 리소스를 차지하며 애플리케이션 지연이나 성능 저하를 유발할 수 있습니다.

성능: 메모리 곡선이 들쭉날쭉합니다.

메모리 누수

메모리 누수는 애플리케이션이 더 이상 필요하지 않은 개체에 대한 참조를 보유할 때 발생하며, 이로 인해 이러한 개체가 가비지 수집기에 의해 재활용되지 않아 해제될 수 있는 메모리 공간을 차지하게 됩니다. 시간이 지남에 따라 메모리 누수로 인해 사용 가능한 메모리가 점점 줄어들고 결국 애플리케이션 충돌이나 성능 저하로 이어질 수 있습니다.

메모리 오버플로

메모리 오버플로는 애플리케이션이 더 많은 메모리 공간을 할당하려고 시도하지만 할당할 메모리 공간이 더 이상 부족하여 시스템이 요청을 충족할 수 없을 때 발생합니다. 이로 인해 일반적으로 응용 프로그램에서 OutOfMemoryError 예외가 발생합니다.

탐지 도구

  • 메모리 프로파일러
  • 메모리 분석기
  • 리크캐나리

메모리 프로파일러

  • 메모리 프로파일러는 Android Studio와 함께 제공되는 메모리 분석 도구입니다.
  • 프로그램 메모리 사용량을 보여주는 실시간 그래프.
  • 메모리 누수, 스래싱 등을 식별합니다.
  • 힙 덤프 캡처, GC 강제 실행, 메모리 할당 추적 기능을 제공합니다.

메모리 분석기

  • 메모리 누수 및 메모리 사용량을 찾아내는 강력한 Java 힙 분석 도구입니다.
  • 전체 보고서 생성, 문제 분석 등을 수행합니다.

리크캐나리

  • 자동 메모리 누수 감지.
    • LeakCanary는 Activity, Fragment, View, ViewModel, Service 개체에서 누수를 자동으로 감지합니다.
  • 공식 홈페이지: https://github.com/square/leakcanary

메모리 관리 메커니즘

자바

Java 메모리 구조: 힙, 가상 머신 스택, 메소드 영역, 프로그램 카운터, 로컬 메소드 스택.

Java 메모리 재활용 알고리즘:

  • 표시 및 스윕 알고리즘:
    1. 재활용이 필요한 물건을 표시하세요
    2. 표시된 모든 개체를 균일하게 재활용합니다.
  • 복사 알고리즘:
    1. 메모리를 동일한 크기의 청크로 나눕니다.
    2. 한 블록의 메모리를 다 사용한 후에는 남아 있는 객체가 다른 블록에 복사됩니다.
  • 마킹-콜레이션 알고리즘:
    1. 마킹 프로세스는 "mark-and-sweep" 알고리즘과 동일합니다.
    2. 생존 개체는 한쪽 끝으로 이동합니다.
    3. 남은 메모리를 삭제하세요.
  • 세대별 수집 알고리즘:
    • 여러 수집 알고리즘의 장점을 결합합니다.
    • 신세대 객체의 생존율이 낮으며 복사 알고리즘이 사용됩니다.
    • Old Generation에서는 객체의 생존율이 높으며 마크 정렬 알고리즘이 사용됩니다.

표시 지우기 알고리즘의 단점: 표시 및 지우기가 효율적이지 않으며 많은 수의 불연속적인 메모리 조각이 생성됩니다.

복제 알고리즘: 구현이 간단하고 실행이 효율적입니다. 단점: 공간의 절반을 낭비했습니다.

Mark-compact 알고리즘: mark-clean으로 인한 메모리 조각화를 방지하고 복사 알고리즘의 공간 낭비를 방지합니다.

기계적 인조 인간

Android 메모리 탄력적 할당, 할당 값 및 최대 값은 특정 장치에 의해 영향을 받습니다.

Dalvik 재활용 알고리즘과 ART 재활용 알고리즘은 모두 Android 운영 체제에서 메모리 관리에 사용되는 가비지 수집 메커니즘입니다.

Dalvik 재활용 알고리즘:

  • 마크 앤 스윕 알고리즘.
  • 장점은 구현이 간단하다는 것입니다. 단점은 표시 및 지우기 단계에서 애플리케이션 실행이 일시 중지되어 애플리케이션이 일시적으로 정지되고 사용자 경험에 영향을 미친다는 것입니다.

미술품 재활용 알고리즘:

  • 가비지 수집 압축 알고리즘. 가비지 수집 중 일시 중지 시간을 줄이기 위해 마크 스윕 알고리즘을 기반으로 개선이 이루어졌습니다.
  • 동시 마킹: ART는 동시 마킹 단계를 도입합니다. 이는 애플리케이션 실행과 동시에 발생할 수 있음을 의미합니다. 이렇게 하면 가비지 수집으로 인한 일시 중지 시간이 줄어듭니다.
  • 정리 및 압축: 정리 단계에서 ART는 표시되지 않은 개체를 지울 뿐만 아니라 메모리를 압축합니다. 즉, 살아남은 개체를 함께 이동하여 메모리 조각화를 줄입니다. 이렇게 하면 메모리 관리가 더욱 효율적으로 이루어지고 메모리 할당 실패 가능성이 줄어듭니다.
  • 적응형 컬렉션: ART는 적응형 컬렉션 개념도 도입했습니다. 즉, 애플리케이션의 동작과 메모리 사용 패턴을 기반으로 가비지 컬렉션의 빈도와 방식을 자동으로 조정합니다. 이를 통해 ART는 다양한 애플리케이션 요구 사항에 더 잘 적응할 수 있습니다.

LMK 메커니즘:

  • 낮은 메모리 킬러 메커니즘.
  • 주요 기능은 시스템 메모리가 부족하여 메모리를 해제하고 시스템 안정성과 응답성을 보장할 때 특정 우선순위 정책에 따라 일부 백그라운드 프로세스를 종료하는 것입니다.

해결하다

메모리 스래싱 문제

시뮬레이션 문제 코드
public class ShakeActivity extends AppCompatActivity {

    private static Handler mHandler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            String str = "";
            for (int i = 0; i < 10000000; i++) {
                str += i;
            }
            mHandler.sendEmptyMessageDelayed(1, 30);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_shake);
    }

    public void startClick(View view) {
        mHandler.sendEmptyMessage(1);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
메모리 프로파일러 도구를 사용하여 감지

메모리 프로파일러에서 메모리 할당을 볼 수 있습니다. 'Record Java/Kotlin 할당'을 클릭하세요.

여기에 이미지 설명을 삽입하세요.

위의 의미:

  • Java: Java 또는 Kotlin 코드로 할당된 메모리입니다.
  • 기본: C 또는 C++ 코드로 할당된 메모리입니다.
  • 그래픽: 픽셀을 화면에 표시하기 위해 그래픽 버퍼 큐에서 사용하는 메모리입니다(GL 표면, GL 텍스처 등 포함). CPU 공유 메모리.
  • 스택: 애플리케이션의 기본 스택 및 Java 스택에서 사용하는 메모리입니다. 이는 일반적으로 앱이 실행 중인 스레드 수와 관련이 있습니다.
  • dex 바이트코드, 최적화되거나 컴파일된 dex 코드, .so 라이브러리, 글꼴 등의 코드와 리소스를 처리하기 위해 애플리케이션에서 사용하는 메모리입니다.
  • 애플리케이션은 시스템이 분류하는 방법을 알 수 없는 메모리를 사용합니다.
  • 애플리케이션에서 할당한 Java/Kotlin 객체의 수입니다. 이 숫자는 C 또는 C++에 할당된 개체를 고려하지 않습니다.

다음과 같은 의미:

  • 할당: 합격 malloc() 또는new 운영자가 할당한 개체 수입니다.
  • 할당 해제: 다음을 통해 free() 또는delete 운영자가 할당을 취소한 개체 수입니다.
  • 할당 크기: 선택한 기간 동안의 모든 할당의 총 크기(바이트)입니다.
  • 할당 취소 크기: 선택한 기간 내에 해제된 총 메모리 크기(바이트)입니다.
  • 총 개수: 할당에서 할당 취소를 뺀 값입니다.
  • 남은 크기: 할당 크기에서 할당 취소 크기를 뺀 값입니다.
  • 얕은 크기: 힙에 있는 모든 인스턴스의 총 크기(바이트)입니다.

위 그림의 분석:

여기에 이미지 설명을 삽입하세요.

이 영역의 Allocations 및 Deallocations 값은 비교적 유사하며 Shallow Size가 상대적으로 커서 개체가 자주 생성되고 소멸될 수 있음을 나타냅니다.

여기에 이미지 설명을 삽입하세요.

클릭 후 콜스택 정보를 확인할 수 있으며, 코드와 결합하여 핸들러에 메모리 지터 문제가 있음을 유추할 수 있습니다.

최적화 팁
  • 객체를 자주 생성하고 파괴하지 마십시오.

메모리 누수 문제

시뮬레이션 문제 코드
public class CallbackManager {
    public static ArrayList<Callback> sCallbacks = new ArrayList<>();

    public static void addCallback(Callback callback) {
        sCallbacks.add(callback);
    }

    public static void removeCallback(Callback callback) {
        sCallbacks.remove(callback);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
public class LeakActivity extends AppCompatActivity implements Callback {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak);
        ImageView imageView = findViewById(R.id.imageView);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.splash);
        imageView.setImageBitmap(bitmap);
        CallbackManager.addCallback(this);
    }

    @Override
    public void onOperate() {

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
LeakCanary 도구를 사용하여 감지

종속 라이브러리를 추가합니다.

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
  • 1

메모리 누수가 발생한 후 LeakCanary는 관련 정보를 생성하고 자동으로 덤프합니다.

여기에 이미지 설명을 삽입하세요.

위 그림에서 볼 수 있듯이 LeakActivity에 메모리 누수가 발생하고 참조 체인 관계가 표시됩니다.

물론 hprof 파일을 생성하고 프로파일러 도구를 통해 특정 정보를 볼 수도 있습니다.

여기에 이미지 설명을 삽입하세요.

위 그림에서 볼 수 있듯이 LeakActivity를 포함하여 10개의 누수 지점이 발생했습니다. LeakActivity를 클릭하면 메모리 누수 개체를 볼 수 있고 참조 체인을 확인하면 ArrayList에 의해 유지되는 것을 볼 수 있습니다.

최적화 팁
  • 수집 요소를 적시에 재활용합니다.
  • 너무 많은 인스턴스에 대한 정적 참조를 피하십시오.
  • 정적 내부 클래스를 사용하십시오.
  • 자원 객체를 즉시 닫으십시오.

비트맵 최적화

Bitmap을 사용한 후 이미지 리소스를 해제하지 않으면 문제가 발생하기 쉽습니다.메모리 누수로 인해 메모리 오버플로가 발생함

비트맵 메모리 모델
  • api10(Android 2.3.3) 이전: 비트맵 객체는 힙 메모리에 배치되고 픽셀 데이터는 로컬 메모리에 배치됩니다.
  • api10 이후: 모두 힙 메모리에 있습니다.
  • api26(Android8.0) 이후: 픽셀 데이터가 로컬 메모리에 배치됩니다. 이를 통해 기본 레이어의 비트맵 픽셀 데이터를 Java 레이어의 개체와 함께 빠르게 릴리스할 수 있습니다.

메모리 재활용:

  • Android 3.0 이전에는 수동으로 호출해야 합니다.Bitmap.recycle()비트맵 재활용을 수행합니다.
  • Android 3.0부터 시스템은 보다 지능적인 메모리 관리를 제공하며 대부분의 경우 비트맵을 수동으로 재활용할 필요가 없습니다.

비트맵 픽셀 구성:

구성점유 바이트 크기(바이트)설명하다
알파_81단일 투명 채널
RGB_5652간단한 RGB 색조
ARGB_8888424비트 트루 컬러
RGBA_F168안드로이드 8.0 신규(HDR)

Btimap이 차지하는 메모리를 계산합니다.

  • 비트맵#getByteCount()
  • getWidth() * getHeight() * 1픽셀이 메모리를 차지합니다.
리소스 파일 디렉터리

리소스 파일 문제:

  • mdpi(중밀도): 약 160dpi, 1x 리소스.
  • hdpi(고밀도): 약 240dpi, 1.5x 리소스.
  • xhdpi(초고밀도): 약 320dpi, 2x 리소스.
  • xxhdpi(초초고밀도): 약 480dpi, 3x 리소스.
  • xxxhdpi(초초고밀도): 약 640dpi, 4x 리소스.

테스트 코드:

private void printBitmap(Bitmap bitmap, String drawable) {
    String builder = drawable +
            " Bitmap占用内存:" +
            bitmap.getByteCount() +
            " width:" +
            bitmap.getWidth() +
            " height:" +
            bitmap.getHeight() +
            " 1像素占用大小:" +
            getByteBy1px(bitmap.getConfig());
    Log.e("TAG", builder);
}

private int getByteBy1px(Bitmap.Config config) {
    if (Bitmap.Config.ALPHA_8.equals(config)) {
        return 1;
    } else if (Bitmap.Config.RGB_565.equals(config)) {
        return 2;
    } else if (Bitmap.Config.ARGB_8888.equals(config)) {
        return 4;
    }
    return 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
// 逻辑密度
float density = metrics.density;
// 物理密度
int densityDpi = metrics.densityDpi;
Log.e("TAG", density + "-" + densityDpi);

// 1倍图
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.splash1);
printBitmap(bitmap1, "drawable-mdpi");

// 2倍图
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.splash2);
printBitmap(bitmap2, "drawable-xhdpi");

// 3倍图
Bitmap bitmap3 = BitmapFactory.decodeResource(getResources(), R.drawable.splash3);
printBitmap(bitmap3, "drawable-xxhdpi");

// 4倍图
Bitmap bitmap4 = BitmapFactory.decodeResource(getResources(), R.drawable.splash4);
printBitmap(bitmap4, "drawable-xxxhdpi");

// drawable
Bitmap bitmap5 = BitmapFactory.decodeResource(getResources(), R.drawable.splash);
printBitmap(bitmap5, "drawable");
/*
        3.0-480
        drawable-mdpi Bitmap占用内存:37127376 width:2574 height:3606 1像素占用大小:4
        drawable-xhdpi Bitmap占用内存:9281844 width:1287 height:1803 1像素占用大小:4
        drawable-xxhdpi Bitmap占用内存:4125264 width:858 height:1202 1像素占用大小:4
        drawable-xxxhdpi Bitmap占用内存:2323552 width:644 height:902 1像素占用大小:4
        drawable Bitmap占用内存:37127376 width:2574 height:3606 1像素占用大小:4
         */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

설명하다:

mdpi 장치의 1dpxhdpi 장치의 경우 1px, 1dpxxhdpi 장치에서는 2px, 1dp==3px.

따라서 현재 장치는 xxhdpi이므로 xxhdpi 리소스 아래에서는 동일한 그림의 너비가 858이고, mdpi 리소스 아래에서는 3배로 확대되어 너비는 2574가 되며, xhdpi 리소스 아래에서는 1.5배로 확대되어 너비가 2574가 됩니다. 너비는 1287입니다.

최적화 팁
  • 여러 이미지 리소스 세트를 설정합니다.
  • 적절한 디코딩 방법을 선택하십시오.
  • 이미지 캐시를 설정합니다.

소스코드 다운로드