1. 程式人生 > >Android學習(十)

Android學習(十)

Service 服務

服務是Android中實現程式後臺執行的解決方案, 他非常適合去執行那些不需要和使用者互動而且還要長期執行的任務, 服務的執行不依賴任何介面, 即使程式被切換到後臺或者使用者打開了另外一個程式,服務仍然能夠保持正常執行,
不過需要注意的是服務並不是一個獨立的程序,而是依賴於建立服務時所在的應用程式程序。

Android的UI和其他的Gui庫一樣,也是執行緒不安全的,如果想要更新程式內的ui,則必須在主執行緒中執行,否則就會出現異常。(
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.


對於這種情況,Android提供了一套非同步訊息處理機制,完美的解決了在子執行緒中進行UI操作的問題,

  1. 建立AndroidThreadDemo專案, 然後修改activity_main中的程式碼如下。

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <Button
            android:id="@+id/btn_change_text"
            android:text="修改內容"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
        <TextView
            android:id="@+id/tv_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!" />
    
    </LinearLayout>
    
  2. 然後修改MainActivity中的程式碼如下:

    public class MainActivity extends AppCompatActivity {
        private Button btnChangeText;
        private TextView tvText;
    
        public static final int UPDATA_TEXT = 1;
    
        private Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                switch(msg.what){
                    case UPDATA_TEXT:
                        //在這裡可以進行Ui操作。
                        tvText.setText("修改好了");
                        break;
                }
            }
        };
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            btnChangeText = (Button) findViewById(R.id.btn_change_text);
            tvText = (TextView)findViewById(R.id.tv_text);
            btnChangeText.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            Message message = new Message();
                            message.what = UPDATA_TEXT;
                            handler.sendMessage(message); // 將Message物件傳送出去
                        }
                    }).start();
                }
            });
        }
    }
    

    這裡我們先是定義了一個整形常量UPDATE_TEXT,用於表示更新TextView這個動作,然後新增了一個Handler物件, 並重寫了父類的handleMessage()方法,在這裡對具體的Message進行處理 如果發現Message的what欄位的值等於UPDATE_TEXT那麼就將TextView中的內容修改。
    在子執行緒中沒有對Ui進行直接操作,而是建立了一個Message物件,並且將他的what欄位的值指定為UPDATA_TEXT然後呼叫Handler的sendMessage()方法,將這條message傳送給handler物件讓他進行處理。

解析一部訊息處理機制。

ANdroid中的非同步訊息處理主要由四個部分組成, Message、 Handler、 MessageQueue、 Looper。

  1. Message
    Message是線上程之間傳遞的訊息, 他可以在內部攜帶少量資訊,用於在不同執行緒之間交換資料,Message中除了what欄位還可以使用arg1和arg2欄位來攜帶一些整形資料,使用obj欄位攜帶一個Object物件。
  2. Handler
    Handler顧名思義也就是處理者的意思,他主要是用於傳送和處理訊息的。傳送訊息一般是使用Handler的sendMessage()方法,而發出的訊息經過一系列的輾轉處理後,最終會傳遞到Handler的handleMessage()方法中。
  3. MessageQueue
    MessageQueue是訊息佇列的意思,它主要用於存放所有通過Handler傳送的訊息,這部分訊息會一致存在於訊息佇列中,等待被處理,每個執行緒中只會有一個MessageQueue物件
  4. Looper
    Looper是每個執行緒中的MessageQueue管家,呼叫Looper的Loop()方法後,就會進入到一個無限迴圈當中,然後每當發現MessageQueue中存在一條訊息,就會將他取出來,並傳遞到Handler的handleMessage()方法中,每個執行緒中也只會有一個Looper物件。

也就是說,首先我們要在主執行緒中建立一個Handlerduix 並重寫他的handleMessage()方法,然後當子執行緒中要進行Ui操作的時候,就建立一個Message物件,並通過Handler將這條訊息傳送出去,之後這條訊息會被新增到MessageQueue的佇列中等待被處理,而Looper則會一致嘗試從MessageQueue中去除待處理訊息,最後分發回Handler的handleMessage()方法中,由於Handler是在主執行緒中建立的,所以此時handleMessage()方法中的程式碼也會在主執行緒中執行, 於是我們就可以安心的進行UI操作了。

使用AsyncTask

不過為了更加方便我們在子執行緒中 對Ui操作, android還提供了另外一些好用的工具 比如說AsyncTask 藉助這個工具即使對一步訊息處理機制不完全瞭解,也可以簡單的從子執行緒切換到主執行緒。

用法:

由於AsyncTask是一個抽象類,所以如果我們想使用它就必須建立一個自雷去繼承他, 在繼承他的時候我們可以為AsyncTask類指定三個泛型引數,這三個引數的用途如下:

Params 。 在執行AsyncTask時需要傳入的引數,可用於在後臺任務中使用。
Progress 。 後臺任務執行時,如果需要在介面上顯示當前的進度, 則使用這裡指定的泛型作為進度單位。,
Result 。 當任務執行完畢後,如果需要對結果進行返回,則使用這裡指定的泛型作為返回值型別。

所以 一個最簡單的AsyncTask就可以寫成如下方式

public class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
    ...
}

這裡我們把AsyncTask的第一個泛型引數指定為Void,表示在執行AsyncTask的時候不需要傳入引數給後臺任務,第二個泛型引數指定為Integer,表示使用整形資料作為進度展示單位,第三個泛型引數指定為Boolean,表示使用布林型資料來反饋執行結果。
我們還需要重寫AsyncTask中的幾個方法才能完成對任務的定製,經常需要重寫的方法由一下4個

  1. onPreExecute() 這個方法會在後臺任務開始執行之前呼叫,對於進行一些介面上的初始化操作,比如顯示一個進度條對話方塊等。
  2. doInBackground(Params...) 這個方法中的所有程式碼都會在子執行緒中執行,我們應該 在這裡去處理所有的耗時任務任務一旦完成就可以通過return語句來將任務的執行結果返回,如果AsyncTask的第三個泛型引數指定的是Void就可以不返回任務執行結果,但是需要注意的是這個方法中是不可以進行Ui操作的,如果需要更新Ui則可以呼叫publishProgress(Progress...)方法來完成
  3. onProgressUpdate(Progress...) 當在後臺任務重呼叫了publishProgress(Progress...)方法後,onProgressUpdate()方法就會很快被呼叫,該方法中攜帶引數就是在後臺任務中傳遞過來的,在這個方法中可以對Ui進行操作,利用引數中的數值就可以對介面元素進行相應的更新,
  4. onPostExecute(Result) 當後臺任務執行完畢通過return語句返回時 這個方法就會很快被呼叫,返回的資料會作為引數傳遞到此方法中,可以利用返回的資料進行一些UI操作。比如提醒任務執行的結果,以及關閉對話方塊等等。

因此 一個比較完整的自定義AsyncTask就可以寫成如下的方式:

public class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
    @Override
    protected void onPreExecute() {
        progressDialog.show(); // 顯示進度條對話方塊
    }

    @Override
    protected void onPostExecute(Boolean aBoolean) {
        progressDialog.dismiss();
        if(aBoolean){
            Toast.makeText(context, "下載成功", Toast.LENGTH_SHORT).show();
        }else{
            Toast.makeText(context, "下載失敗", Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        progressDialog.setMessage("Downloaded "+ values[0] + "%");
    }

    @Override
    protected Boolean doInBackground(Void... voids) {
        while(true){
            try{
                int downloadPercent = doDownload();
                publishProgress(downloadPercent);
                if(downloadPercent >= 100){
                    break;
                }
            }catch (Exception e){
                return false;
            }
        }
        return true;
    }
}

使用AsyncTask的訣竅就是在doInBackground()方法中執行一些耗時的任務,在onProgressUpdate()方法中進行ui操作,在onPostExecute方法中執行一些任務的收尾工作。
如果想要啟動這個任務

new DownloadTask().execute();

Service服務的基本用法。

定義一個服務。在Android中new->Service->Service即可建立一個服務。
也可以手動建立一個類然後去繼承Service最後在AndroidManifest中對這個服務進行註冊。

<service
    android:name=".MyService"
    android:enabled="true"
    android:exported="true"></service>

通常情況下我們會重寫Service類中的3個方法

onCreate() 方法會在服務建立的時候呼叫。
onStartCommand() 方法會在每次啟動服務的時候呼叫
onDestory() 方法會在服務銷燬的時候呼叫。

啟動和停止服務

  1. 首先修改activity_mainz中的佈局程式碼 並新增兩個按鈕分別是啟動服務和停止服務。

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <Button
            android:id="@+id/btn_start_service"
            android:text="啟動服務"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
        <Button
            android:id="@+id/btn_stop_service"
            android:text="停止服務"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    
    </LinearLayout>
    
  2. 然後建立一個Service服務 程式碼如下

    public class MyService extends Service {
        public static final String TAG = "MyService";
        public MyService() {
        }
        @Override
        public IBinder onBind(Intent intent) {
            // TODO: Return the communication channel to the service.
            throw new UnsupportedOperationException("Not yet implemented");
        }
    
        @Override
        public void onCreate() {
            Log.d(TAG, "onCreate: ");
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            Log.d(TAG, "onStartCommand: ");
            return super.onStartCommand(intent, flags, startId);
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
            Log.d(TAG, "onDestroy: ");
        }
    }
    
  3. 修改MainActivity中的程式碼如下:

    public class MainActivity extends AppCompatActivity {
    
        private Button btnStartService;
        private Button btnStopService;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            btnStartService = (Button) findViewById(R.id.btn_start_service);
            btnStopService = (Button) findViewById(R.id.btn_stop_service);
    
            btnStartService.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // 通過intent啟動服務
                    Intent intent = new Intent(MainActivity.this,MyService.class);
                    startService(intent);
                }
            });
    
            btnStopService.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
    
                    Intent intent = new Intent(MainActivity.this,MyService.class);
                    stopService(intent);
                }
            });
    
    
        }
    }
    

    其中onCreate()是第一次啟動服務時候執行的,onStartCommand()方法會在每次服務啟動時都會執行,

活動和服務進行通訊。

比如我們向在MyService中提供一個下載功能,然後在活動中可以決定何時開始下載,以及隨時檢視下載進度,實現這個功能就需要建立一個專門的Binder物件來對下載功能,進行管理,修改MyService中的程式碼如下

public class MyService extends Service {
    public static final String TAG = "MyService";

    private DownloadBinder downloadBinder = new DownloadBinder();

    class DownloadBinder extends Binder {
        public void startDownload(){
            Log.d(TAG, "startDownload: ");
        }
        public int getProgress(){
            Log.d(TAG, "getProgress: ");
            return 0;
        }
    }
    public MyService() {
    }
    @Override
    public IBinder onBind(Intent intent) {
        return downloadBinder;
    }

    @Override
    public void onCreate() {
        Log.d(TAG, "onCreate: ");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand: ");
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy: ");
    }
}

這裡我們建立了一個DownloadBinder類,並繼承自Binder然後在他的內部提供了開始下載和檢視下載進度的方法,
然後在Service中建立了一個DownloadBinder的例項,在onBind()方法中返回了這個例項,
然後我們在activity_main中新增兩個按鈕

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btn_start_service"
        android:text="啟動服務"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <Button
        android:id="@+id/btn_stop_service"
        android:text="停止服務"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/btn_bind_service"
        android:text="繫結服務"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <Button
        android:id="@+id/btn_unbind_service"
        android:text="取消繫結服務"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

最後修改MainActivity中的程式碼如下

public class MainActivity extends AppCompatActivity {

    private Button btnStartService;
    private Button btnStopService;
    private Button btnBindService;
    private Button btnUnbindService;

    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);

        btnStartService = (Button) findViewById(R.id.btn_start_service);
        btnStopService = (Button) findViewById(R.id.btn_stop_service);

        btnStartService.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 通過intent啟動服務
                Intent intent = new Intent(MainActivity.this,MyService.class);
                startService(intent);
            }
        });

        btnStopService.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                Intent intent = new Intent(MainActivity.this,MyService.class);
                stopService(intent);
            }
        });

        btnBindService = (Button)findViewById(R.id.btn_bind_service);
        btnUnbindService = (Button)findViewById(R.id.btn_unbind_service);

        btnBindService.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, MyService.class);
                bindService(intent,connection ,BIND_AUTO_CREATE);//繫結服務
            }
        });
        btnUnbindService.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                unbindService(connection);
            }
        });
    }
}

首先建立了一個ServiceConnection的匿名類,這裡面重寫了onServiceConnected()方法和onServiceDisconnected()方法,這兩個方法分別會在活動與服務成功繫結時候和解除繫結的時候呼叫,在onServiceConnected()方法中通過向下轉型,得到了DownloadBinder的例項,有了這個例項活動和服務之間的關係就變的非常緊密了。我們就可以在活動中根據具體需求呼叫DownloadBinder中的方法了。
在bindService()方法接受三個引數,第一個引數就是構建出來的intent物件,然後第二個引數是ServiceConnection的例項,第三個引數是一個標誌位,這裡傳入的BIND_AUTO_CREATE表示在活動和服務進行繫結後會自動建立服務,這會使myService中的onCreate()方法得到執行,但是onStartCommand()方法不會得到執行,。

服務的宣告週期

  1. 啟動服務

    一旦在專案的任何位置呼叫了Context的startService()方法,相應的服務就會啟動起來,並回調onStartCommand()方法,如果這個服務之前還沒有建立過,onCreate()方法會有限執行,服務啟動了之後會一致保持執行狀態直到stopService()或stopSelf()方法會被呼叫,雖然每呼叫一次startService()方法,onStartCommand()就會執行一次,但實際上每個服務都只會存在一個例項,所以不管呼叫了多少次startService()方法知道用stopService()或者stopSelf()方法服務就會停止下來了。

  2. 繫結服務

    另外,還可以呼叫Context的bindService()來獲取一個服務的持久連線,這時就會回撥服務中的onBind()方法。類似地,如果這個服務之前還沒有建立過,onCreate()方法會先於onBind()方法執行。之後,呼叫方可以獲取到onBind()方法裡返回的IBinder物件的例項,這樣就能自由地和服務進行通訊了。只要呼叫方和服務之間的連線沒有斷開,服務就會一直保持執行狀態。

  3. 停止和解除繫結服務

    當呼叫了startService()方法後,又去呼叫stopService()方法,這時服務中的onDestroy()方法就會執行,表示服務已經銷燬了。類似地,當呼叫了bindService()方法後,又去呼叫unbindService()方法,onDestroy()方法也會執行,這兩種情況都很好理解。但是需要注意,我們是完全有可能對一個服務既呼叫了startService()方法,又呼叫了 bindService()方法的,這種情況下該如何才能讓服務銷燬掉呢?根據Android系統的機制,一個服務只要被啟動或者被綁定了之後,就會一直處於執行狀態,必須要讓以上兩種條件同時不滿足,服務才能被銷燬。所以,這種情況下要同時呼叫stopService()和unbindService()方法,onDestroy()方法才會執行。

使用前臺服務

前臺服務與普通服務最大的區別就在於他會有一個正在執行的圖示在系統的狀態列顯示,下來狀態列後可以看到更加詳細的資訊,非常類似於通知的效果。

修改MyService中的onCreate程式碼如下。

public void onCreate() {
    super.onCreate();
    Log.d(TAG, "onCreate: ");
    Intent intent = new Intent(this,MainActivity.class);
    PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);

    // 適配8.0以上系統
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        NotificationChannel mChannel = null;
        mChannel = new NotificationChannel("builder", "laji", NotificationManager.IMPORTANCE_HIGH);
        manager.createNotificationChannel(mChannel);
        Notification notification = new NotificationCompat.Builder(this,"builder")
                .setContentTitle("這是標題")
                .setContentText("這是內容")
                .setWhen(System.currentTimeMillis())
                .setContentIntent(pi)
                .setSmallIcon(R.mipmap.ic_launcher)
                .build();
        startForeground(1, notification);
    }else{
        Notification notification = new NotificationCompat.Builder(this,"builder")
                .setContentTitle("這是1標題")
                .setContentText("這是1內容")
                .setWhen(System.currentTimeMillis())
                .setContentIntent(pi)
                .setSmallIcon(R.mipmap.ic_launcher)
                .build();
        startForeground(1, notification);
    }

}

使用IntentService

服務中的程式碼都是預設執行在主執行緒中的,如果要執行一些比較費事的操作需要在服務中的具體方法中開啟一個子執行緒,然後去處理一些耗時的邏輯

但是這種服務一旦啟動之後就會一致處於執行狀態,必須呼叫stopService()或者stopSelf()方法才能讓服務停下來,所以要在run()方法的最後一行新增一個stopSelf()方法,

雖然這種方法並不複雜但是總會由一些程式設計師忘記開啟執行緒,或者忘記呼叫stopSelf()為了可以簡單的建立一個非同步的、會自動停止的服務,Android專門提供了一個IntentService類,這個類很好的解決了這些問題,

新建一個MyIntentService類繼承IntentService

public class MyIntentService extends IntentService {
    public static final String TAG = "MyIntentService";
    public MyIntentService(){
        //呼叫父類的有參建構函式
        super("MyIntentService");
    }

    public MyIntentService(String name) {
        super(name);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy: ");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        // 列印當前執行緒的id
        Log.d(TAG, "Thread is : "+ Thread.currentThread().getId());
    }
}

這裡首先要提供一個無參建構函式,並且必須在內部呼叫父類的有參建構函式,然後在子類中實現onHandlerIntent()這個抽象方法,這個方法中可以處理一些具體額邏輯。這個方法已經在子執行緒中運行了,

修改activity_main.xml中的程式碼加入一個用於啟動MyIntentService這個服務的按鈕

然後修改MainActivity中的程式碼

 btnStartIntentService.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.d(TAG, "Thread = "+ Thread.currentThread().getId());
            Intent intent = new Intent(MainActivity.this,MyIntentService.class);
            startService(intent);

        }
    });

然後在AndroidManifest中註冊這個服務

<service android:name=".MyIntentService"/>

可以看到不僅執行緒id不同,而且銷燬方法也得到了執行,

完整版的下載例項。

  1. 首先我們需要新增好依賴 編輯app/build.gradle檔案在dependencies中新增OkHttp

    implementation 'com.squareup.okhttp3:okhttp:3.12.0'
    
  2. 接下來需要定義一個回撥介面,新建一個DownloadListener介面定義如下

    public interface DownloadListener {
        void onProgress(int progress);
        void onSuccess();
        void onFailed();
        void onPaused();
        void onCanceled();
    }
    

    可以看到我們一共定義了5個回撥方法,

    onProgress()方法用於通知當前的下載進度,
    onSucess()方法用於通知下載成功事件,
    onFailed()方法用於通知下載失敗事件,
    onPause()方法用於通知下載暫停事件,
    onCanceled()方法用於通知下載取消事件,
    
  3. 回撥介面定義好後就可以開始編寫下載功能了,這裡使用AsyncTask來進行實現,新建一個DownloadTask繼承AsyncTask 程式碼如下。

    public class DownloadTask extends AsyncTask<String, Integer, Integer> {
        public static final String TAG = "MainActivity";
        public static final int TYPE_SUCCESS = 0;
        public static final int TYPE_FAILED = 1;
        public static final int TYPE_PAUSED = 2;
        public static final int TYPE_CANCELEDE = 3;
    
        private DownloadListener listener;
        private boolean isCanceled = false;
        private boolean isPause = false;
        private int lastProgress;
    
        public DownloadTask(DownloadListener listener){
            this.listener = listener;
        } //初始化回撥方法
    
        @Override
        protected Integer doInBackground(String... strings) { //在後臺執行的方法 會被 execute()方法呼叫後執行。
            InputStream is = null;
            RandomAccessFile savedFile = null; // 隨機讀寫檔案的類。
            File file = null;
    
            try{
                long downloadedLength = 0; // 記錄下載的檔案長度
                String downloadUrl = strings[0];
                String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
                // 所對應的目錄是手機的download資料夾
                String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
                file = new File(directory+fileName);
                if(file.exists()){
                    downloadedLength = file.length();// 取的是檔案已經下載的長度
                }
                long contentLength = getContentLength(downloadUrl); // 獲取需要下載的檔案的長度
    
                if(contentLength == 0){
                    return TYPE_FAILED;
                }else if(contentLength == downloadedLength){
                    return TYPE_SUCCESS; // 如果已下載位元組和檔案總位元組相等說明下載完成了。
                }
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                        // 斷點下載,指定從那個位元組開始下載
                        .addHeader("RANGE","byte="+downloadedLength+"-")
                        .url(downloadUrl)
                        .build();
                Response response = client.newCall(request).execute();
                if(response != null) {
                    is = response.body().byteStream();
                    savedFile = new RandomAccessFile(file, "rw");
                    savedFile.seek(downloadedLength); // 跳過已下載位元組
                    byte[] b = new byte[1024];
                    int total = 0;
                    int len;
                    while ((len = is.read(b)) != -1) {
                        if (isCanceled) {
                            return TYPE_CANCELEDE;
                        } else if (isPause) {
                            return TYPE_PAUSED;
                        } else {
                            total += len;
                            savedFile.write(b, 0, len); // 寫入檔案
                            int progress = (int) ((total + downloadedLength) * 100 / contentLength);
                            publishProgress(progress); // 儘快呼叫onProgressUpdate方法
                        }
                    }
                    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(Exception 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 integer) {// 執行後臺任務完成之後呼叫這個方法
            switch (integer){
                case TYPE_SUCCESS:
                    listener.onSuccess();;
                    break;
                case TYPE_FAILED:
                    listener.onFailed();
                    break;
                case TYPE_PAUSED:
                    listener.onPaused();
                    break;
                case TYPE_CANCELEDE:
                    listener.onCanceled();
                    break;
            }
        }
    
        public void pauseDownload(){
            isPause = true;
        }
    
        public void cancelDownload(){
            isCanceled = true;
        }
    
        private long getContentLength(String downloadUrl){
            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    .url(downloadUrl)
                    .build();
            Response response = null;
            try {
                response = client.newCall(request).execute();
            } catch (IOException e) {
                e.printStackTrace();
            }
            if(response != null && response.isSuccessful()){
                long contentLength = response.body().contentLength(); // 取檔案的相應長度。
                response.body().close();
                return contentLength;
            }
            return 0;
        }
    }
    

    AsyncTask中的三個泛型引數第一個泛型引數指定為String,表示在執行的時候需要傳入一個字串引數給後臺任務。第二個引數指定為Integer表示用整形資料來作為進度顯示單位,第三個引數是Integer表示用整形反饋執行結果。
    在DoInBackground()方法中我們從引數中獲取了下載的URL,並根據地址解析出來檔名,然後指定檔案下載到Download目錄下,然後判斷這個檔案是否已經下載了, 這樣可以在後面使用斷電續傳功能。接下來呼叫了getContentLength()方法來獲取待下載檔案的總程度,如果檔案長度是0說明檔案是有問題的,如果檔案長度等於已經下載的長度說明檔案已經下載完成了。

  4. 然後建立一個service服務 起名DownloadService 並修改其中的程式碼如下。

    public class DownloadService extends Service {
        public static final String TAG = "MainActivity";
    
    
        private DownloadTask downloadTask;
        private String downloadUrl;
    
        private DownloadListener listener = new DownloadListener() {
            @Override
            public void onProgress(int progress) {
                getNotificationManager().notify(1,getNotification("下載中...",progress));
            }
    
            @Override
            public void onSuccess() {
                downloadTask = null;
                // 下載成功時候將前臺服務通知關閉,並建立一個下載成功通知
                stopForeground(true);
                getNotificationManager().notify(1,getNotification("下載成功。",-1));
                Toast.makeText(DownloadService.this, "下載成功", Toast.LENGTH_SHORT).show();
            }
    
            @Override
            public void onFailed() {
                downloadTask = null;
                // 下載失敗時將前臺服務通知關閉, 並建立一個下載失敗的通知
                stopForeground(true);
                getNotificationManager().notify(1,getNotification("下載失敗。",-1));
                Toast.makeText(DownloadService.this, "下載失敗", Toast.LENGTH_SHORT).show();
            }
    
            @Override
            public void onPaused() {
                downloadTask = null;
                Toast.makeText(DownloadService.this, "下載暫停了", Toast.LENGTH_SHORT).show();
            }
    
            @Override
            public void onCanceled() {
                downloadTask = null;
                stopForeground(true);
                Toast.makeText(DownloadService.this, "取消下載", Toast.LENGTH_SHORT).show();
            }
        };
        private DownloadBinder mBinder = new DownloadBinder();
        @Override
        public IBinder onBind(Intent intent) {
            return mBinder;
        }
        class DownloadBinder extends Binder {
            public void startDownload(String url){
                if(downloadTask == null){
                    downloadUrl = url;
                    downloadTask = new DownloadTask(listener);
                    downloadTask.execute(downloadUrl); // 定DownloadTask中的doInBackground方法。
                    startForeground(1,getNotification("下載中...",0));
                    Toast.makeText(DownloadService.this, "下載中", Toast.LENGTH_SHORT).show();
                }
            }
    
            public void pauseDownload(){
                if(downloadTask != null){
                    downloadTask.pauseDownload();
                }
            }
            public void cancelDownload(){
                if(downloadTask != null){
                    downloadTask.cancelDownload();
                }
                if(downloadUrl != null){
                    // 取消下載時候將檔案刪除,並關閉通知
                    String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
                    String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
                    File file = new File(directory+fileName);
                    if(file.exists()){
                        file.delete();
                    }
                    getNotificationManager().cancel(1);
                    stopForeground(true);
                    Toast.makeText(DownloadService.this, "取消", Toast.LENGTH_SHORT).show();
                }
            }
        }
    
        private NotificationManager getNotificationManager(){
            return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        }
        private Notification getNotification(String title , int progress){
    
            Intent intent = new Intent(this, MainActivity.class);
            PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
            NotificationChannel channel = null;
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                channel = new NotificationChannel("前臺下載服務", "隨隨便便", NotificationManager.IMPORTANCE_LOW);
                getNotificationManager().createNotificationChannel(channel);
            }else{
    
            }
            NotificationCompat.Builder builder = new NotificationCompat.Builder(this,"前臺下載服務");
            builder.setSmallIcon(R.mipmap.ic_launcher);
            builder.setContentIntent(pi);
            builder.setContentText(title);
            if(progress>=0){
                builder.setContentText(progress+"%");
                builder.setProgress(100,progress,false);
            }
            return builder.build();
    
        }
    }
    
  5. 修改activity_main中的程式碼如下。

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <Button
            android:id="@+id/btn_start_download"
            android:text="開始下載"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
        <Button
            android:id="@+id/btn_pause_download"
            android:text="暫停下載"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
        <Button
            android:id="@+id/btn_cancel_download"
            android:text="開停止下載"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>
    
  6. 修改MainActivity中的程式碼如下:

    public class MainActivity extends AppCompatActivity {
        public static final String TAG = "MainActivity";
    
        private DownloadService.DownloadBinder downloadBinder;
        private String url = "http://downmini.kugou.com/web/kugou8275.exe";
        private ServiceConnection connection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                downloadBinder = (DownloadService.DownloadBinder)service;
            }
    
            @Override
            public void onServiceDisconnected(ComponentName name) {
            }
        };
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            unbindService(connection);
        }
    
        @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
            switch (requestCode){
                case 1:
                    if(grantResults.length>0 && grantResults[0] != PackageManager.PERMISSION_GRANTED){
                        Toast.makeText(this, "沒有授權", Toast.LENGTH_SHORT).show();
                        finish();
                    }
            }
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            Button btnStartDownload = (Button)findViewById(R.id.btn_start_download);
            Log.d(TAG, "onCreate: ????");
            btnStartDownload.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Log.d(TAG, "onClick: 開始按鈕點選了。");
                    downloadBinder.startDownload(url);
                    Log.d(TAG, "onClick: downloadBinder啟動下載之後了。");
    
    
                }
            });
            Button btnPuaseDownload = (Button)findViewById(R.id.btn_pause_download);
            btnPuaseDownload.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    downloadBinder.pauseDownload();
                }
            });
            Button btnCancelDownload = (Button)findViewById(R.id.btn_cancel_download);
            btnCancelDownload.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    downloadBinder.cancelDownload();
                }
            });
    
    
            if(ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.WRITE_EXTERNAL_STORAGE)
                    !=PackageManager.PERMISSION_GRANTED){
                ActivityCompat.requestPermissions(MainActivity.this,
                        new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
            }
    
            Intent intent = new Intent(this,DownloadService.class);
            startService(intent);
            bindService(intent,connection,BIND_AUTO_CREATE);
        }
    }
    


來自為知筆記(Wiz)