태그 보관물: 안드로이드

안드로이드 프로젝트 빌드 유지(CI)를 위한 팀시티(TeamCity) 설정(2/2)

이 글은 안드로이드 프로젝트의 빌드를 유지하는 세 개의 글 중에 세 번째이다.

이 글은 팀시티가 저장소의 소스를 내려받고, 안드로이드 프로젝트 빌드를 완료하면, 등록된 계정에게 알려주도록 알람을 설정하는 내용이다. 이 글에서는 이메일과 메신저(Jabber 기반) 계정으로 알림을 송/수신 하는 설정, 빌드 파일(apk)을 다운로드 받게 설정, 그리고 수신하는 내용(템플릿)을 수정하는 방법에 대해서 살펴보자.

1. 알람 발송 계정 설정

구글의 서비스를 사용한다고 가정하고, 구글의 이메일과 메신저로 알림을 받도록 설정해보자.
구글 메일과 메신저 서비스(GTalk)는 아래 링크의 설정을 참고하면 된다.
https://confluence.jetbrains.com/display/TCD10/Setting+up+Google+Mail+and+Google+Talk+as+Notification+Servers
설정 메뉴는 Administration > Server Administration > Email, Jabber Notifier 메뉴에서 확인할 수 있다.
– 이메일 설정 화면
setting_noti_jabber

– 메신저(Jabber) 설정 화면
setting_noti_gmail

이제 알람을 발송할 계정 설정을 완료했다.

2. 알람 수신 계정 설정

다음으로, 알람을 받는 계정의 Email과 Jabber 계정을 설정해 보자. Administration > Users 에서 알람을 받을 계정을 선택한다. 우측의 “Watched Builds and Notifications” 에 Email과 Jabber Notifier를 선택한다. 그리고, Add New Rule을 선택하면 아래와 같은 화면을 볼 수 있다. 이 화면에서 알람을 받을 프로젝트의 빌드를 선택하고, 빌드의 상태인 “Build fails”과 “Build is successful” 을 선택하고 저장한다.
setting_jabber setting_email

이제 Email과 Jabber로 알람을 받을 준비가 완료됐다.

3. 빌드 파일 설정

빌드한 apk 파일을 다운로드 할 수 있게 산출물(Artifact)로 설정해야 한다. 아래 설정은 Projects > Build > General Settings에 아래와 같은 설정이 있고, Artifact paths에 apk 파일을 설정한다. 이제 빌드한 파일을 배포할 준비가 되었다.
apk_path_settings

4. 수신 내용 변경

이제 위에서 설정한 이메일과 메신저를 사용해서 소스 빌드에 대한 이벤트(성공/실패)를 수신 받게 된다. 기본적으로 수신받는 화면은 아래와 같다.
jabber_notification_alert email_notification_alert

위에서 보는 이메일이나 메신저의 알림 내용을 추가해서 조금 더 편리하게 알림을 사용할 수 있다. 아래에서 빌드된 apk 파일을 내려받기 쉽게 링크를 추가해 보자. 팀시티의 알림 템플릿은 아래의 위치에서 확인할 수 있고, 알림의 형태에 따라서 다음과 같이 나눠져 있다.

C:\TeamCityData\config\_notifications\email\build_successful.ftl
C:\TeamCityData\config\_notifications\jabber\build_successful.ftl

위에서 email 폴더에 있는 build_successful.ftl 파일을 수정해 보자. 우선 프로젝트의 빌드 파일의 위치를 확인해 보면 다음과 같다.

<#-- MODIFICATION START -->
  <#if buildType.externalId = "Nbarcode">    
    Click here to download APK.
      
  </#if>
<#-- MODIFICATION END -->

팀시티의 10.x 버전의 다운로드 위치는 위의 URI 형태를 띄고 있다. 이제 이 내용을 템플릿 파일의 적절한 위치에 복사를 하고 저장한다.

  <@common.build_comment build/>
  
  <#-- MODIFICATION START -->
  <#if buildType.externalId = "Nbarcode">
    
    Click here to download APK.
    
  </#if>
<#-- MODIFICATION END -->
  
  <@common.build_changes var.changesBean/>

이제 저장하고 소스를 커밋하면 아래와 같이 이메일과 메신저로 알람을 받게 된다.
email_notification_alert jabber_notification_alert

* Reference
– TeamCity에서 안드로이드앱 배포하기

안드로이드 프로젝트 빌드 유지(CI)를 위한 팀시티(TeamCity) 설정(1/2)

이 글은 안드로이드 프로젝트의 빌드를 유지하는 세 개의 글 중에 두 번째이다.

안드로이드 프로젝트를 팀시티에서 빌드하는데 필요한 설정을 살펴보자. 이 글에서는 프로젝트 연결, 빌드 설정, 트리거(Triggers) 설정, 그리고 빌드 확인의 과정으로 살펴보겠다.

1. 프로젝트 연결

프로젝트 연결은 3가지 형태를 지원한다. 저장소 연결(Repository Url), 깃헙(Github), 그리고 비트버킷(Bitbucket)을 지원한다. 여기에서는 저장소 연결을 사용해서 프로젝트에 연결한다.
project_connection_01 project_connection_02
위 우측 화면은 예제로 사용하는 프로젝트의 소스 저장소인 비트버킷을 연결하는 화면이다. 이곳에서 지원하는 연결 프로토콜로 http(s)://, svn://, 그리고 git:// 를 지원하는 것을 알 수 있다. 다음으로, 프로젝트의 “Version Control Settings”에서 소스의 “Check out”할 위치를 변경한다.
version_control_settings
여기에서 사용하는 프로젝트 이름은 Nbarcode이고, 프로젝트 소스의 Check out 위치는 C:\TeamCityProjects\Nbarcode 를 사용한다.

2. 빌드 설정

이제 소스를 빌드하는데 필요한 설정을 알아 보자. 빌드 설정은 “Build Steps” 메뉴를 선택해서 시작할 수 있다. “Auto-detect build steps” 버튼을 클릭해서 빌드 단계를 인식시켜 보자. 아래에서는 Clean, Release, 그리고 Rename 단계로 살펴보자.

2.1 Clean 단계

이 단계는 프로젝트 빌드 이전 단계로, 이전에 빌드에 사용한 각종 파일들을 삭제한다. 이 단계에 필요한 설정은 아래와 같다.

Runner Type : Gradle
Step Name : Clean
Gradle tasks: clean
Working directory: C:\TeamCityProjects\Nbarcode
Gradle Wrapper: 체크
JDK : 사용하는 자바 버전으로 설정한다.

build_step_02

2.2 Release 단계

이 단계에서 프로젝트를 빌드한다. 이전에 빌드에 사용한 각종 파일들을 삭제한다. “Build Configuration Settins” > “Build Step: Gradle” > “+ Add build step”을 클릭해서 빌드 단계를 추가한다. 이 단계에 필요한 설정은 아래와 같다.

Runner type : Gradle
Step Name : Release
Gradle tasks: assembleRelease
Working directory: C:\TeamCityProjects\Nbarcode
Gradle Wrapper: 체크
JDK : 사용하는 자바 버전으로 설정한다.

이제 우측 상단에 있는 “Run” 버튼을 클릭해서 위 단계를 시작해 보자. 이제 아래와 같이 apk 파일이 빌드된 것을 확인할 수 있다.
build_apk_result

2.3 Rename 단계

이 단계에서는 apk 파일 이름을 변경해서, 버전을 명확하게 알 수 있게 파일 이름을 변경하는 단계이다. 이 단계에 필요한 설정은 아래와 같다.

Runner type : Command Line
Step Name : Assemble Release
Working directory: C:\TeamCityProjects\Nbarcode\app\build\outputs\apk
Run : Executable with parameters
Command executable: rename
Command parameters: app-release.apk app-release-%build.counter%.apk

build_step_03

3. 트리거 설정

팀시티로 지속 빌드를 하는데 필요한 설정으로 VCS에 소스가 커밋되면, 빌드를 하는 과정이 필요하다. “Build Configuration Settings” > “Triggers” 메뉴에서 소스 커밋의 트리거 설정을 변경할 수 있다.
trigger
위 이미지는 기본 설정의 트리거 화면이다. 이 화면을 보면 소스코드 코밋이 확인되면 빌드를 시작한다는 것을 알 수 있다. 추가로 Quiet Period 설정에서 일정 시간 뒤에 빌드하도록 설정할 수 있다.

4. 빌드 확인

이제 소스를 커밋해서 빌드가 실행되는 것을 확인해 보자. 소스를 커밋하면 팀시티가 소스가 변경된 것을 확인하고, 소스를 받고, 빌드(위 2.1, 2.2, 2.3의 과정)를 한다. 이 과정이 완료되면 아래 화면에서 보는 apk 파일을 확인할 수 있다.
build_step_04

안드로이드 프로젝트를 지속적으로 빌드할 수 있는 환경을 구성해 봤다.

* Reference
TeamCity에서 안드로이드앱 배포하기

안드로이드 프로젝트 빌드 유지(CI)를 위한 팀시티(TeamCity) 설치

이 글은 안드로이드 프로젝트의 빌드를 유지하는 세 개의 글 중에 첫 번째이다.

팀시티(TeamCity)는 JetBrain에서 개발한 지속 통합(CI, Continuous Integration)을 지원하는 툴이다. 지속 통합 툴로 유명한 것으로는 Jenkins, Travis, 그리고 Cruise Control등이 있다. 지속 통합에 대한 개념은 아래의 링크에서 확인해 보자.
https://en.wikipedia.org/wiki/Continuous_integration
http://happystory.tistory.com/89

팀시티는 기본 무료로 사용할 수 있지만, 프로젝트 개수와 볼륨의 크기에 따라 상업용으로 구매해야 할 수도 있다. 라이선스에 대한 내용은 라이선스 페이지 에서 확인할 수 있고, 현재는 아래의 사용까지는 무료로 사용할 수 있다.

20 build configurations
3 build agents

이 정도면 작은 기업에서는 구입하지 않아도 충분히 사용할 수 있을 것이다. 이제 팀시티를 설치하고, 소스 저장소에 있는 안드로이드 앱을 연결해서 지속 통합을 하는데 필요한 환경을 설치하는 방법을 살펴보자. 아래 내용은 안드로이드 개발환경이 없는 윈도 서버(2012)에 설치하는 것을 가정한다.

1. 자바, 안드로이드 SDK 설치

팀시티로 안드로이드 앱을 빌드하는데는 자바와 안드로이드 SDK가 필요하고, 자바와 안드로이드 SDK를 설치하는 것을 살펴보자.

1.1 자바 설치

자바는 오라클 자바 사이트에서 내려받아 설치한다. 설치한 후에 “내 PC > 속성(R) > 고급 시스템 설정 > 고급 > 환경 변수 > 시스템 변수”에 JAVA_HOME을 추가한다.

1.2 안드로이드 SDK 설치

안드로이드 SDK는 안드로이드 앱을 개발/빌드/실행 하는데 필요한 라이브러리, 툴, 에뮬레이터 환경을 가지고 있다. 이 툴은 개발툴 다운로드 페이지에서 내려받을 수 있고, 이 페이지 아래에 “Get just the command line tools”의 하위에 SDK만을 별로도 다운로드 받을 수 있게 분리되어 있다.

여기에서는 윈도 2012 서버에 android-sdk_r24.4.1-windows.zip 파일을 다운로드 받아서 C:\Dev\android-sdk에 압축을 풀어서 저장하고 사용한다. SDK가 준비되면, “내 PC > 속성(R) > 고급 시스템 설정 > 고급 > 환경 변수 > 시스템 변수”에 ANDROID_HOME 변수를 추가하고 SDK 위치를 설정한다. 아래 이미지에 자바와 안드로이드 SDK가 설정된 것을 볼 수 있다.

system_variable_settings

SDK 설치를 완료하면, SDK 매니저(Manager)를 실행해서 필요한 툴, 플랫폼, 저장소 소스등을 내려받는다. 아래 이미지는 SDK 매니저를 실행해서 필요한 툴, 플랫폼(화면은 없지만, 앱에서 사용하는 플랫폼은 선택해야 한다), 그리고 라이브러리 및 툴등을 선택한 화면이다.

sdk_image_01

위 이미지는 필요한 빌드툴을 선택해서 설치하는 화면이다.

sdk_image_02

위 이미지는 필요한 라이브러리등을 선택한 화면이다.

SDK 매니저를 사용해서 필요한 것들의 설치를 완료하면 팀시티를 사용해서 빌드할 수 있는 기본 환경 구성을 완료한 것이다.

2. 팀시티(TeamCity) 설치

팀시티는 팀시티 웹 사이트에서 다운로드 메뉴를 클릭해서 내려받고 설치한다. 여기에서 사용하는 버전 정보는 다음과 같다.

Version : 10.0.2
Build: 42234
Released: September 19, 2016

이제 팀시티를 설치해 보자. 아래에 설치하면서 필요한 설정이 있는 부분들을 기준으로 살펴본다.

2.1 다운로드 받은 파일을 실행하고, 설치 위치를 확인한다.

아래의 왼쪽 이미지가 설치를 시작하는 화면이고, 오른쪽 화면이 설치 위치(C:\TeamCity)를 설정하는 화면이다.
teamcity_install_01 teamcity_install_02

2.2 웹 서비스를 위한 서버의 포트를 설정하고, 각종 설정을 확인한다.

왼쪽 화면이 팀시티가 실행하면서 접속하는 웹 서비스 포트를 설정하는 화면이고, 오른쪽은 설정된 값들을 확인하고 추가할 수 있는 화면이다.

teamcity_install_03 teamcity_install_04

2.3 팀시티의 시작 계정을 설정한다.

이 단계에서는 팀시티를 구성하는 서버/에이전트를 런칭하는 계정을 설정하고, 서비스를 시작하도록 설정한다. 아래 화면을 보면 시스템 계정으로 시작시키는 것을 알 수 있다. 시스템 계정으로 시작시키기에 위에서 설정한 JAVA_HOME과 ANDROID_HOME을 내 계정이 아닌 시스템으로 설정한 이유이기도 하다.

teamcity_install_05 teamcity_install_06 teamcity_install_07

2.4 팀시티 웹 서비스에 연결해서 필요한 설정을 추가한다.

teamcity_install_08 teamcity_install_09

위 화면은 팀시티 설치를 완료하고, 팀시티 웹서비스에 연결해서 필요한 설정을 시작하는 화면이다. 오른쪽 화면에서 팀시티의 데이터를 저장하는 위치를 설정한다. 기본으로 C:\ProgramData 폴더가 지정되어 있는데, 이 폴더는 윈도에서 앱들의 데이터 저장 위치로, 기본 설정이 숨김폴더이다. 그래서 저장하는 데이터를 쉽게 확인할 수 있게 저장 위치를 C:\TeamCityData로 변경한다.
teamcity_install_10

위 화면은 팀시티가 사용할 데이터베이스를 선택하는 화면이다. 데이터베이스로 Internal(HSQLDB), PostgreSQL, MySQL, Oracle, MS SQL Server를 지원한다. 여기에서는 프로젝트가 많지 않아서, Internal(HSQLDB)을 선택한다.

teamcity_install_11 teamcity_install_12

다음은 라이선스 동의 화면으로, “Accept license agreement”는 선택해야 하고, “Send anonymous usage statistics to TeamCity development team”은 옵션이라서, 선택을 해제한다. 그리고 우측의 화면에서 관리자 계정을 생성한다.

teamcity_install_13

이 화면은 관리자 계정을 생성한 후에, 로그인된 화면이다.

이제 팀시티 설치가 마무리 되었다. 다음 포스트에서 팀시티로 안드로이드 앱을 지속적으로 빌드하는데 필요한 설정을 살펴보겠다.

안드로이드 기기의 USB 메모리 마운트 위치

안드로이드 기기를 제조하는 많은(공식 넥서스를 제외, 루팅 하면 가능) 제조사에서 기기에 USB를 사용할 수 있도록 제공하고 있다. 이것은 USB OTG라는 USB 확장지원을 안드로이드에서 지원하게 되었고, 이제 OTG 케이블을 안드로이드 기기에 연결하면 USB 메모리, 마우스 등을 사용할 수 있게 된다.

이게 편리하기는 하지만, 막상 USB를 외부 메모리로 사용하려고 하면, 연결한 USB가 어디에 마운트(mount)되어 있는지 명확하지가 않다. /mnt 나 /storage에 마운트 하는 것이 일반적이다. 그래서 USB를 연결한 안드로이드 기기별로 USB 마운트 위치를 찾아보니, 스택오버플로에 테스트해서 정리한 것이 있다. 정리된 내용은 아래와 같다.

/storage/UsbDriveA (all Samsung devices)
/storage/USBstorage1 (LG G4, V10, G3, G2, other LG devices)
/storage/usbdisk (Moto Maxx, Turbo 2, Moto X Pure, other Motorola devices)
/storage/usbotg (Sony Xperia devices, Lenovo Tabs)
/storage/UDiskA (Oppo devices)
/storage/usb-storage (Acer Iconia Tabs)
/storage/usbcard (Dell Venue — Vanilla Android 4.3 tablet)
/storage/usb (HTC One M7, and some Vanilla Android devices)

삼성과 LG를 확인해보니, 삼성은 UsbDriveA, B… F와 같은 형태로 마운트 위치를 제공하고, LG는 USBstorage1, 2… 의 형태로 여러 개의 USB를 대응하고 있다.

혹시, 더 자세한 정보를 가지고 있는 분이 있다면 알려주시면 매우 감사합니다.

[번역] 안드로이드 앱이 메모리 누수(Leak)를 만드는 8가지 방법

이 글은 Eight Ways Your Android App Can Leak Memory 라는 내용으로 http://blog.nimbledroid.com에 포스팅된 글을 번역한 내용으로 저자로부터 허락을 받았습니다. 그리고 일부 오역이나 의역이 있을 수 있으니 참고해 주시면 감사하겠습니다.


자바처럼 가비지 콜렉터를 사용하는 언어가 가지는 장점 중 하나는 개발자가 명시적으로 메모리를 관리할 필요가 없다는 것이다. 이것은 잘못된 메모리 참조(segmentation fault)로 앱의 크래시나 메모리를 해제하지 않아 힙 메모리가 증가(bloating)하는 가능성을 줄여 안전한 코드를 만들게 한다. 불행하게도, 자바에서도 논리적으로 메모리를 누수 시킬 수 있는 여러 가지 방법이 있다. 결국 안드로이드 앱은 여전히 불필요한 메모리를 사용하고 OOM(Out Of Memory)의 결과로 크래시가 발생하기 쉽다는 것을 말한다.

일반적인 메모리 누수는, 모든 연관 참조가 범위(일반적으로 메서드 완료나 객체의 소멸)를 벗어나기 전에 할당한 메모리를 해제하지 않는 경우에 발생한다. 일반적인 메모리 누수와 다르게, 논리적인 메모리 누수는 앱에서 더는 필요 없는 객체의 참조를 해제하지 않은 것의 결과이다. 객체에 강한 참조(strong reference)가 있는 경우에, 가비지 컬렉터는 메모리에서 객체를 제거할 수 없다. 만약 한 컨텍스트(Context)에서 누수가 발생한다면 안드로이드 개발에 특히 문제가 된다. 액티비티(Activites)와 같은 컨텍스트는 많은 양의 메모리에 많은 참조(예로, 뷰 계층과 기타 자원 등)를 가지고 있기 때문이다. 컨텍스트가 누수된 경우, 컨텍스트가 가르키는 모든 것들 또한 누수된다. 대부분 안드로이드는 제한된 메모리를 사용하는 모바일 기기에서 실행해서, 누수가 많이 발생하면 앱에서 사용할 가용메모리가 전부 소진될 가능성이 매우 크다.

객체의 생명주기가 명확하기 정의되지 않았다면, 논리적인 메모리 누수를 감지하는 것은 주관적인 문제가 됐을 것이다. 다행히도, 액티비티는 매우 명확하게 정의한 생명주기를 가지고 있고, 생명주기는 액티비티 객체가 누수 했는지를 아주 쉽게 고려할 수 있게 한다. 액티비티의 onDestroy() 메서드는 생명이 종료할 때 그리고 프로그래머의 의도나 안드로이드가 일부 메모리 회수할 필요가 있을 때 객체가 종료된다는 것을 알려주는 경우 호출된다. 액티비티의 onDestory() 메서드가 완료했지만 액티비티 객체는 힙 루트의 강한 참조로 연결되어 있을 수 있고, 이런 경우 가비지 콜렉터는 메모리에서 삭제하기 위해 마크할 수 없다(원래 의도가 객체를 삭제하는 것임에도 불구하고). 결과적으로, 정상적인 생명주기를 지나 계속 유지되는 액티비티 객체는 누수라고 정의할 수 있다.

액티비티는 매우 크고 무거운 객체라서, 안드로이드 프레임웍이 처리하는 것들을 무시하도록 선택하면 안 된다. 하지만, 액티비티 객체는 의도하지 않게 누수될 수 있는 방법들이 있다. 안드로이드에서, 잠재적인 메모리 누수로 이끄는 위험한 것들 모두는 두 가지 기본 상황 위주로 연결되어 있다. 첫 번째 메모리 누수 카테고리는 앱의 상태와 관계없이 존재하고 액티비티에 참조로 유지하는 프로세스 전역 정적 객체로 인해서 발생한다. 다른 카테고리는 액티비티에서 강한 참조를 가진 스레드가 액비티비의 수명보다 오래 지속하는 경우에 발생한다. 이런 상황을 만들 수 있는 몇 가지(다른) 방법을 살펴보자.

아래 테스트 코드는 https://github.com/NimbleDroid/Memory-Leaks/blob/master/app/src/main/java/com/nimbledroid/memoryleaks/MainActivity.java 에서 확인할 수 있다.

1. 정적 액티비티(Static Activities)

액티비티를 누수 시키는 가장 쉬운 방법은 액티비티 클래스에 정적 변수를 선언하고, 다음에 액티비티의 실행 객체를 바인딩(setting)하는 것이다. 액티비티의 생명주기가 완료하기 전에 참조가 제거되지 않는다면, 액티비티는 누수될 것이다. 앱의 전체 실행시간에 액티비티의 클래스(예로 MainActivity)를 나타내는 객체가 정적이고 메모리에 적재되어 남아있기 때문이다. 이 클래스 객체가 액티비티 객체에 참조를 유지한다면, 가비지 콜렉션 대상이 아니게 된다.

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)

비슷한 상황은 액티비티가 종종 참조하거나 객체를 메모리에 로드해서 유지하는데 장점이 있는 싱글톤 패턴(singleton pattern)을 구현하는 것일 수도 있고, 그것은 빠르게 복원될 수 있다. 그러나, 앞에서 언급한 것처럼, 액티비티에 정의된 생명주기를 무시하고 메모리에 정적 뷰를 유지하는 것은 매우 위험하고 불필요하다 – 그리고 모든 비용은 피해야한다.

그러나 객체화 하는데 많은 노력(?)이 필요하지만 같은 액티비티의 다른 생명주기에 걸쳐서 변화하지 않는 특별한 뷰를 가지고 있다면 어떻게 될까? 그렇다면, 아래코드에서처럼, 뷰를 객체로 만들고 뷰 계층에 추가한 뒤에 뷰를 정적으로 만들자.

이제 액티비티가 종료되면, 액티비티의 메모리 대부분을 해제할 수 있어야 한다. 추가된 뷰는 뷰의 컨텍스트(이 경우에는 액티비티)에 참조를 유지할 것이라는 것을 알고 있을 겁니다. 뷰에 정적 참조를 만들어서, 액티비티에 영속적인 참조 연결을 만들었고, 누수되었다. 뷰를 정적으로 추가하지 마세요, 만약 해야 한다면 액티비티가 완료하기 전 특정 시점에 뷰 계층에서 정적 뷰를 제거하세요.

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)

계속해서, 내부 클래스라고 불리는 클래스를 액티비티에 정의했다고 가정해보자. 프로그래머는 가독성과 캡슐화를 향상한다는 이유를 포함한 여러 가지 이유로 내부 클래스를 사용할 수 있을 것이다. 내부 클래스의 객체를 생성하고 정적 참조로 유지한다면 어떻게 될까? 이 지점에서 또한 메모리 누수가 다가오는 것을 예상할 수 있을 것이다.

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)

마찬가지로 익명 클래스 역시 익명 클래스를 선언한 클래스에 참조를 유지할 것이다. 그래서 액티비티에 AsyncTask를 익명으로 선언하고 객체화한다면 메모리 누수가 발생할 수 있다. 만약 액티비티가 종료한 후에 익명 AsyncTask가 백그라운드로 계속 동작한다면, 액티비티에 참조가 계속 유지될 것이고 이것은 백그라운드 작업이 완료될 때까지 액티비티가 가비지 콜렉트되지 않는다는 것이다.

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. 핸들러(Handlers)

매우 비슷한 원리가 Runnable 객체로 익명으로 정의된 백그라운드 태스크에 적용된다. 그리고 이 객체는 핸들러 객체로 실행하는 것을 기다린다. 액티비티에 선언된 Runnable 객체는 암시적으로 액티비티에 참조할 것이고, 다음에 핸들러의 메시지 큐(MessageQueue)에 메시지로 등록될 것이다. 액티비티 종료되기 전에 메시지가 처리되지 않는 한, 참조 연결은 액티비티를 메모리에 유지하게 할 것이고 누수의 원인이 된다.

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. 스레드(Thread)

스레드(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. 타이머 작업

별도 스레드에서 작업을 수행하지만, 익명으로 선언되고 객체화되는 한, 이미 종료한 액티비티에 참조 연결을 계속 유지할 것이고 다시 누수의 원인이 될 것이다.

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. 센서 관리자

마지막으로, 컨텍스트(Context)로 getSystemService() 메서드를 호출해서 찾을 수 있는 시스템 서비스가 있다. 시스템 서비스는 자신의 프로세스에서 동작하고, 백그라운드 작업과 같은 종류를 실행하거나 기기 하드웨어의 기능을 인터페이스 해서 앱을 도와준다. 컨텍스트가 서비스에서 이벤트가 발생할 때마다 노티를 받기 원한다면, 리스너로 컨텍스트 자신을 등록시킬 필요가 있다. 그러나, 리스너로 등록하는 것은 서비스가 액티비티에 참조를 유지하게 할 것이고, 그리고 만약 프로그래머가 액티비티가 종료하기 전에 리스너로 등록해제를 하지 않았다면, 이것은 가비지 콜렉션 대상에서 제외할 것이고 메모리 누수를 발생시킬 것이다.

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();
    }
});

이제 실수로 엄청난 양의 메모리를 누수 하기가 얼마나 쉬운지 알 수 있는 다양한 메모리 누수를 살펴봤다. 최악에는 메모리 누수가 앱이 메모리를 다 소진하게 하거나 크래시를 유발할 것이지만, 누수가 항상 메모리 소진이나 크래시를 유발하는 것은 아니다. 대신에, 메모리 누수는 많은 양을 차지할 수 있지만 앱의 메모리 공간에 치명적이지 않은 양일 수 있다. 이 경우, 앱은 다른 객체에 할당해야 하는 메모리의 양이 적으며, 그래서 새로운 객체를 할당하는 공간을 확보하기 위해서 가비지 콜렉터가 더 자주 실행할 필요가 있다. 가비지 콜렉션은 매우 큰 작업이고 사용자가 느려지는 것을 느끼게 할 것이다. 액티비티에서 객체를 만들 때 잠재적인 참조 연결을 주의하고 자주 메모리 누수를 테스트해라.