android studio 覚書 (お題 2：スレッド処理の実装）

## お題 2：スレッド処理の実装

### 2-1. イメージ

下記のように、ボタンを押すと時刻が更新される画面を実装する。
この時、画面が操作不能にならないこと。
![Test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1723869/f927f5e4-b38a-c48f-0fc0-584870248c00.gif)

---

### 2-2. 実装
実装したコードは下記の通り。

```java:全容
package com.example.helloworld;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Looper;
import android.os.Handler;

import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.time.LocalDateTime;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class MainActivity extends AppCompatActivity {
    private TextView text;
    private Button button;

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

        // TextViewを取得
        text = (TextView)findViewById(R.id.text);

        // Buttonを取得
        button = (Button)findViewById(R.id.button);

        // Buttonをタップされた時の処理
        button.setOnClickListener(buttonClick);
    }

    private View.OnClickListener buttonClick = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            // thread poolにthreadを1つ生成
            Executor executor = Executors.newFixedThreadPool(1);
            // threadを実行
            executor.execute(new WThreadTimer());
        }
    };

    public class WThreadTimer implements Runnable {
        final Handler mainThreadHandler = new Handler(Looper.getMainLooper());

        @Override
        public void run() {
            // ラムダ式だと↓のように書くらしい
            // mainThreadHandler.post(() -> { text.setText("WThreadTimer start."); });
            mainThreadHandler.post(postMessage("WThreadTimer start."));
            for (int cnt = 0; cnt < 20; ++cnt) {
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                }
                mainThreadHandler.post(postMessage(LocalDateTime.now().toString()));
            }
            mainThreadHandler.post(postMessage("WThreadTimer end."));
        }

        public Runnable postMessage(String message) {
            Runnable r = new Runnable() {
                @Override
                public void run(){
                    text.setText(message);
                }
            };
            return r;
        }
    }
}
```

---
### 2-3. Thread生成・起動処理

画面がバインドされないようにするため、threadで処理を行う必要がある。
なので、```java.util.concurrent ```から必要なクラスをインポートしてthread処理を実装した。
ボタンを押すとthreadを起動するよう処理を実装する。

```java:thread処理実行
    private View.OnClickListener buttonClick = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            // thread poolにthreadを1つ生成
            Executor executor = Executors.newFixedThreadPool(1);
            // threadを実行
            executor.execute(new WThreadTimer());
        }
    };
```

```Executors.newFixedThreadPool(number)```でthread実行用のIFをインスタンス化しているが、
ここで生成したthreadはプールされ、60秒間の間実行待ちしてくれるらしい。
threadを作り放題にするとマシン側が耐えられないので、もししっかりアプリを実装するならthread制御は```Executor```, ```Executors```, ```ExecutorService```などを使って上限を定めたい。

---
### 2-4. threadで行う処理

下記のように、threadで行う処理は```Runnable```を継承したクラス内に```run```関数の実体を作って実装した。```Runnable```は、```Executor```で実行する処理を実装する場所らしい。```Runnable```自体は、```void run()```の仮想関数のみを持っている。

```java:thread処理全容
    public class WThreadTimer implements Runnable {
        final Handler mainThreadHandler = new Handler(Looper.getMainLooper());

        @Override
        public void run() {
            // ラムダ式だと↓のように書くらしい
            // mainThreadHandler.post(() -> { text.setText("WThreadTimer start."); });
            mainThreadHandler.post(postMessage("WThreadTimer start."));
            for (int cnt = 0; cnt < 20; ++cnt) {
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                }
                mainThreadHandler.post(postMessage(LocalDateTime.now().toString()));
            }
            mainThreadHandler.post(postMessage("WThreadTimer end."));
        }

        public Runnable postMessage(String message) {
            Runnable r = new Runnable() {
                @Override
                public void run(){
                    text.setText(message);
                }
            };
            return r;
        }
    }
```

下記の```mainThreadHandler```は、UI threadが持つUIオブジェクトに対する更新を行う際に使用するハンドラ。Worker threadが、UIオブジェクトに対して更新を行うことはできないそうなので宣言、生成した。

```java:UI threadへのhandler
        final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
```

```mainThreadHandler.post(Runnable r)```で、UI thread側に画面更新処理を委託するが、
更新処理は、Worker thread側で用意する必要があるようなので、下記で更新処理を実装。
UI thread側で、```public void run()```が実行される...？

```java:UI threadへ依頼する処理
        public Runnable postMessage(String message) {
            Runnable r = new Runnable() {
                @Override
                public void run(){
                    text.setText(message);
                }
            };
            return r;
        }
```

UI threadへ処理依頼をしている箇所は、下記の通り。
ラムダ式は(私の中では)難しいので、```postMessage```関数の中で```Runnable```クラスを生成して返すように実装した。

```java:UI threadへ処理依頼
            // ラムダ式だと↓のように書くらしい
            // mainThreadHandler.post(() -> { text.setText("WThreadTimer start."); });
            mainThreadHandler.post(postMessage("WThreadTimer start."));
```

---
### 2-5. 所感など

#### UI thread、Worker threadについて
どこからでも画面オブジェクトをいじることができるわけでないということが分かった。
アプリが突然落ちるような挙動になるが、android studioのLogcatに原因が記載されていた。

#### 本記事に記載したソースの致命的なバグ
画面が処理でバインドされないため、何回でもthread処理をボタンで実行できる。
必要に応じて排他制御を実装する必要あり。
スレッド生成上限とスレッド多重起動防止制御を実装する必要あり。

おまけ
![Test2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1723869/36f255a3-29fe-c0c8-80d5-f55d99740cfb.gif)