第二行程式碼學習筆記——第十章:後臺默默的勞動者——探究服務
本章要點
Android沿用了諾基亞系統的Symbian作業系統的老習慣,從一開始就支援後臺功能,這使得應用程式即使在關閉的情況下仍然可以在後臺繼續執行。後臺功能屬於四大元件之一,重要程度言不可寓。
10.1 服務是什麼
服務(Service)是Android中是實現程式後臺執行的解決方案,它非常適合執行那些不需要與使用者進行互動還需要長期執行的任務。服務的介面不依賴於任何使用者介面,即使程式被切換到後臺,或者使用者打開了一個應用程式,服務仍然能保持正常執行。
服務並不是執行在一個獨立的程序中的,而是依賴於建立服務時所在的應用程式程序。當某個應用程式程序被殺掉時,所以依賴於該程序的服務也會停止執行。
預設在主執行緒,可能會造成主執行緒阻塞的問題。
10.2 Android多執行緒程式設計
當我們執行耗時操作,就必須放在子執行緒中執行,避免執行緒阻塞的問題,增強使用者的體驗度。
10.2.1 執行緒的基本用法
Android多執行緒與Java多執行緒使用語法相同。比如說,定義一個執行緒只需要定義一個類繼承自Thread,然後重寫父類的run()方法,並在裡面編寫耗時邏輯即可,如下:
class MyThread extends Thread {
@Override
public void run(){
//處理具體的邏輯
}
}
啟動這個執行緒,new出MyThread的例項,然後呼叫start()方法,這樣run()方法就執行在子執行緒中了,如下:
new MyThread().start();
當然,使用繼承的方式高耦合,更多的時候我們選擇實現Runable介面的方式來定義一個執行緒,如下:
class MyThread implements Runable {
@Override
public void run() {
//處理具體的邏輯
}
}
那麼啟動方式如下:
MyThread myThread = new MyThread();
new Thread(myThread).start();
常見的實現方式如下:
new Thread(new Runable() {
@Override
public void run() {
//處理具體的邏輯
}
}).start();
以上就是執行緒的基本用法。在Java中建立和啟動執行緒的方式是一樣的。
10.2.2 在子執行緒中更新UI
更新應用程式中的UI元素,必須放在主執行緒。否則就會出現異常。
那我們就來驗證下吧。新建AndroidThreadTest專案,修改activity_main.xml中的程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btn_change_tv"
android:text="Change Text"
android:textAllCaps="false"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv"
android:layout_centerInParent="true"
android:text="Hello World"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
接下來我們對Hello World 進行修改,修改MainActivity中的程式碼如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_change_tv;
private TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_change_tv= (Button) findViewById(R.id.btn_change_tv);
tv= (TextView) findViewById(R.id.tv);
btn_change_tv.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_change_tv:
new Thread(new Runnable() {
@Override
public void run() {
tv.setText("Nice to meet you");
}
}).start();
break;
default:
break;
}
}
}
我們在子執行緒中更新UI,執行程式,點選Change Text按鈕,我們會發現程式崩潰。觀察logcat日誌,可以看到原因是由於在子執行緒中更新UI操作,如下:
由此我們證明了Android不能在子執行緒中更新UI操作。有時候我們會在耗時操作返回的結果需要進行相應的UI更新。那麼我們通過Android提供的非同步訊息處理機制,完美解決在子執行緒中更新UI操作。
使用非同步處理訊息的方法。
修改MainActivity中的程式碼如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private static final int UPDATE_TEXT=1; //表示更新TextView的動作
private Button btn_change_tv;
private TextView tv;
private Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) { //4.主執行緒
switch (msg.what){
case UPDATE_TEXT: //5.判斷是否相等,進行操作
//在這裡進行UI操作
tv.setText("Nice to meet you");
break;
default:
break;
}
}
};
...
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_change_tv:
new Thread(new Runnable() {
@Override
public void run() {
Message message=new Message(); //1.建立Message物件
message.what=UPDATE_TEXT; //2.指定what值
handler.sendMessage(message); //3.將Message物件傳送過去
}
}).start();
break;
default:
break;
}
}
}
重新執行程式,點選Change Text按鈕,Hello World就會變成Nice to meet you。如圖:
使用Handler機制順利的解決了在子執行緒中更新UI的問題。
10.2.3 解析非同步訊息處理機制
Android中的非同步訊息處理主要有4部分組成:Message,Handler,MessageQueue,Looper。
- Message
Message是線上程之間傳遞的訊息,它可以攜帶少量的資訊,用於在不同執行緒之間交換資料。Message的what欄位,arg1和arg2欄位攜帶整型資料,obj欄位攜帶一個Object物件。 - Handler
Handler處理者,主要用於傳送和處理訊息。傳送訊息Handler的sendMessage()方法,傳送的訊息最終會傳遞到Handler的handleMessage()方法中。 - MessageQueue
MessageQueue訊息列隊,主要用於存放所有通過Handler傳送的訊息。這部分訊息一直會存在於訊息列隊中,等待被處理。每個執行緒中只會有一個MessageQueue物件。 - Looper
Looper是每個執行緒中的MessageQueue的管家,呼叫Looper的lop方法後,就會進入到無線迴圈中,然後每當傳送MessageQueue中存在的一條訊息,就會將它取出,並傳遞到Handler中的handleMessage()方法中。每個執行緒中也只會有一個Looper物件。
非同步訊息處理的整個流程:首先需要在主執行緒中建立一個Handler物件,並重寫handleMessage()方法。然後在子執行緒中進行需要UI操作時,就建立一個Message物件,並通過Handler將這條訊息傳送出去。之後這條訊息被新增在MessageQueue訊息列隊中等待被處理,而Looper會一直嘗試者從MessageQueue中取出待處理的訊息,最後傳送給Handel的handleMessage方法。由於Handler是在主執行緒中建立的,所以此時handleMessage()方法也會在主執行緒中執行,就可以進行UI操作了。整個非同步訊息處理機制流程圖:
整個非同步訊息處理的核心思想:一條Message經過一個流程的輾轉呼叫後,也就從子執行緒進入到了主執行緒,從不能更新UI操作,變成了可以更新UI操作。
之前我們使用的runOnUiThread()方法就是一個非同步訊息處理機制的介面封裝。原理跟上圖描述的一樣。
10.2.4 使用AsyncTask
Android為了更加方便在子執行緒中更新UI操作,使用AsyncTask。它的實現原理也是基於非同步訊息處理機制的封裝。
AsyncTask的基本用法,AsyncTask是一個抽象類,我們必須建立一個類繼承它。繼承時可以為AsyncTask指定3個泛型引數。
- Params。在執行AsyncTask時傳入的引數,可用於在後臺的使用。
- Progress。 後臺執行任務時,如果需要在介面顯示當前的進度條,則使用這裡指定的泛型作為進度單位。
- Result。 任務執行完畢,如果需要對結果進行返回,則使用這裡指定的泛型作為返回值型別。
一個完整的自定義AsyncTask如下:
class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
/**
* 開始執行任務之前,用於初始化見介面。例如:顯示一個進度條
*/
@Override
protected void onPreExecute() {
progressDialog.show();//顯示進度對話方塊
}
/**
* 在子執行緒中執行,處理耗時操作。
* 如果需要更新UI元素,則呼叫publishProgress()
*
* @param params
* @return
*/
@Override
protected Boolean doInBackground(Void... params) {
try {
while (true) {
int downloadPercent = doDownlod(); //這是一個虛方法
publishProgress(downloadPercent);
if (downloadPercent >= 100) {
break;
}
}
} catch (Exception e) {
return false;
}
return true;
}
/**
* 當在後臺中呼叫了publishProgress()方法後,onProgressUpdate()很快就會呼叫,
* 該方法攜帶的引數就是從後臺傳過來的。在這裡執行對UI操作。
*
* @param values
*/
@Override
protected void onProgressUpdate(Integer... values) {
//在這裡執行下載進度
progressDialog.setMessage("Download" + values[0] + "%");
}
/**
* 當後臺執行完畢返回進行返回時。利用返回資料更新UI操作。
* 比如:提醒任務執行的結果,以及關閉對話方塊等。
*
* @param result
*/
@Override
protected void onPostExecute(Boolean result) {
progressDialog.dismiss(); //關閉進度對話方塊
//在這裡提醒下載結果
if (result) {
Toast.makeText(content, "Download succeeded", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(content, "Download failed", Toast.LENGTH_SHORT).show();
}
}
}
第一個泛型引數Void,表示不需要傳入引數給後臺任務。
第二個泛型引數指定為Integer,表示使用整型資料型別作為進度顯示單位。
第三個泛型引數指定為Boolean,表示返回回饋的結果。
在這個DownloadTask中,在doInBackground()方法執行具體的下載任務(子執行緒)。doDownload()方法是一個虛方法,計算當前的下載進度並返回,並讓它顯示到介面中。我們通過呼叫 publishProgress()方法並將下載進度傳進來。這樣onProgressUpdate()就會呼叫,進行UI操作。下載完成後,doInBackground()返回的是布林值,然後通過onPostExecute()彈出相應的Toast提示。完成非同步載入任務。
簡單的來說,在AsyncTask中,doInBackground()進行耗時操作;onProgressUpdate()更新UI操作,onPostExecute()執行收尾工作。
啟動這個任務,程式碼如下:
new DownloadTask().execute();
本章最佳實踐,完善下載這個功能。
10.3 服務的基本用法
Android四大元件之一 —— 服務。
10.3.1 定義一個服務
新建ServiceTest專案,點選包—>New—>Service—>Service。彈出如下視窗:
Enabled:表示是否啟動這個服務。 Exported:是否允許除了當前程式之外的程式進行訪問這個服務。全部勾選,點選Finish完成建立。
MyService的程式碼如下:
public class MyService extends Service {
public MyService() {
}
@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}
}
MyService類繼承自Service。onBind()方法是Service中唯一的一個抽象方法。
處理事情,定義Service其他方法,程式碼如下:
public class MyService extends Service {
...
/**
* 建立服務的時候呼叫
*/
@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 void onDestroy() {
super.onDestroy();
}
}
每個服務都需要在AndroidManifest.xml註冊是才能夠使用,這是Android四大元件的共同點。建立服務的時候Android Studio已經幫我們智慧的建立完成了。開啟AndroidManifest.xml,程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.hjw.servicetest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"></service>
</application>
</manifest>
這樣,我們就定義好了一個服務。
10.3.2 啟動和停止服務
藉助Intent來啟動和停止這個服務,在ServiceTest專案中來實現吧。
修改activity_main.xml中的程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn_start_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Service"
android:textAllCaps="false" />
<Button
android:id="@+id/btn_stop_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Stop Service"
android:textAllCaps="false" />
</LinearLayout>
修改MainActivity中的程式碼如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_start_service, btn_stop_service;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_start_service = (Button) findViewById(R.id.btn_start_service);
btn_stop_service = (Button) findViewById(R.id.btn_stop_service);
btn_start_service.setOnClickListener(this);
btn_stop_service.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_start_service:
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent); //啟動服務
break;
case R.id.btn_stop_service:
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent); //停止服務
break;
default:
break;
}
}
}
測試服務啟動或停止,我們在MyService中的方法加入日誌,如下所示:
public class MyService extends Service {
private static final String TAG = "MyService";
...
/**
* 建立服務的時候呼叫
*/
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate: executed");
}
/**
* 每次啟動的服務的時候呼叫
*
* @param intent
* @param flags
* @param startId
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand: executed");
return super.onStartCommand(intent, flags, startId);
}
/**
* 銷燬服務的時候呼叫
*/
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy: executed");
}
}
接下來我們執行程式,如下:
點選Start Service按鈕,觀察logcat日誌如下:
這時這個服務已經啟動了,我們開啟設定—>開發者選項—>正在執行的服務,如圖:
然後我們點選Stop Service按鈕,觀察日誌如下:
可以看出MyService的確停止服務了。
onCreate()和onStartCommand()區別,onCreate()會在第一次建立的時候呼叫,而onStartCommand()則在每次建立服務的時候呼叫。當我們點選多次Start Service按鈕,第一次會執行兩個方法,而以後只會執行onStartCommand()方法。
10.3.3 活動和服務進行通訊
藉助onBind()方法,使我們的服務與活動關聯起來。
我們希望實現MyService實現一個下載功能,在活動中決定合適開始下載,以及檢視進度。那我們建立一個Binder物件對下載功能進行管理。修改MyService中的程式碼如下:
public class MyService extends Service {
private static final String TAG = "MyService";
public MyService() {
}
private DownloadBinder mBinder=new DownloadBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
class DownloadBinder extends Binder{
/**
* 模擬方法
*/
public void startDownload(){
Log.d(TAG, "startDownload: executed");
}
public int getProgress(){
Log.d(TAG, "getProgress: executed");
return 0;
}
}
...
}
新建DownloadBinder類,並繼承Binder,再裡面實現下載和檢視進度的模擬方法(這裡我們分別列印一行日誌)。
在MyService中建立DownloadBinder的例項,然後在onBind()返回例項。做完MyService工作了。
在活動中呼叫服務裡的方法。新增兩個按鈕(繫結服務,解綁服務),修改activity_main中的檔案:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
...
<Button
android:id="@+id/btn_bind_service"
android:textAllCaps="false"
android:text="Bind Service"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_unbind_service"
android:textAllCaps="false"
android:text="unBind Service"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
當一個活動和服務繫結之後,就可以呼叫服務裡的Binder提供的方法,修改MainActivity中的程式碼如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_start_service, btn_stop_service,btn_bind_service,btn_unbind_service;
private MyService.DownloadBinder downloadBinder;
private ServiceConnection connection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//指定服務去幹什麼
downloadBinder= (MyService.DownloadBinder) service;
downloadBinder.startDownload();
downloadBinder.getProgress();
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
btn_bind_service= (Button) findViewById(R.id.btn_bind_service);
btn_unbind_service= (Button) findViewById(R.id.btn_unbind_service);
...
btn_bind_service.setOnClickListener(this);
btn_unbind_service.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
...
case R.id.btn_bind_service:
Intent bindIntent = new Intent(this, MyService.class);
bindService(bindIntent,connection,BIND_AUTO_CREATE); //繫結服務
break;
case R.id.btn_unbind_service:
unbindService(connection); //解綁服務
break;
default:
break;
}
}
}
首先建立一個ServiceConnection匿名類,重寫了onServiceConnected()和onServiceDisconnected()方法,與服務成功繫結和解綁的時候呼叫。通過向下轉型得到了DownloadBinder的例項,接下來在onServiceConnected()中呼叫DownloadBinder的startDownload()和getProgress()方法。
繫結服務:binderService()接受三個引數,第一個引數Intent,第二個引數ServiceConnection,第三個引數BIND_AUTO_CREATE(表示在活動和服務進行繫結後自動建立服務)。這樣MyService中的onCreate()會得到執行,但onStartCommand()方法不會執行。
解除繫結:unBinderService()方法。
執行程式,點選Bind Service中的按鈕,觀察logcat日誌如下:
這樣就完成了我們的繫結服務,任何一個服務在應用範圍內都是通用的(一個服務可以和任意一個活動繫結)。
10.4 服務的生命週期
服務也有自己的生命週期,官方給出兩種服務的生命週期,一目瞭然,如圖:
10.5 服務的更多技巧
接下來我們學習服務高階使用技巧。
10.5.1 使用前臺服務
希望服務一直保持執行,而不希望系統記憶體不足而被回收,我們可以使用前臺服務。前臺服務和普通服務的最大的區別就在與,它會一直有一個正在執行的圖示在系統的狀態列顯示,下拉選單顯示詳情內容,類似於通知。
建立前臺服務,修改MyService中的程式碼如下:
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);
}
...
}
呼叫startForeground()方法來顯示通知,就會讓MyService變成一個前臺服務,顯示在系統的狀態列。
執行程式,並點選Start Service或Bind Service按鈕,就會啟動前臺服務。如圖:
10.5.2 使用IntentService
服務的程式碼預設在主執行緒,如果在服務裡進行一些耗時操作,很容易出現ANR(Application Not Responding)的情況。
為了解決ANR,我們必須在每個服務的每個具體方法裡開啟一個子執行緒,去處理一些耗時的操作。一個標準的服務的程式碼如下:
public class MyService extends Service {
...
/**
* 每次啟動的服務的時候呼叫
*
* @param intent
* @param flags
* @param startId
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
//處理具體的邏輯
stopSelf();
}
}).start();
return super.onStartCommand(intent, flags, startId);
}
...
}
為了解決我們忘記開啟子執行緒,或忘記呼叫stopSelf()方法。Android提供了一個非同步的,會自動停止服務的IntentService類。
新建MyIntentService類繼承自IntentService,程式碼如下:
public class MyIntentService extends IntentService {
public MyIntentService() {
super("MyIntentService"); //呼叫父類的有參建構函式
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
//列印當前執行緒的id
Log.d("MyIntentService", "Thread id is "+ Thread.currentThread().getId());
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("MyIntentService", "onDestroy: executed ");
}
}
首先我們提供一個無參的建構函式,並且必須呼叫內部父類的有參建構函式。然後在子類中去實現onHandleIntent()抽象方法,在這個方法中處理具體的邏輯,這個方法在子執行緒中執行(解決了ARN問題)。
接下來修改activity_main.xml中的程式碼,加入一個啟動MyiIntentStart按鈕,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
...
<Button
android:id="@+id/btn_start_intent_service"
android:textAllCaps="false"
android:text="Start IntentService"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
修改MainActivity中的程式碼如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_start_service, btn_stop_service,btn_bind_service,btn_unbind_service,btn_start_intent_service;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
btn_start_intent_service= (Button) findViewById(R.id.btn_start_intent_service);
...
btn_start_intent_service.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
...
case R.id.btn_start_intent_service:
Log.d("MainActivity", "Thread id is " + Thread.currentThread().getId());//列印主執行緒的id
Intent intentService=new Intent(this,MyIntentService.class);
startService(intentService);
break;
default:
break;
}
}
}
在AndroidManifest.xml中註冊服務,程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.hjw.servicetest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<service android:name=".MyIntentService"/>
</application>
</manifest>
執行程式,點選Start IntentService按鈕,觀察logcat日誌,如下:
我們可以看到MyIntentService和MainActivity中的所線上程的id不一樣,而且onDestory()也得到了執行。集開啟執行緒和自動停止與一身,IntentService是我們的最愛。
10.6 服務的最佳實踐 — 完整版的下載
實現服務中經常使用到的功能個——下載功能。
建立一個ServiceBestPractice專案。
新增OkHttp依賴庫,如下:
compile 'com.squareup.okhttp3:okhttp:3.8.0'
接下來定義一個DownloadListener介面回撥,用於對下載過程中的各種狀態進行監聽。如下:
public interface DownloadListener {
void onProgress(int progress);
void onSuccess();
void onFailed();
void onPaused();
void onCanceled();
}
編寫下載功能,新建DownloadTask繼承自AysncTask,程式碼如下:
public class DownloadTask extends AsyncTask<String, Integer, Integer> {
private static final int TYPE_SUCCESS = 0;
private static final int TYPE_FAILED = 1;
private static final int TYPE_PAUSED = 2;
private static final int TYPE_CANCELED = 3;
private DownloadListener listener;
private boolean isCanceled = false;
private boolean isPaused = false;
private int lastProgress;
public DownloadTask(DownloadListener listener) {
this.listener = listener;
}
@Override
protected Integer doInBackground(String... params) {
InputStream is = null;
RandomAccessFile savedFile = null;
File file = null;
try {
long downloadLength = 0; //記錄已下載的檔案長度
String downloadUrl = params[0];
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
.getPath();
file = new File(directory + fileName);
if (file.exists()) {
downloadLength = file.length();
}
long contentLength = getContentLength(downloadUrl);
if (contentLength == 0) {
return TYPE_FAILED;
} else if (contentLength == downloadLength) {
//已下載位元組和檔案總位元組相等,就等於已經下載完了
return TYPE_SUCCESS;
}
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
//斷點下載,指定從哪個子節點開始下載
.addHeader("RANGE", "bytes=" + downloadLength + "-")
.url(downloadUrl).build();
Response response = client.newCall(request).execute();
if (request != null) {
is = response.body().byteStream();
savedFile = new RandomAccessFile(file, "rw");
savedFile.seek(downloadLength); //跳過已下載的位元組
byte[] bytes = new byte[1024];
int total = 0;
int len;
while ((len = is.read(bytes)) != -1) {
if (isCanceled) {
return TYPE_CANCELED;
} else if (isPaused) {
return TYPE_PAUSED;
} else {
total += len;
savedFile.write(bytes, 0, len);
//計算已下載的百分比
int progress = (int) ((total + downloadLength) * 100 / contentLength);
publishProgress(progress);
}
}
response.body().close();
return TYPE_SUCCESS;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (savedFile != null) {
savedFile.close();
}
if (isCanceled && file != null) {
file.delete();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return TYPE_FAILED;
}
@Override
protected void onProgressUpdate(Integer... values) {
int progress = values[0];
if (progress > lastProgress) {
listener.onProgress(progress);
lastProgress = progress;
}
}
@Override
protected void onPostExecute(Integer status) {
switch (status) {
case TYPE_SUCCESS:
listener.onSuccess();
break;
case TYPE_FAILED:
listener.onFailed();
break;