細說 Android 下的多執行緒,學會了多執行緒,你就學會了壓榨CPU!

yilian發表於2020-03-17
細說 Android 下的多執行緒,學會了多執行緒,你就學會了壓榨CPU!

楔子

蘇格拉底曾說過:“學會了多執行緒,你就學會了壓榨CPU,就好像資本家對無產階級做的那事一樣。”

多執行緒是開發人員必不可少的技術點,也是初學者不太容易掌握好的一個難點。要想設計出優秀的程式,那必然需要合理的執行緒排程。今天就給大家細說下Android中與多執行緒相關的知識點,揭開多執行緒神秘的面紗。

本篇文章僅介紹多執行緒的各種實現方式,不過多涉及深入的基礎原理探究,達到“所見即所學,所學即可用”的效果。關於各種多執行緒原理的深入探究,有機會放在後面的專欄逐一介紹。

一、多執行緒是什麼?我為什麼要用多執行緒?

1.1 執行緒和程式的概念

按照作業系統中的描述,執行緒是CPU排程的最小單元,同時執行緒是一種有限的系統資源。而程式一般指一個執行單元,在PC和移動裝置上指一個程式或者一個應用。一個程式可以包含多個執行緒。

簡單點理解,一個Android APP就是一個程式,一個APP裡面有多個執行緒,我們多執行緒程式設計的意義就是實現“一個APP多個執行緒”。

有槓精可能會問,那我可不可以一個APP多個程式?又可不可以一個程式只有一個執行緒?

我告訴你,可以,都可以。

單執行緒的APP只包括Android的UI執行緒也是能執行的;一個APP多個程式也是可以達到的,實現方式涉及到Android的IPC機制,這裡不細說。

1.2 為什麼要使用多執行緒?

這裡槓精可能會說,那你單執行緒也能跑,我為啥還要整多執行緒?

我告訴你,首先這句話從Android開發的角度來講,近似於一個假命題。因為谷歌爸爸現在強制規定了不能在UI執行緒進行耗時操作,必須放到子執行緒裡面去,除非你的程式不涉及耗時操作。究其原因,是因為在UI執行緒進行耗時操作的話,給使用者的使用體驗就是介面“卡頓”。同時,如果UI執行緒被阻塞超過一定時間會觸發ANR(Application Not Responding)錯誤。

從底層的角度來講,多執行緒可以使得整個環境能夠非同步執行,這有助於防止浪費CPU時鐘週期從而提高效率。換言之,多執行緒能更充分的利用CPU資源,從而提高程式的執行效率。

二、那我怎麼進行多執行緒程式設計?

2.1 Thread類和Runnable介面

要想定義一個執行緒只需要新建一個類繼承自Thread,然後重寫父類的run方法即可

  class MyThread extends Thread {      @Override
      public void run() {
          doSomething();
      }
  }  //在需要的時候啟動執行緒
  new MyThread().start();

最佳化一下?

我們可以沒必要繼承整個Thread類,只實現Runnable介面就好了

  class MyThread implements Runnable {      @Override
      public void run() {
          doSomething()
      }
  }  //啟動執行緒
  MyThread myThread = new MyThread();  new Thread(myThread).start();

那我不想專門再寫一個執行緒類怎麼辦?可以使用匿名類

  new Thread(new Runnable() {      @Override
      public void run() {
          doSomething();
      }
  }).start();

2.2 執行緒池

2.2.1 執行緒池的意義

既然我都會用Runnable介面來建立執行緒了,還要執行緒池幹啥?其實不然,隨意建立執行緒的操作在實際開發中是極為不推薦的。為啥?因為執行緒也是一種資源,反覆的建立和銷燬執行緒會帶來一定效能上的額外開銷。與其相比,執行緒池主要有以下幾個優點:

  • 重用執行緒池中的執行緒,避免因為執行緒的建立和銷燬所帶來的效能開銷
  • 能有效控制執行緒池的最大併發數,避免大量的執行緒之間因相互搶佔系統資源而導致的阻塞現象
  • 能夠對執行緒進行簡單的管理,並提供定時執行以及指定間隔迴圈執行等功能
2.2.2 執行緒池的結構和原理

一個完整的執行緒池應該有這麼幾個組成部分

  • 核心執行緒
  • 任務佇列
  • 非核心執行緒

當我們透過執行緒池執行非同步任務的時候,其實是依次進行了下面的流程

  1. 檢查核心執行緒數是否到達最大值,否則建立新的核心執行緒執行任務,是則進行下一步
  2. 檢查任務佇列是否已滿,否則將任務新增到任務佇列中,是則進行下一步
  3. 檢查非核心執行緒數是否到達最大值,否則建立新的非核心執行緒執行任務,是則說明這個執行緒池已經飽和了,執行飽和策略。預設的飽和策略是丟擲RejectedExecutionException異常

下面手搓一個執行緒池的實現

  //CPU核心數
  private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();  //核心執行緒數
  private static final int CORE_POOL_SIZE = CPU_COUNT + 1;  //最大執行緒數
  private static final int MAX_POOL_SIZE = CPU_COUNT * 2 + 1;  //非核心執行緒閒置的超時時間
  private static final int KEEP_ALIVE_TIME = 1;  //任務佇列
  private static final BlockingQueue<Runnable> sPoolWorkQueue =          new LinkedBlockingQueue<Runnable>(128);  //執行緒池
  private ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
          MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, sPoolWorkQueue);  private void fun(){
      Runnable runnable = new Runnable() {          @Override
          public void run() {              //子執行緒處理耗時操作
              doSomething();
          }
      };
      poolExecutor.execute(runnable);
  }

這樣我們就實現了一個簡單的執行緒池,核心執行緒數為CPU數量+1,非核心執行緒數為CPU數量*2+1,非核心執行緒的閒置時間為1秒,任務佇列的大小為128。

執行緒池還有具體的好幾種分類和相應不同的實現方式,這裡不再細說。

2.3 Handler

有朋友可能會說,你講的這些都是Java多執行緒裡面的東西,能不能整點我們Android特有的?OK,現在進入專業時間。

Handler是Android提供的一種非同步訊息處理機制,要學會使用Handler我們首先來了解下訊息處理四兄弟:

  • Message
  • Handler
  • MessageQueue
  • Looper

Handler可以幫助我們實現在不同的執行緒之間傳遞訊息,這裡的Message就是訊息本體,也就是我們想要傳遞的那個東西。

Handler在這裡扮演的角色是訊息處理者,它的主要作用是傳送和處理訊息。MessageQueue是一個訊息佇列,Handler傳送過來的訊息會放在這個佇列裡面,每個執行緒只會有一個MessageQueue物件。

Looper是執行緒中訊息佇列的管家,它會無限迴圈執行,每發現MessageQueue中存在一條訊息,它就會把訊息取出然後傳送給Handler。每一個執行緒也只能有一個Looper物件。

好了,基本原理已經瞭解,現在我們來反手搓一個Handler

  private static final int FLAG = 1;  private Handler mHandler = new Handler(){      @Override
      public void handleMessage(@NonNull Message msg) {          if (FLAG == msg.what){              //這裡已經回到主執行緒了
              doSomething();
          }
      }
  };  private void fun(){      new Thread(new Runnable() {          @Override
          public void run() {              //子執行緒傳送訊息
              Message message = new Message();
              message.what = FLAG;
              mHandler.sendMessage(message);
          }
      }).start();
  }

2.4 AsyncTask

除了Handler以外,谷歌爸爸還給我們提供AsyncTask來進行執行緒的切換。AsyncTask是一種輕量級的非同步任務,它可以線上程池中執行後臺任務,然後把執行的進度和最終結果傳遞給主執行緒。從實現原理上來講,AsyncTask是對Thread和Handle的再次封裝。

AsyncTask本身是一個抽象的泛型類,有四個親兒子:

  • onPreExecute()
  • doInBackground(Params...params)
  • onProgressUpdate(Progress...values)
  • onPostExecute(Result result)

最先執行的是方法是onPreExecute()方法,位於主執行緒中,一般用來做一些準備工作。

然後執行doInBackground()方法,位於執行緒池中,用來執行非同步任務,params表示非同步任務的輸入引數。這個方法需要返回結果給onPostExecute()方法。

onProgressUpdate()方法在主執行緒中執行,當後臺任務的執行進度發生變化時這個方法會被呼叫。

onPostExecute()方法在最後非同步任務完成之後會被呼叫,位於主執行緒中,result引數是後臺任務的返回值,即doInBackground()的返回值。

OK,基本原理已經瞭解了,現在我們來手搓一個AsyncTask

  class DownloadTask extends AsyncTask<Void,Integer,Boolean> {      @Override
      protected void onPreExecute() {          //這裡我們使用了一個顯示進度的Dialog,具體實現不表
          progressDialog.show();
      }      @Override
      protected Boolean doInBackground(Void... voids) {          try {              while (true){                  //呼叫我們的doDownload下載方法,具體實現不表
                  int downloadPercent = doDownload();                  //使用publishProgress方法來更新執行的進度
                  publishProgress(downloadPercent);                  if (downloadPercent >= 100)                      break;
              }
          } catch (Exception e) {
              e.printStackTrace();
          }          return true;
      }      @Override
      protected void onProgressUpdate(Integer... values) {          //更新下載進度
          progressDialog.setMessage("Download "+values[0]+"%");
      }      @Override
      protected void onPostExecute(Boolean aBoolean) {          //下載完成
          progressDialog.dismiss();
      }
  }

這裡我們建立了一個Download類繼承自AsyncTask,有三個泛型,void表示不需要給後臺任務傳入引數,Integer表示用整數型別來作為進度顯示的單位,Boolean表示用布林型別來反饋後臺任務的執行結果。

要讓我們的這個AsyncTask跑起來也很簡單,只需要執行:

  new DownloadTask().execute();

2.5 IntentService

IntentService是一種特殊的Service,它繼承了Service並且是一個抽象類,我們可以建立它的子類來使用。IntentService也可以用於執行後臺的耗時任務,並且當任務執行完畢之後它會自動停止。

IntentService因為是服務的原因,所以和單純的執行緒相比它的優先順序要高很多,從而更不容易被系統殺死。

IntentService的內部實現是封裝了HandlerThread和Handler,使用的話要遵循Service的使用方法,這裡先略過後面有機會在Service的專欄裡面再詳細介紹。

2.6 RxJava

有槓精可能會說,你講的這些方法,一個比一個長,一個比一個複雜,就不能整個簡單又粗暴的東西?

這個時候就需要祭出神兵利器RxJava了。

2.6.1 RxJava又是個啥?

其實網路上RxJava的入門文章多如過江之鯽,這裡不打算過多的深入介紹。RxJava是一種響應式程式設計,大家不是很明白的話可以粗暴的理解為更優雅的多執行緒實現即可。

2.6.2 那怎麼操作RxJava?

先手搓一個RxJava的普通實現方式

  private void fun(){
      Observable<Integer> observable = Observable.create(new ObservableOnSubscribe<Integer>() {          @Override
          public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
              emitter.onNext(1);
          }
      });
      observable.subscribeOn(Schedulers.io())     //表示在io執行緒執行訂閱
              .observeOn(AndroidSchedulers.mainThread())  //表示在主執行緒接收訂閱
              .subscribe(new Observer<Integer>() {                  @Override
                  public void onSubscribe(Disposable d) {                      //接收訂閱之前呼叫
                  }                  @Override
                  public void onNext(Integer integer) {                      //接收訂閱成功呼叫
                      doSomething();
                  }                  @Override
                  public void onError(Throwable e) {                      //接收訂閱出錯呼叫
                  }                  @Override
                  public void onComplete() {                      //接收訂閱完成呼叫
                  }
              });
  }

emmmmm看起來好像還是挺複雜的啊,能不能再整簡單點?

OK,鏈式呼叫加lambda安排上

  private void fun() {
      Observable.create(emitter -> emitter.onNext(1))
              .subscribeOn(Schedulers.io())
              .observeOn(AndroidSchedulers.mainThread())
              .subscribe(integer -> {                  //接收訂閱成功
                  doSomething();
              }, throwable -> {});
  }

嗯......有內味了。

這串程式碼我們是傳送了一個Integer型別的資料;

subscribeOn()指定了我們傳送的執行緒是在後臺的io執行緒,就可以理解為一個子執行緒;

observeOn指定了我們接收的執行緒為主執行緒;

subscribe只接收成功的訊息,相當於上面的OnNext()方法,本質上是我們在這裡建立了一個Comsumer物件來接收;

throwable在接收失敗的時候呼叫,相當於上面的onError()方法。

RxJava有多達幾十種的運算子,靈活運用能實現各種不同的非同步任務,這裡就不再花大量的篇幅詳細介紹了,有興趣的朋友可以去檢視ReactiveX中文文件

2.7 RxKotlin

RxKotlin可以理解為RxJava在Kotlin上的一個變種,原理都是一樣的,只是操作語言變成了Kotlin,然後封裝了一下使得可以更優雅的呼叫,這裡給大家一個具體的實現案例,不再過多講解。

  private fun test() {
      Observable.create<Int> { 1 }
              .subscribeOn(Schedulers.io())
              .observeOn(AndroidSchedulers.mainThread())
              .subscribeBy(
                      onNext = {},
                      onError = {}
              )
  }

2.8 Kotlin協程

協程其實和上面所說的執行緒並不是一個概念,協程是什麼?根據官方文件的描述,協程本質上是輕量級的執行緒。既然是輕量,那說明協程的資源消耗和效能等方面和執行緒比起來應該是有優勢的。那這樣看來我們以前使用多執行緒實現的非同步功能,現在基本上都可以用協程來替代了。

協程是一個全新的東西,介於篇幅這裡就不展開講解了,後面會專門寫介紹協程的文章。

三、總結

今天總結了Android平臺上實現多執行緒的幾種方式,希望能給到需要的朋友一些幫助。

附上我的Android核心技術學習大綱,獲取相關內容來我的GitHub一起玩耍:
vx:xx1341452

對於進階這條路而言,學習是會有回報的!

你把你的時間投資在學習上,就意味著你可以收穫技能,更有機會增加收入。

在這裡分享我的Android學習PDF大全來學習,這份Android學習PDF大全真的包含了方方面面了,內含Java基礎知識點、Android基礎、Android進階延伸、演算法合集等等

細說 Android 下的多執行緒,學會了多執行緒,你就學會了壓榨CPU!

我的這份學習合集,可以有效的幫助大家掌握知識點。

總之也是在這裡幫助大家學習提升進階,也節省大家在網上搜尋資料的時間來學習,也可以分享給身邊好友一起學習

獲取方式:關注我看個人介紹,或直接  點選我免費領取


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69952849/viewspace-2681026/,如需轉載,請註明出處,否則將追究法律責任。

相關文章