Android基礎回顧(九)| Android多線程編程

參考書籍:《第一行代碼》 第二版 郭霖
如有錯漏,請批評指出!

Android多線程

在開發過程中,我們經常需要進行網絡請求,而這一過程顯然是一個耗時操作。如果直接在主線程(UI線程)中進行耗時操作,可能會導致主線程被阻塞,造成ANR(Application Not Responding)異常,因此,我們需要在子線程中進行耗時操作。那么,在Android中有哪些開辟子線程的方式呢?

1. 繼承Thread類,重寫run()方法
public class MyThread extends Thread{

    MyThread() {
    }

    @Override
    public void run() {
        //TODO
    }
}

使用方法:

MyThread thread = new MyThread();
thread.start();
2. 實現Runnable接口,并實現其run()方法
public class MyThread implements Runnable{

    MyThread() {
    }

    @Override
    public void run() {
        //TODO
    }
}

使用方法:

MyThread myThread = new MyThread();
new Thread(myThread).start();

上面兩種方式都需要去定義一個類,不過在第二種種方式下,我們可以使用匿名類的方式來實現(這種方式也更加常見):

new Thread(new Runnable() {
    @Override
    public void run() {
        //TODO
    }
}).start();
3. 使用Handler
  • Handler配合sendMessage()使用
public class MainActivity extends AppCompatActivity {

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    //這里可以進行UI操作
                    break;
                default:
                    break;
            }
        }
    };

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

        new Thread(new Runnable() {
            @Override
            public void run() {
                Message msg = new Message();
                msg.what = 1;
                handler.sendMessage(msg);
            }
        }).start();

    }
}
  • Handler配合post()方法使用
public class MainActivity extends AppCompatActivity {

    private Handler handler = new Handler();

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

        handler.post(new Runnable() {
            @Override
            public void run() {
                //這里可進行UI操作
            }
        });
    }
}

這兩種方式都會將消息加入消息隊列,如果是post() / sendMessage(),即將消息加入到消息隊列末尾,當消息隊列沒有消息時會立即執行;如果是postDelayed() / sendMessageDelayed()或postAtTime() / sendMessageAtTime(),則會在延遲時間后執行。這兩種方式中,方式二的優先級高于方式一。

解析異步消息處理機制

Android中的異步消息處理主要由4個部分組成:Message、Handler、MessageQueue、Looper。
1. Message
Message 是在線程之間傳遞的消息,它可以在內部攜帶少量的信息,用于在不同線程之間交換數據。Message中包含四個字段,what、arg1、arg2這三個字段可以攜帶整型數據,obj字段可以攜帶一個Object對象。
2. Handler
Handler主要用于發送和處理消息,發送消息一般是使用Handler的sendMessage()方法,最終消息會傳遞到Handler的handleMessage()方法中。
3. MessageQueen
MessageQueen(即消息隊列),主要用于存放所有通過Handler發送的消息。這部分消息會一直存在于消息隊列中,等待被處理。每個線程中只會有一個MessageQueen對象。
4. Looper
Looper是每個線程中的MessageQueen的管家,調用Looper的loop()方法后,就會進入一個無限循環,每當發現MessageQueen中存在消息,就會依次取出,并傳遞到Handler的handleMessage()方法中。每個線程也只會有一個Looper對象。
以上是基本概念,接下來梳理一下異步消息處理的流程:首先在主線程中創建一個Handler對象,并重寫handleMessage方法。當子線程中需要進行UI操作時,就創建一個Message對象,并通過Handler將這條消息發送出去。之后這條消息會被添加到MessageQueen的隊列中等待被處理,而Looper會一直嘗試從MessageQueen中取出待處理消息,然后分發回Handler的handleMessage()方法中,所以此時handleMessage()方法中的代碼也會在主線程中運行。也就是說我們的UI操作實際上還是在主線程中進行的。
下面是異步消息處理機制流程圖:

AsyncTask 的使用

AsyncTask(異步任務),是Android提供的一個處理異步任務的類,它的實現原理也是基于異步消息處理機制的,只是Android對其進行了很好的封裝。下面來看看關于AsyncTask的基本用法。
AsyncTask是一個抽象類,因此,我們需要創建一個類來繼承它。在繼承時我們可以為AsyncTask類指定3個泛型參數,這三個參數的用途如下:
1. Params:在執行AsyncTask時需要傳入的參數,可用于在后臺任務中使用。
2. Progress:后臺任務執行中,如果需要在界面上顯示當前的進度,則使用這里指定的泛型參數作為進度單位。
3. Result:當任務執行完成后,如果需要對結果進行返回,則使用這里指定的泛型作為返回值類型。
因此,一個簡單的自定義AsyncTask可以寫成下面這樣:

  • public class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
    
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }
    
        @Override
        protected Boolean doInBackground(Void... voids) {
            return null;
        }
    
        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
        }
    
        @Override
        protected void onPostExecute(Boolean result) {
            super.onPostExecute(result);
        }
    }
    

這里我們將AsyncTask 的三個泛型參數依次指定為 Void、Integer、Boolean,因為后面我們會實現一個下載功能,因此我們需要返回實時進度(用整型作為進度顯示單位),最后還要返回下載結果(用布爾型表示)。
為了實現這樣的一個功能,我們還需要重寫上面這四個方法。
1. onPreExecute():這個方法一般用來進行一些初始化操作,比如初始化對象,顯示dialog等。
2. doInBackground(Params...):這個方法會在子線程中執行,因此,我們要在這里執行耗時任務,任務完成后就將執行結果返回,如果AsyncTask的第三個泛型參數指定為Void類型,就不需要返回。由于這個方法是在子線程中執行,因此我們不能在這里進行UI操作(比如更新下載進度),不過我們可以通過下面這個方法來實現。
3. onProgressUpdate(Progress...):在后臺任務中調用 publishProgress(Progress...) 這個方法后,onProgressUpdate(Progress...) 方法就會被調用,這個方法中的參數,就是在 doInBackground(Params...) 方法中調用 publishProgress(Progress...) 方法時傳遞的參數,利用參數中的數值可以進行更新界面中的進度條。
4. onPostExecute(Result):當后臺任務執行完并通過return語句返回數據時,這個方法就會被調用。返回的數據會作為參數傳遞到這個方法中,可以在這里進行一些關于后臺任務執行結果的處理,比如說提示任務執行情況,關閉dialog等。
上面的自定義AsyncTask我們會在后面進行完善。下面先來看一下關于Service的內容。

服務的基本用法

作為Android四大組件之一,Service 是一個可以在后臺執行長時間運行操作而不提供用戶界面的應用組件。服務可由其他應用組件啟動,而且即使用戶切換到其他應用,服務仍將在后臺繼續運行。 此外,組件可以綁定到服務,以與之進行交互,甚至是執行進程間通信 (IPC)。 例如,服務可以處理網絡事務、播放音樂,執行文件 I/O 或與內容提供程序交互,而所有這一切均可在后臺進行。

1.服務的定義

AndroidStudio提供了自動創建Service的方法, 打開項目之后,點擊 File->New->Service->Service,就可以創建自己的Service了。其中有兩個屬性:
Exported 表示是否允許除了當前程序之外的其他程序訪問這個服務;
Enabled 表示是否啟用這個服務。
這兩個屬性是默認勾選的,保持不變就行。
創建完成后,我們會發現,其實就是定義了一個類,繼承了Service類,但是還有一點,那就是AS自動在AndroidManifest文件中對這個Service進行了注冊:

這是Android四大組件的共性,都需要在AndroidManifest文件中進行注冊,也是我們手動創建Service時需要注意的事情。
在我們創建的MyService里面,系統已經自動為我們添加了一個構造方法和一個onBind() 方法,因為onBind()方法是Service類中的一個抽象方法,因此我們必須重寫(這里只是創建了一個空的服務,什么邏輯度沒有寫)。下面先來看看關于Service生命周期的內容,后面再看Service如何使用。

2. Service的生命周期

先來看一張圖:

根據上圖可以看到,服務基本上分兩種方式,并且它們的的生命周期是不同的。

  • 啟動
    當應用組件(如 Activity)通過調用 startService() 啟動服務時,服務即處于“啟動”狀態。一旦啟動,服務即可在后臺無限期運行,即使啟動服務的組件已被銷毀也不受影響。 直到 stopService() 方法(組件調用)或 stopSelf() 方法(Service自身調用)被調用,服務停止并回調onDestroy()方法銷毀,或由于系統資源不足而被回收。需要注意的是,服務只會存在一個實例,因此無論調用多少次 startService() 方法,只需要調用一次 stopService() 方法或 stopSelf() 方法,服務就會停止。
    調用 startService() 方法啟動服務時,生命周期是上圖中的第一種,并且 onCreate() 方法只會回調一次,在服務已經被創建的情況下,再次調用 startService() 方法,會回調 onStartCommand() 方法。下面來打印生命周期看看:
    在前面創建的MyService中重寫所有的生命周期,添加Log打印。

    public class MyService extends Service {
        public MyService() {
        }
    
        @Override
        public IBinder onBind(Intent intent) {
            Log.d("jh", "onBind()");
            throw new UnsupportedOperationException("Not yet implemented");
        }
    
        @Override
        public boolean onUnbind(Intent intent) {
            Log.d("jh", "onUnbind()");
            return super.onUnbind(intent);
        }
    
        @Override
        public void onCreate() {
            super.onCreate();
            Log.d("jh", "onCreate()");
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            Log.d("jh", "onStartCommand()");
            return super.onStartCommand(intent, flags, startId);
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
            Log.d("jh", "onDestroy()");
        }
    }
    

    然后在Activity中添加兩個Button并添加點擊事件,啟動和停止服務:

        case R.id.but_start:
            intent = new Intent(this, MyService.class);
            startService(intent);
            break;
        case R.id.but_stop:
            intent = new Intent(this, MyService.class);
            stopService(intent);
            break;
    

    運行demo,點擊三次 StartService Button,點擊兩次 StopService Button,下面是log:

    由此可以知道,當我們首次創建Service時,會回調onCreate()方法和onStartCommand()方法,在Service運行過程中,再次調用startService() 方法,只會回調onStartCommand()方法。當我們調用 stopService()方法或stopSelf() 方法時,會回調onDestroy()方法銷毀服務。

  • 綁定
    當應用組件通過調用 bindService() 綁定到服務時,服務即處于“綁定”狀態。綁定服務提供了一個客戶端-服務器接口,允許組件與服務進行交互、發送請求、獲取結果,甚至是利用進程間通信 (IPC) 跨進程執行這些操作。 僅當與另一個應用組件綁定時,綁定服務才會運行。 多個組件可以同時綁定到該服務,但全部取消綁定(unBindService()方法)后,該服務即會被銷毀。
    僅僅通過startService() 方法啟動服務后,Activity與服務之間是基本沒有什么聯系的,自然也無法進行通信。如果要實現Activity與Service之間進行通信,我們就需要借助 onBind() 方法將Activity綁定到Service了。下面我們通過前面提到過的下載功能的思路來實現Activity與Service之間的通信。想要實現Activity與Service之間的通信,我們需要借助Binder對象。

    1. 首先,在服務MyService中創建一個DownLoadBinder內部類,并繼承Binder類。
    public class MyService extends Service {
    
        private DownloadBinder mBinder;
    
        public MyService() {
            mBinder = new DownloadBinder();
        }
    
        @Override
        public IBinder onBind(Intent intent) {
            Log.d("jh", "onBind()");
            return mBinder;
        }
    
        ···
    
        public class DownloadBinder extends Binder {
            public void startDownliad() {
                Log.d("jh", "startDownload()");
            }
    
            public int getProgress() {
                Log.d("jh", "getProgress()");
                return 0;
            }
        }
    }
    

    我們可以在Activity中獲取到這個DownloadBinder對象,通過這個 DownloadBinder 對象來對Service中的任務進行控制。下面來看Activity的代碼:

    public class SerMainActivity extends AppCompatActivity {
    
        private MyService.DownloadBinder mDownloadBinder;
    
        private ServiceConnection connection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                Log.d("jh", "onServiceConnected()");
                mDownloadBinder = (MyService.DownloadBinder) service;
                mDownloadBinder.startDownliad();
                mDownloadBinder.getProgress();
            }
    
            @Override
            public void onServiceDisconnected(ComponentName name) {
                Log.d("jh", "onServiceDisconnected()");
            }
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.ser_activity_main);
            ButterKnife.bind(this);
        }
    
        @OnClick({R.id.but_start, R.id.but_stop, R.id.but_bind, R.id.but_unbind})
        public void click(View v) {
            Intent intent;
            switch (v.getId()){
    
                ···
    
                case R.id.but_bind:
                    intent = new Intent(this, MyService.class);
                    bindService(intent, connection, BIND_AUTO_CREATE);
                    break;
                case R.id.but_unbind:
                    unbindService(connection);
                    break;
                default:
                    break;
            }
        }
    }
    

    從上面的代碼可以看到,當我們bind服務的時候,需要傳遞一個ServiceConnection對象。方便起見,我們創建一個ServiceConnection的匿名類,在里面重寫 onServiceConnected() 和 onServiceDisconnected() 方法。onServiceConnection() 方法會在服務成功綁定的時候被調用。這里需要注意:只有當onBind()方法返回IBinder對象時,這個方法才會被調用,如果onBind()方法返回空,這個方法就不會被調用。這個方法有一個IBinder參數,也就是unBind()方法返回的那個IBinder對象,所以在這里我們需要向下轉型,將這個IBinder對象類型強制轉換為我們定義的DownloadBinder對象,然后在這個方法里面對Service中執行的任務進行控制,比如說開始下載任務、獲取下載進度等(當然,這里定義的這兩個方法都是空方法,還沒有實現)。
    bindService() 方法的第三個參數是一個標志位,這里我們傳入 BIND_AUTO_CREATE ,表示在活動和服務進行綁定后自動創建服務,這會使得MyService 中的 onCreate() 方法得到執行,而 onStartCommand() 方法不會執行。
    下面運行看看:
    bindService() -> unBindService()

    從上面的log可以看出,當我們將一個Activity綁定到Service時,如果Service還未創建,會先回調onCreate() 方法創建服務,然后回調onBind()方法,如果這個onBind()方法返回一個IBinder對象,在綁定完成后就會調用onServiceConnected()方法,這個方法是在Activity中實現的,可以通過這個方法來管理Service;然后,當任務執行完后,我們調用unBindService()方法,會依次回調onUnbind()方法解綁,回調onDestroy()方法銷毀服務。

    startService() -> bindService() -> unBindService() -> stopService()

    這里我們創建啟動Service后,又調用bindService()方法將Activity綁定到Service,一直到這里,生命周期并沒有什么特殊,但是當我們unBindService()時,只是回調onUnbind()方法將Service解綁,并沒有銷毀服務,直到我們stopService(),服務才被銷毀,其實就是 onStartCommand()方法被回調的原因,只要回調了onStartCommand()方法,服務就會一直在后臺運行,除非我們調用onStopService()方法或stopSelf()方法將其銷毀或者服務被系統殺死。

    bindService() -> startService() -> stopService() -> unBindService()

    這個執行順序和上面又有一些不同,當我們綁定服務并且回調onStartCommand()方法后,直接調用onStopService()方法,會發現沒有哪個生命周期方法回調,這說明服務還在后臺運行,其實這也很好理解,因為有一個Activity還與Service綁定著,所以服務會等待這個Activity去和它解綁,當我們調用unBindService()方法解綁時,服務和Activity解綁后還會回調onDestroy()方法銷毀服務。

    個人理解:onStartCommand()方法會使服務一直在后臺運行,并且只有onStopService()方法和stopSelf()方法可以讓服務停下來(由于系統資源不足導致Service被kill的情況除外);而僅僅將服務與別的組件綁定時,一旦所有組件都與Service解綁,服務就會被銷毀。

3. 關于前臺服務

我們知道,服務幾乎都是在后臺運行的,但是服務的系統優先級還是比較低的,因此,當系統內存不足時,就有可能會回收掉后臺運行的服務。但是有時候我們需要用服務執行一些很重要的任務,比如說播放音樂,這時就可以使用前臺服務了。
前臺服務其實是通過構建一個通知,顯示在下拉狀態欄,就像我們平時使用的音樂播放器,當我們播放音樂時,就會在狀態欄創建一個通知,用來控制音樂的播放。下面來看看如何創建一個前臺服務:

  • @Override
    public void onCreate() {
        super.onCreate();
        Log.d("jh", "onCreate()");
        Intent intent = new Intent(this, SerMainActivity.class);
        PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
        Notification notification = new NotificationCompat.Builder(this, "default")
                .setLargeIcon(BitmapFactory.decodeResource(getResources(), 
                              R.drawable.ic_huaji))
                .setSmallIcon(R.mipmap.ic_android)
                .setContentTitle("第一行代碼")
                .setContentText("這是一個前臺服務")
                .setContentIntent(pi)
                .build();
        startForeground(1, notification);
    }
    

    只需要重寫onCreate()方法就行了,構建一個Notificaton對象后,沒有通過NotificationManager來將通知顯示出來,而是調用setForeground()方法,這個方法接收兩個參數,第一個參數是通知的id,第二個參數就是Notification對象。這樣,我們的Service就是前臺服務了。看下效果:
4.使用IntentService

我們知道,服務中的代碼都是默認運行在主線程中的,如果我們處理一些耗時的邏輯,就容易出現ANR異常,因此,我們需要在每個具體的方法中開啟一個子線程,用來執行耗時操作,并且需要我們調用stopService()方法或者stopSelf()方法,來停止服務。為了可以簡單的創建一個異步的、會自動停止的服務,Android提供了一個IntentSrvice類,下面我們來創建一個IntentService來看看它和Service到底有什么不同:
File -> New -> Service -> Service(IntentService)
創建一個MyIntentservice

  • public class MyIntentService extends IntentService {
    
        public MyIntentService() {
            super("MyIntentService");
        }
    
        @Override
        protected void onHandleIntent(Intent intent) {
            Log.d("jh", "Thread id:" + Thread.currentThread().getId());
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
            Log.d("jh", "onDestroy()");
        }
    }
    

onHandleIntent()方法是一個抽象方法,我們必須實現,這個方法是運行在子線程中的,因此,我們可以直接在這個方法里處理一些耗時邏輯,我們來打印這個方法所在線程的id,然后重寫onDestroy()方法并打印。
下面我們還需要添加一個Button用來開啟這個IntentService,并給它添加點擊事件:

  • case R.id.but_start_intent_service:
        Log.d("jh", "Thread id:" + Thread.currentThread().getId());
        intent = new Intent(this, MyIntentService.class);
        startService(intent);
        break;
    

    我們在Activity中開啟這個IntentService,并且打印出Activity所在線程的id,用于和IntentService對比,下面來看運行之后的log:

可以看到,打印出來的兩個線程id是不同的,activity運行在id為1的主線程中,也就是說,IntentService中是默認會開啟子線程的,并且當任務執行完成后,onDestroy()方法也自動回調了,這就說明IntentService確實集自動開啟子線程和自動停止于一身,并且用法和Service大同小異,可以說是Service的加強版了。

關于Service的基本內容就到這里,后面會另寫一篇博客,實現前面提到的下載功能,作為服務的實踐。


上一篇:Android基礎回顧(八)| 使用HTTP協議訪問網絡


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379