Android Drawing Process 1

Android 2019. 4. 15. 11:17

(App surface, SF Layer)출처: https://lastyouth.tistory.com/24 [자유로운날개]

 

학기가 끝나서, 지난학기간 한 것들을 포스팅하려고 한다. 언제 다 할지는 미지수지만 그래도 틈틈히 할 예정이다.

 

Smartphone display에 대해 공부를 하다보면 필연적으로 해당 Smartphone 상 동작하는 운영체제에서 어떻게 화면을 그려내는지를 알아야 할 시점이 오게 된다.

 

사실 이전에 수행한 프로젝트 덕분에 Android 운영체제에서 SurfaceFlinger 서비스가 하는 일은 개략적으로 알고 있었으나, SurfaceFlinger는 어찌보면 실제 화면 출력을 담당하는 DisplayController로 보내기 전에 출력될 Layer를 관리하고 필요에 따라서 합성도 해주는 준 최종 관문의 역할을 한다.

 

SurfaceFlinger를 이해하는 것이 분명 Android Drawing Mechanism 을 이해하는데 초석이 되는것은 분명하다. 하지만 SurfaceFlinger는 시스템 서비스로써 여러 Application과 상호작용 한다는 점을 떠올리면, 결국 드로잉의 시작은 Application에서 해야함을 생각할 수 있다.

 

이에 본 포스팅에서는 Application의 Drawing과 그것이 SurfaceFlinger 서비스에 의해 관리되는 과정 일부를 살펴 볼 예정이다.

 

 

위 그림은 사용자의 상호작용에 의해 앱의 화면이 변화할 때, 화면이 다시 갱신되는 과정을 각 계층적으로 나타낸 것이다.

 

앱이 다시 그린 화면은 SurfaceFlinger에 의해 합성되거나 Display Controller에 의해 합성되어 실제 Display에 의해 출력된다. 좀 더 자세히 알아보기 위해 SurfaceFlinger의 역할에 대해 소개한다.

 

 

SurfaceFlinger는 크게 3가지의 역할을 수행하는데, 각각 Layer Management, Vsync Processing 그리고 Screen Refreshing이다.

 

SurfaceFlinger는 하나의 프로세스로 존재하는 시스템 서비스로써 SurfaceFlinger 에 접근하기 위해서는 IPC를 이용해야 하며, 이를 위해 제공하는 인터페이스를 통해 SurfaceFlinger의 기능을 이용한다. SurfaceFlinger는 두 가지 종류의 인터페이스를 지원한다.

 

 

 

이 중 ISurfaceComposerClient는 Client 접미사에서 추측이 가능하듯이 각 어플리케이션마다 관리되는 객체를 컨트롤하기 위한 용도로 사용되며, 주된 기능은 각 어플리케이션 별 Surface를 생성하는 것이다.

 

ISurfaceComposer는 SurfaceFlinger에 의해 관리되는 객체를 컨트롤하기 위한 용도이다.

 

 

이 중 먼저 Layer가 어떻게 관리되는지를 살펴보겠다.

 

Drawing Process (App side)


Layer에 대해 살펴보기 전에 먼저 각 어플리케이션의 View가 어떻게 관리되는지를 알아볼 필요가 있다.

 

 

각 어플리케이션은 여러 뷰를 가질 수 있으며, DecorView를 최종 루트로 가지는 트리 구조로 관리된다.

 

DecorView는 그릴 것이 있는 경우, 할당받은 Surface에 그리게 된다. 따라서 Surface는 각 Application별로 최소 하나는 가지고 있게 된다.

 

그럼 여기서 Surface는 뭐고 Layer는 무엇인가 라는 질문을 할 수 있는데, 엄밀히 말하면 둘은 같은 개념이다. 다음을 보자.

 

 

SurfaceComposerClient로 createSurface 메서드를 호출하면, ISurfaceComposer 인터페이스를 이용해 SurfaceFlinger에 Layer를 하나 만드는 것을 볼 수 있다.

 

즉 Surface는 어플리케이션이 각 소유하는 개념이라면, Layer는 SurfaceFlinger에 의해 관리되는 개념이라고 보면 된다.

 

그러면 이제 각 어플리케이션은 적어도 하나의 Surface를 가지고, 이 Surface는 SurfaceFlinger에 의해 Layer로써 관리된다는 것을 알았다.

 

본격적인 Drawing Process를 설명하기 전 몇 가지 객체에 대해서 설명을 하도록 하겠다.

 

먼저 Choreographer 객체에 대한 설명을 하도록 한다. Choreographer에서는 SurfaceFlinger에서 송신되는 Vsync Event를 요청/수신하여 다음 작업들을 처리한다.

 

1. Input Event Handling

2. Self-Invalidation

3. Animation

 

다음으로 ViewRootImpl은 DecorView와 Choreographer를 연결해주는 일종의 핸들러 역할을 하는 객체로써, DecorView와 Choreographer 사이에서 둘의 요청을 처리하고 중계해주는 역할을 한다.

 

이제 어플리케이션의 Drawing Process를 살펴보도록 하자.

 

실제 그리기는 현재 foreground에 표시되고 있는 어플리케이션에서 그릴 것이 생김으로써 시작된다. 무언가 그릴 것이 생기면 ViewRootImpl 객체 내에 있는 scheduleTraversal 메서드가 호출된다.

 

scheduleTraversal 메서드 내부에서는 Choreographer 객체에게 다음 vsync를 예약해달라는 요청을 보내게 된다. 이 작업이 아래 그림에서 Invalidate와 Vsync scheduling에 해당된다.

 

 

 

 

Vsync 예약 요청을 받은 Choreographer는 SurfaceFlinger로 하여금 다음 Vsync 도착 시 알려달라고 요청한다.

 

다음 Vsync signal이 도착하면 Choreographer는 ViewRootImpl의 performTraversal 메서드를 호출한다. performTraversal 메서드 내부에서는 다시 그려야 될 부분을 measure하고, layout을 재 구성한 다음, performDraw 메서드를 호출하여 그리기를 수행한다.

 

 

ViewRootImpl 객체에서는 performDraw 메서드에 의해 호출되는 draw 메서드가 최종적으로 DecorView로 하여금 할당받은 Surface에 그리게 한다. 

 

이 때, HardwareRenderer를 이용하는 경우와 Software Reneder를 이용하는 경우로 나뉘게 된다.

 

전자는 GPU를 이용하여 Surface에 그리는 것이며, 후자는 CPU로 그리는 것이다. Android 3.0 버전 이후로 하드웨어 가속을 지원하면서 이후 Android에서는 GPU를 이용하여 그리는 것을 기본값으로 한다.

 

GPU든 CPU든 Surface에 View의 변한 부분을 그리는 것은 동일하지만, 그리는 방법이 다른데, 이 부분에 대한 구체적인 내용은 다음을 참조하면 된다.

 

https://developer.android.com/guide/topics/graphics/hardware-accel.html#model

 

간략히 살펴보면, CPU Drawing(Software Rendering)은 Decorview 에서 시작하여 자식 뷰로 내려가면서 그려내는 방법을 사용하고, GPU Drawing은 뷰가 직접 그리는 것이 아닌 DisplayList 라는 추가적인 객체를 이용하여 그리기 작업을 수행하는 것으로 보인다.

 

그리기가 완료되면, 할당받은 Surface가 변했으므로, 이를 SurfaceFlinger에게 통지해야 한다.

 

Layer Management


각 어플리케이션이 Surface를 생성하는 과정과, Surface에 필요한 부분을 그리는 방법을 설명하였다. 위에서도 언급했듯이, 각 어플리케이션은 적어도 하나의 Surface를 가지고, 이 Surface는 SurfaceFlinger에 의해 Layer로써 관리된다는 것을 알았다. 이를 다시 그림으로 나타내면 다음과 같다.

 

 

그러면 Layer와 Surface는 동등한 개념이지만, 존재하는 위치가 다르다. Surface는 어떻게 다시 그려진 내용을 Layer로 하여금 알게 하는 것일까?

 

이를 알기 위해서 실제 그림에 대한 데이터가 존재하는 위치는 다른 데 있다는 것을 알아야 한다. Surface와 Layer는 추상화된 객체일 뿐이며, 실제 화면에 대한 raw data가 존재하는 곳은 따로 있다. 이를 Buffer라고 한다.

 

USB 디버깅이 허용된 디바이스를 연결하고 쉘에 adb shell dumpsys SurfaceFlinger 를 입력하면 현재 표시되는 화면에 대한 SurfaceFlinger의 상태에 대해 표시가 되는데 여기에 보면 다음과 같은 부분을 찾을 수 있다.

 

 

이 부분이 할당된 Buffer로써 실제 화면에 대한 정보를 담고있다. 각 Surface가 Buffer를 하나씩 소유하게 되면, 메모리가 남아나지 않을 것이므로, 어느 정도의 Buffer를 할당해 두고 필요할 때 마다 획득하여 사용하도록 설계되었다. 즉 Producer, Consumer 모델을 따른다고 볼 수 있다.

 

각 Surface와 Layer를 이어주기 위해 BufferQueue가 사용된다. 이름에서 알 수 있듯이 BufferQueue에는 3개의 Buffer가 들어있다. 

 

Buffer는 누가 사용중이냐에 따라서 4가지의 상태를 가지는데, 각각 다음과 같다.

 

1. Free : Queue 안에 존재하며, Surface 혹은 Layer에 의해 사용되고 있지 않으며, 그려지지도 않은 상태

 

2. dequeued : Queue에서 나왔으며, Surface에 의해 점유 중인 상태

 

3. queued : Surface에 의해 쓰여진 buffer가 Queue 안에 있는 상태

 

4. acquired : queued 상태인 buffer가 Layer에 의해 점유 중인 상태

 

그러면 이제 실제로 Surface와 Layer가 어떻게 통신하는지 그림으로 보면서 알아보자.

그림에는 SurfaceFlinger가 소유한 Layer는 표시하지 않았다.

 

초기 상태에는 free buffer가 queue에 3개 들어있는 상태로 시작한다. queue에는 buffer 자체가 들어있는 것이 아니라 buffer의 handle이 들어가 있다.

 

 

여기서 어플리케이션이 소유한 surface가 invalidate 되면, surface는 buffer의 핸들을 획득한다. 이 시점에서 buffer의 상태는 dequeued가 된다.

 

획득한 buffer의 핸들이 가리키는 곳에 변경된 내용을 그린다. 그리는 주체는 앞서 설명한 CPU 혹은 GPU 중 하나가 된다.

 

 

변경된 buffer 핸들을 다시 queue에 넣는다. 이 시점에서 buffer의 상태는 queued가 된다.

 

다음 vsync가 도달하면, Surface는 다시 그리기 작업을 수행하게 되고, SurfaceFlinger는 queued된 buffer가 queue 내에 존재하게 되므로, 이제 Layer Composing 을 수행 하기 위해 해당 buffer를 획득한다.



출처: https://lastyouth.tistory.com/24 [자유로운날개]

블로그 이미지

kuku_dass

,

Android 그래픽 시스템의 발전 과정.


허니컴 이전에는 Surface Flinger 에서만 GPU 사용


허니컴이 Tablet 용 Framework 이다보니 늘어난 pixel 에 대응하기 위해서 GPU 사용이 필요하게 됨.

onDraw() 이후에 실제 그리는 부분을 CPU 에서 하는 것이 아니라 이제는 GPU 에서 하게 됨.


[android] TextureView 에 대한 이야기


기존 View 는 한 View 가 invalidate 가 되면, dirty check를 한 후, parent 로 올라가면서 invalidate 를 쭉 호출하게 되고, 다시 dirty check 된 녀석까지 draw 를 수행하여 그리게 된다.


[android] TextureView 에 대한 이야기


* 기존 View 의 문제

1. UI 스레드에서만 그릴 수 있다.

2. View 의 계층 구조를 타야 한다.

3. 실시간으로 그리기 어렵다.





* 대안으로 등장한 녀석이 SurfaceView

SurfaceView 의 경우 확대, 축소, 비트맵 캡쳐가 안된다.


* 또 다시 대안으로 등장한 녀석이 GLSurfaceView

GLSurfaceView 는 빨리 전환하게 되면 죽는다.


* 또 다른 대안은 RenderScript

RenderScript 는 젤리빈부터 deprecated 되었다.




TextureView 의 등장


SurfaceTexture, TextureView, SurfaceTextureListener 가 한 팀( Set )으로 작동.

이 중 SurfaceTexture 는 framework 가 관리한다.

TextureView 는 일반 View 처럼 사용한다.

Listener 만 잘 override 해서 구현하면 사용이 간편하다.


TextureView 는 Camera 나 OpenGL 에 주로 사용한다.



출처: http://aroundck.tistory.com/2075 [돼지왕 왕돼지 놀이터]

블로그 이미지

kuku_dass

,

Touch Event 와 관련된 메소드는 dispatchTouchEvent, onTouchEvent 가 있다.


가장 최상위에 있는 dispatchTouchEvent가 호출이 되고, 해당 메소드는 하위의 dispatchTouchEvent를 호출한다.


예)

 Activity

  ViewGroup

   View


이런식으로 되어있다고하면, Activity.dispatchTouchEvent -> ViewGroup.dispatchTouchEvent -> View.dispatchTouchEvent

이렇게 호출을 아래로 내려간다.


가장 최하위 View 의 dispatchTouchEvent 까지 도착하게 되면, 이때 View.onTouch , View.onTouchEvent 가 호출된다.

View.onTouch, onTouchEvent 에서 들어온 이벤트를 보고, 처리할지 말지를 판단한 후, 처리를 할거면 (intercept) return true.

처리하지 않을거면 return false 를 한다.

return 된 값은 View.dispatchTouchEvent 로 가게되고 여기서는 Touch 메소드로 부터 받은 리턴값을 그대로 리턴한다.


ViewGroup.dispatchTouchEvent 로 결과값이 넘어오면, 결과값이 true면 이미 View 쪽에서 이벤트가 처리되었음을 의미하기때문에

자신도 그대로 true를 리턴한다.


만약 View 에서 처리되지 않았다면, (return false) 자신의 onTouch, onTouchEvent로 이벤트를 보낸다.




[출처] : http://dktfrmaster.blogspot.kr/2016/09/blog-post_26.html

[안드로이드] 터치 이벤트 흐름

안드로이드는 뷰의 터치 이벤트를 처리하는 3가지 방법이 있다.
 - 엑티비티에서 onTouchEvent 메서드 재정의
 - 이벤트를 처리하고자 하는 뷰의 서브클래스에서 onTouchEvent 메서드 재정의
 - 처리하고자 하는 뷰에 View.OnTouchListener 리스너 인터페이스를 등록

이벤트 처리의 흐름

엑티비티에서의 이벤트 처리

엑티비티는 onTouchEvent 메서드를 재정의하면 모든 뷰의 터치이벤트를 받을 수 있다.

뷰에서의 이벤트 처리

onTouchEvent 메서드를 재정의하거나, OnTouchListener 인터페이스를 등록함으로써, 해당 뷰의 이벤트만 처리할 수 있다.
 - onTouchEvent 메서드를 재정의하려면 상속받아서 서브클래스를 구성해야 하기에 번거롭다.
 - OnTouchListener를 등록하면 인터페이스를 구현한 메서드에서 터치이벤트를 받을 수 있으므로 상속이 필요없다.
만약 2가지 방법이 다 구현되어 있다면??
 - OnTouchListener의 메서드(onTouch) -> onTouchEvent의 순서로 호출된다.
 - onTouch 메서드가 true를 리턴하면, onTouchEvent는 이벤트를 받지 못한다. 

Activity, ViewGroup, View 사이의 이벤트 처리

화면이 다음과 같이 구성되어 있을 때, 기본적인 터치 이벤트의 흐름은 다음과 같다.
Activity(dispatchTouchEvent)              : 하위 dispatchTouchEvent 호출
  ViewGroup(dispatchTouchEvent)       : onInterceptTouchEvent 호출
    ViewGroup(onInterceptTouchEvent) : 하위 dispatchTouchEvent 호출
    View(dispatchTouchEvent)              : 자신의 이벤트 핸들러 메서드 호출
      View(onTouch)                           : 이벤트 처리 & 처리여부 리턴
      View(onTouchEvent)                    : 이벤트 처리 & 처리여부 리턴
    View(dispatchTouchEvent)              : 자신의 이벤트 핸들러 메서드의 결과값을 리턴
    ViewGroup(onTouch)                     : 이벤트 처리 & 처리여부 리턴
    ViewGroup(onTouchEvent)             : 이벤트 처리 & 처리여부 리턴
  ViewGroup(dispatchTouchEvent)       : 자신 or 하위 레이어의 결과값을 리턴
  Activity(onTouchEvent)                    : 이벤트 처리 & 처리여부 리턴
Activity(dispatchTouchEvent)              : 자신 or 하위 레이어의 결과값을 리턴

- Aictivity, ViewGroup, View는 모두 dispatchTouchEventonTouchEvent 메서드를
  가지고 있다.
- ViewGroup에는 추가적으로 onInterceptTouchEvent 메서드가 있다.
- 이벤트가 발생하면 각 레이어의 dispatchTouchEvent가 가장 먼저 호출된다.
  dispatchTouchEvent의 역할은 하위 레이어의 dispatchTouchEvent를 호출하고,
  하위 레이어가 터치 이벤트를 처리했는지 결과를 받아서 처리하지 않은 경우
  자기 자신의 이벤트 처리 메서드(onTouch, onTouchEvent) 메서드로 이벤트를 보낸다.
  그리고는 이벤트 처리 여부를 상위 dispatchTouchEvent에 리턴한다.
- onInterceptTouchEvent는 ViewGroup의 dispatchTouchEvent의 로직을 대신 담당하여,
  자신에게 속한 하위뷰에게 이벤트를 전달할지 결정한다.
- 모든 메서드는 리턴값이 boolean형인데 true일 경우 이벤트를 처리했음을 의미하고,
  false일 경우 이벤트가 처리되지 않았음을 의미한다.


블로그 이미지

kuku_dass

,

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

,

안드로이드의 SharedPreferences.Editor에는

비슷한 역할을 하는 apply()와 commit() 두 개의 메소드가 존재한다.

 

이 둘의 차이점을 알아보자.

 

(*아래 내용은 http://developer.android.com/reference/android/content/SharedPreferences.Editor.html 의 영어 원문 일부를 번역한 것이다.)

 

 

public abstract boolean commt()

 

해당 Editor에서 변경한 preferences 값을 이 Editor의 SharedPreferences 객체에 전달한다.

 

두 개의 Editor가 동시에 preferences를 수정 중이라면 마지막에 호출된 commit()가 적용된다.

 

만약 반환값을 필요로 하지 않는다면 apply()를 써도 좋다.

 

반환값: 변경한 값이 성공적으로 저장장치에 반영되면 true를 반환한다.

 

 

public abstract void apply()

 

해당 Editor에서 변경한 preferences 값을 이 Editor의 SharedPreferences 객체에 전달한다.

 

자동적으로, 원래 SharedPreferences 안의 내용이 무엇이었든간에 요청받은 값으로 내용을 대체한다.

 

두 개의 Editor가 동시에 preferences를 수정 중이라면 마지막에 호출된 apply()가 적용된다.

 

저장공간에 지속적인 동기를 유지하며 preferences를 작성하는 commit()과는 달리,

 

apply()는 메모리 내의 SharedPreferences를 즉시 변경하지만 디스크로의 반영은 비동기적으로 시작되며

 

작업 실패에 대한 어떠한 알림도 받을 수 없다.

 

SharedPreferences는 프로세스 내에서 싱글톤 방식의 인스턴스이기 때문에

 

만약 반환 값을 필요로 하지 않는다면 commit() 대신에 apply()를 쓰는 것이 안전하다.

 

apply()의 경우 안드로이드 컴포넌트의 라이프사이클 및 디스크 쓰기의 상호작용을 신경 쓸 필요가 없다.

 

구조적으로 apply()를 한 후에 이루어지는 in-flight 디스크의 쓰기는 상태(state) 전환이 이루어지기 전에 완료되도록 되어있다

 

 

 

출처 : http://kugistory.net/24

블로그 이미지

kuku_dass

,

Black Box Test

- apk 파일만 있으면, Robotium으로 테스트를 자동화 할 수 있다.
- 조건 : apk 파일이 있어야 한다.
- apk파일에서 aapt dump badging <path>/twitter-3.1.1.apk
  위 명령어를 통해, package 명과 activity 명을 알아내야 한다.

- package, activity 명과 apk 파일이 있으면 테스트가 가능하다.

- package명의 경우, android test project를 만들게 되는데(로보티움 용)
해당 프로젝트 파일 내의 xml 파일의 instrumentation 태그 안에
targetPackage명에 적어줘야 한다.


[중요 내용]
- 테스트할 app의 package 명과, Robotium을 위한 android test project의
package명은 서로 달라야 한다.
 안그러면 에러 발생 : Test run failed: Instrumentation run failed due to 'Native crash'

블로그 이미지

kuku_dass

,

Android: Robotium을 이용한 Twitter 테스팅

Robotium은 Android SDK가 제공하는 테스팅 프레임워크를 더 편하게 사용할 수 있도록 해준다. Robotium을 사용하여 개발중인 프로그램의 테스트도 가능하지만 다른 개발자의 프로그램도 자동화 테스트가 가능하다. (예를 들어, Twitter 프로그램이 네트워크에 미치는 영향을 평가해보기 위해 자동화를 할 필요가 있을 수 있다)

이 글에서는 Twitter Android 클라이언트를 Robotium을 사용하여 실행하고 간단한 사용자 동작을 자동화하는 방법에 대해 살펴보겠다.

1. Twitter Client API 파일 다운 받기

http://www.apkdot.com/download.php?url=http://www.apkdot.com/apk/Twitter_3.1.1.apk

2. APK 파일을 debug certificate으로 사인하기

테스트하려는 대상과 테스트 프로그램이 서로 다른 certificate으로 사인되어있으면 테스트 수행이 불가하다. 이를 해결하기 위해 Twitter APK 파일을 내가 가진 certificate으로 다시 사인해주어야한다.

  • APK 파일의 압축을 푼다. ZIP 포맷이므로 확장자를 ZIP으로 바꾸고 압축을 푼다.
  • META-INF 폴더를 삭제한다.
  • 다시 ZIP으로 압축한 후 APK 확장자로 변경한다.
  • jarsigner로 APK 파일을 signing한다.
    • jarsigner -keystore ~/.android/debug.keystore -storepass android -keypass android twitter-3.1.1.apk androiddebugkey
      • debug.keystore 위치 확인 방법 : Window - Preferences - Android - Build 에 보면..
        Default debug keystore : 에 debug.keystore의 위치가 나와 있을 것이다.
      • 위의 android 는 패스워드 이다.!
      • 내 pc : /usr/lib/jvm/java-7-openjdk-amd64/bin$
  • zipalign을 실행한다.
    •  zipalign 4 twitter-3.1.1.apk twitter-3.1.1-aligned.apk
  • 최종 생성된 twitter-3.1.1-aligned.apk를 사용할 것이다.


3. APK 파일 설치

Android 에뮬레이터에 APK 파일을 설치한다. Emulator를 실행하기 위해 emulator -avd <device name> 명령을 터미널에서 수행한다. Emulator 실행 전에 해당 device를 AVD Manager에서 미리 만들어 놓아야한다. 

adb install <path>/twitter-3.1.1.apk

위 명령을 실행하여 APK를 설치할 수 있다.

4. APK 파일의 정보 얻기

테스트 프로그램 실행에 필요한 Twitter 프로그램의 정보를 얻어야한다. 패키지 이름과 Activity 이름을 얻기 위해 아래와 같은 명령을 실행한다.

aapt dump badging <path>/twitter-3.1.1.apk
(aapt 위치 : (작성자의 컴퓨터 내의 주소 ) ~/AndroidDev/sdk/build-tools/17.0.0)
패키지 이름은 "com.twitter.android" 이고 launchable-activity: name='com.twitter.android.StartActivity' 항목을 보면 실행 가능한 Activity는 com.twitter.android.StartActivity이다.

5. Robotium 테스트 프로그램 작성하기

 Eclipse에서 Android Test Project로 새 프로젝트를 만든다. 새 프로그램의 Java Package 이름은 반드시 com.twitter.android.test로 지정한다. AndroidManifest.xml 파일이 아래와 같이 package name과 target package를 수정한다.


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.twitter.android.test"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="10" />

    <instrumentation
        android:name="android.test.InstrumentationTestRunner"
        android:targetPackage="com.twitter.android" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <uses-library android:name="android.test.runner" />
    </application>
</manifest>

아래와 같이 main 소스 코드를 작성한다. 이 코드를 실행하기 위해서는  Run As에서 Android Junit Test로 실행해주어야 한다. 정상적으로 실행된다면 Twitter가 실행된 후 계정을 만드는 화면으로 자동적으로 전환될 것이다. Good Luck!

====

package com.twitter.android.test;

import android.test.ActivityInstrumentationTestCase2;
import android.app.Instrumentation;
import android.view.MotionEvent;
import android.util.Log;
import android.app.Activity;
import com.jayway.android.robotium.solo.Solo;
import java.lang.Runnable;
import java.lang.Thread;

@SuppressWarnings("unchecked")
public class TestApk extends ActivityInstrumentationTestCase2 {
    private static final String TARGET_PACKAGE_ID = "com.twitter.android";
    private static final String LAUNCHER_ACTIVITY_FULL_CLASSNAME = "com.twitter.android.StartActivity";
    private static Class launcherActivityClass;
    
    static {
        try {
            launcherActivityClass = Class.forName(LAUNCHER_ACTIVITY_FULL_CLASSNAME);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
    
    public TestApk() throws ClassNotFoundException {
        super(TARGET_PACKAGE_ID,launcherActivityClass);
        Log.i("TestApk", "Constructor");
    }
    
    private Solo solo;
    private Instrumentation inst;
    private Activity activity;
    
    @Override
    public void setUp() throws Exception {
        super.setUp();
        solo = new Solo(this.getInstrumentation());
        
        // 실행 시 Hang 이슈를 해결하기 위해 Thread를 사용하였다.
        final Runnable r = new Runnable() 
        {
            public void run()
            {
                TestApk.this.getActivity();
            }
        };
        
        Thread t = new Thread(r);
        t.start();
    }
    
    @Override
    public void tearDown() throws Exception {
        Log.i("TestApk", "tearDown");
    }
    
    public void testLogin() throws InterruptedException {
        Log.i("TestApk", "testLogin");
        solo.sleep(15000);
        solo.clickOnScreen(100, 750);
        Log.i("TestApk", "clicked");
        solo.sleep(5000);
    }
}

출처 : 
http://androidkr.blogspot.kr
블로그 이미지

kuku_dass

,

'Android' 카테고리의 다른 글

[Android] Robotium - BlakcBoxTest 코멘트  (0) 2014.09.11
[Android] Robotium - Twitter 테스트 예제  (0) 2014.09.11
[Android] Context  (0) 2014.09.04
[Android] Communicating with Other Fragments  (0) 2014.09.04
[Android] Fragment  (0) 2014.09.03
블로그 이미지

kuku_dass

,