今天的筆記我打算來聊聊在Android中是如何呈現計時器的方法
恩...其實也沒那麼複雜,固中訣竅就是"執行緒"
又或者要理解為"背景執行"也可以˚ᆺ˚
不過我覺得大家所認為的背景執行應該比較貼近關了APP還能繼續跑的那種意思吧(-д-;)
算了不重要XD
總之今天的比較我打算來寫關於使用執行緒的的方法
執行緒是什麼呢?有哪些呢?
我在這邊回答,最常聽到的就是以下三種
Thread、Handler及AsyncTask
而其中的Thread跟AsyncTask其實就最後出來的效果而言感覺根本是一樣的東西
這邊我不討論他們的差異在哪也沒打算分析源碼給你聽((我就爛(・ω・)b
反正分析了大多數人也看不懂....(-m-)
OK,那今天的主題就是應用這三個東西完成一個計數器&時間即時顯示表
多說無益,看範例
還有Github
https://github.com/thumbb13555/BackgroundWorkDemo/tree/master
1. 什麼是執行緒?
在軟體開發中(請注意,是軟體開發),世界上所有的通訊一定會存在延遲
也因此我們不可能什麼事情都像你在設定字串一樣,眼睛都還沒眨字串就設定完成了
有些通訊就是手機裝置必須等待回傳的,像是藍芽連接、網路連接等等
那執行緒的功用是什麼?講個學術一點的用詞叫做分隔主要UI線程及背景執行線程
...
喂等等先不要急著走啊(゜ロ゜)(゜ロ゜)(゜ロ゜)
我用圖來解釋什麼是UI線程(主)跟背景執行線程吧
一般我們在執行程式時程式是這樣跑的
雖然看起來很順利,但是程式執行時每個程式時都會耗一點時間的
如果照我這張圖看起來的話,那我執行這些程式需要0.48秒
恩...0.48秒,好像還可以,至少你不會覺得很奇怪
當然,我只是隨便舉例而已,不要跟我說你0.48秒就會介面卡頓一些有的沒的QQ
好,那假設今天程式B需要執行5秒,會發生什麼事呢?
答案是你會在顯示完程式A後,整個APP會卡住五秒鐘才會顯示介面B、然後執行C、顯示C
所以整題程式會執行5.28秒
這就很有感覺了,對吧!
5.28秒足以讓你懷疑手機是不是要完蛋了
程式是很一版一眼的,他不會自動幫你繞路OK?
古人云:山不轉路轉、路不轉人轉、人不轉地球自轉
沒錯,既然山就擋在那邊那就轉個彎不就得了?
於是如果我們加入了執行緒,整體程式會變成這樣
那麼,這個程式從開始執行程式A到最後顯示介面C總共會執行0.28秒
咦?聽起來好像是一個可以省時間的工具?
錯!
有句話你一定聽過
所以說那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執行緒的檔案,你可以先創好,等一下再來寫它
然後,介面檔在這
<?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的全部內容
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我改變一種模式
我讓他成為一個實時時間表
開關按下則開始顯示,再按一次則停止顯示
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!
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
基本上這個錯誤有很大的原因就是出在執行緒問題上面
那麼希望今天的文章對大家有幫助
覺得我有幫到你就幫我按推推或留言鼓勵吧!
留言列表