帶你從理論到代碼搞定Service(上)

Service是什麼

1)簡介

四大組件之一,計算型組件,對於用戶不可見,執行後臺任務。

 

2)xml聲明

代碼示例

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

</service>

 

啓動方式

1)Context.startService(intent)

屬於啓動狀態,用於後臺計算,不和外界交互。用這種方式啓動的Service的生命週期是:

onCreate()--onStartCommand()--onDestroy()

 

2)Context.bindService(intent,serviceConnection,BIND_AUTO_CREATE)

屬於綁定狀態,用於後臺計算,但此時的Service可方便和外界進行交互。

這種啓動方式下的Service的生命週期是:

onCreate()--onBind()--onDestroy()

 

3)Binder

創建一個Service的基本如下所示:

public class MyService extends Service {

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

}

可以看到,onBind()最終返回的是IBinder對象示例,所以這裏要延伸到另一個知識點Binder。而Binder,是實現進程間通信(IPC)的組件。既然如此,將圍繞多進程模式、序列化對象、Binder本身以及各種IPC方式來講解。

 

多進程模式

指定android:process後便開啓了多進程模式,但這也引發了很多問題:

    1)靜態成員/單例模式失效

    2)線程同步機制失效

    3)SharedPreferences可靠性下降

    4)Application會多次創建

所以,這纔有了IPC機制的誕生,爲的就是解決這些問題,而且方便開發者使用。

 

序列化對象

當要使用Intent和Binder傳輸數據時,要對數據進行序列化後才能傳輸,而Android提供了Parcelable接口和Serializable接口來實現序列化。

1)比如現在要傳輸對象Student,那麼用Parcelable方法來實現序列化:

 

public class Student implements Parcelable {

    private String name;
    private int age;

    @Override
    public int describeContents() {
        //一般都爲0,只有當該對象具有文件描述時返回1
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        //寫入屬性
        dest.writeString(name);
        dest.writeInt(age);
    }

    public static final Parcelable.Creator<Student> CREATOR = new Parcelable.Creator<Student>(){

        @Override
        public Student createFromParcel(Parcel source) {
            Student student= new Student();
            //讀取寫入的數據
            student.name = source.readString();
            student.age = source.readInt();
            return student;
        }

        public Student[] newArray(int index) {
            return new Student[index];
        }

    };
}

 

2)同樣Student對象如果要用Serializable方式實現序列化,則如下所示: 

public class Student implements Serializable {
    //根據UID來檢測類的版本,從而能序列化與反序列化
    private static final long serialVersionUID = -3983343865310862317L;

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

很明顯使用Serializable更加簡單。

至此,總結一下,使用Parcelable方式雖然沒有Serializable方式那麼簡單,但Parcelable方式要比Serializable方式的效率高,所以開發者要根據實際情況來選擇。

 

Binder的工作機制

Binder機制很好理解,它的整個流程如下圖所示:

 

客戶端發起遠程請求後線程被掛起直至服務端進程返回數據後才被喚醒,Binder收到請求後就寫入參數到Data,然後Transact給服務端。服務端接收到後通過onTransact方法交給線程池處理,線程池處理完畢後就會reply數據給回Binder,最終Binder返回給客戶端。

 

使用步驟簡單,而且可自動生成,就那三個文件(Student.java、Student.aidl、IStudentManager.aidl),這裏就不細講了。

 

Binder涉及到了跨進程通信(IPC),IPC可能以後再花一篇章節去詳細講解它,而這裏則爲大家總結一下各種IPC的對比以及它們的特點:

Bundle:簡單易用,只傳它支持的數據類型,常用於四大組件間的進程間通信;

文件共享:簡單易用,不適合高併發,且無法做到進程間進程間即時通信,適用於無併發和交換簡單的數據實時性不高的場景;

AIDL:功能強大,支持一對多併發通信,支持實時通信,使用稍複雜,需處理好線程同步,適用於一對多通信且有RPC需求的場景;

Messenger:功能一般,支持一對多串行通信,支持實時通信,不能很好處理高併發情形,不支持RPC,數據通過Message傳遞,故而值傳Bundle支持的數據類型;

ContentProvider:在數據源訪問方面功能強大,支持一對多併發數據共享,可通過Call方法擴展其操作。可以理解爲受約束的AIDL,主要提供數據源的CRUD操作,適用於一對多的進程間的數據共享;

Socket:功能強大, 可通過網絡傳輸字節流,支持一對多併發實時通信,實現細節有點繁瑣,不支持直接的RPC,適用於網絡數據交換場景。

 

線程與線程池

爲什麼要說線程這塊知識點,因爲Service是依賴於創建它的進程。而且在Android裏子線程不能直接操作UI,否則報錯,所以需要異步消息機制來實現子線程裏操作UI,而AsyncTask、HandlerThread和IntentService這些線程工具類則很好地方便開發者直接使用它們來操作UI。

 

AsyncTask

指定三個泛型參數,重寫四個方法:

 

public class MyAsyncTask extends AsyncTask<String, Integer, Boolean> {

    @Override
    protected void onPreExecute() {
        //onPreExecute方法是開始執行後臺任務前調用
        super.onPreExecute();
        //界面的初始化工作
    }

    @Override
    protected Boolean doInBackground(String... strings) {
        //doInBackground在線程池中執行異步任務,處理耗時任務
        //publishProgress()方法是更新任務進度
        publishProgress();
        return null;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        //當publishProgress()方法調用後,onProgressUpdate也就會觸發
        super.onProgressUpdate(values);
        //進行UI操作
    }

    @Override
    protected void onPostExecute(Boolean aBoolean) {
        //後臺任務執行完畢後觸發
        super.onPostExecute(aBoolean);
        //將doInBackground處理完後的數據返回
    }

}

三個泛型參數

第一個參數Params:執行後臺任務時傳入的參數

第二個參數Progress:顯示任務進度的單位

第三個參數Result:執行任務完畢後返回的結果

 

其他四個方法已經在代碼中註釋裏說明了,要注意的是在doInBackground方法中可以使用publishProgress()來更新任務進度,一旦調用了publishProgress()後,onProgressUpdate()方法不久後也會被調用。onPreExecute()、onProgressUpdate()和onPostExecute()都是在主線程中執行,而doInBackground()則是在線程池中執行,要注意這些方法哪些可進行UI操作與哪些可進行耗時操作。

 

HandlerThread

HandlerThread繼承了Thread,而且可以使用Handler,所以可以在run方法裏調用Looper.prepare()來創建消息隊列,然後調用Looper.loop()開啓消息循環。

 

IntentService

優先級比一般線程高,可以執行後臺耗時任務,而且執行完畢後會自動停止。高級版Service。

public class MyIntentService extends IntentService {

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

    @Override
    protected void onHandleIntent(Intent intent) {
        //onHandleIntent在子線程中執行,所以該方法就是處理耗時任務
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

 

線程池

線程池作用

1)重用線程池中的線程可避免多次創建新線程帶來的性能開銷

2)有效控制線程池中的併發數,從而避免線程間因互相搶佔系統資源導致的阻塞

3)方便對線程進行管理(定時執行、執行循環間隔時間等)

 

Android的線程池都是直接或間接通過ThreadPoolExecutor實現的,以下是它的構造方法:

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,
	long keepAliveTime,TimeUnit unit,BolckingQueue<Runnable> workQueue,
	ThreadFsctory threadFavtory){
	

corePoolSize:核心線程數
maximumPoolSize:最大線程數
keepAliveTime:非核心線程閒置時的超時時長
unit:keepAliveTime參數的時間單位
workQueue:任務隊列,存儲execute提交的Runnable對象
threadFavtory:線程工廠,創建新線程


}

各參數的含義都在代碼註釋中解釋了。這裏要注意的是當ThreadPoolExecutor的allowCoreThreadTimeOut屬性設置爲true時,keepAliveTime同樣會作用於核心線程。

 

Android中常見的四類線程池:

1)FixedThreadPool:線程數量固定,線程處於空閒狀態不會被回收,快速響應外界請求。

2)CachedThreadPool:線程數量不限定,只有非核心線程,最大線程數可任意大。適合執行大量的耗時較少的任務。

3)ScheduledThreadPool:核心線程數量固定,非核心線程數量則不限制,非核心線程閒置時會被回收。主要用於執行定時任務和具有固定週期的重複任務。

4)SingleThreadPool:只有一個核心線程,確保所有外界任務在同一個線程中按順序執行,將所有外界任務統一到一個線程中,使得這些任務之間不需要處理線程同步問題。

 

異步消息機制Handler

Android的異步消息機制就是Handler,它的工作流程如圖所示:

 

可以看到,此過程中有四個角色,分別是:

Handler:發送與處理消息(Message);

Message:線程間傳遞的消息,可攜帶字段(用於識別哪個線程);

MessageQueue:消息隊列,存放消息;

Looper:循環消息,有新消息則處理,沒有則一直阻塞

另外,在此過程中,我們的ThreadLocal是線程內部的數據存儲類,在指定線程中存儲數據後,只有指定線程纔可以獲取該數據,所以它可以實現Looper的存取。

注意:使用Handler必須爲線程創建Looper。

 

其他知識點

1. 隱性啓動,Service也可以隱性啓動的,常用於不同應用要啓動Service時的場景。跟Activity的隱性啓動類似,也是設置action:intent.setAction(""),不過要注意的是5.0以上版本後使用會報錯,要解決這個問題就要設置action和PackageName,然後隱式改爲顯示啓動。

 

2. 前臺服務:一直運行,不會因爲內存不足而被系統回收。使用Notification構建,然後調用startForeground()。

 

3. 如何保證服務不被殺死:

    1)因內存資源不足而被殺死的Service:可將onStartCommand()返回 值設爲START_STICKY或START_REDELIVER_INTENT,表示服務在內存緊張時被殺死後,在內存足夠時再恢復;當然另一種方法是將服務改爲前臺服務;

    2)因在手機設置:setting-->Apps-->Running-->Stop過程後而被殺死的Service:因爲次過程中onDestroy()會被調用,所以可在該方法內發送廣播來重新啓動Service,也就是可採取兩個服務互相監聽互相啓動(使用廣播來啓動)。

 

小案例

服務通常就是用來針對後臺下載任務的,所以這次模擬一個後臺下載功能,即使應用退出,也能繼續在後臺中下載,類似如圖這樣的效果:

 

 

而要在後臺中顯示下載進度等情況,而且又是比較耗時,既然如此就直接使用封裝好的AsyncTask,來實現下載功能: 

 

public class MyTaskService extends AsyncTask<String, Integer, Integer> {
    private int lastProgress;
    //記錄上一次下載的進度
    
    @Override
    protected Integer doInBackground(String... strings) {
        InputStream is = null;
        RandomAccessFile raf = null;
        File file = null;
        
        try {
            //已下載的文件長度
            long downloadFileLength = 0;
            //從傳入參數string獲取下載url
            String downloadUrl = strings[0];
            //將文件名解析出來
            String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
            //將文件下載到SD卡下的Download目錄
            String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
            file = new File(directory + fileName);
            
            if (file.exists()) {
                downloadFileLength = file.length();
                
            }
            //獲取下載回來的文件的長度字節
            long fileContentLength = getContentLength(downloadUrl);
            if (fileContentLength == 0) {
                return 0;
                //代表下載失敗
            } else if (fileContentLength == downloadFileLength) {
                //已下載的長度字節和文件總字節一樣,說明已經下載完成
                return 1;
                
            }

            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    //斷點下載,指定從已經下載的字節開始下載
                    .addHeader("RANGE", "bytes=" + downloadFileLength + "-")
                    .url(downloadUrl)
                    .build();
            
            Response response = client.newCall(request).execute();
            if (response != null) {
                is = response.body().byteStream();
                raf = new RandomAccessFile(file, "rw");
                //注意要跳過已經下載的字節
                raf.seek(downloadFileLength);
                //讀取下載回來的文件
                byte[] bytes = new byte[1024];
                int total = 0;
                int len;
                while ((len = is.read(bytes)) != -1) {
                    total += len;
                    //寫入要下載到的目錄裏
                    raf.write(bytes, 0, len);
                    //記錄下載的進度
                    int progress = (int) ((total + downloadFileLength) * 100 / fileContentLength);
                    publishProgress(progress);
                    
                }

                response.body().close();
                
            }
            
        } catch (Exception e) {
            e.printStackTrace();
            
        } finally {
            try {
                if (is != null) {
                    is.close();
                    
                }
                if (raf != null) {
                    raf.close();
                    
                }
                
            } catch (Exception e) {
                e.printStackTrace();
                
            }
            
        }
        return 0;
        
    }
    
    /**
     * 使用Okhttp3定義的一個下載文件方法
     * @param downloadUrl
     * @return 返回的是文件長度字節
     * @throws IOException
     */
    private long getContentLength(String downloadUrl) throws IOException {
        
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(downloadUrl).build();
        Response response = client.newCall(request).execute();
        
        if (response != null && response.isSuccessful()) {
            long fileContentLength = response.body().contentLength();
            response.body().close();
            return fileContentLength;
            
        }
        return 0;
        
    }
    
    @Override
    protected void onProgressUpdate(Integer... values) {
        int progress = values[0];
        if (progress > lastProgress) {
            //如果當前進度大於上一次進度,代表有變化
            // 所以調用接口方法來更新進度
            lastProgress = progress;
            
        }
        
    }

    @Override
    protected void onPostExecute(Integer integer) {
        super.onPostExecute(integer);
        
    }

}

代碼同樣很好理解,構造Notification來開啓前臺服務,而且通過Binder來實現。最後Activity綁定服務然後調用Binder就可以調用下載方法。 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章