Android 메모리 유출 실전 분석

자바는 쓰레기 회수 언어의 하나로 개발자가 메모리 분배를 특별히 관리할 필요가 없고 국부 고장(segmentationfault)으로 인한 응용 프로그램의 붕괴를 낮추는 동시에 방출되지 않은 메모리가 창고(heap)를 폭발시킬 가능성을 방지하기 때문에 쓴 코드가 더욱 안전하다는 장점이 있다.
불행하게도 자바에는 메모리 유출을 일으키기 쉬운 논리적 가능성이 많이 존재한다.만약 조심하지 않는다면, 당신의 안드로이드 응용 프로그램은 방출되지 않은 메모리를 낭비하기 쉬우며, 결국 메모리를 다 쓰는 오류 (out-of-memory, OOM) 를 초래하게 될 것이다.
1. 일반적인 메모리 유출(traditional memory leak)의 원인은 이 대상의 모든 인용이 방출되었는데도 대상이 방출되지 않았기 때문이다.(번역자 주: Cursor 닫는 것을 잊어버리는 등)
2. 논리 메모리 유출(logical memory leak)의 원인은 응용 프로그램이 더 이상 이 대상을 필요로 하지 않을 때, 이 대상의 모든 인용을 방출하지 않을 때이다.
만약 소지 대상의 강제 인용이 있다면, 쓰레기 수거기는 메모리에서 이 대상을 회수할 수 없습니다.
안드로이드 개발에서 가장 쉽게 발생하는 메모리 유출 문제는 Context다.예를 들어 Activity의 Context는 View Hierarchies와 다른 자원과 같은 대량의 메모리 인용을 포함한다.일단 Context가 누설되면 그것이 가리키는 모든 대상을 누설하는 것을 의미한다.Android 기기의 메모리가 제한되어 있기 때문에 너무 많은 메모리가 유출되면 OOM이 발생하기 쉽다.
논리적 메모리 유출을 검출하려면 주관적인 판단이 필요하다. 특히 대상의 생명주기가 뚜렷하지 않다.다행히도 Activity는 명확한 생명주기를 가지고 있어 유출의 원인을 쉽게 발견할 수 있다.Activity.onDestroy () 는 Activity 생명의 종결로 여겨집니다. 프로그램상 삭제되었거나 Android 시스템에서 이 메모리를 회수해야 합니다. (메모리가 부족할 때 Android는 보이지 않는 액티비티를 회수합니다.)
만약 이 방법이 실행된다면, 창고에 이 액티비티를 가지고 있는 강력한 인용이 존재하면, 쓰레기 수거기는 그것을 이미 회수한 메모리로 표시할 수 없고, 우리의 본래 목적은 그것을 회수하는 것이다!
결과적으로 Activity는 라이프 사이클 밖에서 살아남습니다.
Activity는 중량급 대상이므로 Android 시스템에서 처리해야 합니다.그러나 논리적 메모리 유출은 무심코 일어난다.(역자 참고: 액티비티가 20M 메모리 유출을 초래한 적이 있습니다.)Android에서 잠재적 메모리 유출을 초래하는 함정은 두 가지에 불과하다.
전역 프로세스 (process-global) 의 static 변수입니다.응용 프로그램을 무시한 상태로 액티비티의 강력한 인용을 가진 몬스터입니다.
Activity 라이프 사이클 외부에 있는 스레드Activity에 대한 강제 참조를 비우지 않았습니다.
당신이 아래의 상황에 부닥쳤는지 검사해 보세요.
1.Static Activities
클래스에서 정적 Activity 변수를 정의하고 현재 실행 중인 Activity 실례를 이 정적 변수에 부여합니다.
만약 이 정적 변수가 Activity 생명주기가 끝난 후에 비우지 않으면 메모리 유출을 초래할 수 있습니다.static 변수는 이 응용 프로그램의 생명 주기를 관통하기 때문에 유출된 액티비티는 응용 과정에 계속 존재하고 쓰레기 수거기에 회수되지 않습니다.

static Activity activity;  
void setStaticActivity() {
  activity = this;
}
View saButton = findViewById(R.id.sa_button);
saButton.setOnClickListener(new View.OnClickListener() {   
   @Override public void onClick(View v) {
    setStaticActivity();
    nextActivity();
   }
});
2.Static Views
유사한 상황은 단일 모드에서 발생할 수 있습니다. 만약에 액티비티가 자주 사용된다면 메모리에 실례를 저장하는 것이 매우 실용적입니다.앞서 말한 바와 같이 Activity의 생명주기를 강제로 연장하는 것은 상당히 위험하고 불필요한 것이기 때문에 어쨌든 이렇게 해서는 안 된다.
특수한 상황: 만약에 View 초기화가 대량의 자원을 소모하고 하나의 Activity 생명주기 내에 변하지 않는다면 이것을 static으로 바꾸어 보기 트리(View Hierachy)에 불러올 수 있습니다. 이렇게 하면 Activity가 소각될 때 자원을 방출해야 합니다.(번역자 주: 예시 코드에 메모리가 방출되지 않았습니다. 이staticview를null로 설정하면 되지만 이staticview를 사용하는 방법은 권장하지 않습니다.)

static view;  
void setStaticView() {
  view = findViewById(R.id.sv_button);
}
View svButton = findViewById(R.id.sv_button);
svButton.setOnClickListener(new View.OnClickListener() {   
  @Override public void onClick(View v) {
    setStaticView();
    nextActivity();
  }
});
3.Inner Classes
계속해서, 액티비티에 내부 클래스가 있다고 가정하면, 이렇게 하면 가독성과 봉인성을 높일 수 있다.우리가 내부 클래스를 만들고 정적 변수의 인용을 가지고 있는 것을 축하합니다. 메모리 유출은 멀지 않습니다.

private static Object inner;    
void createInnerClass() {    
  class InnerClass {
   }
  inner = new InnerClass();
}
View icButton = findViewById(R.id.ic_button);
icButton.setOnClickListener(new View.OnClickListener() {    
   @Override public void onClick(View v) {
     createInnerClass();
     nextActivity();
   }
});
내부 클래스의 장점 중 하나는 외부 클래스에 접근할 수 있다는 것이다. 불행하게도 메모리 유출의 원인은 내부 클래스가 외부 클래스를 가진 실례에 대한 강한 인용이다.
4.Anonymous Classes
비슷하게 익명 클래스도 외부 클래스의 인용을 유지했다.그래서 메모리 유출은 액티비티에 익명의 AsyncTsk를 정의하면 쉽게 발생한다.비동기 작업이 백그라운드에서 시간 소모 작업을 수행하는 동안 Activity는 불행하게도 삭제되었습니다. (사용자 종료, 시스템 회수) 이 AsyncTask가 가지고 있는 Activity 실례는 쓰레기 수거기에서 회수되지 않습니다. 비동기 작업이 끝날 때까지.

void startAsyncTask() {    
  new AsyncTask<Void, Void, Void>() {      
   @Override protected Void doInBackground(Void... params) {        
      while(true);
   }
  }.execute();
} 
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {    
  @Override public void onClick(View v) {
     startAsyncTask();
     nextActivity();
  }
});
5.Handler
같은 맥락에서 익명의 Runnable를 정의하고 익명 클래스 Handler로 실행합니다.Runnable 내부 클래스는 외부 클래스의 은밀한 인용을 가지고 Handler의 메시지 대기열 메시지Queue에 전달됩니다. 메시지 메시지가 처리되지 않기 전에,Activity 실례가 삭제되지 않아 메모리가 유출됩니다.

void createHandler() {    
  new Handler() {      
    @Override public void handleMessage(Message message) {        
      super.handleMessage(message);
    }
  }.postDelayed(new Runnable() {      
     @Override public void run() {        
       while(true);
    }
  }, Long.MAX_VALUE >> 1);
}
View hButton = findViewById(R.id.h_button);
hButton.setOnClickListener(new View.OnClickListener() {    
  @Override public void onClick(View v) {
    createHandler();
    nextActivity();
  }
});
6.Threads
우리는 다시 Thread와 TimerTask를 통해 메모리 유출을 보여 줍니다.

void spawnThread() {    
  new Thread() {      
   @Override public void run() {        
     while(true);
      }
  }.start();
}
View tButton = findViewById(R.id.t_button);
tButton.setOnClickListener(new View.OnClickListener() {   
  @Override public void onClick(View v) {
     spawnThread();
     nextActivity();
   }
});
7.TimerTask
익명류의 실례라면 작업 라인이든 아니든 액티비티의 인용을 가지고 있어 메모리 유출을 초래할 수 있다.

void scheduleTimer() {    
  new Timer().schedule(new TimerTask() {      
    @Override
   public void run() {        
       while(true);
   }
  }, Long.MAX_VALUE >> 1);
}
View ttButton = findViewById(R.id.tt_button);
ttButton.setOnClickListener(new View.OnClickListener() {    
  @Override public void onClick(View v) {
    scheduleTimer();
    nextActivity();
    }
});
8.Sensor Manager
마지막으로 Context를 통해getSystemService(intname)는 시스템 서비스를 받을 수 있습니다.이러한 서비스는 각자의 프로세스에서 백그라운드 작업을 처리하고 하드웨어 상호작용을 처리하는 데 도움을 준다.이 서비스를 사용해야 한다면 감청기를 등록할 수 있습니다. 이로 인해 서비스가Context의 인용을 가지고 있으며, Activity에서 삭제할 때 이 감청기를 취소하지 않으면 메모리 유출을 초래할 수 있습니다.

void registerListener() {
    SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
    Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
    sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}
View smButton = findViewById(R.id.sm_button);
smButton.setOnClickListener(new View.OnClickListener() {      
     @Override public void onClick(View v) {
      registerListener();
      nextActivity();
      }
});
총결산
그렇게 많은 메모리 유출을 초래하는 예를 보면 핸드폰의 메모리를 다 먹어버려서 쓰레기 회수 처리가 더욱 빈번해지고 심지어 최악의 경우 OOM을 초래할 수 있다.쓰레기 회수 작업은 매우 비싼 비용으로 육안으로 볼 수 있는 끊김을 초래할 수 있다.따라서 실례화할 때 가지고 있는 인용 체인에 주의하고 메모리 유출 검사를 자주 한다.

좋은 웹페이지 즐겨찾기