多線程
多線程可以讓你同時異步執行多種任務,是各種編程語言里很重要的一個概念。合理的采用多線程可以讓你的 App 擁有更好的運行性能,但是如果使用不當可能會讓你的程序非?;靵y,出現很多令人費解且難以定位的問題。
1. 多線程初探
當用戶打開一個 App 時,Android 系統會創建一個 Linux 進程,同時在進程中創建一個執行線程,我們稱之為“主線程”,因為 Andfoid 規定只能在主線程更新 UI,所以又叫“UI線程”。
系統在創建主線程的時候幫我們創建好了一套消息處理機制,包含了第 38 節提到的 Handler、Looper、MessageQueue 等模塊,主線程就利用這一套消息機制來實現 Actvity、Fragment 的生命周期回調以及和其他 App 之間的通信。所有需要在 UI 線程執行的任務都要首先被 push 到任務隊列中,然后等待主線程 Looper 來輪詢。如果我們將所有的任務都放到主線程的任務隊列,那么可能需要等很久才能執行到,所以一個比較好的選擇就是將耗時任務單獨放到一個子線程中,這樣就可以獨享一個 MessageQuene,并且不再占用主線程的資源。
2. 多線程注意事項
- Android 規定刷新 UI 的操作必須在主線程執行;
- 網絡請求、數據庫或者文件 I / O 等都屬于耗時操作,非常容易導致主線程的阻塞造成 App 卡頓;
- 由于主線程的 Looper 是按順序輪詢 MessageQueue 的,所以主線程的所有任務都是同步執行。這樣如果有耗時操作那么會阻塞主線程,后面的任務都需要等待耗時操作的執行;
- 除了 I / O 操作外,開發人員需要自行評估任務的耗時情況,合理采用多線程避免主線程的阻塞;
- Android 提供了多種創建和管理線程的方法,當然如果有高并發的場景還有一些第三方庫可以使用,但是系統的線程、線程池可以應對大部分常見場景。
接下來我們來看看具體怎么使用 Android 多線程。
3. 線程的使用方法
Java 虛擬機支持多線程并發編程,并發意味著同時執行多個任務。在 Android 中常見的多線程常見就是在子線程執行耗時操作,然后將結果通過線程間通信傳遞給主線程,主線程僅僅拿到結果進行 UI 的刷新。
3.1 線程的創建
我們有兩種方式進行線程的創建
- 繼承
Thread
實現一個線程類:
class TestThread extends Thread {
@Override
public void run() {
Log.d("Threading", "繼承 Thread 的線程:"+Thread.currentThread().getName());
}
}
- 實現
Runnable
接口
class TestRunnable implements Runnable {
@Override
public void run() {
Log.d("Runable", "實現 Runable 的線程>"+Thread.currentThread().getName());
}
}
無論是哪種方式,都需要在類中實現一個無參的run()
方法,然后將線程的實際執行任務放在run()
方法中,在要用多線程的類中需要創建出一個Runnable
接口實例。
3.2 啟動進程
對于第一種創建方式,直接創建 TestThread 實例調用start()
即可:
new TestThread().start()
而對于第二種方式,在創建 Thread 的同時傳入 Runnable 接口實例,然后調用start()
:
new Thread(new TestRunnable()).start()
調用了 Thread 對象的 start()
之后,run()
方法就會在我們的子線程中執行了。
3.3 線程生命周期
和 Activity 一樣,Thread 在執行過程中也有自己的生命周期,一共有 5 種狀態:
- **New:**剛創建好,還未執行
- **Runnable:**已經調用了
start()
,等待 CPU 分配時間片 - **Running:**正在運行
- **Blocked:**由于某些原因(等待、睡眠、CPU暫時回收資源等)線程進入阻塞
- **Dead:**線程任務執行結束,或者主動關閉
各個生命周期的切換如下圖:
4. 多線程示例
本節創建兩個耗時子線程,在線程的開始和結束分別打上日志,然后觀察兩個線程任務是同時執行,還是需要等待其中一個線程的耗時任務執行結束才能執行第二個。
MainActivity 代碼很簡單,在里面創建兩個線程,為了方便演示我們用“繼承自 Thread”和“實現 Runnable”兩種方式來創建兩個線程:
package com.emercy.myapplication;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.Nullable;
import java.io.IOException;
import java.util.Random;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
public class MainActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread() {
@Override
public void run() {
Log.d("ThreadTest", "Thread1 start");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("ThreadTest", "Thread1 end");
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
Log.d("ThreadTest", "Thread2 start");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("ThreadTest", "Thread2 end");
}
}).start();
}
private void task() {
for (int i = 0; i < 10; i++) {
Log.d("Thread", Thread.currentThread().getName() + " 當前i = " + i);
}
}
}
在兩個線程中通過sleep()
來模擬 500 毫秒的耗時任務,在任務的開始和結束都打上日志,觀察結果如下:
可以看到首先會同時開啟兩個子線程,然后分別同時執行 500 毫秒的任務,在執行結束打上結束的 Log,可以證明兩個 Thread 是同時執行的。
5. 小結
本節學習了一個能讓你的 App 并發高效執行任務的方式,多線程可以幫助你提升 App 的整體性能,但用之不當可能會造成一定的資源浪費,所以一定要謹記本節所提到的注意事項。然后按照步驟去創建、運行子線程,了解線程執行的生命周期,讓程序更好的為用戶服務。