반응형

안녕하세요. Simple& Happy Dev입니다.

이번에는 Camera2 API 이용해서 손전등 앱을 만들어 보도록 하겠습니다.
앞선 글에서 Camera2 API 이용하는 방법은 다른 방법들 대비해서 장점이 없기 때문에 일반적으로 사용하지 않는다고 말씀드렸습니다.

그래도, 궁금해하실 분이나 필요하실 분도 계실 것 같아서 글을 작성해봅니다.

[Camera2 API 이용한 손전등 앱]

안드로이드 초기 버전부터 사용되던 Camera API는 Android 5.0(API Level: 21)이 되어서야 Camera2 API로 Camera Framework이 변경되면서 구조적으로 큰 변화가 생기게 됩니다. 그 이전까지는 Camera API에서 기본 Feature(전면 카메라 지원, 얼굴인식 지원, 측광/초점 영역 지원, CAF 지원, AE/AWB Lock 지원 등) 추가하는 데 집중되었습니다.
(혹, 오해하실까 봐 말씀드리면, 위의 Feature들은 그 당시 단말기 업체의 카메라 앱에서는 추가되기 전부터 이미 있던 기능들입니다. 다만, 안드로이드 SDK에 API가 없어서 일반 앱 개발자들이 해당 기능들이 들어간 앱을 만들 수가 없었던 것이죠. 안드로이드 SDK에서 API로 지원하지 않는데, 단말기 업체에서는 어떻게 기능을 넣을 수 있게 된 건지에 대해서는 나중에 별도로 다룰 예정입니다)

Camera2 API로 넘어오면서 Camera Framework의 큰 구조적 변화는 성능 개선과 새로운 Feature 구현이 목적이었습니다.
플래시의 경우 이런 변화에 크게 영향은 없지만, 플래시를 제어하기 위한 접근이 조금 더 복잡해졌습니다.

아래는 여기에서 다루고 있는 예제로 만든 앱의 Sequence Diagram입니다. 이거 만드는 데 시간이 오래 걸렸습니다. :)

이후에 소스를 볼 때 아래 그림을 참조하면서 따라오시면 됩니다.

원본이미지 다운받기
FlashlightAppUsingCamera2API_sequence_diagram.zip

 

[소스 설명]

레이아웃 (activity_main.xml) 

화면 한가운데 별 모양 버튼을 하나 추가합니다. 별 모양 버튼은 SDK에 내장된 리소스를 사용하면 됩니다.
향후 이 버튼을 누르면 노란색 별모양으로 변하면서 플래시가 켜지게 할 예정입니다.
노란색 별모양은 자바 소스에서 버튼 클릭되면 변경되도록 처리할 것입니다. (이전 글의 레이아웃과 같습니다)

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<ImageButton
android:id="@+id/ibFlashOnOff"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/btn_star_big_off"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>


자바 (MainActivity.java)

onCreate에서는 기기에 플래시가 지원하지 않으면 메시지 출력하고 3초 후 종료하도록 처리(delayedFinish())하도록 만들어 줍니다.
또한 별 모양 버튼에 대한 클릭 리스너를 만들어서 클릭 시 플래시를 켜주거나 꺼주고(flashlight()), 플래시 켜짐 상태에 따른 별 모양 이미지를 변경하도록 처리해줍니다. (여기까지는 이전 글의 소스와 같습니다)

카메라 특성 정보를 얻고, 카메라 연결을 위해서는 먼저 CameraManager 객체의 인스턴스(mCameraManager)를 얻어야 합니다.

그 아래에 이미지 버튼의 클릭 리스너와 클릭시의 플래시 동작 및 이미지 버튼의 모양 변경에 대한 처리가 있습니다.
(이 부분은 뒷부분에 따로 빼내어서 설명합니다.) 


 

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

if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
Toast.makeText(getApplicationContext(), "There is no camera flash.\n The app will finish!", Toast.LENGTH_LONG).show();
delayedFinish();
return;
}

mCameraManager = (CameraManager) getSystemService(CAMERA_SERVICE);

mImageButtonFlashOnOff = findViewById(R.id.ibFlashOnOff);
mImageButtonFlashOnOff.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
flashlight();
mImageButtonFlashOnOff.setImageResource(mFlashOn ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
}
});
}

 

onResume의 openCamera()에서 CameraManager를 통해 카메라 id들을 가져온 후 필요한 카메라 특성을 구하고(getCameraCharacteristics), 원하는 정보(여기서는 플래시 사용이 가능하고, 화면 기준으로 뒤쪽에 있는 카메라) 조건에 해당하는 카메라 id(주로 "0")를 구합니다.
 
 

  

@Override
protected void onResume() {
super.onResume();
openCamera();
}


private void openCamera() {
try {
for (String id : mCameraManager.getCameraIdList()) {
CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id);
Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING);
if (flashAvailable != null && flashAvailable
&& lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
mCameraId = id;
break;
}
}

...

}

 

다음으로 CameraManager 객체의 openCamera()에 대한 설명입니다.
이것은 Camera API에서 Camera 객체의 open()에 해당합니다. 즉, 카메라 연결하는 것입니다.

openCamera()의 argument를 보시면, 첫 번째는 카메라 id로 앞서 구한 id를 넣어줍니다.
두 번째는 카메라 장치(CameraDevice)의 상태가 변경되면 콜백되는 CameraDevice.StateCallback 객체의 인스턴스를 넣어줍니다.
마지막은 핸들러인데, 여기서는 사용하지 않을 것입니다.

CameraDevice.StateCallback 콜백은 4가지 상태에 대해서 알려줍니다.
그중 아래 3가지는 반드시 구현해서 넣어줘야 합니다.
 - onOpened: 카메라 연결이 완료될 경우
 - onDisconnected: 카메라 장치를 더 이상 사용할 수 없을 경우
 - onError: 카메라 오류가 발생할 경우

onOpened이 콜백되면 parameter인 cameraDevice는 카메라 장치(CameraDevice) 객체의 인스턴스로 카메라 장치 연결종료에 사용이 필요하므로 mCameraDevice에 저장해둡니다. onOpened의 추가설명은 다음단계에서 계속합니다.
onDisconnected와 onError이 콜백될 경우 카메라 장치 연결을 종료합니다.

 

private void openCamera() {

...

mCameraManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
mCameraDevice = cameraDevice;

... 

}

@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
cameraDevice.close();
mCameraDevice = null;
}

@Override
public void onError(@NonNull CameraDevice cameraDevice, int error) {
cameraDevice.close();
mCameraDevice = null;
Toast.makeText(getApplicationContext(), "A camera error has occurred. The app will finish!", Toast.LENGTH_LONG).show();
delayedFinish();
}
}, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

 

다음으로 CaptureRequest를 만들어줘야 하는데, 이름만 보면 Capture 처리요청하는 객체로 생각될 수도 있지만,
이것은 Capture 뿐만 아니라 Preview, Recording, Video Snapshot, Manual control에 대한 처리요청을 위한 객체입니다.

CameraDevice의 CreateCaptureRequest()를 호출하면 argument로 넣은 템플릿 설정(여기서는  TEMPLATE_PREVIEW)으로 초기화되고, CaptureRequest.Builder 객체의 인스턴스를 구할 수 있습니다.

참고로 CaptureRequest.Builder는 CaptureRequest 객체를 포함한 빌더 클래스입니다.

사전에 CaptureRequest의 출력 Target을 설정(addTarget)해주는데, Target은 Surface가 됩니다.
플래시의 경우 Surface로 Preview가 되지 않아도 되기 때문에 Surface는 dummy surface로 만들어줍니다.
혹시 Surface 필요 없다 생각해서 출력 Target을 설정하지 않으면, Exception이 발생하게 됩니다.
최소한 한 개의 Target Surface가 있어야 합니다.

예제에서는 CreateCaptureRequest()를 호출하면 CaptureRequest.Builder 객체의 인스턴스를 mPreviewRequestBuilder에 저장하게 되어 있습니다. 


private void openCamera() {

...

mCameraManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
mCameraDevice = cameraDevice;

try {
List<Surface> list = new ArrayList<>();
if (mTexture != null) {
mTexture.release();
}
mTexture = new SurfaceTexture(1);
Surface surface = new Surface(mTexture);
list.add(surface);
mPreviewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewRequestBuilder.addTarget(surface);

...

 

 

다음으로 CaptureSession을 만들어줘야합니다. CaptureSession은 전달받은 CaptureRequest들이 처리되는 곳입니다.
CameraDevice의 createCameraSession()를 호출후 콜백으로 CaptureSession의 인스턴스를 넘겨받을 수 있습니다.

creatureCamreaSeesion()의 argument를 보시면, 첫 번째는 Target Surface 리스트를 넣어줍니다.
이것은 앞 단계의 소스에 보면 dummy surface를 만들어서 리스트에 추가해주는 부분이 있습니다.
두 번째는 CaptureSession의 상태에 대한 업데이트가 되면 콜백되는 CameraCaptureSession.StateCallback 객체의 인스턴스를 넣어줍니다.
마지막은 핸들러인데, 여기서는 사용하지 않을 것입니다.

카메라 장치가 자체 구성을 완료하면 onConfigured가 호출됩니다. 이때 parameter로 cameraCaptureSession 객체의 인스턴스가 넘어옵니다. 이것은 다른곳에서도 사용이 필요하니, mCaptureSession에 저장해 둡니다.

마지막으로 CaptureRequest에 대한 처리요청을 CaptureSession에서 보내는 곳입니다.
이것을 수행 후 실제 동작이 이뤄지게 됩니다.
CameraCaptureSession의 setRepeatingRequest() 에 CaptureRequest를 argument로 넣어서 보내면 됩니다.
mPreviewRequestBuilder는 CaptureRequest.Builder의 인스턴스인데, build() 메서드를 이용하면 CaptureRequest를 가져올 수 있습니다.

setRepeatingRequest 이름처럼 CaptureRequest에 대한 처리를 반복하도록 합니다.
캡처의 경우 일회성이지만, 프리뷰의 경우 프레임이 계속 들어와야해서 setRepeatingRequest을 사용해야 하고 플래시도 사용자가 요청하기 전까지는 계속 켜져 있어야 하기 때문에 역시 setRepeatingRequest을 사용합니다.

Target Surface가 dummy surface라서 레이아웃으로 만든 화면에 어떠한 변화도 없지만, 만약 레이아웃에 Texture를 추가해주고 이것을 Surface로 만들어서 Target Surface로 추가해주면 카메라 프리뷰화면이 나올 것입니다.

여기까지 하면 플래시 제어를 위한 기본 작업이 완료됩니다.

 

private void openCamera() {

...

mCameraManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
                    ...
cameraDevice.createCaptureSession(list, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
if (null == mCameraDevice) {
return;
}
try {
mCaptureSession = cameraCaptureSession;
cameraCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

@Override
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
}
}, null);

}

...

}

 

이제부터 flashlight() 호출만으로 플래시가 작동합니다.

CaptureRequest.Builder의 set()은 Camera API의 Camera.Parameters의 set()이라고 생각하시면 쉽게 이해되실 겁니다.

그리고, mPreviewRequestBuilder.build()를 하면 플래시 세팅을 해준 CaptureRequest가 만들어져서 나오고, 이를 CameraCaptureSession에 실행하도록 전달하면 플래시가 동작하게 됩니다.
앞에서 설명했지만, setRepeatingRequest는 만든 CaptureRequest를 반복해서 CameraCaptureSession에 요청하기 때문에 사용자가 꺼주는 플래시 세팅을 다시 해주기 전까지는 계속 플래시가 켜져 있게 됩니다.

 

 

 

private void flashlight() {
mFlashOn = !mFlashOn;

mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE, mFlashOn ? CaptureRequest.FLASH_MODE_TORCH : CaptureRequest.FLASH_MODE_OFF);

try {
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

메니페스트(AndroidManifest.xml)

CameraManager의 openCamera() 실행시 카메라 권한을 요구하기 때문에 아래와 같이 <uses-permission android:name="android.permission.CAMERA" />를 메니페스트 파일에 넣어주면 됩니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.help.permissionflashlight">

<uses-permission android:name="android.permission.CAMERA" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

프로젝트 압축화일
PermissionFlashlight3.zip

 

앱 동작 영상


이상으로 "손전등 앱 만들기 두 번째 - Camera2 API 이용방법"에 대해서 설명해 드렸습니다.
Camera2 API를 처음 접하신 분들은 어려울 수도 있는 내용이었습니다.
다음번에는 간단하고 쉬운 방법인 Flashlight API를 이용한 손전등 앱을 만들어 보도록 하겠습니다.

조금이나마 도움이 되셨으면 아래 공감 버튼을 눌러주세요.
(If this article helps you, please press the button below.)

반응형

안녕하세요. Simple& Happy Dev입니다.

구글 플레이에는 수많은 손전등 / 플래시라이트(이하 손전등) 앱이 존재합니다.

일반적으로 스위치 버튼 하나 정도만 있으면 되는 비교적 간단한 UI에 카메라를 다뤄본 경험이 있다면 쉽게 만들 수 있기 때문일 겁니다. 여기서 카메라를 언급한 이유는 손전등의 불빛은 카메라 플래시(Camera Flash LED)를 사용하기에 그렇습니다.

손전등 앱을 만드는 방법은 몇 가지가 있습니다.
앞서 설명했듯이 카메라 플래시를 사용한다는 공통점이 있으나, 카메라 플래시 제어를 위해서 어떻게 접근하는지에 따라서 구분됩니다.

[손전등 앱의 카메라 플래시 제어하는 3가지 방법]

아래 그림에 보듯이 크게 카메라 권한을 가지고 (= 카메라 권한 필요) 플래시를 제어하는 방식과 카메라 권한 없이(카메라 권한 불필요) 플래시를 제어하는 방식으로 만들 수 있습니다.

카메라 플래시 제어 접근 방식

방법1 (Camera API 이용하는 방법)
 - 장점: 안드로이드 구버전도 지원 가능, 구현 용이
 - 단점: 카메라 권한 필요, deprecated API, 느린 속도(but, 약간 개선 가능 - 이글의 마지막에 다룰 예정)

방법2 (Camera2 API 이용하는 방법)
- 장점: 최신 Camera API
- 단점: 카메라 권한 필요, 구현 복잡, 안드로이드 5.0 (API Level 21) 이상부터 가능

방법3 (Flashlight API 이용하는 방법)
- 장점: 카메라 권한 불필요, 구현 매우 용이, 빠른 속도
- 단점: 안드로이드 6.0 (API Level 23) 이상부터 가능

Camera2 API 이용하는 방법은 거의 단점만 존재해서 일반적으로 사용하지 않는 방법입니다.
하지만, 궁금하시거나 해당 방법이 필요하신 분들이 있을 수도 있어서 다음 글에 다루도록 하겠습니다.

일단 이 글에서는 Camera API 이용해서 손전등 앱을 만드는 방법에 관해서 설명하도록 하겠습니다. 

[Camera API 이용한 손전등 앱]

애초에 플래시는 카메라를 보조해주기 위해서 들어간 디바이스입니다.
그래서, 하드웨어적인 연결이나 소프트웨어적인 접근이 카메라에 가깝게 되어 있습니다.

그 결과 플래시를 제어하기 위해서는 카메라에 접근한 이후 카메라 파라미터로 플래시를 넘겨줘야 했습니다.

플래시가 동작하는 순서는 다음과 같습니다.

플래시 켜기: 카메라 객체의 인스턴스 생성 및 연결(open)
                    - 카메라 파라미터에 플래시 모드를 Torch로 설정(setFlashMode)
                    - 카메라 프리뷰 시작(startPreview)

플래시 끄기 방법1: 카메라 프리뷰 종료 (stopPreview) - 카메라 연결 해제 및 릴리즈(release)

플래시 끄기 방법2: 카메라 파라미터에 플래시 모드를 Off로 설정(setFlashMode)
                             (중요 - 추후 카메라 프리뷰 종료 및 카메라 인스턴스를 릴리즈하는 코드가 존재해야 합니다.)

동작하는 순서에 대한 설명은 여기까지 하고 이제 소스로 들어가 보겠습니다.
아래 소스는 플래시를 끌 때 위에 있는 "플래시 끄기 방법1"로 하게 되어 있습니다.
"플래시 끄기 방법2"는 글의 마지막 부분에 설명되어 있습니다.

[소스 설명]

레이아웃 (activity_main.xml)
 

화면 한가운데 별 모양 버튼을 하나 추가해서 이걸 누르면 플래시가 켜지도록 할 예정입니다. 그리고, 한 번 더 누르면 버튼이 토글되어서 꺼지도록 하겠습니다. 별 모양 버튼은 SDK에 내장된 리소스를 사용하시면 됩니다.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<ImageButton
android:id="@+id/ibFlashOnOff"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/btn_star_big_off"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

자바 (MainActivity.java)

onCreate에서는 기기에 플래시가 지원하지 않으면 메시지 출력하고 3초 후 종료하도록 처리(delayedFinish())하도록 만들어 줍니다.
또한 별 모양 버튼에 대한 클릭 리스너를 만들어서 클릭 시 플래시를 켜주거나 꺼주고(flashlight()), 플래시 켜짐 상태에 따른 별 모양 이미지(켜짐: 노란색 별, 꺼짐: 흰색 별)를 변경하도록 처리해줍니다.

package com.example.help.permissionflashlight;

import android.content.pm.PackageManager;
import android.hardware.Camera;
import android.os.Handler;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
private static Camera mCamera = null;
private ImageButton mImageButtonFlashOnOff;
private boolean mFlashOn;

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

if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
Toast.makeText(getApplicationContext(), "There is no camera flash.\n The app will finish!", Toast.LENGTH_LONG).show();
delayedFinish();
return;
}

mImageButtonFlashOnOff = findViewById(R.id.ibFlashOnOff);
mImageButtonFlashOnOff.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
flashlight();
mImageButtonFlashOnOff.setImageResource(mFlashOn ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
}
});
}

private void delayedFinish() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
finish();
}
}, 3000);
}

초기에는 플래시가 꺼진 상태이고, 카메라 인스턴스인 mCamera가 null이므로 onCreate에 있던 flashlight()는 openCamera()에서 카메라 객체의 인스턴스를 생성 및 연결하고 문제가 없으면 true로 리턴하고, 이후 mFlashOn를 토글시켜주어서 켜진 상태(mFlashOn => true)로 세팅됩니다.

그리고, 플래시를 Torch 모드로 파라미터 세팅을 해주고, 프리뷰를 시작해주면 플래시가 켜지게 됩니다.

반대로 플래시가 켜진 상태에서 openCamera()에 진입하면 이전에 mCamera가 생성된 상태라서 별다른 처리 없이 true로 리턴되고 mFlashOn이 토글되면서 꺼진 상태(mFlashOn => false)로 세팅됩니다.

꺼진 상태일 경우 프리뷰를 종료하도록 하고, 카메라 인스턴스를 릴리즈해줍니다. 이후 mCamera는 꼭 null로 만들어주어야 이후 오동작을 하지 않게 됩니다.

private boolean openCamera() {
if (mCamera == null) {
try {
mCamera = Camera.open();
} catch (RuntimeException e) {
Toast.makeText(getApplicationContext(), "Camera open failed", Toast.LENGTH_LONG).show();
e.printStackTrace();
return false;
}
}
return true;
}

private void flashlight() {
if (openCamera()) {
mFlashOn = !mFlashOn;

if (mFlashOn) {
Camera.Parameters params = mCamera.getParameters();
params.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);

mCamera.setParameters(params);
mCamera.startPreview();

} else {
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
} else {
delayedFinish();
}
}
}

여기까지 작업 후 실행을 하게 되면 아래와 같은 에러를 만나게 되고 openCamera()의 try ~ catch 에 의해 강제 종료(Force Close)가 되지 않고, 메시지 출력 후 openCamera() 리턴을 false로 해서 3초 후 종료할 수 있게 되어 있습니다. 

W 3858     3858     ServiceManager:    Permission failure: android.permission.CAMERA from uid=10210 pid=3165
E  3858     3858     CameraService:      Permission Denial: cant use the camera pid=3165, uid=10210
W 3165     3165     CameraBase:         An error occurred while connecting to camera 0: Service not available
W 3165     3165     System.err:             java.lang.RuntimeException: Fail to connect to camera service
W 3165     3165     System.err:             at android.hardware.Camera.<init>(Camera.java:519)
W 3165     3165     System.err:             at android.hardware.Camera.open(Camera.java:379)
W 3165     3165     System.err:             at com.example.help.permissionflashlight.MainActivity.openCamera(MainActivity.java:50)
W 3165     3165     System.err:             at com.example.help.permissionflashlight.MainActivity.flashlight(MainActivity.java:61)
W 3165     3165     System.err:             at com.example.help.permissionflashlight.MainActivity$1.onClick(MainActivity.java:32)

위와 같은 현상이 일어난 것은 에러 메시지의 내용과 같이 카메라 권한이 없는 상태에서 접근했기 때문에 발생한 것입니다.


메니페스트(AndroidManifest.xml)

카메라 권한을 추가하기 위해서는 아래와 같이 <uses-permission android:name="android.permission.CAMERA" />를 메니페스트 파일에 넣어주면 됩니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.help.permissionflashlight">

<uses-permission android:name="android.permission.CAMERA" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

프로젝트 압축화일
PermissionFlashlight.zip


실행 결과


cf) 플래시 끄기 방법2

플래시 끄기 1번의 방법은 stopPreview 및 release도 해주기 때문에 플래시가 꺼질 때 시간이 좀 걸립니다.
개선된 방법인 플래시 끄기 방법2에서는 카메라 플래시 모드를 FLASH_MODE_OFF로 파라미터 설정해주기만 하면 되기에 속도가 빠릅니다.

그 외에 몇 군데 다르게 처리해야 하는 부분들도 있습니다.

앞서 간단히 언급한 부분이기도 한데, 플래시 끄는 곳에서는 카메라 파라미터에 플래시 모드를 off 해주는 것 외에는 다른 처리를 하지 않고 있어서 카메라 프리뷰 종료 및 카메라 인스턴스를 릴리즈하는 코드는 onPause 쪽에 넣어주어야 합니다.

그리고, 그 코드가 onPause로 이동했기에 객체의 인스턴스 생성하는 코드는 onResume에 넣어주는 게 적당합니다.

자세한 코드는 소스를 첨부하니 참고하시길 바랍니다.
PermissionFlashlight2.zip


이상으로 "손전등 앱 만들기 첫 번째 - 개요 및 Camera API 이용방법"에 대해서 설명해 드렸습니다.
다음번에는 Camera2 API 이용해서 손전등 앱을 만들어 보도록 하겠습니다.

조금이나마 도움이 되셨으면 아래 공감 버튼을 눌러주세요.
(If this article helps you, please press the button below.)

반응형

 

 

안드로이드 앱에 다국어 지원이라는 주제로 글을 적어봅니다.

 

문자열은 별도의 xml 파일에 저장 후 Layout이나 Java에서 이것을 이용해야 하지만, 귀찮다는 이유로 Java에 하드코딩 하는 경우도 있을 것입니다.

 

추천하는 방법은 아니지만, 앱에서 지원해야 언어가 많지 않고 굳이 Java에서 다국어 지원을 처리해야겠다 할 경우에 어떻게 하면 되는지 기술해보겠습니다.

 

간단한 테스트를 위해서 버튼을 누르면 텍스트가 출력되는 앱을 만들도록 하겠습니다.

 

언어 변경을 위해서는 "설정(Settings) 앱 - 언어(Language)" 에 가서 원하는 언어로 변경하면 되지만, 테스트를 위해서 매번 그렇게 하기에는 번거롭기 때문에 앱에서 볼륨키를 눌러서 언어를 변경하도록 예제를 만들어보겠습니다.

 

[다국어지원 - JAVA에서 하드코딩 방식]

 

레이아웃 (activity_main.xml)

 

레이아웃은 가운데 Button이 하나가 있고, 그 위에 문자열 출력을 위한 TextView가 있도록 만들었습니다.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="35dp"
android:textColor="@android:color/black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</android.support.constraint.ConstraintLayout>

자바 (MainActivity.java)

실행하면 onCreate에서 TextView와 Button에 "Thanks"와 "Default(English)"가 화면에 출력됩니다.

 

dispatchKeyEvent에서는 볼륨 상하키를 누르는 방향에 따라서 언어를 지정하는 변수가 변경[각주:1]되고, 버튼을 누르면 기본 언어인 영어 "Thanks"에 해당하는 변경된 언어가 TextView에 표시됩니다.

 

package com.example.help.multilanguagetest;

import android.app.Activity;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity {
private TextView mTextView;
private Button mButton;
private int mLang;

private final static String [] THANKS = {"Thanks.", "Gracias.", "Danke.", "Merci.", "감사합니다."};
private final static String [] LANGUAGES = {"Default(English)", "Spanish", "German", "French", "한국어"};

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

mTextView = findViewById(R.id.textView);
mTextView.setText(THANKS[0]);

mButton = findViewById(R.id.button);
mButton.setText(LANGUAGES[0]);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mTextView.setText(THANKS[mLang]);
}
});
}

@Override
public boolean dispatchKeyEvent(KeyEvent event) {

switch(event.getKeyCode()) {
case KeyEvent.KEYCODE_VOLUME_UP:
if (event.getAction() == KeyEvent.ACTION_UP) {
if (mLang < LANGUAGES.length - 1) {
mLang++;
}
mButton.setText(LANGUAGES[mLang]);
}
return true;
case KeyEvent.KEYCODE_VOLUME_DOWN:
if (event.getAction() == KeyEvent.ACTION_UP) {
if (mLang > 0) {
mLang--;
}
mButton.setText(LANGUAGES[mLang]);
}
return true;
default:
break;
}

return super.dispatchKeyEvent(event);
}
}

프로젝트 압축화일

MultiLanguageTest_VolUpDn.zip

 

실행결과

 

 

 

※볼륨키로 언어 변경 에뮬레이팅 없이 실제 세팅에서 언어를 변경할 경우

onCreate에서 로케일의 언어 값을 읽어와서 해당하는 언어에 맞는 문자열을 세팅해주면 됩니다.

세팅에서 변경한 언어가 지원하지 않으면 기본 언어인 영어로 해줍니다.

package com.example.help.multilanguagetest;

import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.util.Locale;

public class MainActivity extends Activity {
TextView mTextView;
Button mButton;

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

Locale locale;
String language;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
locale = getResources().getConfiguration().getLocales().get(0);
} else {
locale = getResources().getConfiguration().locale;
}
language = locale.getLanguage();

mTextView = findViewById(R.id.textView);
mButton = findViewById(R.id.button);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});

switch(language) {
case "en":
mButton.setText("Default(English)");
mTextView.setText("Thanks.");
break;
case "es":
mButton.setText("Español");
mTextView.setText("Gracias.");
break;
case "de":
mButton.setText("Deutsch");
mTextView.setText("Danke.");
break;
case "fr":
mButton.setText("Français");
mTextView.setText("Merci.");
break;
case "ko":
mButton.setText("한국어");
mTextView.setText("감사합니다.");
break;
default:
mButton.setText(language);
mTextView.setText("Thanks.");
break;
}
}
}

프로젝트 압축화일

MultiLanguageTest.zip

 

 

[다국어지원 - xml 처리 방식]

 

Java에서 하드코딩 하는 방식은 가능은 하지만 지원하는 언어가 많거나 문자열이 여러 개인 경우 비효율적입니다.

 

xml에서 다국어 지원하는 방식은 기본 언어에 대해서만 작업을 해주고, 나머지 언어에 대해서는 번역업체에 아웃소싱으로 처리할 수 있기 때문에 프로젝트 진행하는 데 있어서 효율적입니다.

 

xml 방식은 기본언어의 경우 res/values 디렉터리에 strings.xml(프로젝트 생성시 기본으로 만들어짐)에 저장을 하고 추가 언어는 res/values-"language"-"country"/ 디렉터리에 언어별로 strings.xml 파일을 만들어줍니다.

 

예로 들면, 기본 언어(영어)와 한국어, 프랑스어(캐나다)를 지원하는 앱을 개발한다고 했을 경우 아래와 같이 디렉터리를 구성 후 strings.xml 파일을 만들어주면 됩니다.

res/values/strings.xml 
values-ko/strings.xml
values-fr-rCA/strings.xml

초기에는 디렉터리만 만들어주고, 파일은 기본언어의 strings.xml을 복사해서 넣어주면 됩니다.

그런 다음에 아웃소싱한 번역업체에 넘겨주어서 번역 작업을 의뢰합니다.

 

다행히도 안드로이드 스튜디오에서는 다국어 작업은 조금 편하게 할 수 있도록 Translations Editor라는 것을 가지고 있습니다. 이것을 이용하면 외부에서 언어별 디렉터리를 만들어주거나 디렉터리마다 파일을 복사해야 하는 번거로움이 해결됩니다.

 

앞서 만든 예제와 같게 xml 처리 방식으로 만들어 보겠습니다.

 

Translations Editor

 

먼저 strings.xml 편집 창에서 오른쪽 위에 있는 Open editor를 클릭하면 Translations Editor가 실행됩니다.

 

Translations Editor가 실행되면 아래와 같은 화면이 나옵니다.

 

아래 노란색 상자에 있는 버튼들에 대해 설명을 하겠습니다.

1번: 키를 추가합니다. 여기서 키는 그 문자열의 이름이 되겠습니다.

2번: 키를 삭제합니다.

3번: 로케일를 추가합니다. 지원하기 위한 언어를 추가하는 걸 말합니다.

 

한국어를 추가해보겠습니다.

3번 로케일 추가 버튼을 눌러서 한국어를 선택합니다.

 

지원하려는 나머지 언어도 추가해주면 아래와 같은 화면이 나옵니다.

언어별로 첫 번째 키(app_name)의 키값이 기본값으로 모두 채워져 있습니다.

Translations Editor가 동작하기 위해서 첫 번째 키값이 모두 채워진 상태로 시작되어야 합니다.

혹시 나중에 번역하면 넣으려고 첫 번째 키값을 삭제해버린 상태에서 Translations Editor를 종료하면, 그다음에 Translations Editor 진입하면 추가한 언어들이 나오지 않게 됩니다. (중요함)

 

번역이 필요한 키 항목은 빨간색 글자로 나타납니다.

 

 

첫 번째(app_name) 키값들은 나중에 처리하고, 두 번째(language) 키값들은 언어별 키값의 Translation 편집 창에 채워 넣습니다.

첫 번째 키값은 앱의 이름으로 번역을 하지 않고, 타 언어들에도 기본값으로 나오도록 할 예정입니다.

이때는 Untranslatable을 선택해줍니다.

 

이때 키 항목의 글자가 빨간색으로 표시되는 것은 번역이 필요 없는데 타언어들에 키값이 채워져있어서 발생하는 현상입니다. 그래서, 이 키 항목의 기본값을 제외하고 타언어들의 키값들은 삭제해줍니다. 

 

아래가 지원하는 언어의 번역을 추가해서 완료된 화면입니다.

왼쪽에 보면 지원하는 언어별 values 디렉터리가 만들어져 있는 것을 확인하실 수 있습니다.

 

레이아웃 (activity_main.xml)

앞선 JAVA에서 하드코딩 방식과 달리 레이아웃에 문자열을 바로 삽입해줍니다. "@string/키" 형태로 넣어주면 됩니다.

<android.support.constraint.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/thanks"
android:textColor="@android:color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="35dp"
android:text="@string/language"
android:textColor="@android:color/black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</android.support.constraint.ConstraintLayout>

자바 (MainActivity.java)

다국어 작업을 Translations Editor로 처리했기 때문에 자바 쪽에서는 처리해줘야 할 것이 없습니다.

package com.example.help.multilanguagetest;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity {
TextView mTextView;
Button mButton;

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

mTextView = findViewById(R.id.textView);
mButton = findViewById(R.id.button);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//TODO
}
});
}
}

프로젝트 압축화일

MultiLanguageTest2.zip

 

실행결과

 

 

이상으로 "안드로이드 다국어 지원"에 대해서 알아보았습니다.

 

조금이나마 도움이 되셨으면 아래 공감 버튼을 눌러주세요.

 

 

 

  1. 설정앱(Settings) - 언어(Language) 변경을 에뮬레이팅하는 것. [본문으로]
반응형

 

 

Build type(user/userdebug/eng)과 루트 권한에 대해서 알아봅니다.

아래는 Build type의 용도에 관한 간단한 설명입니다.

  Buildtype

  Use

  user

  limited access; suited for production 

  userdebug

  like "user" but with root access and debuggability; preferred for debugging

  eng

  development configuration with additional debugging tools

 

각 Build type 별 보안 및 디버그 관련 설정되는 System Property는 다음과 같습니다.

Buildtype

System Property

   user

  ro.secure = 1 , ro.debuggable = 0

   userdebug

  ro.secure = 1 , ro.debuggable = 1

   eng

  ro,secure = 0 , ro.debuggable = 1

 

System Property는 개별 또는 두 개 조합으로 Framework와 Application에서 Buildtype에 따른 로직 처리를 위해서 사용됩니다. 

대표적으로 Framework에서 Debug 모드 시 분기 조건으로 많이 사용되는 IS_DEBUGGABLE은 아래와 같이 정의되어 있습니다.

/frameworks/base/core/java/android/os/Build.java

 

/** * Returns true if we are running a debug build such as "user-debug" or "eng". * @hide */ public static final boolean IS_DEBUGGABLE = SystemProperties.getInt("ro.debuggable", 0) == 1;

 

 

안드로이드 단말기는 양산할 때 user 바이너리(Build type: user)가 들어갑니다.

물론 시중에 나와 있는 단말기 중에는 userdebug 바이너리가 들어간 것도 있습니다.

 

아래에서 다루겠지만, userdebug인 경우 쉽게 루트 권한을 가질 수 있고[각주:1] 커널 로그도 확인할 수 있어서 개발자 입장에서 선호하는 Build type입니다.

양산단계에 안드로이드 단말기에 user 바이너리가 들어가는 이유

 - 기본탑재 앱(Preload app)의 보호 (기본탑재 앱의 삭제 또는 변형에 따른 오동작 방지)

 - A/S 이슈 (루트 권한을 이용해서 시스템에 리스크를 줄 수 있는 변경에 따른 고장 유발)

 - 보안 이슈

단말기 제조업체에서는 개발 중에는 eng 또는 userdebug 바이너리로 단말기를 개발 또는 테스트합니다.

개발단계에 단말기는 eng / userdebug 바이너리를 사용하는 경우

 - 단말기 브링업/셋업시는 eng 바이너리가 작업에 유리한 점이 많습니다.

 - 단말기 안정화된 후 검증에는 user 바이너리와 가까운 userdebug 바이너리가 적합합니다.

    (예로들면 eng 바이너리는 상대적으로 로드가 있기 때문에 성능관련한 검증 및 사용성 검증 등에는 적합하지 않습니다.)

안드로이드 초기버전에는 user 바이너리 단말에서도 안드로이드 취약점을 이용한 루팅이 가능했지만, 현재는 안드로이드 보안패치로 인하여 막혔습니다. 단, 리커버리를 통한 커스텀 바이너리 업데이트로 루팅은 여전히 가능합니다.

 

userdebug 바이너리 단말기에서는 ADB(Android Debug Bridge) 데몬을 루트 권한으로 동작하게끔 명령(adb root)을 날려줌으로써 루트 권한을 가진 ADB로 restart 하는 방법과 ADB restart 없이 쉘에서 su 명령에 의한 루트 권한으로 해당 명령을 동작하게 하는 방법이 있습니다.

 

[userdebug 바이너리 단말기에서 su 명령에 의한 루트 권한으로 동작하게 하는 방법]

아래와 같이 커널로그(kmsg 로그)는 루트 권한이 없으면 명령이 먹히지 않습니다. 

 

그러나, su 명령으로 루트 권한을 주면 명령이 먹힙니다.

 

 

[userdebug 바이너리 단말기에서 루트 권한을 가진 ADB로 restart 하는 방법]

- adb root 명령 실행 후 uid/gid 모두 root로 변경된 것을 확인할 수 있습니다.

- adb root 명령 실행 후 adb는 루트 권한을 가지고, 이후 su 명령없이도 동작하는 것을 확인할 수 있습니다.

 

 

이상으로 Build type(user/userdebug/eng)과 루트 권한에 대해서 알아보았습니다.

 

조금이나마 도움이 되셨으면 아래 공감 버튼을 눌러주세요.

  1. 루팅(rooting) [본문으로]
반응형

 

 

 

1. Battery Service state (adb shell dumpsys battery) 정보

 

현재 폰의 배터리 상태를 알기 위해서 adb shell dumpsys battery 명령을 실행하면 아래와 같이 출력됩니다.

 

adb shell dumpsys battery
Current Battery Service state:
  mBootCompleted: true
  AC powered: false
  USB powered: true
  Wireless powered: false
  Max charging current: 0
  Max charging voltage: 0
  Charge counter: 0
  status: 2
  health: 2
  present: true
  level: 84
  scale: 100
  voltage: 4063
  temperature: 326
  technology: Li-ion
  batterySWSelfDischarging: false
  batteryMiscEvent: 0
  batteryCurrentEvent: 0
  mSecPlugTypeSummary: 2
  LED Charging: true
  LED Low Battery: true
  current now: 321
  charge counter: 0
  Adaptive Fast Charging Settings: true
USE_FAKE_BATTERY: false
SEC_FEATURE_BATTERY_SIMULATION: false
FEATURE_WIRELESS_FAST_CHARGER_CONTROL: true
  mWasUsedWirelessFastChargerPreviously: true
  mWirelessFastChargingSettingsEnable: true
LLB CAL:
LLB MAN: 20160426
LLB CURRENT: YEAR2018M9D15
LLB DIFF: 123
BatteryInfoBackUp
  mSavedBatteryAsoc: 77
  mSavedBatteryMaxTemp: 490
  mSavedBatteryMaxCurrent: 2872
  mSavedBatteryUsage: 176488
  FEATURE_SAVE_BATTERY_CYCLE: true

 

 

위의 각 항목에 관해서 설명하면 (단말기 제조사에서 추가해준 항목 제외) 아래와 같습니다.

AC Powered

 AC 어댑터 전원으로 충전여부

 

USB Powered

 USB 전원으로 충전여부

 

Wireless powered

무선충전기 전원으로 충전여부

 

Max charging current

최대 충전 전류

 

status

 1: 알수없음, 2: 충전중, 3: 방전중, 4: 충전완료후 충전중지, 5: 충전완료

 

health

 1: 알수없음, 2: 좋음, 3: 과열, 4: 불량  5: 과전압, 6: 불특정 실패, 7: 저온

 

present

 배터리 존재여부

 

level

 현재 배터리 레벨 (아래 scale 이 100일 경우 배터리양은 [level %] 로 읽으면 됨, 위의 경우 배터리양이 14%)

 

scale

 배터리 레벨의 최대값 (보통 100)

 

voltage

 현재 배터리 전압 레벨 (단위: mV)

 

temperature

 배터리 온도 (값을 10으로 나눠서 읽으면 됨, 위의 경우 28.9℃)

 

technology

 배터리 종류

 

 

2. 커널 메시지(kmsg) 배터리 정보

 

커널 메시지중에 배터리 정보를 얻기위해서 cat /proc/kmsg | grep battery 명령을 실행하면 아래와 같이 출력됩니다.

 

 

 

위 로그는 배터리를 모니터링 하는 데몬인 healthd에서 출력합니다.

 

l=59

현재 배터리 레벨

 

v=3878

 배터리 전압 레벨 (단위 mv)

 

t=25.0

 배터리 온도

 

h=2

 Health (1: 알수없음, 2: 좋음, 3: 과열, 4: 불량, 5:과전압, 6: 불특정 실패, 7: 저온)

 

st=3

 Status (1: 알수없음, 2: 충전중, 3: 방전중, 4: 충전완료후 충전중지, 5 충전완료)

 

c=-400

 현재 전류 (단위: mA, 마이너스(-)는 방전상태를 의미)

 

chg=

 충전기 종류 (' ': 충전상태아님, a: AC 어댑터 충전, u: USB 충전, w: 무선 충전) 

 

 

조금이나마 도움이 되셨으면 아래 공감 버튼을 눌러주세요.

반응형

 

메모리 누수 (Memory Leak) 이야기

 

간단하게 정의하면 앱에서 사용한 메모리를 반환하지 않는 현상입니다.

결과적으로 이후 가용 메모리가 줄어듭니다.

 

좀 더 자세히 설명하면, 안드로이드에서는 메모리가 부족하면 GC(Garbage Collector)가 동작해서 더는 유용하지 않는 객체들을 정리해서 메모리를 확보하게 됩니다.

 

그래서, 자바에서는 GC가 있기 때문에 메모리 누수가 없다고 하는데, 꼭 그런 것만은 아닙니다.

 

유용한 객체에서 유용하지 않는 객체를 참조(reference)하는 경우에 GC는 유용하지 않는 객체지만 제거할 수 없게 되어서 메모리 누수가 발생합니다. 

 

메모리 누수는 Exception이나 ANR 처럼 FC(Force Close) 되어서 문제가 있다는 것을 즉각 인지할 수 있는 것도 아니고, 직접적인 단서가 될 로그 정보가 있는 것도 아닙니다. 물론 누수가 심각해서 이후 메모리 할당을 못 할 경우 OOM(Out Of Memory) Exception까지 되는 경우도 있기는 합니다.

 

재현되는 경로가 확인되었고, 누수되는 메모리량이 커서 OOM 까지 되는 경우에는 그나마 인지할 수 있어서 다행이지만, 재현되는 경로 확인이 어렵고 누수되는 메모리량이 많지 않을 때에는 인지하기 어렵기 때문에 그대로 릴리즈될 수 있습니다.

 

이런 경우 문제는 메모리 누수가 있는 앱이 사용 빈도가 높을 경우 메모리 누수량이 계속 누적된다는 것입니다.

 

단말기 제조업체의 경우 QA에서 최종 릴리즈 전에 메모리 누수 테스트를 하고 있지만, 구글 플레이에 올라와 있는 앱들 중에서는 메모리 누수되는 앱들이 존재합니다.

 

이런 앱들은 사용자에게 어떤 영향을 미칠까요?

 

다운로드 한 앱들이 메모리 누수가 지속적으로 발생하게 되면 가용 메모리가 부족하게 되어 이후 메모리 확보를 위해서 GC가 동작하게 됩니다.

 

이때 화면이 렌더링 처리되어야 하는 16ms를 초과하는 GC시간이 빈번할 경우(특히 100ms 이상의 GC)에 일반적으로 버벅거린다고 말하는 Sluggish 현상을 사용자들이 경험하게 됩니다.

 

가용 메모리가 일정 수준 이하이면 LMK(Low Memory Killer) 를 트리거시킵니다. LMK 는 중요한 프로세스를 보호하기 위해서 중요하지 않은 프로세스를 Kill 후 메모리를 확보합니다. LMK 관련해서는 나중에 별도로 다룰 예정입니다.

 

최악의 경우 앞서 언급한 OOM 상황이 되면 다른 앱의 실행이 안 될 수도 있습니다.
많은 메모리 할당이 요구되는 앱들이 OOM으로 인한 FC로 괜한 오해를 받게 됩니다.

 

메모리 누수는 사용자 입장에서도 개발자 입장에서도 불편한 이슈입니다.

 

메모리 누수를 검출하는 많이 알려진 방법은 meminfo 나 procstats 명령을 통한 메모리양의 단순 비교로 메모리 누수 여부에 대해서 검출하는 것입니다. 하지만, 좀 더 편리하고 정확한 위치를 찾기 위한 툴들을 이용합니다.

 

Eclipse에서 안드로이드 개발할 때 DDMS(Dalvik Debug Monitor Server) 의 Heap dump와 Allocation Tracker를 이용했고, 플러그인 형태의 MAT(Memory Analysis Tool)를 사용해서 메모리 누수를 찾았습니다.

 

Android studio로 넘어오면서 Android Profiler라는 이름으로 CPU, MEMORY, NETWORK 항목들의 각 Profiler 가 통합되었고, MEMORY Profiler에서 이전 DDMS처럼 메모리 누수를 찾을 수 있었습니다.

 

이후 소개할 LeakCanary는 PC에서 동작하던 툴들과 달리 폰에서 메모리 누수가 있는 앱을 실행후 액티비티를 종료하게 되면 백그라운드 프로세스로 동작하면서 모니터링 후 메모리 누수가 검출시 Notificaiton으로 알려주기에 이전 툴들에 비해 사용이 쉽고 직관적입니다.

 

LeakCanary 를 이용한 메모리 누수 검출

 

먼저 LeakCanary 에 대한 간단한 설명을 하자면, 미국에 있는 Square라는 회사에서 메모리 누수를 검출하기 위해서 만든 라이브러리입니다.


참고로 이 회사는 모바일 지불 및 금융/가맹점 서비스 관련한 기업인데, 트위터 창업자 잭도시가 이 회사도 만들었습니다.

 

안드로이드 개발하면서 이 회사에서 공개한 라이브러리(Picasso, Moshi, OkHttp, Retrofit 등)를 알게 모르게 사용하거나 들어본 적이 있을 것입니다.


자사 앱의 메모리 누수를 해결하는 과정에 만들어진 라이브러리라고 하고, 이후 외부에 공개되었습니다.

 

LeakCanary는 액티비티가 종료(Destroy)되었지만, GC에 의해 회수되지 않는 메모리가 검출되면 별도의 프로세스에서 Heap Dump 후 hprof 파일로 저장하고 분석을 해서 결과를 리포트합니다. 

 

[LeakCanary 적용방법]

 

1. 아래 라이브러리를 build.gradle 화일에 추가합니다.

     LeakCanary는 Debug 모드에서만 동작합니다.

     Release 모드 라이브러리는 자바 쪽 LeakCanary 코드가 아무런 동작하지 않도록 처리되어 있습니다.

debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'

 

2. Custom Application class를 만들어주고, LeakCanary 설치 코드를 추가합니다.

package com.example.help.myapplication;

import android.app.Application;
import com.squareup.leakcanary.LeakCanary;

public class myapplication extends Application {

@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
}
}

android:name도 추가해줍니다.

<application
android:name=".myapplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

[LeakCanary 이용 메모리 누수 검출 테스트]

뷰나 액티비티를 static field로 참조하는 경우 액티비티가 종료되어도 GC로 메모리가 회수되지 않습니다.
아래의 경우 mText가 여기에 해당합니다.

public class MainActivity extends AppCompatActivity {
static TextView mText;

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

mText = new TextView(this);
mText.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
mText.setText(getString(R.string.app_name));
((ConstraintLayout) findViewById(R.id.mainActivityLayout)).addView(mText);
}
}

앞서 기술한 LeakCanary를 적용 후 앱 실행해서 액티비티를 종료하면 잠시후에 아래와 같이 Notification과 더불어 메모리를 Heap Dump 하고 있다고 알려주는 창이 나옵니다.

  

이때 Notification 창을 내려서 확인해보면 Heap Dump 완료 직후 분석을 진행하고 있습니다.
분석이 완료되면, 메모리 누수가 발생한 액티비티가 표시됩니다.

  

 

더 자세한 내용을 확인하기 위해서 Notification 창을 클릭하게 되면 아래와 같이 확인할 수 있습니다.

또한 Logcat을 통해서도 확인이 가능합니다.

프로젝트 압축화일  MyApplication.zip

이상으로 메모리 누수에 대한 이야기와 LeakCanary를 이용한 메모리 누수 검출방법에 대해서 알아보았습니다.

조금이나마 도움이 되셨으면 아래 공감 버튼을 눌러주세요.

+ Recent posts