posted by Kyleslab 2024. 5. 3. 22:58

목표

  • OpenGL 기본개념 알기
  • Android에서 OpenGL로 삼각형 그리기

OpenGL ES란?

OpenGL ES는 3차원 컴퓨터 그래픽스 API인 OpenGL(Open Graphic Library)의 임베디드 시스템을 위한 버전이다. ES가 Embaedded System을 의미한다.
OpenGL은 다양한 API를 제공하며 해당 API들을 통해 점, 선, 삼각형, 사각형, 빛 등을 화면에 그릴 수 있다. 즉, OpenGL은 화면에 뭔가 그릴 수 있도록 도와주는 API들로 이루어진 라이브러리다.
비슷한 API로 Windows에는 DirectX가 있다. 차이점은 OpenGL은 크로스 플랫폼을 지원한다.

Vertex

  • 3차원 그래픽의 가장 원자적인 요소
  • 정점의 정보를 가지고 있는 data structure
  • Position을 가지고 있고 2D라면 x,y로 3D라면 x,y,z로 됨

Polygon

  • vertex를 연결해서 만든 면을 polygon이라고 한다.
  • 최소 단위는 vertex이지만 면의 최소단위는 triangle
  • 보통은 삼각형, 초창기에는 삼각형과 사각형의 싸움이었지만 대부분 삼각형을 이어서 표현

좌표계

  • Normalized Device Coordinates (NDC)
    • OpenGL에서는 정사각형의 균일 좌표계를 가정하며, 대체로 정사각형이 아닌 화면에서 완전히 정사각형인 것처럼 좌표를 그립니다.
    • vertex shader에서 여러분의 정점 좌표들이 처리가 되면 정점들이 normalized device coordinates 내부에 들어가게 됩니다. normailized device coordinates는 x, y, z 값이 모두 -1.0와 1.0 사이에 있는 작은 공간입니다. 이 영역 밖에있는 모든 좌표들은 폐기되고 여러분의 화면에 보이지 않게 됩니다. 아래에서 normalized device coordinates에 명시된 삼각형을 볼 수 있습니다(z 축은 무시)
  • 위의 그림에서는 OpenGL 프레임에 사용하는 것으로 간주된 균일 좌표계를 왼쪽에 표시하고, 이 좌표가 가로 모드 방향으로 일반 기기 화면에 실제로 매핑되는 방식을 오른쪽에 보여줍니다. 이 문제를 해결하려면 OpenGL 투영 모드와 카메라 보기를 적용하여 좌표를 변환함으로써 화면에서 그래픽 객체의 비율을 정확하게 맞출 수 있습니다.
  • android view에서는 왼쪽 위가 0,0이지만 openGL에서는 가운데가 0,0이다.

 

그리는 방향

  • 3개 이상의 3차원 점 조합(OpenGL의 꼭짓점이라고 함)에는 앞면과 뒷면이 있습니다.
  • 답은 권선, 즉 도형의 점을 정의하는 방향과 관련이 있습니다.
  • 기본적으로 OpenGL에서는 시계 반대 방향으로 그린 면이 앞면입니다.
  • 도형의 앞면이 어떤 면인지를 아는 것이 왜 중요한가요? 그 이유는 일반적으로 사용되는 면 선별이라고 하는 OpenGL의 기능과 관련됩니다. 면 선별은 렌더링 파이프라인에서 도형의 뒷면을 무시(계산하거나 그리지 않음)하도록 하여 시간과 메모리를 절약하고 처리 주기를 단축하는 Open GL 환경의 한 옵션입니다.

OpenGL Rendering Pipeline

  • 삼각형을 화면에 나타내기 위해 여러가지 과정을 거쳐 처리를 하게 되고 이러한 과정들을 렌더링 파이프라인이라고 합니다. 그리고 이 파이프라인을 이용하려면 셰이더(Shader)라는 서브루틴(프로그램)을 이용해야합니다.
  • 컴퓨터 과학에서 파이프라인(영어pipeline)은 한 데이터 처리 단계의 출력이 다음 단계의 입력으로 이어지는 형태로 연결된 구조를 가리킨다. 이렇게 연결된 데이터 처리 단계는 한 여러 단계가 서로 동시에, 또는 병렬적으로 수행될 수 있어 효율성의 향상을 꾀할 수 있다. 각 단계 사이의 입출력을 중계하기 위해 버퍼가 사용될 수 있다. (출처 : https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8_(%EC%BB%B4%ED%93%A8%ED%8C%85))

 

Shader

  • 셰이더는 그래픽 처리 장치 (GPU)에 데이터를 그리는 방법을 알려줍니다. 셰이더에는 두 가지 유형이 있으며 화면에 무엇인가를 그릴 수 있으려면 두 가지를 모두 정의해야합니다.
  • OpenGL ES 2.0부터는 셰이더를 사용하여 프로그래밍 가능한 API를 추가했습니다. 이때부터는 각 정점이 화면에 제어하는 버텍스 ​​쉐이더와 각 프레그먼트가 그려지는 방식을 제어하는 프레그먼트 쉐이더가 추가되었습니다.

GLSL

  • OpenGL Shading Language. GLslang로도 알려져 있다
  • C 언어를 기초로 한, 상위 레벨 셰이딩 언어이다. 
  • 어셈블리 언어나 하드웨어에 의존한 언어를 사용하지 않고, 개발자가 그래픽스 파이프라인을 직접 제어할 수 있는 언어이다.
uniform mat4 uMVPMatrix;
attribute vec4 vPosition;
void main() {
    gl_Position = uMVPMatrix * vPosition;
}
  • attributes - vertex array를 사용하여 제공되는 vertex data
  • uniforms - shader에서 사용되는 상수 데이터

Vertex Shader

  • 각 정점의 최종 위치를 생성하며 정점마다 한번씩 실행됩니다. 일단 최종 위치가 알려지면, OpenGL은 보이는 정점 집합을 취해 점, 선 및 삼각형으로 만듭니다.

Fragment Shader

  • 점, 선 또는 삼각형의 각 프레그먼트 최종 색상을 생성하며 프레그먼트마다 한 번 실행됩니다. 프래그먼트는 컴퓨터 화면의 픽셀과 유사한 단일 색상의 작고 직사각형 인 영역입니다.
  • Fragment Shader의 주목적은 GPU에게 최종적인 프레그먼트의 색이 무엇이 되어야 하는지 알려주는 것이다. 기본 요소의 모든 프레그먼트에 대해 한 번씩 호출되므로 삼각형이 1000개의 프레그먼트로 매핑되면 Fragment Shader는 1000번 호출된다.

Program

  • Shape를 그리기 위해 사용되는 shader를 포함하는  OpenGL ES 객체

Primitive

Primitive Assembly

  • primitive : 점, 선, 삼각형 등 기본 도형들, OpenGL이 무엇을 그려야하는 지 알려준다.
  • vertex들의 위치정보를 이용하여 삼각형 같은 primitive를 만들어 낸다. vertex들을 서로 연결할때 일정한 순서에 따라 연결이 된다. 

  • 아래의 draw mode등을 선택하면 그것에 맞게 그려준다.

 

Primitive Processing (Clipping)

  • 앞에서 생성한 primitive가 View Volume 내부에 포함되는지 태스트한다. 이 태스트를 통과하지 못한 primitive는 이후 단계에서 무시된다. 이 태스트를 Clipping이라고 부른다. 쉽게 얘기해서 3차원 공간상의 물체를 카메라로 찍었을때 보이지 않는 부분들을 제외시키는 것이다.
  • 클립영역은 일반적으로 렌더링 퍼포먼스향상을 위해 정의된다. 잘 정의된 클립은 렌더러(프로그램)이 사용자가 보지못하는 픽셀과 관련된 계산을 무시함으로써 에너지와 시간을 절약하게 해준다.
  • 앞으로 그려질 픽셀은 클립영역안에 있어야한다. 마찬가지로 그려질 필요가 없는 픽셀은 클립영역 바깥에 있어야 한다. 추가적으로, 이러한 픽셀 들을 '클립됐다'라고 한다.

Rasterizer

  • Primitive를 fragment 단위로 쪼갬
  • Primitive를 화면에 보여주기 위해서는 Primitive의 형태를 화면의 출력 기본단위인 pixel로 근사화해야 한다. primitive가 나누진 각각의 작은 유닛을 fragment라고 부른다.
  • Fragment는 primitive보다는 작고 pixel보다는 크지만 유사한 크기의 단일 색상의 작고 직사각형 인 영역입니다.

 

안드로이드에서의 OpenGL ES 환경 빌드

안드로이드 버전별로 지원목록

  • 안드로이드 1.0 이상 : OpenGL ES 1.0과 1.1
  • 안드로이드 2.0(API Level 8) 이상 : OpenGL ES 2.0
  • 안드로이드 4.3(API Level 18) 이상 : OpenGL ES 3.0
  • 안드로이드 5.0(API Level 21) 이상 : OpenGL ES 3.1

  • OpenGL ES 1.x API 호출과 OpenGL ES 2.0 메서드를 혼합하지 마세요! 두 API는 서로 호환되지 않으므로 이 둘을 함께 사용하면 문제만 발생합니다.
  • 주의: 기기에서 OpenGL ES 3.0 API를 지원하려면 기기 제조업체에서 제공하는 이 그래픽 파이프라인이 구현되어 있어야 합니다.
  • OpenGL ES 3.x API는 2.0 API와 역호환되므로 애플리케이션에서 OpenGL ES를 더욱 유연하게 구현할 수 있습니다.
    • OpenGL ES 2.0 API를 manifest에서 요구사항으로 선언하면 이 API 버전을 기본으로 사용하고, 런타임 시 3.x API를 사용할 수 있는지 확인한 다음, 기기에서 지원하는 경우 OpenGL ES 3.x 기능을 사용할 수 있습니다.

AndroidManifest.xml

<!-- 시스템에게 OpenGL ES 2.0이 필요하다는 것을 알려줍니다. -->
<!-- 이 선언을 추가하면 Google Play에서 OpenGL ES 2.0을 지원하지 않는 기기에 애플리케이션이 설치되지 못하게 제한합니다. -->
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
 
<!-- 시스템에게 OpenGL ES 3.0이 필요하다는 것을 알려줍니다. -->
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
 
<!-- 시스템에게 OpenGL ES 3.1이 필요하다는 것을 알려줍니다. -->
<uses-feature android:glEsVersion="0x00030001" android:required="true" />

 

GLSurfaceView

  • GLSurfaceView는 OpenGL ES 그래픽을 그릴 수 있는 특수 뷰입니다.
  • 객체의 실제 그리기는 이 뷰에 설정한 GLSurfaceView.Renderer에서 제어됩니다.
  • 터치 이벤트 캡쳐를 위해서는 GLSurfaceView를 실제로 이 객체의 코드는 매우 간단하므로 상속을 건너뛰고 수정되지 않은 GLSurfaceView 인스턴스를 생성하려고 할 수 있지만, 그러면 안 됩니다. 터치 이벤트를 캡처하려면 이 클래스를 확장해야 합니다. 이 내용은 터치 이벤트에 응답 학습 과정에 설명되어 있습니다.
import android.content.Context
import android.opengl.GLSurfaceView
 
class MyGLSurfaceView(context: Context) : GLSurfaceView(context) {
 
    private val renderer: MyGLRenderer
 
    init {
 
        // Create an OpenGL ES 2.0 context
        setEGLContextClientVersion(2)
 
        // Render the view only when there is a change in the drawing data
        renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
 
        renderer = MyGLRenderer()
 
        // Set the Renderer for drawing on the GLSurfaceView
        setRenderer(renderer)
    }
}

 

GLSurfaceView.Renderer

  • 이 클래스는 연결된 GLSurfaceView에 그려지는 항목을 제어합니다.
  • onSurfaceCreated() - 뷰의 OpenGL ES 환경을 설정하기 위해 한 번 호출합니다.
  • onDrawFrame() - 뷰를 다시 그릴 때마다 호출합니다.
  • onSurfaceChanged() - 예를 들어 기기의 화면 방향이 변경될 때와 같이 뷰의 도형이 변경되면 호출합니다.
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
 
import android.opengl.GLES20
import android.opengl.GLSurfaceView
 
class MyGLRenderer : GLSurfaceView.Renderer {
 
    override fun onSurfaceCreated(unused: GL10, config: EGLConfig) {
        // Set the background frame color
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
    }
 
    override fun onDrawFrame(unused: GL10) {
        // Redraw background color
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
    }
 
    override fun onSurfaceChanged(unused: GL10, width: Int, height: Int) {
        GLES20.glViewport(0, 0, width, height)
    }
}

 

StateMachine

  • OpenGL은 그리기에 필요한 여러 가지 정보들을 상태 머신(State Machine)에 저장한다.
  • 상태 머신이란 상태를 저장하는 장소이며 그리기에 영향을 미치는 여러 변수값들의 집합이다.
  • 상태 머신은 전역적이며 영속적인 저장소이므로 한번 지정해 놓은 상태 변수는 다른 값으로 바꾸기 전에는 계속 유효하다.
  • 그래서 같은 값이라면 이전 값을 계속 사용할 수 있으며 매번 새로 지정할 필요가 없다. 
  • 색깔만 바꿔 세개의 선을 그리려면, 똑같은 명령어를 약간의 수정만하여 여러번 반복해서 써야만 하지만 상태머신을 통해 현재 설정된 스타일, 두께 색상 등의 값을 시스템 테이블에서 찾아 자동으로 적용하게 된다. 물론 프로그램이 처음 시작될 때에는 디폴트값이 들어있다.
  • State를 변경하는 함수, State를 가져오는 함수등으로 구성되어 있다.

GLThread

  • Renderer를 주기적으로 호출한다.
  • GLSurfaceView의 일관적인 업데이트를 위한 메카니즘
  • 일반적인 UI 처리 방식과 동일
  • 사용자의 비동기적인 요구에 대응하기 위해 이벤트 큐를 유지하며 사용자의 요구는 이벤트 큐에 일단 저장되었다가 적절한 시점에 일괄적으로 처리

https://android.googlesource.com/platform/frameworks/base/+/master/opengl/java/android/opengl/GLSu

public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback2 {
    ...
    private GLThread mGLThread;
    ...
     
    public void setRenderer(Renderer renderer) {
        mRenderer = renderer;
        mGLThread = new GLThread(mThisWeakRef);
        mGLThread.start();
    }
 
    public void queueEvent(Runnable r) {
        mGLThread.queueEvent(r);
    }
 
    ...
 
    static class GLThread extends Thread {         
        ...
        private ArrayList<Runnable> mEventQueue = new ArrayList<Runnable>();                  
        ...
        GLThread(WeakReference<GLSurfaceView> glSurfaceViewWeakRef) {
            super();
            mWidth = 0;
            mHeight = 0;
            mRequestRender = true;
            mRenderMode = RENDERMODE_CONTINUOUSLY;
            mWantRenderNotification = false;
            mGLSurfaceViewWeakRef = glSurfaceViewWeakRef;
         }         
 
         @Override
         public void run() {
              while(true) {
                 ...
                 view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);
                 ...
                 view.mRenderer.onSurfaceChanged(gl, w, h);
                 ...
                 view.mRenderer.onDrawFrame(gl);
                 ...
              }
           }
       }
}

삼각형 그리기

  • OpenGL ES를 사용하면 3차원 공간에서 좌표를 사용하여 그린 객체를 정의할 수 있습니다.
  • 삼각형을 그리려면 먼저 좌표를 정의해야 합니다.
  • OpenGL에서는 일반적으로 부동 소수점 숫자로 된 꼭짓점 배열을 좌표로 정의합니다.
  • 효율성을 극대화하려면 이 좌표를 ByteBuffer에 쓴 다음 OpenGL ES 그래픽 파이프라인에 전달하여 처리하게 합니다.
  • 기본적으로 OpenGL ES는 좌표계에서 [0,0,0](X,Y,Z)은 GLSurfaceView 프레임의 중심을 지정하고, [1,1,0]은 프레임의 오른쪽 상단을 지정하며, [-1,-1,0]은 프레임의 왼쪽 하단을 지정한다고 간주합니다.

 

Vertex 정의

// number of coordinates per vertex in this array
const val COORDS_PER_VERTEX = 3
var triangleCoords = floatArrayOf(     // in counterclockwise order:
        0.0f, 0.622008459f, 0.0f,      // top
        -0.5f, -0.311004243f, 0.0f,    // bottom left
        0.5f, -0.311004243f, 0.0f      // bottom right
)
 
class Triangle {
 
    // Set color with red, green, blue and alpha (opacity) values
    val color = floatArrayOf(0.63671875f, 0.76953125f, 0.22265625f, 1.0f)
 
    private var vertexBuffer: FloatBuffer =
            // (number of coordinate values * 4 bytes per float)
            ByteBuffer.allocateDirect(triangleCoords.size * 4).run {
                // use the device hardware's native byte order
                order(ByteOrder.nativeOrder())
 
                // create a floating point buffer from the ByteBuffer
                asFloatBuffer().apply {
                    // add the coordinates to the FloatBuffer
                    put(triangleCoords)
                    // set the buffer to read the first coordinate
                    position(0)
                }
            }
}

 

Initialize a triangle

class MyGLRenderer : GLSurfaceView.Renderer {
    ...
    private lateinit var mTriangle: Triangle
 
    override fun onSurfaceCreated(unused: GL10, config: EGLConfig) {
        ...
        // initialize a triangle
        mTriangle = Triangle()
    }
    ...
}

 

Draw a triangle

class Triangle {
 
    private val vertexShaderCode =
            "attribute vec4 vPosition;" +
            "void main() {" +
            "  gl_Position = vPosition;" +
            "}"
 
    private val fragmentShaderCode =
            "precision mediump float;" +
            "uniform vec4 vColor;" +
            "void main() {" +
            "  gl_FragColor = vColor;" +
            "}"
 
    ...
}
  • GLSL Shader 코드를 컴파일하고 Programe 객체를 만든다.
fun loadShader(type: Int, shaderCode: String): Int {
 
    // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
    // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
    return GLES20.glCreateShader(type).also { shader ->
 
        // add the source code to the shader and compile it
        GLES20.glShaderSource(shader, shaderCode)
        GLES20.glCompileShader(shader)
    }
}

-----------------------------------------------------------------

class Triangle {
    ...
 
    private var mProgram: Int
 
    init {
        ...
 
        val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
        val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
 
        // create empty OpenGL ES Program
        mProgram = GLES20.glCreateProgram().also {
 
            // add the vertex shader to program
            GLES20.glAttachShader(it, vertexShader)
 
            // add the fragment shader to program
            GLES20.glAttachShader(it, fragmentShader)
 
            // creates OpenGL ES program executables
            GLES20.glLinkProgram(it)
        }
    }
}

-----------------------------------------------------------------

private var positionHandle: Int = 0
private var mColorHandle: Int = 0
 
private val vertexCount: Int = triangleCoords.size / COORDS_PER_VERTEX
private val vertexStride: Int = COORDS_PER_VERTEX * 4 // 4 bytes per vertex
 
fun draw() {
    // Add program to OpenGL ES environment
    GLES20.glUseProgram(mProgram)
 
    // get handle to vertex shader's vPosition member
    positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition").also {
 
        // Enable a handle to the triangle vertices
        GLES20.glEnableVertexAttribArray(it)
 
        // Prepare the triangle coordinate data
        GLES20.glVertexAttribPointer(
                it,
                COORDS_PER_VERTEX,
                GLES20.GL_FLOAT,
                false,
                vertexStride,
                vertexBuffer
        )
 
        // get handle to fragment shader's vColor member
        mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor").also { colorHandle ->
 
            // Set color for drawing the triangle
            GLES20.glUniform4fv(colorHandle, 1, color, 0)
        }
 
        // Draw the triangle
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount)
 
        // Disable vertex array
        GLES20.glDisableVertexAttribArray(it)
    }
}

-------------------------------------------------------------------

override fun onDrawFrame(unused: GL10) {
    ...
 
    triangle.draw()
}

 

Wrong Result

  • 삼각형이 약간 찌그러져 있으며 기기의 화면 방향을 변경하면 도형이 변경됩니다.
  • 도형이 비뚤어지는 이유는 객체의 꼭짓점이 GLSurfaceView가 표시되는 화면 영역의 비율에 맞게 수정되지 않았기 때문입니다.
  • 다음 학습 과정에서 투영과 카메라 보기를 사용하여 이 문제점을 수정할 수 있습니다.

 

MVP Matrix 적용하기

  • Model :  화면에 나타내고자 하는 객체, 삼각형 혹은 구
  • View : Camera 객체, 즉 Model 객체를 바라보는 대상
  • Projection : 투영, 카메라가 모델을 볼 수 있는 공간
    • 점선 모양의 사각뿔 모양이 Frustum(절두체)라고 하는 Projection이다.
    • Perspective Projection : 이 프로젝션은 사람의 눈이나 카메라가 세상을 바라 보는 것처럼 원근법을 적용하여 객체를 표현합니다.
    • Orthographic Projection : 이 프로젝션은 절두체 안에 있는 모든 오브젝트들을 원근과 왜곡 없이 표현합니다. 최종적으로 렌더링 되는 결과물을 보면 프로젝션내의 모든 오브젝트들을 그대로 압축해서 보여주는 느낌이 듭니다.

MVP Matrix 연산 방법

"MVPMatrix = Projection * View * Model"

  • Model : 삼각형은 원점에 존재하므로 Model 행렬은 따로 연산할 필요 없이 단위행렬로 지정
  • View(Camera) : 원점에 있으면 삼각형을 보기 힘드므로 z축으로 일정거리만큼 떼기
  • Projection : Perspective

Projection Matrix 계산

  • 이 변환은 그린 객체가 표시되는 GLSurfaceView의 너비와 높이에 기반하여 객체의 좌표를 조정합니다. 일반적으로 투영 변환은 렌더기의 onSurfaceChanged() 메서드에서 OpenGL 뷰의 비율이 설정되거나 변경될 때만 계산해야 합니다.
// vPMatrix is an abbreviation for "Model View Projection Matrix"
private val vPMatrix = FloatArray(16)
private val projectionMatrix = FloatArray(16)
private val viewMatrix = FloatArray(16)
 
override fun onSurfaceChanged(unused: GL10, width: Int, height: Int) {
    GLES20.glViewport(0, 0, width, height)
 
    val ratio: Float = width.toFloat() / height.toFloat()
 
    // this projection matrix is applied to object coordinates
    // in the onDrawFrame() method
    Matrix.frustumM(projectionMatrix, 0, -ratio, ratio, -1f, 1f, 3f, 7f)
}

View Matrix 계산

  • 이 변환에서는 가상 카메라 위치를 기준으로 그릴 객체의 좌표를 조정합니다.
  • OpenGL ES에서는 실제 카메라 객체를 정의하는 것이 아니라, 그린 객체의 표시를 변환하여 카메라를 시뮬레이션하는 유틸리티 메서드를 제공한다는 점을 기억하는 것이 중요합니다.
  • 카메라 뷰 변환은 GLSurfaceView를 설정할 때 한 번만 계산하거나 사용자 작업 또는 애플리케이션의 기능에 기반하여 동적으로 변경할 수 있습니다.
  • 렌더기에 그리기 프로세스의 일부로 카메라 보기 변환을 추가하면 그린 객체의 변환 프로세스가 완료됩니다.
override fun onDrawFrame(unused: GL10) {
    ...
    // Set the camera position (View matrix)
    Matrix.setLookAtM(viewMatrix, 0, 0f, 0f, -3f, 0f, 0f, 0f, 0f, 1.0f, 0.0f)
 
    // Calculate the projection and view transformation
    Matrix.multiplyMM(vPMatrix, 0, projectionMatrix, 0, viewMatrix, 0)
 
    // Draw shape
    triangle.draw(vPMatrix)

 

  • Camera up : camera의 위가 어디 인지를 표현한다. 0, 1, 0이면 의도한 삼각형이 보이지만 0, -1, 0이면 뒤집혀 보인다.

MVP matrix 적용

class Triangle {
 
    private val vertexShaderCode =
            // This matrix member variable provides a hook to manipulate
            // the coordinates of the objects that use this vertex shader
            "uniform mat4 uMVPMatrix;" +
            "attribute vec4 vPosition;" +
            "void main() {" +
            // the matrix must be included as a modifier of gl_Position
            // Note that the uMVPMatrix factor *must be first* in order
            // for the matrix multiplication product to be correct.
            "  gl_Position = uMVPMatrix * vPosition;" +
            "}"
 
    // Use to access and set the view transformation
    private var vPMatrixHandle: Int = 0
 
    ...
}

------------------------------------------------------------------------

fun draw(mvpMatrix: FloatArray) { // pass in the calculated transformation matrix
 
    // get handle to shape's transformation matrix
    vPMatrixHandle = GLES20.glGetUniformLocation(program, "uMVPMatrix")
 
    // Pass the projection and view transformation to the shader
    GLES20.glUniformMatrix4fv(vPMatrixHandle, 1, false, mvpMatrix, 0)
 
    // Draw the triangle
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount)
 
    // Disable vertex array
    GLES20.glDisableVertexAttribArray(positionHandle)
}

 

Correct Result

 

 

posted by Kyleslab 2024. 5. 3. 20:38

Dependency

  • 의존성 : A가 B를 의존한다. = A가 B를 참조한다.
  • 의존대상 B가 변하면, 그것이 A에 영향을 미친다.
    • B의 기능이 추가 또는 변경되거나 형식이 바뀌면 그 영향이 A에 미친다.

  • Car는 Engine이 없으면 동작하지 않는다.
    • Engine은 Car의 의존성
    • Car가 Engine에 의존한다. = Car가 Engine을 참조한다.
// Without DI

class Car {
    private val engine: Engine = Engine()
    ...
    ...
}
  • Car class는 engine instance 생성과 어떻게 구성하는지까지 책임을 지고 있다.
    • SRP(단일 책임 원칙) 위반
    • 하나의 Class에 하기에는 책임이 많다.
  • Car와 Engine은 타이트한 의존성을 갖는다.
    • Engine에 대한 하위 클래스등을 사용하기가 어렵다.
    • Gas나 Electric 타입의 차를 생성하려면 Car를 재사용하지 못한다.
    • 테스트가 어렵다.
      • Car는 실제 Engine인스턴스를 사용하므로 Test Double을 사용하여 Engine을 수정할 수 없다.

 

Dependency Injection

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}
 
fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}
  • Engine객체를 생성자의 인자로 의존성 주입함으로써 기존 Car클래스가 지닌 책임을 나눈다.
  • 그럼으로써 Car class의 실제 역할에만 집중하게 한다.

그래서 DI란?

  • 클래스가 내부에서 스스로 의존성을 생성하는 대신 외부에서 의존성을 부여하는 것이 의존성 주입(Dependency Injection)이다.

Dependency Injection의 장점

  • Good balance of loosely-coupled dependencies
  • Reusability of code
    • 클래스에서 의존성 생성을 제어하지 않기 때문에 어떤 구성과도 동작할 수 있다.
  • Ease of refactoring
    • Smaller-focused classes
    • Dependency가 API에 노출되어 검증가능한 요소가 되므로 객체 생성 타임 또는 컴파일 타임에 확인할 수 있습니다. 
  • Ease of testing
    • 클래스가 Dependency를 관리하지 않으므로 테스트 시 다양한 구현을 전달하여 다양한 TestCase를 만들어 테스트하는 것이 가능하다.

 

Reusability of code

fun main(args: Array<String>) {
    ...
    val electricCar = Car(ElectricEngine())
    val combustionCar = Car(CombustionEngine())
    ...
}
  • 어떤 엔진의 구현을 넘기더라도 Car를 재사용할 수 있다.

 

Ease of refactoring

class Car {
    // Very, very long source code
}
  • Car class 에서 engine관련 로직을 추출해서 다른 클래스로 만들고 의존성으로 바꿔도 된다.
class Car(
    private val engine: Engine,
    private val battery: Battery,
    private val airFilter: AirFilter,
    private val radiator: Radiator,
    private val wipers: List<Wiper>   
) {
    // Small source code
}
  • 코드 범위가 줄면서 더 단순해진다.
  • 클래스 작업의 인지 부하도 낮아졌다.
  • SRP(단일 책임 원칙)을 지키도록 만들기 쉬워진다.
  • 해당 작업을 계속 반복하면 코드도 줄어들고, Car가 해야할 일이 줄어든다.

Ease of testing

class CarTest {
    @Test
    fun `Car happy path`() {
        val car = Car(FakeEngine())
        ...
    }
 
    @Test
    fun `Car with failing Engine`() {
        val car = Car(FakeFailingEngine())
        ...
    }
}
  • 고장난 엔진을 단 자동차가 어떻게 작동하는 지 본다든지, 다양한 TestCase를 쉽게 만들 수 있다.

Manual dependency injection

Constructor Injection

class Car(private val engine: Engine) {
 
}
  • class의 dependency를 생성자로 전달한다. 

Field Injection (or Setter Injection)

class Car {
    lateinit var engine: Engine
 
    fun start() {
        engine.start()
    }
}
 
fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}
  • Activity나 Fragment와 같은 특정 Android component등은 시스템에서 인스턴스화하므로 생성자 삽입이 불가능하다.

Manual dependency injection's problem

Large amount of boilerplate code

class Car(
    private val engine: Engine,
    private val battery: Battery,
    private val airFilter: AirFilter,
    private val radiator: Radiator
) {
    ...
}
 
fun main(args: Array) {
    val cylinder = Cylinder()
    val engine = Engine(cylinder)
 
    val battery = Battery()
 
    val filterGrade = FilterGrade()
    val airFilter = AirFilter(filterGrade)
 
    val radiator = Radiator()
 
    val car = Car(engine, battery, airFilter, radiator)
}

 

  • 대규모 앱일수록 모든 종속 항목을 가져와 올바르게 연결하려면 대량의 보일러플레이트 코드가 필요할 수 있다.
  • 다중 레이어 아키텍처에서는 최상위 레이어의 객체를 생성하기 위해 아래의 모든 레이어의 모든 dependency가 필요하다.
  • 애플리케이션이 커지면 상용구 코드(예: 팩토리)를 많이 작성하게 되고 상용구 코드는 오류가 발생하기 쉽다.

Android에서 의존성 주입이 어려운 이유?

  • Android class(Activity, Fragment, Service)들이 Framework에 의해 인스턴스화 됨
    • 생성자에 접근할 수 없음
    • Factory를 API 28부터 제공하지만 현실적이지는 않음
    • Instance의 생명주기 관리도 직접해주어야 함

Hilt

Dagger와 Hilt

  • Dagger : 단검, 단도, 비수.
  • Hilt : (칼·단도의) 자루; (무기·도구의) 손잡이.
  • Hilt는 Dagger가 제공하는 컴파일 타임 정확성, 런타임 성능, 확장성 및 Android 스튜디오 지원의 이점을 누리기 위해 인기 있는 DI 라이브러리 Dagger를 기반으로 빌드되었습니다.

Hilt의 특징

  • Android에서 Dependency을 위한 Jetpack의 권장 라이브러리
  • 모든 Android 클래스에 컨테이너를 제공하고 수명 주기를 자동으로 관리
  • Annotation을 통한 Boilerplate code 삭제
  • 컴파일 타임에 dependency를 연결하는 코드를 생성하는 정적 솔루션
    • 컴파일 타임의 의존성 체크
    • 생성된 코드는 명확하고 디버깅이 가능함
    • 성능상의 이점 - 리플렉션 사용X
  • Android Studio 지원

 

Hilt의 단점?

  • LearningCurve가 있음
  • 간단한 프로그램을 만들 때는 번거로움
  • 코드의 가독성을 떨어뜨릴 수 있음

Manual DI와 Hilt

Basics of manual dependency injection

class MemoRepository(
     private val database: MemoDatabase,
     private val remoteDataSource: RemoteDataSource
) {
    fun load(id: String){..}
 }
 
 
 class MemoActivity: Activity() {
 
      private lateinit var repository: MemoRepository
 
      override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
 
          val db = MemoDatabase.getInstance(context)
          val remoteDataSource = RemoteDataSource()
 
          val memoRepository = MemoRepository(db, remoteDataSource)
      }
  }
  • Boilerplate code가 많음
    • 다른 곳에서 MemoRepository을 만드려면 동일하게 코드가 중복될 수 있다.
  • Dependency 선언에 순서가 있다.
  • 객체를 재사용하기 어렵다. 

 

Managing dependencies with a container

// Container of objects shared across the whole app
class AppContainer {
 
   val db = MemoDatabase.getInstance(context)
   val remoteDataSource = RemoteDataSource()
 
   val memoRepository = MemoRepository(db, remoteDataSource)
}

// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
class MemoApplication : Application() {
 
    // Instance of AppContainer that will be used by all the Activities of the app
    val appContainer = AppContainer()
}
  • 이러한 종속 항목은 전체 애플리케이션에 걸쳐 사용되므로 모든 활동에서 사용할 수 있는 일반적인 위치, 즉 애플리케이션 클래스에 배치해야 합니다.
class MemoActivity: Activity() {
 
    private lateinit var memoRepository: MemoRepository
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        val appContainer = (application as MemoApplication).appContainer
        memoRepository = appContainer.memoRepository
    }
  • AppContainer를 직접 관리해야하고, 모든 종속 항목의 인스턴스를 수동으로 만들어야 합니다.
  • 여전히 Boilerplate code가 많다. 

 

Dependency 추가

// Root/build.gradle

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}


// app/build.gradle

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
 
android {
    ...
}
 
dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

Hilt Application class

@HiltAndroidApp
class MemoApplication : Application() { ... }
  • @HiltAndroidApp은 애플리케이션 수준 종속 항목 컨테이너 역할을 하는 애플리케이션의 기본 클래스를 비롯하여 Hilt의 코드 생성을 트리거합니다.

 

Android 클래스에 종속 항목 삽입

  • @AndroidEntryPoint: @Inject annotation이 달린 변수에 의존성 주입을 수행한다.
@AndroidEntryPoint
class MemoActivity : AppCompatActivity() {
 
  @Inject lateinit var memoRepository: MemoRepository
  ...
}
  • @Inject 주석을 사용하여 필드 삽입
    • Hilt가 삽입한 필드는 private일 수 없다. 

Hilt bindings 정의

  • Binding: Hilt에게 dependency의 instance를 제공하는 방법을 알려주는 것

생성자 삽입

class MemoRepository @Inject constructor(
  private val database: MemoDatabase,
  private val remoteDataSource: RemoteDataSource
) { ... }
  • 클래스의 생성자에서 @Inject 주석을 사용하여 클래스의 인스턴스를 제공하는 방법을 Hilt에 알려준다.
  • Annotation이 지정된 클래스 생성자의 매개변수는 그 클래스의 dependency 항목이다.
    • 따라서 Hilt는 database의 instance를 제공하는 방법도 알아야한다. 

 

Hilt Module

  • @Module로 주석이 지정된 클래스
  • 이 모듈은 특정 유형의 instance를 생성하는 방법을 Hilt에 알려준다.
    • 생성자 주입을 사용할 수 없는 경우에서 사용한다.
      • 예를 들어 interface는 생성자가 없으므로 생성자 주입을 사용할 수 없다. 내가 작성하지 않은 클래스(Room 등)를 사용할 때에도 생성자 주입을 사용할 수 없다. 이런 경우에는 Hilt Module을 이용하여 Binding 정보를 제공해야 한다.
  • @InstallIn 주석을 지정하여 각 모듈을 사용하거나 설치할 Android 클래스를 Hilt에 알려야 한다.
    • 예를 들어 앱 전체에서 사용할 모듈이라면 application 범위에, 특정 fragment에서만 사용한다면 fragment 범위에 모듈을 설치해야 한다.
@Module
@InstallIn(ApplicationComponent::class)
object DataModule {
 
  @Provides
  fun provideMemoDB(
     @ApplicationContext context: Context)
  ) = Room.databaseBuilder(context, MemoDatabase::class.java, "Memo.db")
}
  • 클래스가 외부 라이브러리에서 제공되므로 클래스를 소유하지 않은 경우 또는 빌더 패턴으로 instance를 생성해야 하는 경우에도 생성자 삽입이 불가능하다.

주요 Annotation들

@HiltAndroidApp

  • Hilt 코드 생성을 시작하는 시작점, 반드시 Application 클래스에 추가해야 한다.
@HiltAndroidApp
class MemoApplication : Application() {
 
   override fun onCreate() {
      super.onCreate() // 의존성 주입은 super.onCreate()에서 이뤄짐 (bytecode 변환)
   }
}

 

  • 생성된 Hilt_MemoApplication 클래스를 개발자가 직접 상속할 필요없다.

 

  • Bytecode를 수정하지 않고 직접 상속하도록 수정할 수 도 있다.

 

@AndroidEntryPoint

  • @Inject가 붙은 변수에 의존성을 주입하는 역할을 한다.
  • 각 Android 클래스에 관한 개별 Hilt 구성요소를 생성
    • @HiltAndroidApp → Component 생성
    • @AndroidEntryPoint → Subcomponent 생성
  • @AndroidEntryPoint를 지원하는 타입
    • Activity
    • Fragment
    • View
    • Service
    • BroadcastReceiver
  • Component
    • Component는 각 객체의 생명주기에 연결 되고 이와 관련된 의존성을 제공합니다.
    • Hilt 2.28.2 버전부터 @ApplicationComponent 어노테이션이 @SingletonComponent 을 상속받는 형식으로 변경되었고, @ApplicationComponent 가 추후 릴리즈에서 제거된다고 알렸다.

 

  •  
  •  Component Hierarchy
    • Hilt는 이미 정의된 표준화된 component set와 scope를 제공한다.
    • 하위 component는 상위 component가 가지고 있는 의존성에 접근할 수 있다.

  • Component Lifetime
    • 해당 Android 클래스의 수명 주기에 따라 생성된 Component의 인스턴스를 자동으로 만들고 제거합니다. 

Scope와 @InstallIn

  • 범위가 지정되지 않음 (unscoped) - 어노테이션이 지정되지 않았을 때, 인스턴스 주입이 필요할 때마다 항상 새로운 인스턴스를 생성한다.
  • 커스텀 범위가 지정됨 (Custom scoped) - @Singleton 어노테이션과 같이 앱 컨테이너 등의 지정된 컴포넌트 범위동안 같은 인스턴스를 제공한다.
  • Module에서 Scope 지정은 @InstallIn으로 지정한 Component의 범위와 일치해야 합니다.

@Singleton
class MemoRepository @Inject constructor(
  private val database: Database,
  private val remoteDataSource: RemoteDataSource
) { ... }

 

 

@InstallIn

  • 어떤 Component에 이 모듈을 설치할 지 가리킨다.
  • Module의 필수 요소
  • Module에서 Scope 지정은 @InstallIn으로 지정한 Component의 범위와 일치해야 합니다.
  • Hilt에 항상 동일한 데이터베이스 인스턴스를 제공하도록 하려면 @Provides provideMemoDB함수에 @Singleton 어노테이션을 추가한다.
@Module
@InstallIn(ApplicationComponent::class)     // 이곳과
object DataModule {
 
  @Provides
  @Singleton    // 이곳의 범위가 일치해야 한다.
  fun provideMemoDB(
     @ApplicationContext context: Context)
  ) = Room.databaseBuilder(context, MemoDatabase::class.java, "Memo.db")
}

@EntryPoint

  • Hilt가 지원하지 않는 클래스에서 의존성이 필요한 경우 사용
  • Example) ContentProvider, DynamicFeatureModule, Dagger를 사용하지 않는 3rd-party 라이브러리 등등
  • 특징
    • @EntryPoint는 인터페이스에서만 사용
    • @InstallIn이 반드시 함께 있어야 함
    • EntryPoints 클래스의 정적 메서드를 통해 그래프에 접근
// EntryPoint 생성하기
@EntryPoint
@IntallIn(ApplicationComponent::class)
interface FooBarInterface {
    fun getBar(): Bar
}
 
// EntryPoint로 접근하기
val bar = EntryPoints.get(
   applicationContenxt,
   FooBarInterface::class.java
).getBar()

Hilt의 사전 정의된 qualifier 

  • @ApplicationContext 또는 @ActivityContext를 사용하여 적절한 Context를 제공 받을 수 있다.
class AnalyticsServiceImpl @Inject constructor(
  @ApplicationContext context: Context
) : AnalyticsService { ... }


class AnalyticsAdapter @Inject constructor(
  @ActivityContext context: Context
) { ... }

 

AndroidX Extension

  • Hilt에는 다른 Jetpack 라이브러리의 클래스를 제공하기 위한 extension이 있다.
    • ViewModel
    • WorkManager
...
dependencies {
  ...
  implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01'
  // When using Kotlin.
  kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
  // When using Java.
  annotationProcessor 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
}

class ExampleViewModel @ViewModelInject constructor(
  private val repository: ExampleRepository,
  @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
  ...
}

 

  • Component 인스턴스화가 끝난 뒤에 변경될 수 있는 동적인 매개변수인 SavedStateHandle에 @Assisted annotation 을 붙임으로써 해결된다.
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
  private val exampleViewModel: ExampleViewModel by viewModels()
  ...
}

 

Reference

posted by Kyleslab 2013. 3. 26. 18:48

출처 : http://osh2357.blog.me/130098179373


안드로이드에서 사용하는 SQLite 는 모바일에서 가볍게 사용하기에 너무 좋다.

 하지만 다수의 Insert 문을 반복할때의 수행속도는 입이 쩍벌어지게 느렸는데......

 원인을 파해쳐보니 SQLite 에서는 Transaction을 사용하고 안하고의 차이가 다량 Insert 시 엄청난 시간 차이를 보여주고 있었다.

 다음 사이트의 Insert Test 를 보면 이해가 되리라 -> http://www.sqlite.org/speed.html

 

1000 건의 record 를 Insert 했을시에

SQLite 는 13.061 초

MySQL 은 0.114 초가 걸린다

 

반면에 25000 건의 record 를 Transaction 을 이용하여 Insert 했을시에

SQLite 는 0.914 초

MySQL 은 2.184 초가 걸렸다..

 

record 의 수는 25배가 늘었지만 Transaction 처리를 함으로

MySQL 의 속도 향상 대비 SQLite 의 경우 오히려 처리 속도 차이는 어마어마 하다

 

건수로만 비교했을때 25배 늘어난 데이터 처리속도는, Transaction 비 처리시

SQLite 는 326.525초 (헉), MySQL 은 2.85초가 예상된다

 

326.525초 -> 0.914 초 어마어마하지 않은가..

 

물론 이 SQLite 를 Android 에서 구동하느냐 iPhone 에서 구동하느냐 PC 에서 구동하느냐에 따른 차이도 많이 있을것이다..

하지만 확실한건 반복된 Insert 구문 사용시에는 Transaction 을 반드시 걸어야 할것으로 보인다~

- Android 에서 테스트시 Select 구문이 포함된 반복문이어서 정확한 속도비교는 불가능했으나 소모시간 약  1/10 수준으로 빨라졌다

 

 

자. 그럼 이제 Android 에서 Insert 시 Transaction 사용 구문 예제를 알아보자.

늘상 사용하는 방식대로라면 아래와 같이 사용하게 될것이다.

 

db.insert(TABLE_NAME, null, VALUE);

 

이것을 이제는 다음과 같이 사용하면 된다. (물론 반복문에서 사용할때 큰 효과를 발휘한다)

 

db.beginTransaction();

db.insert(TABLE_NAME, null, VALUE);

db.setTransactionSuccessful();

db.endTransaction();

 

그런데 이렇게 쓰고나면 뭔가 좀 허전해보인다.. SQL Exception 이 발생할 경우를 대비해야 하는데, 아래와 같이 수정한다.

 

try{

db.beginTransaction();

db.insert(TABLE_NAME, null, VALUE);

db.setTransactionSuccessful();

} catch (SQLException e){

} finally {

db.endTransaction();

}

 

보통의 경우라면 insert 구문 부근에는 조건문이나 반복문이 있으리라~

posted by Kyleslab 2013. 3. 15. 11:52


Deploying a Grails application to Tomcat 7 for the first time you may encounter the following error:
java.lang.OutOfMemoryError: PermGen space
Assumption: Windows Service

To correct the problem, just adjust the default memory and Perm Gen space allocated in Tomcat 7.

Start the service config tool "tomcat7w.exe:


Change the Java Options:

Switch to the Java tab:
  • Change the “Initial Memory pool” to 128
  • Change the “Maximum memory pool”  to 512
Perm Gen settings: 
Add the follow two lines to the Java options
--XX:PermSize=256m
--XX:MaxPermSize=265m
  

Restart the service.
Test your application

References:
  • http://www.grails.org/FAQ
  • http://tomcat.apache.org/tomcat-7.0-doc/windows-service-howto.html

posted by Kyleslab 2013. 3. 13. 16:01


Error generating final archive: Debug certificate expired on ...

이클립스에서 안드로이드 애플리케이션 빌드 중에 이런 오류 메시지가 나오는 경우가 있다.

Debug certificate가 만료되어서 발생하는 문제인데, Debug certificate는 만들어진 후 365일이 지나면 만료된다. 그러므로 만료된 Debug certificate를 새로 갱신해주면 된다. 방법은 간단하다. debug.keystore 파일을 삭제한 후, 프로젝트를 클린하면 이 debug.keystore파일 생성해준다고 모든 사이트들이 알려주나 나의 경우는 절대 다시 생성되지 않았다.

콘솔창으로 debug.keystore가 저장되있던 경로로 이동한후 아래의 명령어로 생성해준 후에야 문제가 해결되었다.

keytool -genkey -v -keystore debug.keystore -alias androiddebugkey -storepass android -keypass android -keyalg RSA -validity 14000

debug.keystore 파일의 위치는 'Window > Preferences > Android > Build'에서 'Default debug keystore' 항목을 참조하면 된다.


'Mobile > Android' 카테고리의 다른 글

Dependency Injection과 Hilt 맛보기  (0) 2024.05.03
안드로이드 SQLite 속도 향상!  (0) 2013.03.26
xml에서 include로 포함한 뷰들의 inflate여부  (0) 2012.11.13
JNIEnv* 와 jclass  (0) 2012.11.13
JNI를 Xcode이용하기  (2) 2012.11.13
posted by Kyleslab 2012. 11. 13. 18:34

layout xml을 만들때 다른 xml layout을 include라는 명령어로 포함시킬수 있습니다. 반복적으로 사용되는 layout인경우 자주이렇게 합니다. 



여기서 생긴궁금증 include가 포함된 xml레이아웃을 setContentView등으로 inflate를 시킨다면 include된 xml레이아웃 안의 뷰들도 inflate되어 바로 사용할 수 있을까?


정답은 가능합니다. 




    

가령 이런경우 레퍼런스 사이트에서는 layout_textview xml안에있는 뷰들을 소스코드에서 사용하려면 위의 예제처럼 id를 주어 먼저 불러오고 사용하게 되있습니다. 가령 이런식입니다. 


// layout_textview를 받아온다. layout_textview를 받아온 View textView 객체는 하위 자식들을 그대로 포함하고 있다.
        // 이렇게 받아온 View textView는 layout_textview의 root뷰이다.
        View textView = (View)findViewById(R.id.main_layout_textview);
         
        // 받아온 textview를 통해 해당 View가 가진 하위 자식들을 받아온다.(reference한다.)
        TextView tv01 = (TextView)textView.findViewById(R.id.layout_textview_01);
        TextView tv02 = (TextView)textView.findViewById(R.id.layout_textview_02);

하지만 상식적으로 생각해볼때 include의 명령어는 java의 inline명령어처럼 그부분에 xml코드를 대입해준것 불과합니다. 그러므로 include부분에 id를 줄필요도 다르게 생각할 필요도 없습니다. 그냥 원래 포함된 레이아웃인양 사용하면됩니다. 왜냐하면 setContentView에서 해당 xml전체를 inflate시켰기때문입니다. 다시말해 위와 같은 작업이 필요없습니다. 그냥 아래처럼 id만주면 사용할 수 있습니다.


        TextView tv01 = (TextView)findViewById(R.id.layout_textview_01);
        TextView tv02 = (TextView)findViewById(R.id.layout_textview_02);


'Mobile > Android' 카테고리의 다른 글

안드로이드 SQLite 속도 향상!  (0) 2013.03.26
Error generating final archive: Debug certificate expired on ... 문제해결법  (0) 2013.03.13
JNIEnv* 와 jclass  (0) 2012.11.13
JNI를 Xcode이용하기  (2) 2012.11.13
Fragment(2)  (0) 2012.11.12
posted by Kyleslab 2012. 11. 13. 15:24

JNIEnv*

JNI 인터페이스 포인터는 자바 메쏘드와 매핑되는 각 네이티브 함수(i.e., c/c++ 함수)를 위한 매개 변수로 전달된다. 이 포인터(매개변수)는 JNI환경에 있는 함수들과 상호작용하도록 해 준다. JNI 인터페이스 포인터는 저장될 수 있으나 오로지 현재의 쓰레드(동작 중인 쓰레드)에서 유효한 상태(사용 가능한 상태)로 유지된다. 다른 쓰레드들(정지 혹은 휴면 상태로 된 쓰레드들)은 먼저 반드시 AttachCurrentThread() 메소드를 호출하여 VM에 자신을 연결하고 JNI 인터페이스 포인터를 획득해야 한다. 일단 연결이 되고 나면 네이티브 쓰레드는 네이티브 메쏘드에서 동작하는 정상적인 자바쓰레드처럼 동작한다. 이 네이티브 쓰레드는 자신이 연결 해제하기 위해 DetachCurrentThread()를 호출하기 전까지는 VM에 연결된 상태로 유지된다.

현재의 쓰레드에 연결하고 JNI 인터페이스 포인터를 획득하기 위해서는:

 

JNIEnv *env;
(*g_vm)->AttachCurrentThread (g_vm, (void **) &env, NULL);

To detach from the current thread:

(*g_vm)->DetachCurrentThread (g_vm);
최종 결론:
JNIEnv *env는 자바와 네이티브 메소드를 연결하는 인터페이스 포인터
출처 : http://blog.naver.com/PostView.nhn?blogId=ethyo&logNo=80137761570

jclass

jclass 객체는 자바 클래스에 대한 정보를 나타냄. Class 오브젝트와 비슷

posted by Kyleslab 2012. 11. 13. 12:03

JNI를 공부하는데 Mac에서는 C혹은 C++코딩을 하기위해서는 Xcode를 이용해야한다. 그러나 Xcode를 이용해본적이 없는 나로서는 어려웠다.


http://crystalcube.co.kr/119


이 블로그의 글에서 친절히 설명이 나와있었다 그러나 한가지점 User Header Search Paths에 jni.h 헤더파일이 있는 경로를 추가해줘도 계속 jni.h file not found. 에러가 뜨면서 동작하지 않았다.


이 문제는 User Header Search Paths가 아니라 Header Search Paths에 추가해줌으로써 간단히 해결되었다.




'Mobile > Android' 카테고리의 다른 글

xml에서 include로 포함한 뷰들의 inflate여부  (0) 2012.11.13
JNIEnv* 와 jclass  (0) 2012.11.13
Fragment(2)  (0) 2012.11.12
fragment에서 attach, detach method!  (0) 2012.11.12
Fragment 에서 방향 전환시 null 체크  (0) 2012.11.09
posted by Kyleslab 2012. 11. 12. 14:13

당연히 Activity와는 다른 생명주기를 갖기때문에 fragment를 사용하는 개념은 액티비티와는 완전히 다르다. 모든 것은 FragmentManager를 통해 독립적으로 관리된다. 액티비티는 UI중심 즉 각각의 layout파일 중심으로 작동하면 된다고 생각하면되나 fragment는 fragmentManager중심이라고 생각하는 것이 맞다.

한 가지 예를 들어보자면 fragment는 혼자서 보일수는 없다 반드시 액티비티에 포함되어야한다. 하지만 그렇다고 해도 액티비티가 다시 생성되어서 모든 정보가 초기화되어도 fragmentManager에 기록된 정보는 그대로 살아 있다. 액티비티가 onCreate부터 다시 불리더라도 이전에 만들어서 fragmentManager에 add해둔 fragment는 아직 살아있다는 것이다. 

fragment를 포함하고 있는 Activity가 onCreate부터 다시불려초기화 되어도 add된 fragment들이 살아 있는 이유는 생명주기 자체가 따로 관리 되기 때문이다.



- Fragment LifeCycle(출처 android:Reference)

onAttach()(Activity Layout에 추가된 순간 호출! )부터 onResume()되면 화면에 display 되게 되고 onDetach되어 FragmentManager에서 사라질때까지 display되게 된다.


Fragment 생명주기에 대한 자세한 설명

http://blog.saltfactory.net/190

http://nkstudy.tistory.com/1


언제든 잘못된 설명은 말씀해주세요^^


'Mobile > Android' 카테고리의 다른 글

JNIEnv* 와 jclass  (0) 2012.11.13
JNI를 Xcode이용하기  (2) 2012.11.13
fragment에서 attach, detach method!  (0) 2012.11.12
Fragment 에서 방향 전환시 null 체크  (0) 2012.11.09
Fragment를 이용한 TabActivity  (0) 2012.11.09
posted by Kyleslab 2012. 11. 12. 11:24

fragmentManager에 추가된 fragment를 어떻게 떼어내어 다시 fragment로 만들수 있을까? 혹은 이미 있는 fragment를 어떻게 다시 붙일수 있을까? 이때 attach와 detach메소드가 있다.

먼저 reference를 보자 

public abstract FragmentTransaction attach (Fragment fragment)

Re-attach a fragment after it had previously been deatched from the UI with detach(Fragment). This causes its view hierarchy to be re-created, attached to the UI, and displayed.

Parameters
fragmentThe fragment to be attached.
Returns
  • Returns the same FragmentTransaction instance.

public abstract FragmentTransaction detach (Fragment fragment)

Detach the given fragment from the UI. This is the same state as when it is put on the back stack: the fragment is removed from the UI, however its state is still being actively managed by the fragment manager. When going into this state its view hierarchy is destroyed.

Parameters
fragmentThe fragment to be detached.
Returns
  • Returns the same FragmentTransaction instance.

보면 attach보다 detach를 먼저 사용하도록 되어있다. detach는 UI로부터 fragment를 떼어내는 것이고 attach는 그것을 다시 UI에 붙인다는 의미이다. 이것을 제대로 사용하는 예제는 tab fragment소스에 찾아볼수 있다.


        @Override
        public void onTabChanged(String tabId) {
            TabInfo newTab = mTabs.get(tabId);	// 넣어놨던 tabinfo찾아온다.
            if (mLastTab != newTab) {//라스트탭이 뉴탭과 같지 않다면
                FragmentTransaction ft = mActivity.getSupportFragmentManager().beginTransaction();
                if (mLastTab != null) {	//라스트탭이 널이 아니면
                    if (mLastTab.fragment != null) {	//라스트탭의 프래그먼트가 널이 아니면
                        ft.detach(mLastTab.fragment);	//떼어낸다
                    }
                }
                if (newTab != null) {	// 뉴탭이 널이 아니면
                    if (newTab.fragment == null) {	//프래그먼트가 널이면
                        newTab.fragment = Fragment.instantiate(mActivity,
                                newTab.clss.getName(), newTab.args);//새로 만들어준다
                        ft.add(mContainerId, newTab.fragment, newTab.tag);	//추가해준다
                    } else {
                        ft.attach(newTab.fragment);	//널이 아니면 그대로 붙여준다.
                    }
                }

                mLastTab = newTab;	//새로운탭이 라스트탭이 된다.
                ft.commit();
                mActivity.getSupportFragmentManager().executePendingTransactions();
            }
        }


위에 소스처럼 detach로 잘떼어놨다가 다시 attach로 붙여준다.

이것만 봐도 위에서 설명한 것처럼 Activity와는 사용하는 방법, 개념이 완전히 다르고 조금 더 유동적을 사용할 수 있다는 것을 알수 있다. 

그런데 여기서 한가지 생기는 의문은 fragmentTransaction에는 add, remove와 같은 메소드도 있다. 이것을 왜 사용하지 않았을까? 내가 보기에는 자원 재활용과 fragment정보 유지라는 생각이 들었다. add와 remove를 반복적으로 사용하려면 fragment를 계속 동적으로 생성하고 지우고를 반복해야한다. 그리고 위의 예제처럼 TabInfo라는 자료형을 만들어 fragment를 유지한다면 그안의 정보도 유지될것으로 보인다.

그런데 이 detach메소드가 작동하지 않은경우가 있다. 바로 정적으로 fragment가 선언된 경우이다.

xml 파일로 아래와 같이 class가 지정되어 fragment가 선언된경우에는 detach, remove모두 작동하지 않는다.

    


###. Fragment생명주기에 onAttach, onDetach라는 콜백메소드가 있어 attach(), detach()메소드 실행시 이 콜백메소드들을 호출하지 않을까 했지만 호출하지는 않았다.


언제든 틀린 설명은 말씀해주세요^^





'Mobile > Android' 카테고리의 다른 글

JNI를 Xcode이용하기  (2) 2012.11.13
Fragment(2)  (0) 2012.11.12
Fragment 에서 방향 전환시 null 체크  (0) 2012.11.09
Fragment를 이용한 TabActivity  (0) 2012.11.09
Fragment(1)  (0) 2012.11.09