知識點目錄
知識點回顧
10.1 服務是什麼
服務(Service)是Android中實現程序後臺運行的解決方案。它非常適合去執行一些不需要和用戶交互而且還要求長期運行的任務。
-
服務並不是運行在獨立的進程中,而是依賴創建服務時所在的應用程序進程
-
服務並不會自動開啓線程,所有的代碼都是默認運行在主線程中。
正常情況下,我我們都需要手動創建子線程,並在子線程中執行具體的任務,否則就有可能出現主線程被阻塞住的情況。
10.2 Android多線程編程
當我們需要執行一些耗時操作(例如:網絡請求)時,都會將這些操作放在子線程中去運行,否則容易被阻塞住,從而影響用戶對軟件的正常使用。
10.2.1 線程的基本用法
創建線程的方式一般有如下兩種:
1. 繼承Thread
新建一個類繼承Thread,然後重寫父類的run()方法,在run()方法裏面寫耗時邏輯。
public class MyThread extends Thread {
@Override
public void run() {
// 處理具體的邏輯
}
}
然後new出一個MyThread實例,調用它的start()方法,這樣run()方法中的代碼就會在子線程中運行。
new MyThread().start();
2. 實現Runnable接口
繼承的耦合性太高,我們可以通過實現Runnable接口的方式來定義一個線程。
public class MyThread implements Runnable {
@Override
public void run() {
// 處理具體的邏輯
}
}
啓動線程的方式如下:
MyThread myThread = new MyThread();
new Thread(myThread).start();
3. 匿名類
如果不想去專門再定義一個類去實現Runnable接口,那麼也可以使用匿名類的形式,這種方式更加常見。
new Thread(new Runnable() {
@Override
public void run() {
// 處理具體的邏輯
}
}).start();
10.2.2 在子線程中更新UI
下面我們寫一個Demo在子線程中更新UI看看效果。
1.佈局文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/change_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="Change text"/>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textAllCaps="false"
android:text="Hello world"
android:textSize="20sp"/>
</RelativeLayout>
2.在子線程中更新UI
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mText = (TextView) findViewById(R.id.text);
Button changeText = (Button) findViewById(R.id.change_text);
changeText.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.change_text:
//開一個子線程更新UI
new Thread(new Runnable() {
@Override
public void run() {
mText.setText("Nice to meet you");
}
}).start();
break;
}
}
}
運行程序,並點擊"Change text"按鈕。程序會直接崩潰:
觀察logcat中的錯誤日誌如下:
爲了解決這種情況,Android提供了一套異步消息處理機制,很好地解決了子線程中進行UI操作的問題。
異步消息機制的基本用法:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
public static final int UPDATE_TEXT = 1; // 定義個整型常量,用於表示Handler中某個動作
private TextView mText;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case UPDATE_TEXT:
//在這裏進行UI操作
mText.setText("Nice to meet you");
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mText = (TextView) findViewById(R.id.text);
Button changeText = (Button) findViewById(R.id.change_text);
changeText.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.change_text:
new Thread(new Runnable() {
@Override
public void run() {
Message message = new Message();
message.what = UPDATE_TEXT;
mHandler.sendMessage(message); //將Message對象發出去
}
}).start();
break;
default:
break;
}
}
}
10.2.3 解析異步消息處理機制
在瞭解了Android異步消息處理的基本用法後,我們來深入學習下異步消息機制的原理。
Android異步消息處理主要由4個部分組成:Message、Handler、MessageQueue和Looper。
Message
在線程之間傳遞消息,可以在內部攜帶少量的信息,主要用於在不同線程之間交換數據。
Handler
主要用於發送和處理消息。sendMessage()方法用於發送消息,handleMessage()方法用於處理消息。
MessageQueue
MessageQueue是消息隊列,主要用於存放所有通過Handler發送的消息,這部分消息會一直存在於消息隊列中,等待被處理。每一個線程中只會有一個MessageQueue對象。
Looper
Looper是每個線程中的MessageQueue的管家,調用Looper的loop()方法後,就會進入到一個無限循環中,每當發現MessageQueue中存在一條消息,就會將它取出,並傳遞到Handler的handleMessage()方法中。每一個線程中只會有一個Looper對象。
綜上所述:異步消息處理的整個流程如下:
-
主線程中創建一個Handler對象,並重寫handleMessage()方法
-
子線程中需要進行UI操作時,創建一個Message對象,並通過Handler的sendMessage()方法將這條消息發送出去
-
發送出去的消息被添加到MessageQueue中等待被處理
-
Looper會一直嘗試從MessageQueue中取出待處理的消息
-
分發到Handler的handleMessage()方法中進行處理
整個異步消息處理機制的流程示意圖如下所示:
前面使用的runOnUiThread()方法就是一個異步消息處理機制的接口封裝。
10.2.4 使用AsyncTask
AsyncTask是Android幫我們封裝好的對異步消息處理的工具,背後實現的原理也是異步消息處理機制。
AsyncTask是一個抽象類,需要創建一個類去繼承它。在繼承時可以爲AsyncTask類指定3個泛型參數:
-
第一個參數Params。可用於在後臺任務中使用
-
第二個參數Progress。後臺任務執行時,如果需要在界面上顯示當前進度,則使用這裏指定的泛型作爲進度單位
-
第三個參數Result。當任務執行完畢,如果需要對結果進行返回,則使用這裏指定的泛型作爲返回值類型
基本使用方法:
class DownloadTask extends AsyncTask <Void,Integer,Boolean>{
/**
* 在後臺任務開始之前調用
* 用於一些界面的初始化操作,例如顯示一個進度條對話框
* 主線程中運行
*/
@Override
protected void onPreExecute() {
super.onPreExecute();
//顯示進度對話框
progressDialog.show();
}
/**
*在這裏處理所有的耗時操作
* 如果需要更新UI元素,例如反饋當前任務的執行進度,可以調用publishProgress(Progress...)
* 如果任務完成可以通過return語句來將任務執行的結果返回,返回類型是AsyncTask中的第三參數
* 如果AsyncTask的第三個參數是Void,那麼就可以不返回任務執行的結果
* 子線程中運行
* @param voids
* @return
*/
@Override
protected Boolean doInBackground(Void... voids) {
try {
while (true) {
int downloadPercent = doDownload(); //這是一個虛構的方法
publishProgress(downloadPercent);
if (downloadPercent >= 100) {
break;
}
}
} catch (Exception e) {
return false;
}
return true;
}
/**
* 當doInBackground()中調用了publishProgress(Progress...),則此方法會很快被調用
* 該方法中攜帶的參數就是在後臺任務中傳遞多來的
* 可以對UI進行操作,利用參數中的值可以對界面上的元素進行更新
* 主線程中運行
* @param values
*/
@Override
protected void onProgressUpdate(Integer... values) {
//在這裏更新下載進度
progressDialog.setMessage();
}
/**
* 當doInBackground()中調用了return語句時,這個方法會很快被調用
* 返回的數據作爲參數傳遞到此方法中
* 可以利用返回的數據來進行一些UI操作,比如提醒任務執行的結果或關閉掉進度條對話框等
* 主線程中運行
* @param aBoolean
*/
@Override
protected void onPostExecute(Boolean aBoolean) {
//關閉對話框
progressDialog.dismiss();
if (aBoolean) {
Toast.makeText(MainActivity.this, "Download succeeded", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "Download failed", Toast.LENGTH_SHORT).show();
}
}
}
AsyncTask的用法就是:
-
在onPreExecute()中做一些初始化工作
-
在doInBackground()中執行具體的耗時操作
-
在onProgressUpdate()中進行UI操作
-
在onPostExecute()中執行一些任務的收尾工作
10.3 服務的基本用法
10.3.1 定義一個服務
可以通過Android Studio的快捷鍵來創建:
和
通過這樣創建的Service,會自動在AndroidManifest.xml中註冊。
public class MyService extends Service {
public MyService() {
}
/**
* 在服務創建的時候調用
*/
@Override
public void onCreate() {
super.onCreate();
}
/**
* 在每次服務啓動的時候調用
* @param intent
* @param flags
* @param startId
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
/**
* 在服務銷燬的時候調用,一般用於回收那些不再使用的資源
*/
@Override
public void onDestroy() {
super.onDestroy();
}
}
10.3.2 啓動和停止服務
啓動和停止Service的方法都是藉助Intent來實現的。
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.start_service:
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent); // 啓動服務
break;
case R.id.stop_service:
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent); // 停止服務
break;
}
}
服務啓動後,可以在Settings—>Developer options—>Running servcies中找到它,如下圖所示:
onCreate()與onStartCommand()的區別:
-
onCreate()只在服務第一次創建的時候使用
-
onStartCommand()是在服務每次啓動的時候都會調用
10.3.3 活動和服務進行通信
如果在活動中想決定何時開始下載和隨時查看下載進度,我們可以創建一個專門的Binder對象來對下載功能進行管理。同時藉助onBind()方法。
public class MyService extends Service {
private static final String TAG = "MyService";
public MyService() {
}
private DownloadBinder mDownloadBinder = new DownloadBinder();
class DownloadBinder extends Binder {
public void startDownload() {
Log.d(TAG, "startDownload executed");
}
public int getProgress() {
Log.d(TAG, "getProgress executed");
return 0;
}
}
/**
* 在服務創建的時候調用
*/
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate executed ");
}
@Override
public IBinder onBind(Intent intent) {
return mDownloadBinder;
}
/**
* 在服務銷燬的時候調用,一般用於回收那些不再使用的資源
*/
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy executed");
}
}
在活動中綁定和解綁服務:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private MyService.DownloadBinder mDownloadBinder;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mDownloadBinder = (MyService.DownloadBinder) service;
mDownloadBinder.startDownload();
mDownloadBinder.getProgress();
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button bindService = (Button) findViewById(R.id.bind_service);
Button unbindService = (Button) findViewById(R.id.unbind_service);
bindService.setOnClickListener(this);
unbindService.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.bind_service:
Intent bindIntent = new Intent(this, MyService.class);
//BIND_AUTO_CREATE 表示在活動和服務進行綁定後自動創建服務
bindService(bindIntent, mConnection, BIND_AUTO_CREATE); // 綁定服務
break;
case R.id.unbind_service:
Intent unbindIntent = new Intent(this, MyService.class);
unbindService(mConnection); // 解綁服務
break;
default:
break;
}
}
}
備註:服務在整個應用程序範圍內都是通用的,即可以和不同的活動進行綁定,而且在綁定完成後它們都可以獲取到相同的DownloadBinder實例。
10.4 服務的生命週期
服務的生命週期主要有以下兩種路徑:
-
啓動服務
該服務在其他組件調用startService()時創建,並回調onStartCommand()方法。如果這個服務之前沒有創建過,onCreate()方法會先於onStartCommand()方法執行。直到調用stopService()或stopSelf()方法服務纔會停止。注意:雖然每調用一次startService()方法onStartCommand()都會執行一次,但實際上每個服務都只會存在一個實例。
-
綁定服務
該服務在其他組件調用bindService()時創建,並回調服務中的onBind()方法。如果服務之前沒有創建過,onCreate()方法會優於onBind()方法執行。客戶端獲取到onBind()方法裏面返回的IBinder對象的實例,就能與服務端進行通信了。
當我們對服務同時進行了startService()和bindService()時,這時候我們需要同時調用stopService()和unbindService()方法,服務纔會被銷燬,onDestory()方法纔會執行。
Service兩種情況下生命週期圖如下:
10.5 服務的更多技巧
10.5.1 使用前臺服務
服務一直都在後臺運行,優先級比較低,當系統出現內存不足的情況時,有可能被系統回收掉。如果想服務一直保持運行,可以考慮使用前臺服務。
前臺服務與後臺服務的區別:
-
不會被系統回收掉
-
狀態欄會有一個正在運行的圖標,下拉狀態欄後可以看到更加詳細的信息。
讓服務變成前臺服務的方法如下:
public class MyService extends Service {
......
/**
* 在服務創建的時候調用
*/
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate executed ");
Intent intent = new Intent(this, MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("This is content title")
.setContentText("This is content text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentIntent(pi)
.build();
startForeground(1,notification);
}
......
}
具體做法就是創建一個Notification,然後調用startForeground()方法即可。
startForeground()有兩個參數:
參數一:通知的id
參數二:構建的Notification對象。
點擊StartService或BindService按鈕後,MyService就會以前臺服務的模式啓動,並且在系統狀態欄會顯示一個通知圖標,下拉狀態欄後可以看到該通知的詳細內容,如下圖所示:
10.5.2 使用IntentService
IntentService是一種異步的,會自動停止的服務。
基本用法如下:
public class MyIntentService extends IntentService {
private static final String TAG = "MyIntentService";
public MyIntentService() {
super("MyIntentService"); //調用父類的有參構造函數
}
/**
* 處理一些耗時操作
* 運行在子線程中,不會出現ANR問題
* @param intent
*/
@Override
protected void onHandleIntent(Intent intent) {
Log.d(TAG, "Thread id is " + Thread.currentThread().getId());
}
/**
* 服務運行結束後,會自動停止
*/
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy executed");
}
}
點擊Start IntentServcie按鈕後,打印log如下:
可以看出IntentServcie集開啓線程和自動停止於一身。
10.6 服務的最佳實踐——完整版的下載示例
完整版的功能需求:結合AsyncTask和Service實現開始下載、暫停下載和取消下載的功能。
代碼已上傳到我的github上,需要的朋友可以點擊如下鏈接查看:
10.7 小結與點評
本章主要學習了Android多線程編程、服務的基本用法、服務的生命週期、前臺服務和IntentService等。最後通過完整版下載示例對前面學習的知識進行了綜合運用。