今天的筆記我打算來聊聊在Android中是如何呈現計時器的方法

恩...其實也沒那麼複雜,固中訣竅就是"執行緒"

又或者要理解為"背景執行"也可以˚ᆺ˚

不過我覺得大家所認為的背景執行應該比較貼近關了APP還能繼續跑的那種意思吧(-д-;)

 

算了不重要XD

總之今天的比較我打算來寫關於使用執行緒的的方法

 

執行緒是什麼呢?有哪些呢?

我在這邊回答,最常聽到的就是以下三種

ThreadHandlerAsyncTask

而其中的Thread跟AsyncTask其實就最後出來的效果而言感覺根本是一樣的東西

這邊我不討論他們的差異在哪也沒打算分析源碼給你聽((我就爛(・ω・)b

反正分析了大多數人也看不懂....(-m-)

 

OK,那今天的主題就是應用這三個東西完成一個計數器&時間即時顯示表

多說無益,看範例

Gif_20200829185845395_by_gifguru

 

還有Github

https://github.com/thumbb13555/BackgroundWorkDemo/tree/master

 


 

1. 什麼是執行緒?

 

在軟體開發中(請注意,是軟體開發),世界上所有的通訊一定會存在延遲

也因此我們不可能什麼事情都像你在設定字串一樣,眼睛都還沒眨字串就設定完成了

有些通訊就是手機裝置必須等待回傳的,像是藍芽連接、網路連接等等

那執行緒的功用是什麼?講個學術一點的用詞叫做分隔主要UI線程及背景執行線程

...

喂等等先不要急著走啊(゜ロ゜)(゜ロ゜)(゜ロ゜)

 

我用圖來解釋什麼是UI線程(主)跟背景執行線程吧

一般我們在執行程式時程式是這樣跑的

截圖 2020-08-29 下午8.52.54

 

雖然看起來很順利,但是程式執行時每個程式時都會耗一點時間的

截圖 2020-08-29 下午8.58.43

如果照我這張圖看起來的話,那我執行這些程式需要0.48秒

 

恩...0.48秒,好像還可以,至少你不會覺得很奇怪

當然,我只是隨便舉例而已,不要跟我說你0.48秒就會介面卡頓一些有的沒的QQ

好,那假設今天程式B需要執行5秒,會發生什麼事呢?

截圖 2020-08-29 下午9.03.12

 

答案是你會在顯示完程式A後,整個APP會卡住五秒鐘才會顯示介面B、然後執行C、顯示C

所以整題程式會執行5.28秒

這就很有感覺了,對吧!

5.28秒足以讓你懷疑手機是不是要完蛋了

 

程式是很一版一眼的,他不會自動幫你繞路OK?

古人云:山不轉路轉、路不轉人轉、人不轉地球自轉

 

沒錯,既然山就擋在那邊那就轉個彎不就得了?

於是如果我們加入了執行緒,整體程式會變成這樣

截圖 2020-08-29 下午9.12.46

 

那麼,這個程式從開始執行程式A到最後顯示介面C總共會執行0.28秒

咦?聽起來好像是一個可以省時間的工具?

 

錯!

有句話你一定聽過

iTaigi 愛台語

所以說那5秒去哪裡了?

 

答案是雖然跳過了程式B,但是對應程式B結果顯示的介面顯示B會在程式結束後晚5秒才會被顯示出來

也就是說,程式是有執行到它的,但是程式把較耗時的工作放到一邊慢慢處理讓程式繼續跑,等耗時的工作處理完成後才回頭把它顯示出來

而這個被分開出去紫色箭頭的部分,我們稱之為子線程

而其它上排的部分,我們稱之為主線程,或UI線程

 

Okay,希望這幾張圖能夠幫助你了解執行緒要解決的問題

那接下來的重點就是應用了,我們先來看今天要寫的程式的一部分

private void threadRun() {
    new Thread(() -> {
        for (int i = 1; i < 6; i++) {
            int count = i;
            /**有要在UI上面顯示的內容的話,就必須使用runOnUiThread!*/
            runOnUiThread(() -> {
                tvResult.setText(String.valueOf(count));
            });
            SystemClock.sleep(1000);
        }
        runOnUiThread(() -> {
            tvResult.setText("完成Thread計數!");
        });
    }).start();
}

 

簡單講述一下,只要是被包在Thread裡面的程式都是被執行緒分開的部分

而SystemClock.sleep(1000) 則代表的是程式整體暫停一秒

再來runOnUiThread所括起來的內容則代表的是主線程的部分

也就是說,雖然for迴圈每執行一次就要一秒

但是我把系統所延遲的內容放在子線程當中

所以雖然UI會慢慢數,但是在這之中你還是可以操作其他UI的

UI也不會有卡頓的事情發生

那麼,接著就來講述今天的程式吧

 


 

2. 介面以及所需要的Java檔案

 

今天要用的Java檔案除了MainActivity之外,還有要拿來寫AsyncTask執行緒的檔案,你可以先創好,等一下再來寫它

截圖 2020-08-29 下午9.38.18

 

然後,介面檔在這

activity_main.xml

截圖 2020-08-29 下午9.39.10

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView_Result"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        android:textStyle="bold"
        app:layout_constraintBottom_toTopOf="@+id/button_Thread"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button_Thread"
        style="@style/Widget.AppCompat.Button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="sans-serif-medium"
        android:text="使用Thread"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ToggleButton
        android:id="@+id/button_Handler"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:textOff="使用Handler"
        android:textOn="取消Handler"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button_AsyncTask" />

    <Button
        android:id="@+id/button_AsyncTask"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="使用Async"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button_Thread" />
</androidx.constraintlayout.widget.ConstraintLayout>

 


 

3. 撰寫Thread以及畫面準備工作

 

到看到這邊,如果不想看程式分解解釋的朋友們直接看拉到最後面就好,最後會放全部的程式

但是CountTask的程式會在第四段落,要複製的話不要找不到呦XD

 

我這邊先把點擊事件連結以及程式架構先PO給你

public class MainActivity extends AppCompatActivity {
    TextView tvResult;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btThread, btAsync;
        ToggleButton btHandler;

        btThread = findViewById(R.id.button_Thread);
        btHandler = findViewById(R.id.button_Handler);
        btAsync = findViewById(R.id.button_AsyncTask);
        tvResult = findViewById(R.id.textView_Result);

        btThread.setOnClickListener(v -> {
            threadRun();
        });
        btHandler.setOnCheckedChangeListener((buttonView, isChecked) -> {
           
        });
        btAsync.setOnClickListener(v -> {
            asyncTaskRun();
        });

    }//onCreate

    /**使用Thread做背景執行處理*/
    private void threadRun() {
      
    }

    /**使用AsyncTask做背景執行處理*/
    private void asyncTaskRun() {
       
    }
    
}

 

OK,我們先來做Thread的程式

為什麼要先做Thread呢?因為它最簡單,也最直觀

先來看副程式內容吧

/**使用Thread做背景執行處理*/
private void threadRun() {
    new Thread(() -> {
        for (int i = 1; i < 6; i++) {
            int count = i;
            /**有要在UI上面顯示的內容的話,就必須使用runOnUiThread!*/
            runOnUiThread(() -> {
                tvResult.setText(String.valueOf(count));
            });
            SystemClock.sleep(1000);
        }
        runOnUiThread(() -> {
            tvResult.setText("完成Thread計數!");
        });
    }).start();
}

 

Thread只有兩個重點:

一個是新增一個Thread

也就是new Thread的部分,然後請特別特別注意

.start一定要加,不然程式不會跑喔!

然後就是runOnUiThread了

其意義在於在Thread中如果你做完某些事了,呼叫它!

他就會在畫面中即刻顯示你現在完成的事喔!

完成這幾行後你可以執行看看,應該程式會讀5秒然後顯示完成計數。

 


 

3. 撰寫AsyncTask

 

AsyncTask為耗時工作,就實際面而言跟Thread非常接近

但是AsyncTask他設有多種回調,大部分的人都會拿他來處理網路資料下載,會者資料輸出這種需要跑進度條及背景執行的東西

那麼,我先給你CountTask.java的全部內容

 

CountTask.java

class CountTask extends AsyncTask<String, Integer, String> {

    public OnTimerCount onTimerCount;

    /**要在背景執行緒做的事*/
    /**return的值將會在onPostExecute顯現*/
    @Override
    protected String doInBackground(String... strings) {
        for (int i = 1; i < 6; i++) {
            /**上傳執行進度,此進度會顯示在onProgressUpdate*/
            publishProgress(i);
            SystemClock.sleep(1000);
        }
        return "完成Async計數!";
    }
    /**取得執行進度*/
    @Override
    protected void onProgressUpdate(Integer... values) {
        super.onProgressUpdate(values);
        onTimerCount.onCountering(values[0]);
    }
    /**完成進度後要做的事會在這邊做處理*/
    @Override
    protected void onPostExecute(String s) {
        super.onPostExecute(s);
        onTimerCount.onCounterFinish(s);
    }

    interface OnTimerCount{
        void onCountering(int progress);
        void onCounterFinish(String text);

    }
}

 

其實我已經幫你把程式寫得蠻完整了,基本上你可以當成一個模組來使用

關於程式模組化的相關文章請往這邊走

->碼農日常-『Android studio』NumberPicker 配合 Interface (接口)完成一個時間選擇器

 

Async有很多眉眉角角,首先來看他幾個複寫

doInBackground:這邊是要在背景(子線程)做的事,切記不可在這邊修改UI,否則會閃退

onProgressUpdate:這邊為接收從doInBackground中的publishProgress方法傳出的數值,在這邊可取得程式處理的進度

onPostExecute:這邊的string為接收doInBackgreund所return的字串,當doInBackground跑完後就會呼叫這一個副程式

 

再來請注意到最上面

class CountTask extends AsyncTask<String, Integer, String> {

 

AsyncTask跟著三個型別,這三個代表的就是

doInBackground的輸入型別

跑進度onProgressUpdate的型別

回傳值onPostExecute的型別

 

這三個要特別注意要設定,不可以漏喔!

如果不需要型別的話,也可以設定為void

這樣就可以不必設置回傳啦!

最後回到MainActivity,使用他吧

/**使用AsyncTask做背景執行處理*/
private void asyncTaskRun() {
    CountTask countTask = new CountTask();
    /**送出初始值並觸發*/
    countTask.execute("0");
    /**取得背景執行之類別回傳*/
    countTask.onTimerCount = new CountTask.OnTimerCount() {
        @Override
        public void onCountering(int progress) {
            tvResult.setText(String.valueOf(progress));
        }

        @Override
        public void onCounterFinish(String text) {
            tvResult.setText(text);
        }
    };
}

 


 

5. 撰寫Handler

 

其實在今天的範例中,Handler是讓我最頭痛的

因為Handler我認為並不能算是子執行緒

他的功能比較偏向是給他一個工作,然後讓他持續一直做

這才是他當初被設置出來的目的

所以,今天Handler我改變一種模式

我讓他成為一個實時時間表

開關按下則開始顯示,再按一次則停止顯示

 

Gif_20200829221523915_by_gifguru

 

OK,來先PO重點

public class MainActivity extends AppCompatActivity {
    TextView tvResult;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btThread, btAsync;
        ToggleButton btHandler;

        btThread = findViewById(R.id.button_Thread);
        btHandler = findViewById(R.id.button_Handler);
        btAsync = findViewById(R.id.button_AsyncTask);
        tvResult = findViewById(R.id.textView_Result);

        btThread.setOnClickListener(v -> {
           
        });
        btHandler.setOnCheckedChangeListener((buttonView, isChecked) -> {
            if (isChecked){
                /**執行Handler*/
                handler.post(task);
            }else{
                /**取消Handler*/
                handler.removeCallbacks(task);
                tvResult.setText("結束使用Handler執行緒!");
            }
        });
        btAsync.setOnClickListener(v -> {
         
        });

    }//onCreate

    /**為Handler制定Runnable執行緒*/
    private Runnable task = new Runnable() {
        @Override
        public void run() {
            /**在Task中將指令傳送到Handler內部的方法*/
            handler.sendEmptyMessage(1);
            /**每1秒會再重複執行此task*/
            handler.postDelayed(this,  1000);
        }
    };
    /**為Handler制定他要在畫面做的事*/
    @SuppressLint("HandlerLeak")//<-有加不加不影響
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    SimpleDateFormat format1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
                    Date currentDate = new Date();
                    String displayTime1 = format1.format(currentDate);
                    tvResult.setText(displayTime1);
                    break;
                default:
                    break;
            }
            super.handleMessage(msg);
        }
    };
}

 

可以看到要完成一個handler必須要有兩個部分

/**為Handler制定Runnable執行緒*/
private Runnable task = new Runnable() {
    @Override
    public void run() {
        /**在Task中將指令傳送到Handler內部的方法*/
        handler.sendEmptyMessage(1);
        /**每1秒會再重複執行此task*/
        handler.postDelayed(this,  1000);
    }
};
/**為Handler制定他要在畫面做的事*/
@SuppressLint("HandlerLeak")//<-有加不加不影響
private Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case 1:
                SimpleDateFormat format1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
                Date currentDate = new Date();
                String displayTime1 = format1.format(currentDate);
                tvResult.setText(displayTime1);
                break;
            default:
                break;
        }
        super.handleMessage(msg);
    }
};

 

通常task可以理解為一個中控台,這裡為管理整體執行的順序

再來Handler裡面的話,這邊就是實際要運作UI的部分了

而handler有個很大的有點就是他可以藉由postDelayed來決定這個程式要被執行多久

像是藍芽掃描中如果想讓藍芽只掃五秒,那麼這個就是你最合適的選擇XD

 

最後,啟動與關閉就在這邊

if (isChecked){
    /**執行Handler*/
    handler.post(task);
}else{
    /**取消Handler*/
    handler.removeCallbacks(task);
    tvResult.setText("結束使用Handler執行緒!");
}

 

分別就是post(或者postDelay也可以)開啟

然後removeCallbacks關閉

 

最後是全部的MainActivity.java!

 

MainActivity.java

public class MainActivity extends AppCompatActivity {
    TextView tvResult;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btThread, btAsync;
        ToggleButton btHandler;

        btThread = findViewById(R.id.button_Thread);
        btHandler = findViewById(R.id.button_Handler);
        btAsync = findViewById(R.id.button_AsyncTask);
        tvResult = findViewById(R.id.textView_Result);

        btThread.setOnClickListener(v -> {
            threadRun();
        });
        btHandler.setOnCheckedChangeListener((buttonView, isChecked) -> {
            if (isChecked){
                /**執行Handler*/
                handler.post(task);
            }else{
                /**取消Handler*/
                handler.removeCallbacks(task);
                tvResult.setText("結束使用Handler執行緒!");
            }
        });
        btAsync.setOnClickListener(v -> {
            asyncTaskRun();
        });

    }//onCreate

    /**使用Thread做背景執行處理*/
    private void threadRun() {
        new Thread(() -> {
            for (int i = 1; i < 6; i++) {
                int count = i;
                /**有要在UI上面顯示的內容的話,就必須使用runOnUiThread!*/
                runOnUiThread(() -> {
                    tvResult.setText(String.valueOf(count));
                });
                SystemClock.sleep(1000);
            }
            runOnUiThread(() -> {
                tvResult.setText("完成Thread計數!");
            });
        }).start();
    }

    /**使用AsyncTask做背景執行處理*/
    private void asyncTaskRun() {
        CountTask countTask = new CountTask();
        /**送出初始值並觸發*/
        countTask.execute("0");
        /**取得背景執行之類別回傳*/
        countTask.onTimerCount = new CountTask.OnTimerCount() {
            @Override
            public void onCountering(int progress) {
                tvResult.setText(String.valueOf(progress));
            }

            @Override
            public void onCounterFinish(String text) {
                tvResult.setText(text);
            }
        };
    }
    /**為Handler制定Runnable執行緒*/
    private Runnable task = new Runnable() {
        @Override
        public void run() {
            /**在Task中將指令傳送到Handler內部的方法*/
            handler.sendEmptyMessage(1);
            /**每1秒會再重複執行此task*/
            handler.postDelayed(this,  1000);
        }
    };
    /**為Handler制定他要在畫面做的事*/
    @SuppressLint("HandlerLeak")//<-有加不加不影響
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    SimpleDateFormat format1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
                    Date currentDate = new Date();
                    String displayTime1 = format1.format(currentDate);
                    tvResult.setText(displayTime1);
                    break;
                default:
                    break;
            }
            super.handleMessage(msg);
        }
    };
}

 


 

結語

 

線程問題算是搞我搞了很久的東西

這種東西也是一般市售書著墨不多的地方(不過最近的書越來越多了,但是還是有很多書寫得不詳細)

大部分的文章都會把Thread跟Handler放在一組,然後AsyncTask另外講

但是我就機車↜(╰ •ω•)╯ψ我偏偏要把他們放在一起講

最近我有收到一個問題,他問我的就是跟這個有關係

他的程式會報出一種錯誤叫做Runtimeexception

基本上這個錯誤有很大的原因就是出在執行緒問題上面

 

那麼希望今天的文章對大家有幫助

覺得我有幫到你就幫我按推推或留言鼓勵吧!

 

thank-you-meme-05 -

 

 

 

arrow
arrow
    創作者介紹
    創作者 碼農日常 的頭像
    碼農日常

    碼農日常大小事

    碼農日常 發表在 痞客邦 留言(2) 人氣()