Android Lollipop (API 21) 버전 부터 기존 Camera API는 deprecated 되고 Camera2 API가 적용되었는데, Pipeline Stream 기반으로 기존에 비해 세밀한 세팅이 가능하고 비동기 루틴이 많아 Google의 샘플 소스 android-Camera2Basic을 봐서는 분석이 쉽지 않다. Google I/O 2014 프레젠테이션에서 Camera2 API에 대한 적용 흐름이 잘 설명되어 있어 해당 루틴을 기준으로 분석 및 이해를 목적으로 코드를 작성하였으며 실제 프로젝트에 적용하기에는 적절치 않다.

Architecture

source: https://source.android.com/devices/camera/index.html#architecture

Camera HAL(Hardware Abstraction Layer)에서 Application Framework단 사이에 구현된 스택. 왼쪽의 기존 Camera API와 비교하여 CameraService단에 Callback, Listener C++ Binder Interface가 보인다.

source: https://source.android.com/devices/camera/camera3_requests_hal.html

코드를 통해 다시 설명하겠지만, App - Camera2 API - HAL(Hardware Abstraction Layer)모델의 API 적용 프로세스를 표현한 그림.

Process

source: https://www.youtube.com/watch?v=92fgcUNCHic&feature=youtu.be&t=2130

Google I/O 2014 프레젠테이션에서 캡쳐한 Camera2 API 시퀀스. 다음은 영상의 프로세스 시퀸스 다이어그램.

  1. CameraManager
    • 사용가능한 카메라를 나열하고, CameraDevice를 취득하기 위한 Camera2 API의 첫번째 클래스.
  2. CameraCharacteristics
    • CameraManager에 의에 나열된 Camera 하드웨어, 사용가능 세팅등에 대한 정보 취득.
  3. CameraDevice
    • 실질적인 해당 카메라를 나타내는 클래스.
    • CameraManager에 의해 비동기 콜백으로 취득.
  4. CameraCaptureSession
    • CameraDevice에 의해 이미지 캡쳐를 위한 세션 연결.
    • 해당 세션이 연결될 surface를 전달.
  5. CaptureRequest
    • CameraDevice에 의해 Builder패턴으로 생성하며, 단일 이미지 캡쳐를 위한 하드웨어 설정(센서, 렌즈, 플래쉬) 및 출력 버퍼등의 정보(immutable).
    • 해당 리퀘스트가 연결될 세션의 surface를 타겟으로 지정.
  6. CaptureResult
    • CaptureRequest가 수행되고 비동기 CameraCaptureSession.CaptureCallback으로 취득.
    • 해당 세션의 리퀘스트 정보 뿐만 아니라 캡쳐 이미지의 Metadata정보도 포함.

Code Project

분석 및 이해를 용이하게 하기 위해 Camera2 APIs는 클래스 파일(Camera2APIs.java)하나만 작성하여 몰아 넣고 MainActivity에서 해당 클래스만 사용.

AndroidManifest.xml

카메라 디바이스 접근을 위한 권한. Android 6.0 이상에서는 권한요청 코드(requestPermission)가 추가되어야 한다.

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

activity_main.xml

Preview 화면을 위한 TextureView 추가. CaptureSession의 surface로 사용.

<TextureView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/textureView"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true" />

Camera2APIs.java

CameraManager

카메라 시스템 서비스 매니저 리턴.

public CameraManager CameraManager_1(Activity activity) {
    CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
    return cameraManager;
}

CameraCharacteristics

사용가능한 카메라 리스트를 가져와 후면 카메라(LENS_FACING_BACK) 사용하여 해당 cameraId 리턴. StreamConfiguratonMap은 CaptureSession을 생성할때 surfaces를 설정하기 위한 출력 포맷 및 사이즈등의 정보를 가지는 클래스. 사용가능한 출력 사이즈중 가장 큰 사이즈 선택.

public String CameraCharacteristics_2(CameraManager cameraManager) {
    try {
        for (String cameraId : cameraManager.getCameraIdList()) {
            CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
            if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK) {
                StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                Size[] sizes = map.getOutputSizes(SurfaceTexture.class);

                mCameraSize = sizes[0];
                for (Size size : sizes) {
                    if (size.getWidth() > mCameraSize.getWidth()) {
                        mCameraSize = size;
                    }
                }

                return cameraId;
            }
        }
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }

    return null;
}

참고로, INFO_SUPPORTED_HARDWARE_LEVEL키값으로 카메라 디바이스의 레벨을 알 수 있는데 LEGACY < LIMITED < FULL < LEVEL_3 순으로 고성능이며 더 세밀한 카메라 설정이 가능하다. LEGACY 디바이스의 경우 구형 안드로이드 단말 호환을 위해 Camera2 API는 기존 Camera API의 인터페이스에 불과하다. 즉, 프레임 단위 컨트롤 등의 Camera2 기능은 사용할 수 없다.

CameraDevice

비동기 콜백 CameraDevice.StateCallback onOpened()로 취득. null파라미터는 MainThread를 이용하고, 작성한 Thread Handler를 넘겨주면 해당 Thread로 콜백이 떨어진다. 비교적 딜레이가 큰(~500ms) 작업이라 Thread 권장.

public void CameraDevice_3(CameraManager cameraManager, String cameraId) {
    try {
        cameraManager.openCamera(cameraId, mCameraDeviceStateCallback, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

onOpened()에서 취득한 CameraDevice로 CaptureSessionCaptureRequest가 이뤄지는데 Camera2 APIs 처리과정을 MainActivity에서 일원화하여 표현하기 위해 인터페이스로 처리.

private CameraDevice.StateCallback mCameraDeviceStateCallback = new CameraDevice.StateCallback() {
    @Override
    public void onOpened(@NonNull CameraDevice camera) {
        mCameraDevice = camera;
        mInterface.onCameraDeviceOpened(camera, mCameraSize);
    }

    @Override
    public void onDisconnected(@NonNull CameraDevice camera) {
        camera.close();
    }

    @Override
    public void onError(@NonNull CameraDevice camera, int error) {
        camera.close();
    }
};

CaptureSession

일단 세션이 생성(비동기)된 후에는 해당 CameraDevice에서 새로운 세션이 생성되거나 종료하기 이전에는 유효.

public void CaptureSession_4(CameraDevice cameraDevice, Surface surface) {
    try {
        cameraDevice.createCaptureSession(Collections.singletonList(surface), mCaptureSessionCallback, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

생성된 세션에 카메라 프리뷰를 위한 CaptureRequest정보 설정. 프리뷰 화면은 연속되는 이미지가 보여지기 때문에 CONTROL_AF_MODE_CONTINUOUS_PICTURE로 포커스를 지속적으로 맞추고, setRepeatingRequest로 해당 세션에 설정된 CaptureRequest세팅으로 이미지를 지속적으로 요청.

setRepeatingRequest의 null파라미터는 MainThread를 사용하고, 작성한 Thread Handler를 넘겨주면 해당 Thread로 콜백이 떨어진다. 프리뷰 화면은 지속적으로 화면 캡쳐가 이뤄지기 때문에, MainThread 사용 시 Frame drop이 발생할 수 있다. Background Thread 사용 권장.

Google의 android-Camera2Basic샘플 코드에서는 CaptureRequest를 먼저 수행하는데, 여기서는 Google 프레젠테이션 자료 및 모델 그림의 프로세스를 기준으로 작성하기 위해 CaptureSession을 먼저 수행.

private CameraCaptureSession.StateCallback mCaptureSessionCallback = new CameraCaptureSession.StateCallback() {
    @Override
    public void onConfigured(CameraCaptureSession cameraCaptureSession) {
        try {
            mCaptureSession = cameraCaptureSession;
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            cameraCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {

    }
};

캡쳐된 이미지 정보 및 Metadata가 넘어오는데, 프리뷰에서는 딱히 처리할 작업은 없다. 사진 촬영의 경우라면, onCaptureCompleted()에서 촬영이 완료되고 이미지가 저장되었다는 메세지를 띄우는 시점. 캡쳐 이미지와 Metadata 매칭은 Timestamp로 매칭가능하다.

private CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() {
    @Override
    public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
        super.onCaptureProgressed(session, request, partialResult);
    }

    @Override
    public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
        super.onCaptureCompleted(session, request, result);
    }
};

CaptureRequest

카메라 프리뷰(CameraDevice.TEMPLATE_PREVIEW)를 위한 Builder 패턴의 CaptureRequest생성. 예를 들어, 사진 촬영의 경우에는 CameraDevice.TEMPLATE_STILL_CAPTURE로 리퀘스트를 설정한다. surface는 해당 세션에 사용된 surface를 타겟으로 설정.

public void CaptureRequest_5(CameraDevice cameraDevice, Surface surface) {
    try {
        mPreviewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        mPreviewRequestBuilder.addTarget(surface);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

이상으로 프리뷰를 위한 Camera2 APIs 코드 작성은 완료.

Camera2APIs.java 전체 코드.

public class Camera2APIs {

    interface Camera2Interface {
        void onCameraDeviceOpened(CameraDevice cameraDevice, Size cameraSize);
    }

    private Camera2Interface mInterface;
    private Size mCameraSize;

    private CameraCaptureSession mCaptureSession;
    private CameraDevice mCameraDevice;
    private CaptureRequest.Builder mPreviewRequestBuilder;

    public Camera2APIs(Camera2Interface impl) {
        mInterface = impl;
    }

    public CameraManager CameraManager_1(Activity activity) {
        CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
        return cameraManager;
    }

    public String CameraCharacteristics_2(CameraManager cameraManager) {
        try {
            for (String cameraId : cameraManager.getCameraIdList()) {
                CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
                if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK) {
                    StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                    Size[] sizes = map.getOutputSizes(SurfaceTexture.class);

                    mCameraSize = sizes[0];
                    for (Size size : sizes) {
                        if (size.getWidth() > mCameraSize.getWidth()) {
                            mCameraSize = size;
                        }
                    }

                    return cameraId;
                }
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

        return null;
    }

    private CameraDevice.StateCallback mCameraDeviceStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            mCameraDevice = camera;
            mInterface.onCameraDeviceOpened(camera, mCameraSize);
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            camera.close();
        }

        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            camera.close();
        }
    };

    public void CameraDevice_3(CameraManager cameraManager, String cameraId) {
        try {
            cameraManager.openCamera(cameraId, mCameraDeviceStateCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private CameraCaptureSession.StateCallback mCaptureSessionCallback = new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(CameraCaptureSession cameraCaptureSession) {
            try {
                mCaptureSession = cameraCaptureSession;
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                cameraCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, null);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {

        }
    };

    public void CaptureSession_4(CameraDevice cameraDevice, Surface surface) {
        try {
            cameraDevice.createCaptureSession(Collections.singletonList(surface), mCaptureSessionCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    public void CaptureRequest_5(CameraDevice cameraDevice, Surface surface) {
        try {
            mPreviewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            mPreviewRequestBuilder.addTarget(surface);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() {
        @Override
        public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
            super.onCaptureProgressed(session, request, partialResult);
        }

        @Override
        public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
            super.onCaptureCompleted(session, request, result);
        }
    };

    public void closeCamera() {
        if (null != mCaptureSession) {
            mCaptureSession.close();
            mCaptureSession = null;
        }

        if (null != mCameraDevice) {
            mCameraDevice.close();
            mCameraDevice = null;
        }
    }
}

MainActivity.java

준비

TextureView의 surface가 사용가능할 때 카메라 오픈을 위한 Listener 및 MainActivity에서 CaptureSessionCaptureRequest호출을 위한 Camera2Interface 설정.

public class MainActivity extends AppCompatActivity
        implements Camera2APIs.Camera2Interface, TextureView.SurfaceTextureListener {

    private TextureView mTextureView;
    private Camera2APIs mCamera;

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

        mTextureView = (TextureView)findViewById(R.id.textureView);
        mTextureView.setSurfaceTextureListener(this);

        mCamera = new Camera2APIs(this);
    }
}

Open Camera

openCamera()만 호출하면 5단계 과정이 전부 수행되며, 프리뷰가 이뤄진다.

private void openCamera() {
    CameraManager cameraManager = mCamera.CameraManager_1(this);
    String cameraId = mCamera.CameraCharacteristics_2(cameraManager);
    mCamera.CameraDevice_3(cameraManager, cameraId);
}

@Override
public void onCameraDeviceOpened(CameraDevice cameraDevice, Size cameraSize) {
    SurfaceTexture texture = mTextureView.getSurfaceTexture();
    texture.setDefaultBufferSize(cameraSize.getWidth(), cameraSize.getHeight());
    Surface surface = new Surface(texture);

    mCamera.CaptureSession_4(cameraDevice, surface);
    mCamera.CaptureRequest_5(cameraDevice, surface);
}

Surface Texture가 준비 완료된 콜백을 받으면, 카메라 오픈.

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

    if (mTextureView.isAvailable()) {
        openCamera();
    } else {
        mTextureView.setSurfaceTextureListener(this);
    }
}

/* Surface Callbacks */
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
    openCamera();
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {

}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
    return true;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {

}

Close Camera

private void closeCamera() {
    mCamera.closeCamera();
}

@Override
protected void onPause() {
    closeCamera();
    super.onPause();
}

실행화면

References

블로그 이미지

kuku_dass

,

안드로이드 UI는 기본적으로 싱글 스레드 모델로 작동하므로, 이 영향을 고려해 개발하지 않으면 애플리케이션의 성능이 저하될 수 있습니다. 따라서 메인 스레드에서 긴 작업을 하는 것을 피하기 위해 여분의 스레드를 사용해야 합니다. 다른 스레드에서 UI 스레드로 접근할 수 있도록 안드로이드에서 제공하는 스레드 간 통신 방법을 소개합니다.


소개

안드로이드의 애플리케이션을 실행하면 시스템은 메인 액티비티를 메모리로 올려 프로세스로 만들며, 이 때 메인 스레드가 자동으로 생성됩니다. 메인 스레드는 안드로이드의 주요 컴퍼넌트를 실행하는 곳이자 UI를 그리거나 갱신하는 일을 담당할 수 있는 유일한 스레드이므로 UI 스레드라고도 불립니다.

안드로이드 화면을 구성하는 뷰나 뷰그룹을 하나의 스레드에서만 담당하는 원칙을 싱글 스레드 모델이라고 합니다. 싱글 스레드 모델의 규칙은 첫째, 메인 스레드(UI 스레드)를 블럭하지 말 것, 둘째, 안드로이드 UI 툴킷은 오직 UI 스레드에서만 접근할 수 있도록 할 것, 이 두 가지입니다. 이런 싱글 스레드 모델의 영향을 고려하지 않으면 애플리케이션의 성능이 저하될 수 있습니다. 긴 시간이 걸리는 작업을 메인 스레드에서 담당한다면 애플리케이션의 반응성이 낮아질 수 있고, 급기야 사용자의 불편함을 방지하고자 시스템이 애플리케이션을 ANR(Appication Not Responding) 상태로 전환시킬 수도 있습니다. 따라서 시간이 걸리는 작업을 하는 코드는 여분의 스레드를 사용하여 메인 스레드에서 분리해야 하고, 자연스럽게 메인 스레드와 다른 스레드가 통신하는 방법이 필요하게 됩니다.

다른 스레드에서 메인 스레드로 접근하기 위해 Looper와 Handler를 사용할 수 있으며, 안드로이드는 Java의 Thread를 좀 더 쉽게 사용할 수 있도록 래핑한 HandlerThread, 더 나아가 Thread나 Message Loop 등의 작동 원리를 크게 고려하지 않고도 사용이 가능한 AsyncTask 등의 클래스를 제공합니다. 이 글에서는 먼저 Thread-Looper-Handler의 개념을 이해하고, 나아가 HandlerThread와 AsyncTask에 대해 정리해보도록 하겠습니다.

이런 개발 뉴스를 더 만나보세요

 

Looper와 Handler의 사용 목적

왜 안드로이드는 메인 스레드에서만 UI 작업이 가능하도록 제한할까요? 메인 스레드가 아닌 스레드가 병렬적으로 실행되고 있을 때, 메인 스레드와 다른 스레드, 두 개 이상의 스레드가 동시에 같은 텍스트뷰에 setText()를 시도하는 경우를 생각하면 간단합니다.

둘 중 어느 스레드의 setText()가 적용될지 예측할 수 없고, 사용자는 둘 중 하나의 값만을 볼 수 있어 다른 한 스레드의 결과는 버려집니다. 이같이 두 개 이상의 스레드를 사용할 때의 동기화 이슈를 차단하기 위해서 Looper와 Handler를 사용하게 됩니다.

Looper와 Handler의 작동 원리

먼저 스레드와 Looper, Handler가 어떻게 작동하는지 알아볼까요? 메인 스레드는 내부적으로 Looper를 가지며 그 안에는 Message Queue가 포함됩니다. Message Queue는 스레드가 다른 스레드나 혹은 자기 자신으로부터 전달받은 Message를 기본적으로 선입선출 형식으로 보관하는 Queue입니다. Looper는 Message Queue에서 Message나 Runnable 객체를 차례로 꺼내 Handler가 처리하도록 전달합니다. Handler는 Looper로부터 받은 Message를 실행, 처리하거나 다른 스레드로부터 메시지를 받아서 Message Queue에 넣는 역할을 하는 스레드 간의 통신 장치입니다.

이제 Handler와 Looper, Message Queue에 대해 좀 더 자세히 살펴보겠습니다.

Handler

Handler는 스레드의 Message Queue와 연계하여 Message나 Runnable 객체를 받거나 처리하여 스레드 간의 통신을 할 수 있도록 합니다. Handler 객체는 하나의 스레드와, 해당 스레드의 Message Queue에 종속됩니다. 새로 Handler 객체를 만든 경우 이를 만든 스레드와 해당 스레드의 Message Queue에 바인드됩니다. 다른 스레드가 특정 스레드에게 메시지를 전달하려면 특정 스레드에 속한 Handler의 post나 sendMessage 등의 메서드를 호출하면 됩니다. 앞서 Message Queue는 전달받은 Message를 선입선출 형식으로 보관한다고 설명했지만, 전달 시점에 다른 메서드를 사용하여 Queue의 맨 위로 보내거나, 원하는 만큼 Message나 Runnable 객체의 전송을 지연시킬 수도 있습니다. 자주 쓰이는 Handler의 메서드를 아래 표에 정리했습니다.

리턴값메서드명인자설명
voidhandleMessageMessage msgLooper가 Message Queue에서 꺼내준 Message나 Runnable 객체를 처리 
(상속 시 구현 필수)
final booleanpostRunnable rMessage Queue에 Runnable r을 전달
final booleansendMessageMessage msgMessage Queue에 Message msg를 전달
final booleanpostAtFrontOfQueueRunnable rMessage Queue의 맨 앞에 Runnable r을 전달
final booleansendMessageAtFrontOfQueueMessage msgMessage Queue의 맨 앞에 Message msg를 전달
final booleanpostDelayedRunnable r, long delayMillisdelayMillis만큼 지연 후 Message Queue에 Runnable r을 전달
final booleansendMessageDelayedMessage msg, long delayMillisdelayMillis만큼 지연 후 Message Queue에 Message msg를 전달

외부, 혹은 자기 스레드로부터 받은 메시지를 어떤 식으로 처리할 지는 handleMessage() 메서드를 구현하여 정합니다. sendMessage()나 post()로 특정 Handler에게 메시지를 전달할 수 있고, 재귀적인 호출도 가능하므로 딜레이를 이용한 타이머나 스케줄링 역할도 할 수 있어 편리합니다.

Looper와 Message Queue

Looper는 무한히 루프를 돌며 자신이 속한 스레드의 Message Queue에 들어온 Message나 Runnable 객체를 차례로 꺼내서 이를 처리할 Handler에 전달하는 역할을 합니다. 메인 스레드는 Looper가 기본적으로 생성돼 있지만, 새로 생성한 스레드는 기본적으로 Looper를 가지고 있지 않고, 단지 run 메서드만 실행한 후 종료하기 때문에 메시지를 받을 수 없습니다. 따라서 기본 스레드에서 메시지를 전달받으려면 prepare() 메서드를 통해 Looper를 생성하고, loop() 메서드를 통해 Looper가 무한히 루프를 돌며 Message Queue에 쌓인 Message나 Runnable 객체를 꺼내 Handler에 전달하도록 합니다. 이렇게 활성화된 Looper는 quit()이나 quitSafely() 메서드로 중단할 수 있습니다. quit() 메서드가 호출되면 Looper는 즉시 종료되고, quitSafely() 메서드가 호출되면 현재 Message Queue에 쌓인 메시지들을 처리한 후 종료됩니다.

Message와 Runnable

Message란 스레드 간 통신할 내용을 담는 객체이자 Queue에 들어갈 일감의 단위로 Handler를 통해 보낼 수 있습니다. 일반적으로 Message가 필요할 때 새 Message 객체를 생성하면 성능 이슈가 생길 수 있으므로 안드로이드가 시스템에 만들어 둔 Message Pool의 객체를 재사용합니다. obtain() 메서드는 빈 Message 객체를, obtain(Handler h, int what …)은 목적 handler와 다른 인자들을 담은 Message 객체를 리턴합니다. Runnable을 설명하려면 스레드를 만드는 두 가지 방법부터 말씀드려야 합니다. 새 스레드는 Thread() 생성자로 만들어서 내부적으로 run()을 구현하던지, Thread(Runnable runnable) 생성자로 만들어서 Runnable 인터페이스를 구현한 객체를 생성하여 전달하던지 둘 중 하나의 방법으로 생성하게 됩니다. 후자에서 사용하는 것이 Runnable로 스레드의 run() 메서드를 분리한 것입니다. 따라서 Runnable 인터페이스는 run() 추상 메서드를 가지고 있으므로 상속받은 클래스는 run()코드를 반드시 구현해야 합니다. 앞서 언급한대로 Message가 int나 Object같이 스레드 간 통신할 내용을 담는다면, Runnable은 실행할 run() 메서드와 그 내부에서 실행될 코드를 담는다는 차이점이 있습니다.

HandlerThread

Looper에서 언급했듯이 안드로이드의 스레드는 Java의 스레드를 사용하기 때문에 안드로이드에서 도입한 Looper를 기본으로 가지지 않는다는 불편함이 있습니다. 이 같은 불편함을 개선하기 위해 생성할 때 Looper를 자동으로 보유한 클래스를 제공하는데, 이것이 바로 HandlerThread입니다. HandlerThread는 일반적인 스레드를 확장한 클래스로 내부에 반복해서 루프를 도는 Looper를 가집니다. 자동으로 Looper 내부의 Message Queue도 생성되므로 이를 통해 스레드로 Message나 Runnable을 전달받을 수 있습니다.

AsyncTask

AsyncTask는 스레드나 메시지 루프 등의 작동 원리를 몰라도 하나의 클래스에서 UI작업과 backgrond 작업을 쉽게 할 수 있도록 안드로이드에서 제공하는 클래스입니다. 캡슐화가 잘 되어 있기 때문에 사용시 코드 가독성이 증대되는 장점이 있으며, 태스크 스케쥴을 관리할 수 있는 콜백 메서드를 제공하고, 필요할 때 쉽게 UI 갱신도 가능하며 작업 취소도 쉽습니다. 따라서 리스트에 보여주기 위한 데이터 다운로드 등 UI와 관련된 독립된 작업을 실행할 경우 AsyncTask로 간단하게 구현할 수 있습니다.

그림: AsyncTask의 구조

그러나 AsyncTask를 사용해서 스케줄링 할 수 있는 작업 수의 제한이 있고, 몇 초 정도의 짧은 작업에서만 이상적으로 동작한다는 한계가 있습니다. 또한, 안드로이드의 버전 별로 병렬 처리 동작이 다르므로 허니콤 이후 버전에서 멀티 스레드로 병렬적인 동작을 원한다면 AsyncTask를 실행할 때 AsyncTask.THREAD_POOL_EXECUTOR 스케줄러를 지정해야 합니다. 한편 앞서 살펴본 Handler와 Looper를 사용한다면 작동 원리를 고려해야 하며 구현을 직접 해야 하고 코드가 복잡해져서 가독성을 저해한다는 단점이 있지만 그만큼 개발 범위가 자유롭습니다. 또한 UI 스레드에서만 작업하지 않아도 되므로 보다 많은 자율성을 가지고 코드를 제어하기를 원한다면 Handler나 HandlerThread 사용을 고려해 보세요.

도움된 사이트

제목에 링크된 안드로이드 개발자 사이트를 정독하면 해당 클래스나 인터페이스의 개념을 정밀하게 이해하고 사용법을 익힐 수 있습니다. 그 밖에도 다음 사이트들이 크게 도움됐습니다.


블로그 이미지

kuku_dass

,

출처 : http://javacan.tistory.com/entry/Android-Baisc-Resolution-and-DP


안드로이드를 좀 더 잘 해 보기 위해 기초를 다지고 있는데, 그 중 첫 번째로 공부하고 있는 부분이 해상도 및 레이아웃과 관련된 내용이다. 


안드로이드의 주요 단위


안드로이드 기기들이 해상도와 물리적인 크기가 저마다 다르기 때문에, UI 레이아웃을 기기별로 깨지지 않게 만들어주려면 주요 단위에 대한 이해가 필요하다. 다음은 안드로이드 개발시 알아야 하는 용어/단위를 정리한 것이다.


용어 및 단위

설명 

Pixel

화면상의 픽셀 

해상도(Resolution)

픽셀 단위의 화면 크기. 예를 들어, 갤럭시노트 10.1의 해상도는 1280*800인데, 이는 픽셀이 1280개 및 800개임을 의미한다.

DPI (Dots Per Inch) / 밀도

물리적인 1 인치 당 포함되는 픽셀 개수. 예를 들어, 160 DPI는 1인당 픽셀이 160개 포함된다는 것을 의미한다. 주요 DPI는 다음과 같다.

- LDPI (low) : 120 DPI

- MDPI (medium) : 160 DPI

- TVDPI : 213 DPI

- HDPI (high) : 240 DPI

- XHDPI (extra high) : 320 DPI

스크린 크기

물리적인 크기의 종류를 나타낸다. 다음의 4종류가 존재한다.

- X-Large: 주로 10.1 인치 이상의 디바이스

- Large: 주로 5인치 이상의 디바이스

- Normal: 3인치에서 5인치 미만의 사이의 디바이스

- Small: 3인치 미만의 디바이스

 px

픽셀 기반의 단위 

 dip (density-independent pixels) 또는 dp

밀도 독립 단위로, 장치의 밀도에 상관없이 물리적으로 (거의) 동일한 크기를 갖는다. 

 sp (scale-independent pixels)

스케일 독립 픽셀 단위로 , dip와 유사하며, 글꼴 크기를 지정할 때 주로 사용된다.


실제 테스트 해 볼 수 있는 기기별로 확인해보니 주요 값은 다음과 같았다.


 

 갤럭시노트 10.1

옵LTE 2 

넥서스7 

옵Q 

해상도 (픽셀단위)

800 x 1280 

720 x 1280 

800 x 1280 

480 x 800 

해상도 (DP 단위)

800 x 1280 

360 x 640

600 x 961 

320 x 533 

DPI

160 DPI (mdpi)

320 DPI (xhdpi)

213 DPI 
(tvdip, hdip) 
240 DPI 

스크린 크기

xlarge 

normal 

large 

normal 

밀도 비율

(DPI / 160)

1.331250

1.5


안드로이드의 기준 DPI는 중간 수준인 160 DPI이다. 160 DPI를 기준으로 DPI가 크면 밀도가 높아지고, DPI가 작으면 밀도가 낮아진다. 또한, 160 DPI인 경우 밀도독립 단위인 DP(DIP)와 픽셀이 같은 크기를 갖는다. 즉, 160 DPI에서 1 DP는 1 PX이 된다.


PX와 DP


옵LTE2와 넥서스7 그리고 갤럭시노트 10.1에서 트위터를 실행해보면, 기기의 크기는 다르지만, 상단 바 부분의  물리적 높이가 동일한 것을 확인할 수 있다. 또한, 글자 크기도 동일한 것을 확인할 수 있다.


[옵티머스LTE2(좌)와 넥서스7(우)에서 트위터를 실행한 화면. 상단 바와 메뉴의 높이가 (거의) 같다]


위 그림에서 두 기기의 높이 해상도는 1280이지만, 물리적인 크기는 넥서스7이 더 크다. 따라서, 위 그림에서 실제 px 단위의 높이 값은 좌측의 옵티머스LTE2가 넥서스7보다 커야 위와 같이 물리적으로 동일한 크기로 표시된다. 모든 기기마다 물리적으로 동일한 높이를 갖는 px 값을 구해서 계산한다는 것은 매우 힘든데, dp 단위를 사용하면 위 그림처럼 기기의 크기에 상관없이 물리적으로 동일한 크기로 레이아웃을 구성할 수 있다.


XML 레이아웃 설정 파일에서 dp 단위로 크기를 지정하면, 안드로이드는 내부적으로 알맞은 px 단위로 값을 변환해서 크기를 구성한다. 따라서, 개발자는 dp 단위를 사용해서 물리적으로 동일한 크기를 갖는 레이아웃을 구성할 수 있다.


코드에서 직접 크기를 설정하는 경우에는 픽셀 단위로 지정하게 되는데, 이 경우 다음의 공식을 이용해서 dp 단위의 값을 px 단위의 값으로 변환할 수 있다.


px = dp * (DPI / 160)


기기의 DPI 구하기


dp 단위의 값으로부터 px 단위의 값을 구하려면 기기의 DPI를 구해야 하는데, 다음의 코드를 이용하면 DPI를 구할 수 있다.


Display dis = ((WindowManager) getSystemService(WINDOW_SERVICE)).getDefaultDisplay();

DisplayMetrics metrics = new DisplayMetrics();

dis.getMetrics(metrics);

// 해상도: dis.getWidth() * dis.getHeight() / metrics.widthPixels * metrics.heightPixels

// DPI: metrics.densityDpi

// 밀도비율 (DPI / 160) : metrics.density


참고자료

  • 기기별 DPI/해상도/크기 등: http://developer.android.com/tools/revisions/platforms.html
  • Supporting Multiple Screens: http://developer.android.com/guide/practices/screens_support.html



출처: http://javacan.tistory.com/entry/Android-Baisc-Resolution-and-DP [자바캔(Java Can Do IT)]

블로그 이미지

kuku_dass

,