지난 포스팅에서는 Kotlin의 Class 의 생성자와 상속에 대해 살펴봤다면

이번 포스팅에서는 Class의 여러 쓰임에 대해 알아보고자 합니다.

 

Data Class

data를 저장하기 위해 Class를 사용할 때가 많은데요. Kotlin에서는 이런 Class를 Data Class라고 부릅니다.

data class Customer(val name: String, val phone: String, val age: Int = 0)

 

컴파일러는 data class 를 선언하면 자동으로 primary constructor에 멤버함수를 생성합니다.

equals() / hashCode() / toString() / conponentN() / copy()

 

위의 자동으로 생성된 멤버함수의 동작을 위해 아래의 조건을 만족해야만 합니다.

- primary constructor 는 반드시 하나 이상의 인자를 가져야 합니다.

- 모든 primary constructor 는 val 혹은 var 선언이 되어야 합니다.

- Data class는 abstract, open, seal, inner class 이면 안됩니다.

(abstract class는 java와 동일하며, open class는 상속가능한 class, seal class는 inner class는 말 그대로 다른 class 내부의 class입니다.)

- Kotlin 1.1 이전에는 interface 구현만 가능했으나 Kotlin 1.1 부터는 다른 class 를 상속받아 구현하는 것이 가능합니다.

 

개발을 하다보면 객체를 복사해서 사용하는 경우가 많은데요.

일부만 복사하고 일부는 변경해서 사용 할 수 있습니다.

 

fun copy(name:String = this.name, phone: String = this.phone, age: Int = this.age) = Customer(name, phone, age)

 

위와 같은 copy 함수가 자동으로 생성되면 아래와 같이 사용 가능합니다.

val newCustomer = User(name = "Gil-Dong", phone = "0101112222", age = 24)
val addCustomer = newCustomer.copy(phone = "0102223333")

또한 Data class 에서는 아래와 같이 사용하여 변수에 Data class의 값을 얻어올 수 있습니다.

val checkCustomer = User("Gil-Dong", "0101112222", 24)
val (name, phone, age) = checkCustomer
println("customer info : $name, $phone, $age")

 

Sealed Class

sealed class 는 JAVA 에서도 들어본적이 없어서 생소한 이름인데요.

제한된 형태의 클래스를 나타내기 위해 사용됩니다. enum 의 확장이라고 생각하시면 됩니다.

enum 타입과 Sealed class 모두 값이 제한되지만, enum 상수는 단일 인스턴스로만 존재하고 sealed class의 하위클래스는 state를 포함하는 여러 인스턴스를  가질 수 있습니다.

Sealed class의 하위클래스는 Sealed class와 같은 파일에 선언해야만 합니다.

 

아래 예제 코드는 Expr 이라는 sealed class 가 선언되고,

Expr 을 상속받는 Const 클래스와, Expr을 상속받으며 인자로 Expr 을 받는 Sum 이라는 클래스가 있습니다.

또한 Expr 타입의 object도 선언해서 사용할 수 있습니다.

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber: Expr()

sealed class는 abstract class 이며 직접 인스턴스화 할수 없습니다. abstract class처럼 abstract member 를 가질 수 있습니다.

sealed class 를 상속받는 Const, Sum 은 같은 파일에 있어야 하지만, 이들을 상속받는 다른 클래스들은 동일한 파일에 있을 필요는 없습니다.

sealed class를 사용할때 큰 이점은 when 문을 사용할 때인데요.

아래 예제 코드에서 eval 함수는 Expr을 인자로 받아 Double 형을 리턴하는 함수입니다.

when 문에서 인자로 받은 expr 이 어떤 형태인지 아래와 같이 확인이 가능합니다.

JAVA로 하면 인자로 받은 expr 이 어떤 객체인지 확인을 하고 구현을 해야하는데 아래와 같이 구현이 가능하니

코드량이 확실히 줄어드는 것을 바로 확인할 수 있습니다.

fun eval(expr: Expr) : Double = when(expr) {
    is Const-> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NAN
}

 

'Language > Kotlin' 카테고리의 다른 글

[Kotlin] Range 사용하기  (0) 2018.01.25
[Kotlin] if, when, for, while 제어문 사용하기  (0) 2018.01.04
[Kotlin] 함수 사용하기  (0) 2017.12.28
[Kotlin] Class 사용하기  (0) 2017.12.15
안드로이드 공식 개발언어 Kotlin  (0) 2017.12.01

Android 에서 제공하는 Recording API 중, 지난번에 보았던 AudioRecorder 말고 더 편리한 MediaRecorder 가 있습니다.

AudioRecorder 는 오디오만 레코딩 가능 하지만, MediaRecorder 의 경우 Audio 및 Video 컨텐츠의 레코딩이 가능 합니다.

다만 인코딩 된 파일을 받기 때문에 AudioRecorder 처럼 PCM Data 를 바로 받아올 수는 없는 단점이 있습니다.

본인이 구현하려는 레코더의 사용 용도에 맞게 AudioRecorder/MediaRecorder 를 선택하여 사용하시면 되겠습니다.

 

Android Developer 의 MediaRecorder API Guide 를 참고하면, MediaRecorder 는 아래와 같은 state machine 을 갖습니다.

따라서 MediaRecorder API 를 사용할 때는 아래의 State 를 잘 따라서 코딩해야 합니다. 예를들어 Initail 상태에서는 바로 Prepared 상태로 갈 수 없고, Initailized 와 DataConfigured 상태를 거쳐야지 Prepared 상태가 될 수 있습니다.

이를 어기게 되면 StateIllegalException 이 발생하고, 원하는 동작을 얻을 수 없으니 조심해야 합니다.

 

 

Android Developer 사이트에서 설명하는 일반적인 Recording Flow 는 아래 코드와 같습니다.

각 단계별로 실제 레코딩시 일어나는 동작들에 대해 살펴보도록 하겠습니다.

MediaRecorder recorder = new MediaRecorder();
 recorder
.setAudioSource(MediaRecorder.AudioSource.MIC); //----------- (1)
 recorder
.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); // -- (2)
 recorder
.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); // ------(3)
 recorder
.setOutputFile(PATH_NAME);
 recorder
.prepare(); // -----------------------------------------------(4)
 recorder
.start();   // Recording is now started ----------------------(5)
 
...
 recorder
.stop();
 recorder
.reset();   // You can reuse the object by going back to setAudioSource() step
 recorder
.release(); // Now the object cannot be reused

 

(1) AudioSource 설정

AudioSource 의 종류는 아래 표에 정리되어 있습니다.

DEFAULT 

 0

 Default audio source

MIC 

 1

 Microphone audio source

VOICE_UPLINK  2  Voice call uplink (TX) audio source

VOICE_DOWNLINK 

 3

 Voice call downlink (RX) audio source 

VOICE_CALL

 4

 Voice call uplink + downlink audio source

CAMCORDER   5

 Microphone audio source tuned for video recording. wit the same orientation as the camera if available

VOICE_RECOGNITION

 6  Microphone audio source tuned for voice recognition

VOICE_COMMUNICATION

 7

 Microphone audio source tuned for voice communications such as VoIP

REMOTE_SUBMIX 

 8

 Audio source for a submix for audio streams to be presented remotely. An application can use this audio source to capture a mix of audio streams that should be transmitted to a remote receiver such as a Wifi display

UNPROCESSED

 9

 Microphone audio source tuned for unprocessed sound if available.

RADIO_TUNER

 1998

 Audio source for capturing broadcast radio tuner output

HOTWORD

 1999

 Audio source for preemptible, low-priority software hotword detection

보통은 마이크로 들어오는 음성을 많이 녹음하기 때문에 MIC 값을 많이 사용하고, 용도에 맞게 그 외의 값들을 설정할 수 있습니다.

제일 아래 RADIO_TUNER 와 HOTWORD 는 System API 로 일반 Application 에서는 사용 불가 합니다.

setAudioSource 함수가 호출되지 않을 경우에는 output file 에 audio track 이 포함되지 않습니다. 즉, 오디오가 녹음되지 않는다는 이야기입니다.

오디오를 녹음하고 싶으면 setAudioSource 함수를 prepare 전에 꼭 호출해 주어야 합니다.

 

(2) Output Format 설정

레코딩 후 실제 파일로 저장할 때, 저장할 파일의 format 을 설정해 줄 수 있습니다. 

DEFAULT

 0

THREE_GPP

 1

MPEG_4

 2

AMR_NB

 3

AMR_WB

 4

AAC_ADIF

 5

AAC_ADTS

 6

OUTPUT_FORMAT_RTP_AVP

 7

MPEG_2_TS

 8

WEBM

 9

비디오 포맷으로 가장 많이 사용되는 THREE_GPP 혹은 MPEG_4 가 있고, 오디오만 녹음 할 경우에는 오디오 전용 파일포맷인 AMR 이나 AAC 가 많이 사용 됩니다.

setOutputFormat() 함수는 setAudioSource()/setVideoSource() 호출 이후 / prepare() 이전에 호출되어야 합니다.

 

(3) AudioEncoder 설정

Audio Recording 시 Audio Encoder 를 설정해 주는 함수 입니다. 이 함수를 호출하지 않을 경우에 녹음된 파일에 audido track 은 포함되지 않습니다.

즉, 이 함수도 setAudioSource 와 동일하게 오디오를 녹음하고 싶다면 setOutputFormat() 이후 / prepare() 이전에 꼭 호출해 주어야 합니다.

 DEFAULT

 0

 AMR_NB

 1

 AMR_WB

 2

 AAC

 3

 HE_AAC

 4

 AAC_ELD

 5

 VORBIS

 6

 

(4) prepare

prepare 단계는 앞서 설정한 설정값들로 recording 을 준비하는 단계 입니다.

때문에 이 함수는 audio/video source와 audio/video encoder 를 모두 설정하고 file format 을 확정 지은 후에 불려야 합니다.

이 함수가 불리면 위의 State Machine 에서 recorder 는 prepared 상태가 되어 start 나 reset 을 호출할 수 있습니다.

 

(5) start

start 함수를 호출하면 정해놓은 audio/video source 에서 실제 레코딩을 시작하게 되고, state 는 Recording 상태가 됩니다.

start 이후 원하는 시점에 stop 을 호출하면, 그 시점까지 레코딩이 진행 됩니다.

 

이 외에도 API Level 24 부터 제공되는 pause() / resume() 함수를 이용해 레코딩을 잠시 멈추는 것이 가능합니다.

stop() 과 다르게 pause() 함수로 레코딩을 멈추면 기존의 configuration 은 유지된 채로 MediaRecorder 가 잠시 멈추게 됩니다.

이 상태에서 resume() 함수로 레코딩을 다시 시작할 수 있습니다. 당연한 이야기지만 paused 상태에서 레코딩 되는 내용은 버려지게 됩니다.

 

developer 사이트를 참고하면 위의 간단한 API 이외에도 MediaRecorder 에서 제공하는 다양한 API 들을 볼 수 있으니 참고하시면 되겠습니다.

https://developer.android.com/reference/android/media/MediaRecorder.html

 

다음 포스팅에서는 MediaRecorder 를 이용한 실제 레코딩 예제를 살펴보도록 하겠습니다.

 

 

 

앞서 안드로이드 "안드로이드 메인 스레드 포스팅"을 통하여 메인스레드의 특징에 대해서 알아보았습니다. 

그 중에서 메인 스레드가 아닌 스레드에서 Button UI를 조작하다가 CalledFromWrongThreadException를 경험하기도 하였습니다.

이런 문제를 해결할 수 있는 방법에 대해서 알아보겠습니다.


Activity.runOnUiThread(Runnable) 사용

Developer Android 사이트에서 찾아보면 다음과 같이 설명이 있습니다.

설명 중 다음 부분이 우리가 처한 상황을 설명해 주고 있습니다.

"특정 동작을 UI 스레드에서 동작하도록 합니다. 만약 현재 스레드가 UI 스레드이면 그 동작은 즉시 수행됩니다."

하지만 "현재 스레드가 UI 스레드가 아니면, 필요한 동작을 UI 스레드의 이벤트 큐로 전달한다" <== 이부분 입니다.


우선 설명을 믿고 코드를 만들어 보겠습니다. 먼저 CalledFromWrongThreadException를 발생시켰던 코드는 아래와 같습니다.

 
    public class MainActivity extends Activity {
    private String TAG = "AndroidThread";
    private Button mDownloadButton;
    private Button mCancelButton;
    private Download mDownload;
    private boolean isDownloading = false;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDownloadButton = (Button)findViewById(R.id.button_download);
        mCancelButton = (Button)findViewById(R.id.button_cancel);
    }

    public void onDownload(View view) {
        Log.d(TAG, "Press Download button");
        isDownloading = true;
        mDownload = new Download();
        mDownload.start();
    }

    public void onCancel(View view) {
        Log.d(TAG, "Press Cancel button");
        isDownloading = false;
    }

    public class Download extends Thread {

        @Override
        public void run() {
            for(int i=1;i<=10;i++) {

                if (!isDownloading) {
                    break;
                }
                try {
                    Thread.sleep(1000);
                    Log.d(TAG, "Downloading ..." + i*10 + "%");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if(i == 10) {
                    mDownloadButton.setText("Downloaded"); // CalledFromWrongThreadException 유발 코드!!
                }
            }
            isDownloading = false;
        }
    }
}

public class MainActivity extends Activity {
    private String TAG = "AndroidThread";
    private Button mDownloadButton;
    private Button mCancelButton;
    private Download mDownload;
    private boolean isDownloading = false;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDownloadButton = (Button)findViewById(R.id.button_download);
        mCancelButton = (Button)findViewById(R.id.button_cancel);
    }

    public void onDownload(View view) {
        Log.d(TAG, "Press Download button");
        isDownloading = true;
        mDownload = new Download();
        mDownload.start();
    }

    public void onCancel(View view) {
        Log.d(TAG, "Press Cancel button");
        isDownloading = false;
    }

    public class Download extends Thread {

        @Override
        public void run() {
            for(int i=1;i<=10;i++) {

                if (!isDownloading) {
                    break;
                }
                try {
                    Thread.sleep(1000);
                    Log.d(TAG, "Downloading ..." + i*10 + "%");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if(i == 10) {

                    (MainActivity.this).runOnUiThread(new Runnable(){

                        @Override
                        public void run() {
                            Log.d(TAG, "Change Button text");
                            mDownloadButton.setText("Downloaded");
                        }
                    });
                }
            }

            isDownloading = false;
        }
    }
}


다음은 runOnUiThread 함수를 사용한 코드입니다.

public class MainActivity extends Activity {
    private String TAG = "AndroidThread";
    private Button mDownloadButton;
    private Button mCancelButton;
    private Download mDownload;
    private boolean isDownloading = false;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDownloadButton = (Button)findViewById(R.id.button_download);
        mCancelButton = (Button)findViewById(R.id.button_cancel);
    }

    public void onDownload(View view) {
        Log.d(TAG, "Press Download button");
        isDownloading = true;
        mDownload = new Download();
        mDownload.start();
    }

    public void onCancel(View view) {
        Log.d(TAG, "Press Cancel button");
        isDownloading = false;
    }

    public class Download extends Thread {

        @Override
        public void run() {
            for(int i=1;i<=10;i++) {

                if (!isDownloading) {
                    break;
                }
                try {
                    Thread.sleep(1000);
                    Log.d(TAG, "Downloading ..." + i*10 + "%");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if(i == 10) {
                       (MainActivity.this).runOnUiThread(new Runnable(){

                        @Override
                        public void run() {
                            Log.d(TAG, "Change Button text");
                            mDownloadButton.setText("Downloaded");
                        }
                    });
                }
            }

            isDownloading = false;
        }
    }
}


run()함수를 오버라이드하여 Runnable 인터페이스를 구현하였습니다. 

이제 run() 함수에 있는 작업은(여기서는 setText함수 실행이겠죠) main thread(UI thread) 에서 실행 됩니다. 

로그로 확인 해보겠습니다.   

12-08 16:05:22.968 3443-3443/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-08 16:05:23.986 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-08 16:05:24.987 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-08 16:05:25.990 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-08 16:05:26.994 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-08 16:05:27.998 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...50%

12-08 16:05:29.000 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...60%

12-08 16:05:30.003 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...70%

12-08 16:05:31.006 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...80%

12-08 16:05:32.011 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...90%

12-08 16:05:33.013 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...100%

12-08 16:05:33.014 3443-3443/com.example.codetravel.androidthread D/AndroidThread: Change Button text 

다운로드 진행 상태를 출력하는 로그는 TID3467 thread에서 동작합니다. 

의도한대로 다운로드 완료 후,  setText 함수를 수행하는 run 함수가 UI thread에서 실행된 것을 확인 할 수 있습니다.

다음 그림은 다운로드 버튼의 텍스트가 정상적으로 변경된 모습입니다.



runOnUiThread 함수는 어떻게 이런 일을 할까요? xref에서 runOnUiThread 함수를 살펴 보겠습니다.

"특정 동작을 UI 스레드에서 동작하도록 합니다. 만약 현재 스레드가 UI 스레드이면 그 동작은 즉시 수행됩니다."

하지만 "현재 스레드가 UI 스레드가 아니면, 필요한 동작을 UI 스레드의 이벤트 큐로 전달합니다 설명이 그대로 코드화 되어 있네요


runOnUiThread가 실행된 스레드는 TID3467이므로 즉 UI thread가 아니므로 mHandler.post(action) 코드가 실행됩니다.

mHandler.post(action)을 실행하면 아래와 같이 순서대로 함수가 호출됩니다.(1 -4)

결과적으로 Runnable r은 UI thread의 queue에 메시지 형식으로 enqueue가 됩니다. 

후에 queue에 있는 메세지를 꺼내서 UI thread에서 동작을 수행할 것입니다.

1. post(Runnable r)

2. sendMessageDelayed(Message msg, long delayMillis)

3. sendMessageAtTime(Message msg, long uptimeMillis)

 - MessageQueue queue = mQueue;

4. enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis)


간략하게 말씀드리면 처음 startActivity() 함수를 통해서 MainActivity가 생성되는 과정은 zygoteInit에 의해서 ActivityThread가 생성됩니다.

ActivityThread는 아래와 같이 Main Looper와 Handler를 생성합니다. 

 // frameworks/base/core/java/android/app/ActivityThread.java

public final class ActivityThread {

...

public static void main(String[] args) { 

...

Looper.prepareMainLooper(); // 1. UI thread의 루퍼를 생성 

ActivityThread thread = new ActivityThread(); 

thread.attach();


if(sMainThreadHandler == null) {     

    sMainThreadHandler = thread.getHandler(); // 2. 핸들러 설정

 } 

Looper.loop(); // 3. 루퍼 동작 시작

}

}


이것이 안드로이드 시스템에서 자동으로 생성되는 UI thread의  Looper와  Handler 입니다. 

그리고 ActivityThread는 ActivityManagerService 와 바인더 통신을 하여 MainActivity를 생성하게 됩니다. 

따라서 ActivityThread가 생성한 MainActivity는 ActivityThread에서 생성한 Looper와 Handler를 사용하게 됩니다.

즉 위의 3번에서 mQueue는 UI thread의 Looper가 관리하는 queue를 가리킵니다.

결국 queue에 메세지 형태로 들어간 Runnable 작업은 UI Thread에서 dequeue 되어 UI Thread상에서 수행됩니다.

 UI thread에서 Button의 Text 변경 작업을 했기 때문에 CalledFromWrongThreadException와 같은 문제가 발생하지 않는 것입니다.


아래 콜스택은 MainActivity 액티비티의 onCreate가 불리는 함수 호출 과정입니다.

"main@4450" prio=5 tid=0x1 nid=NA runnable
  java.lang.Thread.State: RUNNABLE
      at com.example.codetravel.androidthread.MainActivity.onCreate(MainActivity.java:17)
      at android.app.Activity.performCreate(Activity.java:6757)
      at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1119)
      at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2703)
      at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2811)
      at android.app.ActivityThread.-wrap12(ActivityThread.java:-1)
      at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1528)
      at android.os.Handler.dispatchMessage(Handler.java:102)
      at android.os.Looper.loop(Looper.java:154)
      at android.app.ActivityThread.main(ActivityThread.java:6316)
      at java.lang.reflect.Method.invoke(Method.java:-1)
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:872)

      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:762) 


ActivityThread의 main 함수에서 실행된 Looper.loop() 함수에 의해서 메세지가 처리되고 있는것을 볼 수 있습니다.

그리고 MainActivity가 생성되면서 onCreate가 호출됩니다.

위의 과정은 붉은색으로 표시된 "main"이라는 표시로 main thread 즉 UI thread의  콜스택임을 알 수 있습니다.  

이상으로 runOnUiThread 함수의 사용에 대해서 간략하게 보았습니다.






Kotlin 에서의 Class 사용에 대해서 알아보도록 하겠습니다.

Java 와의 호환성이 Kotlin 의 큰 장점으로 부곽되는 만큼 Java 와 비교하게 되는 부분이 있습니다.

 

Kotlin 에서 Class는 Java와 같이 아래처럼 사용합니다.

class Person{ }

그러나 Java 와 다르게 Body 가 없이 아래와 같이 사용될 수도 있습니다.

class Person

 

Kotlin에서는 primary constructor 과 하나 이상의 secondary constructor 를 가질 수 있습니다.

primary constructor 는 class 선언시 함께 가능합니다.

constructor 키워드는 생략 가능합니다.

annotation 이나 접근자(private, pubilc 등) 와 함께 사용되는 경우 생략할 수 없습니다.

class Person constructor(firstName: String) {

}
class Person(firstName: String) {

}

primary constructor

primary constructor 는 어떤 코드도 포함하지 않으므로 initializer block 을 사용하여 초기화 코드를 구현하면 됩니다.

constructor 가 불리면 객체 생성이 되면서 initializer block 이 불립니다.

primary constructor 의 parameter 는 class 내 어디서든(init 블럭이나 속성값 초기화) 사용 가능합니다.

class Person(name: String) {
    val nameInfo = "Name : $name".also(::println)

    init {
        println("First initializer block that prints ${name}")
    }

    val secondProperty = "Second property: ${name.length}".also(::println)

    init {
        println("Second initializer block that prints ${name.length}")
    }
}

 

secondary constructor

constructor 키워드가 반드시 사용되어야 합니다.

class Person {
    constructor(parent: Person) {
        parent.children.add(this)
    }
}

primary constructor 가 있는 경우 모든 second constructor는 primary constructor 에게 위임해야 합니다. 동일 클래스 내에서 다른 생성자로의 위임은 this 키워드를 사용합니다.

class Person(val name: String) {
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

initializer block의 코드는 primary constructor 의 일부가 된다. primary constructor 로의 위임은 secondary constructor 코드 실행 전에 실행되므로 모든 initializer block 의 코드는 가장 먼저 실행됩니다.

class Constructors {
    init {
        println("Init block")    // 먼저 실행됨
    }

    constructor(i: Int) {
        println("Constructor")   // 나중에 실행됨
    }
}

 

class의 instance 생성하기

class의 instance 를 생성하는데 JAVA에서는 new 키워드를 사용했다면, Kotlin 에서는 constructor 호출만 하면 됩니다.

val invoice = Invoice()

val customer = Customer("Joe Smith")

 

class 상속받기

Kotlin 에서 class 를 상속받기 위해서는 아래와 같이 사용할 수 있습니다.

C++과 같이 : 기호를 사용하는데 super class의 클래스명에 괄호기호가 붙는 점이 다르네요

물론 함수 override 도 아래와 같이 할 수 있습니다.

open annotation은 class로부터의 상속이 가능하다는 의미입니다. Kotlin에서는 모든 class가 default로 final 로 선언됩니다

open annotation을 사용하는 경우에만 상속이 가능합니다.

method 의 경우에도 open annotation이 있는 method 만 override 가능합니다.

open class Base {
    open fun v() {}
    fun nv() {}
}
class Derived() : Base() {
    override fun v() {}
}

변수의 경우 val 변수(Read only) 를 var 변수로 재정의 할 수 있지만, 반대는 안됩니다.

interface Foo {
    val count: Int
}

class Bar1(override val count: Int) : Foo

class Bar2 : Foo {
    override var count: Int = 0
}

 

 

안드로이드 어플리케이션이 실행되면 안드로이드 시스템은 하나의 실행 스레드로 어플리케이션의 프로세스를 실행합니다. 

어플리케이션의 구성 요소가 생성될 때 별도의 스레드가 생성되는 것은 아니며 앞서 말한 하나의 실행 스레드에서 실행됩니다. 이 스레드를 Main thread라고 합니다.

안드로이드 시스템에 의해 생성된 이 Main thread는 화면 구성에 관한 역할을  담당합니다.  

예를 들어 Button, CheckBox, TextView 등의 UI도구 키트 구성 요소를 생성 및 조작하였을 때 상호작용하는 스레드가 Main Thread입니다. 

그래서 Main thread를 UI thread라고도 합니다.  


Main thread에 대해서 좀더 이야기하기 전에 우리가 만든 어플리케이션에서 main thread가 존재하고 있는 모습을 잠시 보도록 하겠습니다.

안드로이드 스튜디오를 열어 Empty Activity를 추가하고 별도의 코드 추가없이 빌드 한 후 APK를 실행해 보았습니다. 

이 때 adb shell 명령어를 사용하여 실행된 APK의 스레드를 출력해보면 아래와 같습니다.(ps PID -t)

붉은 박스로 표시된 부분이 main thread입니다. 참고로 실행된 APK는 PID 26914, main thread TID 26914입니다.

그리고 프로세스간 통신을 위해 사용되는 바인더 스레드와 그외 런타임 내부 스레드들이 존재합니다. 

안드로이드 시스템은 빈 액티비티만 존재하는 어플리케이션일지라도 이렇게 여러 스레드들을 기본적으로 생성하고 있습니다.

바인더 스레드와 그외 내부 스레드라고 표시한 부분을 아는 것도 필요하겠지만 지금은 main thread에 초점을 맞추도록 하겠습니다.


Main thread에 대해서 좀더 알아보겠습니다. 

어플리케이션을 실행했을 때, 버튼을 누르거나 화면터치를 했는데 반응성(응답성)이 떨어진다면 사용자는 답답함을 느낄 것입니다.

그리고 간혹  ANR이 발생하여 어플리케이션이 강제 종료되는 것을 본 경험도 있을 것입니다.

반응성은 Main thread(UI thread)와 실제 작업이 실행되는 스레드간의 관계와 연관이 있습니다.

실제 작업이라고 하면 예를 들어 동영상 다운로드 버튼을 눌렀을 때 "네트워크를 통하여 동영상 데이터를 다운로드 하는 작업"을 의미 할 수 있을 것입니다.

사용자가 다운로드 버튼을 누르면 동영상 데이터 다운로드가 시작될 것입니다. 

만약 개발자가 데이터를 다운로드하는 작업을 Main Thread(UI thread) 에 구현하였다면 다운로드가 완료될 때까지 화면은 멈추어있는 것처럼 보일것입니다.

이런것들이 어플리케이션의 반응성을 떨어지게 만들고 사용자는 답답함을 느끼게 됩니다.

반응성 좋은 어플리케이션을 만드려면 시간이 오래걸리는 작업은 main thread에 구현하지 않고 별도의 스레드에서 동작하도록 구현하면 됩니다.


그리고 안드로이드 UI 도구 키트는 스레드로부터 세이프하지 않습니다

그렇기 때문에 UI thread가 아닌 스레드(직접 생성한 별도의 스레드)에서는 UI 구성요소를  조작하는 것은 허용되지 않습니다. ( 스레드 세이프 하다는 의미는 여러 스레드에서 동시에 사용해도 정상적으로 동작한다는 의미입니다)

그렇다면 UI 구성 요소는 모두 UI thread에서 조작되어야 하고 결국은 UI 입장에서는 보면 UI 동작은 단일 스레드로 동작하는 구조가 됩니다. 

따라서 UI thread에서 시간이 오래 걸리는 작업을 수행하는 것은 그 작업이 수행되는 동안 UI업데이트가 지연(또는 차단)되고 있다는 것을 의미합니다.

이런 이유로 시간이 오래 걸리는 작업은 별도의 스레드를 생성하여 UI thread로 부터 분리해야 반응성이 좋은 프로그램을 만들 수 있습니다.

다시 말하면 오래 걸리는 작업을 별도의 스레드를 생성한 후 그 스레드에서 동작시키면 됩니다.


위에서 이야기했던, 안드로이드 스레드 사용시 유의 사항을 정리하면 다음의 2가지 입니다.


1. 시간이 오래 걸리는 작업은 UI 스레드로 부터 분리하고 별도의 스레드에서 실행함으로써 UI 스레드 작업이 지연(또는 차단)되는 것을 방지합니다 .

2. UI 스레드가 아닌 스레드에서는 UI 구성 요소를 조작하지 않습니다.


지금부터는 1번, 2번 사항에 대해서 예제 코드로 하나씩 살펴보도록 하겠습니다.

예제 코드는 "Download" 버튼과 "Cancel" 버튼이 존재합니다. "Download" 버튼을 누르면 1초에 한번씩 다운로드 상태로그를 출력하도록 하였습니다. 

예제 코드에서는 단순히 로그를 출력하는 것이지만 실제로 데이터를 다운로드한다고 가정해도 좋습니다.(예제 코드니까요)

그리고 "Cancel" 버튼을 누르면 취소되었다는 로그를 출력합니다. 

이것 역시 실제로는 다운로드를 취소한다고 가정합니다.


그럼 예제 코드를 보겠습니다.

public class MainActivity extends Activity {
    private String TAG = "AndroidThread";
    private Button mDownloadButton;
    private Button mCancelButton;
    private Download mDownload;
    private boolean isDownloading = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDownloadButton = (Button)findViewById(R.id.button_download);
        mCancelButton = (Button)findViewById(R.id.button_cancel);
    }

    public void onDownload(View view) {  // Download 버튼 누르면 호출되는 함수
        Log.d(TAG, "Press Download button");
        isDownloading = true;

        for(int i=1;i<=10;i++) { // 데이터 다운로드 작업(사실은 단순 로그 출력입니다^^)

            if (!isDownloading) {
                break;
            }
            try {
                Thread.sleep(1000);
                Log.d(TAG, "Downloading ..." + i*10 + "%");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        isDownloading = false;
    }

    public void onCancel(View view) { // Cancel 버튼 누르면 호출되는 함수
        Log.d(TAG, "Press Cancel button");
        isDownloading = false;
    }
}


아래 로그는 "Download" 버튼을 한번 눌렀을 때 출력되는 로그입니다.

22509-22509를 보아 PID22509 TID22509 이므로 main thread에서 다운로드 작업이 동작하고 있음을 알 수 있습니다.

별도의 스레드 생성없이 다운로드 코드를 작성하였기 때문에 당연한 결과입니다.

12-06 14:28:02.530 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-06 14:28:03.537 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-06 14:28:04.541 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-06 14:28:05.545 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-06 14:28:06.548 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-06 14:28:07.549 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...50%

12-06 14:28:08.552 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...60%

12-06 14:28:09.554 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...70%

12-06 14:28:10.555 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...80%

12-06 14:28:11.557 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...90%

12-06 14:28:12.560 22509-22509/com.example.codetravel.androidthread D/AndroidThread: Downloading ...100% 

어쨌든 다운로드는 100% 완료되었습니다.


이번에는 "Download" 버튼을 누른 후, 바로 "Cancel"  버튼을 2~3번 정도 눌러 보겠습니다.

아래와 그림처럼  ANR(Application Not Responding) 다이얼로그가 발생하였습니다.

사용자가 "Cancel" 버튼을 눌렀지만 5초 이상 UI가 멈춰있었기 때문에 안드로이드 시스템에서 발생시킨 에러 다이얼로그입니다.

반응성(응답성)이 좋지못한 어플리케이션의 사례가 되겠습니다.



로그를 통해 좀더 자세히 보겠습니다.

60% 다운로드 로그 출력 이후에 ANR 로그가 생성된 것을 볼 수 있습니다.

5.7초 정도 서스펜드되었다는 로그도 함께 보입니다.

그리고 "Download" 버튼을 누른 직후 "Cancel" 버튼을 바로 눌렀지만 다운로드는 중단 되지 않았습니다.

취소 로그 출력은 다운로드 100% 완료된 후에 찍혔습니다.

이유는 앞에서도 언급했듯이 main thread에서 "Download"와 "Cancel" 버튼 처리와 다운로드 작업을 모두 처리하기 때문에 단일 스레드로 동작하는 구조입니다.

따라서 "Cancel" 버튼보다 먼저 실행된 다운로드 작업이 완료되기 전에는 취소 로그가 출력되지 않는 것입니다.

12-06 14:36:10.170 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-06 14:36:11.172 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-06 14:36:12.175 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-06 14:36:13.176 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-06 14:36:14.179 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-06 14:36:15.181 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...50%

12-06 14:36:16.185 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...60%

12-06 14:36:17.058 26810-26815/com.example.codetravel.androidthread W/art: Suspending all threads took: 5.770ms

12-06 14:36:17.090 26810-26815/com.example.codetravel.androidthread I/art: Wrote stack traces to '/data/anr/traces.txt'

12-06 14:36:17.187 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...70%

12-06 14:36:18.189 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...80%

12-06 14:36:19.190 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...90%

12-06 14:36:20.192 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Downloading ...100%

12-06 14:36:20.228 26810-26810/com.example.codetravel.androidthread D/AndroidThread: Press Cancel button 


ANR을 회피하고 반응성 좋은 프로그램을 만들기 위해서 1번 사항인 시간이 오래 걸리는 작업은 UI thread로 부터 분리하라 는 의미를 알 수 있었습니다.

별도의 스레드를 생성하여 다운로드 작업(오래걸리는 작업)을 UI 스레드에서 분리하도록 하겠습니다.

public class MainActivity extends Activity {
    private String TAG = "AndroidThread";
    private Button mDownloadButton;
    private Button mCancelButton;
    private Download mDownload;
    private boolean isDownloading = false;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDownloadButton = (Button)findViewById(R.id.button_download);
        mCancelButton = (Button)findViewById(R.id.button_cancel);
    }

    public void onDownload(View view) {
        Log.d(TAG, "Press Download button");
        isDownloading = true;
        mDownload = new Download();  // Download 스레드 생성
        mDownload.start();  // Download 스레드 시작
    }

    public void onCancel(View view) {
        Log.d(TAG, "Press Cancel button");
        isDownloading = false;
    }

    public class Download extends Thread{  // 다운로드 작업을 위한 스레드입니다.

        @Override
        public void run() {
            for(int i=1;i<=10;i++) { // 이제 다운로드 작업은 Download 스레드에서 동작합니다.

                if (!isDownloading) {
                    break;
                }
                try {
                    Thread.sleep(1000);
                    Log.d(TAG, "Downloading ..." + i*10 + "%");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            isDownloading = false;
        }
    }
}


이번에는 "Download"버튼을 누르고 약 3초 후에 "Cancel"버튼 3~4번  눌러보겠습니다.

취소 버튼을 누른 후에 다운로드가 중지되는 것을 볼 수 있습니다.

이렇게 취소 버튼이 바로 실행될 수 있는 이유는 다운로드 작업이 TID13679 스레드에서 동작하고 있기 때문입니다.

이제 이 프로그램은 main thread와 별도로 생성한 스레드가 멀티로 동작하는 프로그램입니다.

12-06 15:20:23.709 13571-13571/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-06 15:20:24.721 13571-13679/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-06 15:20:25.723 13571-13679/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-06 15:20:26.725 13571-13679/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-06 15:20:27.437 13571-13571/com.example.codetravel.androidthread D/AndroidThread: Press Cancel button

12-06 15:20:27.727 13571-13679/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-06 15:20:27.914 13571-13571/com.example.codetravel.androidthread D/AndroidThread: Press Cancel button

12-06 15:20:28.298 13571-13571/com.example.codetravel.androidthread D/AndroidThread: Press Cancel button

12-06 15:20:28.765 13571-13571/com.example.codetravel.androidthread D/AndroidThread: Press Cancel button

12-06 15:20:29.171 13571-13571/com.example.codetravel.androidthread D/AndroidThread: Press Cancel button 

이전보다 반응성이 좋아진 것을 확인할 수 있었습니다.

UI 편의성을 위해서 한가지 기능을 더 추가해 보겠습니다.

다운로드가 완료되면 "Download"버튼의 텍스트를 "Downloaded"으로 바꿔 보도록 하겠습니다


추가된 코드는 아래와 같습니다.

public class Download extends Thread {

        @Override
        public void run() {
            for(int i=1;i<=10;i++) {

                if (!isDownloading) {
                    break;
                }
                try {
                    Thread.sleep(1000);
                    Log.d(TAG, "Downloading ..." + i*10 + "%");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if(i == 10) {
                    mDownloadButton.setText("Downloaded"); // 다운로드가 완료되면 버튼의 텍스트를 Downloaded로 변경
                }
            }

            isDownloading = false;
        }
    }


이제 실행해보면 "Unfortunately, AndroidThread has stopped." 라는 에러 팝업을 발생시키면서 어플리케이션은 종료됩니다.

아래의  로그에서와 같이 Exception이 발생하였습니다.

12-06 15:31:12.849 18227-18227/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-06 15:31:13.863 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-06 15:31:14.864 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-06 15:31:15.865 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-06 15:31:16.866 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-06 15:31:17.869 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...50%

12-06 15:31:18.871 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...60%

12-06 15:31:19.873 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...70%

12-06 15:31:20.876 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...80%

12-06 15:31:21.879 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...90%

12-06 15:31:22.883 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...100%

12-06 15:31:22.883 18227-18307/com.example.codetravel.androidthread E/AndroidRuntime: FATAL EXCEPTION: Thread-204

                               Process: com.example.codetravel.androidthread, PID: 18227

                               android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

                               at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6556)

                               at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:942)

                               at android.view.ViewGroup.invalidateChild(ViewGroup.java:5081)

                               at android.view.View.invalidateInternal(View.java:12713)

                               at android.view.View.invalidate(View.java:12677)

                               at android.view.View.invalidate(View.java:12661)

                               at android.widget.TextView.checkForRelayout(TextView.java:7159)

                               at android.widget.TextView.setText(TextView.java:4342)

                               at android.widget.TextView.setText(TextView.java:4199)

                               at android.widget.TextView.setText(TextView.java:4174)

                               at com.example.codetravel.androidthread.MainActivity$Download.run(MainActivity.java:53) 


CalledFromWrongThreadException 이 발생하였습니다.

그리고 "Only the original thread that created a view hierarchy can touch its views" 라고 부연 설명을 해줍니다.

직역하면 뷰 계층 구조를 만든 원래 스레드(main thread)만 해당 뷰를 조작할 수 있다는 의미입니다.

안드로이드 시스템은 setText()함수를 실행하고 있는 스레드가 main thead인지 검사하였고

이를 위반 하였기 때문에 런타임 에러를 발생시킨 것입니다.

위 로그를 보면 TID18037 setText() 함수가 실행되었습니다. main thread인 TID18227 에서 실행되어야 합니다.

이제서야 UI 구성요소는 main thread(UI thread) 에서만 동작시켜야 한다는 2번 사항이 떠오릅니다.


그렇다면 이 문제를 해결할 수 있는 방법은 무엇일까요?

setText() 함수 호출같은 UI구성 키트 조작을 main thread에서 동작하도록 해줘야 합니다.

안드로이드에서는 다양한 방법으로 이것을 지원하고 있습니다.


1.  Activity.runOnUiThread(Runnable) 사용

2. View.post(Runnable) 사용

3. AsyncTask 사용


다음 포스팅에서 위의 3가지 방법을 하나씩 살펴봄으로써 CalledFromWrongThreadException 에러를 해결해보고

어떻게 UI구성 요소를 main thread에서 조작하는지 알아보도록 하겠습니다.



지난 2017년 5월 Google I/O 에서 Kotlin이 안드로이드의 공식 개발 언어로 발표되었습니다.

Kotlin 공식 사이트를 보면 아래와 같이 메인에서 소개하고 있습니다.

말 그대로 Java와 Android 에서 100% 호환가능하며 여러 platform 에서 동작하는 언어라고 정의하고 있는데요.

 

Kotlin은 Anroid Studio 개발사인 JetBrain 社 에서 만든 언어로 Kotlin/JVM, Kotlin/JS, Kotlin/Native 등 여러 환경에서 동작을 합니다.

Java 와 100% 호환이 되므로 Android의 API 들을 그대로 사용할 수 있고, Ant, Maven, Gradle과 같은 빌드 시스템을 사용할 수 있어 기존의 Android 개발자들이 낯설치 않게 사용 가능할 것이라 생각됩니다.

 

Java 코드를 Kotlin코드로 변환하는 도구를 제공하고 있으며, Java 로 개발하는 것보다 비약적으로 코드의 양이 줄어들고 여러 개발자들의 번거로움을 덜어줄 수 있도록 NullPointerException에 대한 안정성이 증가하였습니다.

 

Kotlin 에 대해 기본문법에 대해 좀더 알아보겠습니다.

 

1. Kotlin 은 문장 끝에 세미콜론이 없다.

C, C++, Java 많은 언어들이 세미콜론으로 문장의 끝을 알리는데요. Kotlin의 문장 끝에는 세미콜론이 없습니다.

한 줄에 여러 문장을 표현할 때만 세미콜론을 이용합니다.

 

2. 전역함수 사용이 가능하다

함수가 반드시 클래스 내에 있지 않습니다.

 

3. new 키워드 삭제

 

4. 함수 선언은 [접근지정자][inline][final] fun 함수명(매개변수1이름: 매개변수1타입, 매개변수2이름: 매개변수2타입) :반환형의 형태로 한다.

 

5. val과 var

val은 읽기전용 변수(final 개념) 이며 var는 읽기&쓰기가 가능한 변수를 뜻합니다.

변수선언과 동시에 초기화를 하면 타입을 알아서 추론하기 때문에 변수의 타입이 생략 가능합니다.

 

아래의 3개 sample 코드를 통해 1~5번까지의 문법 설명이 가능하다.

MainActivity.kt

class MainActivity : AppCompatActivity() {

   
override fun onCreate(savedInstanceState: Bundle?) {
       
super.onCreate(savedInstanceState)
        setContentView(R.layout.
activity_main)

        sayHello(
"GoodFortune")

       
val ck = ClassKotlin("GoodFortune");
       
val ck2 = ClassKotlin("GoodManner", "01055563262");
       
var ck3 = ClassKotlin("GoodMan", "01044778855", "ourhome")
    }
}
 

KotlinTest.kt

fun sayHello(who:String){
    val TAG: String = "KotlinTest"
    Log.d(TAG, "sayHello "+who)
}

ClassKotlin.kt

class ClassKotlin(var name: String, var phone: String="", var addr: String ="")
{

}

 

6. Nullable

Kotlin 의 가장 큰 장점 중 하나라고 할 수 있습니다. Kotlin에서는 NullPointException 이 발생하지 않는다고 하니 많은 분들이 반가워 할 것 같습니다.

Kotlin 에서는 변수에 null을 저장하고 싶으면 타입명 뒤에 "?"를 붙여 null을 가질 수 있는 변수임을 알려줍니다.

Nullable은 null을 가질 수 있는 변수 타입을 말합니다. (그렇지 않은 변수는 Non-Null 이라고 합니다)

Non-Null 변수인 경우 값이 null 이 아님을 보장할 수 있으므로 Null 체크가 필요없으며 NullPointerException도 발생하지 않습니다.

Nullable 변수에 대해서는 null 체크가 없으면 컴파일러에 의해 오류가 발생합니다.

아래 sample code 에서 checkphone 은 Nullable 변수이므로 checkphone.length 를 사용시 compile error 를 발생시킵니다.

class ClassKotlin(var name: String, var phone: String="", var addr: String ="")
{
    fun checkInformation(){
        var checkname: String = name
        var checkphone: String? = phone

        if ( checkname.length > 0 ) {   // OK
           
        }
       
        if ( checkphone.length > 0) {   // compile error
           
        }

    }
}

 

7. When 문

switch 문의 변형이라고도 볼 수 있는데요. switch-case 에서는 하나의 변수값에 대해 integer 형으로만 구분할 수 있는데 when 문은 아래와 같이 다양하게 사용될 수 있습니다.

when{
    name.equals("GoodFortune") -> Log.d(TAG, "GoodTortune")
    phone.length < 9 -> Log.d(TAG, "phone number is short")
}

 

간단한 문법에 대해서 알아보았는데, 다음번엔 Android Studio 3.0 에서 직접 사용하는 방법에 대해서도 알아보려고 합니다.

Android Studio 3.0 에서 JAVA 코드를 Kotlin으로 변환도 가능하다고 하네요^^

 

 

'Language > Kotlin' 카테고리의 다른 글

[Kotlin] Range 사용하기  (0) 2018.01.25
[Kotlin] if, when, for, while 제어문 사용하기  (0) 2018.01.04
[Kotlin] 함수 사용하기  (0) 2017.12.28
[Kotlin] Class 두번째 이야기  (0) 2017.12.22
[Kotlin] Class 사용하기  (0) 2017.12.15

Android 에서 제공하는 AudioRecord Class 를 사용하면 음성녹음 기능을 쉽게 구현할 수 있습니다.

Application 에서는 AudioRecord Class 에서 제공하는 세가지 형태의 read method를 통해서 음성 데이터를 가져올 수 있습니다.

- read(byte[], int, int), read(short[], int, int), read(ByteBuffer, int)

어떤 형태의 함수를 쓸 것인지는 Application 에서 다루는 Data 형태에 따라 사용자가 결정하면 됩니다.

 

이 포스팅에서는 간단한 음성녹음 예제를 다루어 볼 것입니다.

간단하게 버튼 두개를 두고, 하나의 버튼으로는 녹음 시작/정지 를 수행하고, 다른 하나의 버튼으로는 재생/정지를 구현해 보도록 하겠습니다.

 

1. main layout 에 button 추가 하기

Record Button 과 Play Button 을 추가하고, 각각 Button 이 클릭 되면 해당 동작을 수행할 함수 onRecord/onPlay 를 정의 합니다.

<Button
android:id="@+id/bt_record"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Record"
android:onClick="onRecord"/>

<Button
android:id="@+id/bt_play"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Play"
android:onClick="onPlay"/>

 

2. AudioRecord 생성 및 초기화

AudioRecord 생성자를 보면, AudioRecord 객체를 생성하기 위해 필요한 인자들을 확인 할 수 있습니다.

- public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)

첫번째 인자인 AudioSource 의 경우 MediaRecorder.AudioSource 에 int 값이 정의되어 있고, Mic 로 들어오는 음성을 녹음하기 위해서는 1의 값을 넣어주면 됩니다.

두번째 세번째 네번째 인자는 실제 녹음될 오디오 데이터의 samplerate / channel / format 값을 의미합니다.

Sample Rate 는 아날로그 신호를 디지털로 변환 할 때, 1초당 몇개의 sample 을 추출할 것인가에 대한 내용으로 보통 8000Hz~48000Hz 가 많이 사용됩니다. 물론 값이 클수록 아날로그 신호와 비슷한 형태가 되니 고음질의 컨텐츠가 되겠지요.

Channel 값은 mono/streo 중에 선택하면 되고, audioformat 의 값은 PCM Data 를 받을 것이기 때문에 AudioFormat.ENCODING_PCM_16BIT 를 넣어주도록 하겠습니다.

마지막으로 buffer size 는 한번에 전달 받을 audio data 의 크기를 나타내고, 보통은 AudioRecord 의 getMinBufferSize 함수를 호출해서 return 되는 값으로 지정을 해줍니다.

 

public class MainActivity extends Activity {
    private static final String TAG = "MainActivity";

    private int mAudioSource = MediaRecorder.AudioSource.MIC;
    private int mSampleRate = 44100;
    private int mChannelCount = AudioFormat.CHANNEL_IN_STEREO;
    private int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
    private int mBufferSize = AudioTrack.getMinBufferSize(mSampleRate, mChannelCount, mAudioFormat);

    public AudioRecord mAudioRecord = null;

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

        mAudioRecord = new AudioRecord(mAudioSource, mSampleRate, mChannelCount, mAudioFormat, mBufferSize);
        mAudioRecord.startRecording();
    }

여기까지 하면 실제 레코딩 할 준비가 완료 되었고, 다음은 실제 레코딩을 시작해 보도록 하겠습니다.

 

3. 녹음 시작 - PCM Data 가져오기

녹음을 시작하고 PCM Data 를 가져오는 부분은 UI가 멈춰보이지 않도록 별도의 Thread 로 분리해야 합니다. 

이를 위해 mRecordThread 를 추가로 정의하고, 레코딩 중인지 아닌지를 알기 위해 isRecording 이라는 boolean 변수를 정의하였습니다.

mRecordThread 는 onCreate 함수 내부에 정의를 해놓고, 실제 start 되는 것은 사용자가 버튼을 누른 순간에 이루어지도록 합니다.

읽어온 PCM 데이터를 파일에 쓰기 위해 filepath 를 지정해 주고, FileOutputStream 을 통해 file open/write/close 를 수행하였습니다.

 

    public Thread mRecordThread = null;
    public boolean isRecording = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ..
        mRecordThread = new Thread(new Runnable() {
            @Override
            public void run() {
                byte[] readData = new byte[mBufferSize];
                mFilepath = Environment.getExternalStorageDirectory().getAbsolutePath() +"/record.pcm";
                FileOutputStream fos = null;
                try {
                    fos = new FileOutputStream(mFilepath);
                } catch(FileNotFoundException e) {
                    e.printStackTrace();
                }

                while(isRecording) {
                    int ret = mAudioRecord.read(readData, 0, mBufferSize);  //  AudioRecord의 read 함수를 통해 pcm data 를 읽어옴
                    Log.d(TAG, "read bytes is " + ret);

                    try {
                        fos.write(readData, 0, mBufferSize);    //  읽어온 readData 를 파일에 write 함
                    }catch (IOException e){
                        e.printStackTrace();
                    }
                }

                mAudioRecord.stop();
                mAudioRecord.release();
                mAudioRecord = null;

                try {
                    fos.close(); 
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public void onRecord(View view) {
        if(isRecording == true) {
            isRecording = false;
            mBtRecord.setText("Record");
        }
        else {
            isRecording = true;
            mBtRecord.setText("Stop");

            if(mAudioRecord == null) {
                mAudioRecord =  new AudioRecord(mAudioSource, mSampleRate, mChannelCount, mAudioFormat, mBufferSize);
                mAudioRecord.startRecording();
            }
            mRecordThread.start();
        }

    }

여기까지 수행 후 실행 시켜 보면 앱이 바로 죽는데, 로그를 확인해 보면 음성녹음을 위한 permission 이 없어서 AudioRecord 가 생성되지 못한 것을 알 수 있습니다.

11-28 20:06:31.467   770 14497 W ServiceManager: Permission failure: android.permission.RECORD_AUDIO from uid=10419 pid=-1
11-28 20:06:31.467   770 14497 E         : Request requires android.permission.RECORD_AUDIO
11-28 20:06:31.467   770 14497 E AudioFlinger: openRecord() permission denied: recording not allowed
11-28 20:06:31.472 31635 31635 E AndroidRuntime: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.codetravel.audiorecord/com.codetravel.audiorecord.MainActivity}: java.lang.IllegalStateException: startRecording() called on an uninitialized AudioRecord.

음성녹음과 추후 파일 write 를 위해 아래 두개의 permission 을 AndroidManifest.xml 파일에 추가해 줍니다.

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

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

Record 버튼을 누르게 되면 녹음이 쭉 진행 되고, Stop 버튼을 누르면 /storage/emulated/0/ 위치 밑에 record.pcm 이름으로 녹음된 파일이 저장 된 것을 확인 할 수 있습니다.

 

4. 녹음 완료 -  생성된 파일 재생하기

생성된 파일을 재생하기 위해 AudioTrack 을 사용해 보도록 하겠습니다.

AudioTrack 도 AudioRecord 와 마찬가지로 PCM Audio Data 를 다룰 수 있는 API 중에 하나입니다.

AudioTrack 의 생성자는 AudioRecord class 의 생성자와 비슷한 형태를 띄기 때문에, 각 매개변수는 어렵지 않게 채울 수 있습니다.

- AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode) 

첫번째 매개변수인 streamtype 은 하기 링크에 정의되어 있는 stream type 중, 용도에 맞는 Stream type 값을 넣어주면 됩니다. (Music/Alarm/Voice call 등)

http://androidxref.com/8.0.0_r4/xref/frameworks/base/media/java/android/media/AudioManager.java#319

 

AudioRecord Class 에서 PCM Data 를 읽어오기 위해 read 함수를 이용했다면, 이번에는 write 함수를 이용해 Track 에 PCM Data 를 써주어서 스피커로 송출 될 수 있도록 하겠습니다.

파일에서 PCM Data 를 읽어와 Track 에 쓰기 위한 별도의 Thread 인 mPlayThread 를 onCreate 함수 내부에 정의 하였습니다.

mPlayThread 는 FileInputStream 을 이용해서 File 의 내용을 읽어다가 AudioTrack 에 write 해주는 역할을 합니다.

 

   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ..
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, mSampleRate, mChannelCount, mAudioFormat, mBufferSize, AudioTrack.MODE_STREAM); // AudioTrack 생성
        ..
        mPlayThread = new Thread(new Runnable() {
            @Override
            public void run() {
                byte[] writeData = new byte[mBufferSize];
                FileInputStream fis = null;
                try {
                    fis = new FileInputStream(mFilePath);
                }catch (FileNotFoundException e) {
                    e.printStackTrace();
                }

                DataInputStream dis = new DataInputStream(fis);
                mAudioTrack.play();  // write 하기 전에 play 를 먼저 수행해 주어야 함

                while(isPlaying) {
                    try {
                        int ret = dis.read(writeData, 0, mBufferSize);
                        if (ret <= 0) {
                            (MainActivity.this).runOnUiThread(new Runnable() { // UI 컨트롤을 위해 
                                @Override
                                public void run() {
                                    isPlaying = false;
                                    mBtPlay.setText("Play");
                                }
                            });
                            break;
                        }
                        mAudioTrack.write(writeData, 0, ret); // AudioTrack 에 write 를 하면 스피커로 송출됨
                    }catch (IOException e) {
                        e.printStackTrace();
                    }

                }
                mAudioTrack.stop();
                mAudioTrack.release();
                mAudioTrack = null;

                try {
                    dis.close();
                    fis.close();
                }catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public void onPlay(View view) {
        if(isPlaying == true) {
            isPlaying = false;
            mBtPlay.setText("Play");
        }
        else {
            isPlaying = true;
            mBtPlay.setText("Stop");

            if(mAudioTrack == null) {
                mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, mSampleRate, mChannelCount, mAudioFormat, mBufferSize, AudioTrack.MODE_STREAM);
            }
            mPlayThread.start();
        }

    }

아직 많은 예외처리들이 필요하지만 , 여기까지 하면 아주 아~주 간단한 Recorder 와 Player 가 완성 되었습니다. ^^

전체 코드 첨부하니 필요하신 분들은 참고하세요.

 

AudioRecord.zip

 

+ Recent posts