Android 後臺Service下載 (一)

一、前言

 

        原理其實大傢都懂,隻不過沒動手實際好好的寫過,項目中也沒有涉及到用這塊內容,所以….所以被人問及細節時,就說不清個123瞭,為瞭一改我的慵懶,因此,我寫這篇文章,至少下次再被問起時,不會尷尬。

 

        本篇文章會涉及到以下知識點:

 

        1. Service (兩種啟動方法,對應的不同生命周期不同);

 

        2. Binder;

 

        3. Activity如何與Service交互;

 

        4. Service如何更新帶進度條的狀態欄;

 

二、Service & Binder

 

        2.1 Service

 

        Service有兩個方法來啟動:startService 和 bindService,采用不同的方法,service的生命周期也不同(本篇隻講同進程,不講跨進程):

 

        1. startService啟動,其生命周期不會因啟動它的組件Destroy而消亡,而是依賴於mainThread(即應用主線程),一但主線程退出,即代表整個應用退出,因為Service就會Destroy。

 

        2. bindService啟動,其生命周期依賴啟動它的組件,組件Destroy時,Service也隨之一起Destroy。

 

        2.2 Binder

 

        Binder是Android系統中一個重要的“設備”,之所以加引號,實際上它是虛擬出來的,類似於Linux中的塊設備,因此,它也是基於IO的。

 

        Binder在Android中,是被用做進程間通信使用的,而且,Binder是Parcelable的,通過Transaction,與它的代理端,即Binder Server端交互,本章隻是簡單的使用Binder來做同一進程中的線程間通信。

 

三、Activity與Service交互

 

        Question:如何將Service用做後臺下載,其生命周期不依賴啟動它的組件,且能夠與它的組件相互通信?

 

        分析問題:

 

        該問題,表述瞭三點信息:

 

        1. 後臺下載;

 

        2. 生命周期不依賴其它組件;

 

        3. 數據交互;

 

        3.1 後臺下載

 

        通常,我們使用Service,會有這麼幾點需求:

 

        1. 若是前臺Service,一般是用來做類似於音樂播放器的;

 

        2. 若是後臺Service,則通常是用來和服務器進行交互(數據下載),或是其它不需要用戶參與的操作;

 

        同一進程中,啟動Service,若直接與服務器交互,則很容易引起ANR,因為,Service是由mainThread創建出來,因此,此時Service是運行在UI主線程的,如果需要聯網下載,則需要開啟一個Thread,然後在子線程中來運行。在Service中創建/使用線程,與在Activity中一樣,無區別。

 

        3.2 生命周期不依賴其它組件

 

        這點,我前面說過瞭,使用startService來啟動該service就行;

 

        3.3 數據交互

 

        組件通常是Activity,可以通過bindService,當成功綁定時,可以獲取Service中定義後的一個IBinder接口,我們可以通過這個接口,返回該Service對象,從而,可以直接訪問該Service中的公有方法;

 

        當Service想要把數據傳遞給某個組件時,最簡單最好的辦法就是通過Broadcast,在Intent中帶上數據,廣播給組件即可(記住,BroadcastReceiver中,onReceive也不能運行太久,否則也會ANR,隻有10秒哦)。

 

四、Service刷新帶有進度條的狀態欄

 

        通常,我們會發一些Notification到系統狀態欄上,以提醒用戶做一些事情,但是,如果大傢仔細看瞭Notification的參數,就會發現裡面有一個RemoteViews類型的成員,是不是有點像在哪見過?對的,如果你做個Widget應用,那麼RemoteViews你應該很熟悉:

 

        RemoteViews可以讓我們自定義一個View,裡面放一些小的控件,系統有定義的,不是所有的控件都能放!那麼,我們就可能自定義一個帶有ProgressBar的layout,然後綁定到Notification對象上,並通過NotificationManager來通知更新即可。

 

        註:網上有提醒說,建議不要更新太頻繁,否則會使系統很卡!

 

五、用例子說話

 

        本節,就將寫一個Demo,帶大傢一起瞭解如何活用以上這些概念,能夠讓大傢應用到將來自己的項目中。文件不多,三個類,一個Service,一個Activity,和一個任務類(因為我在Service中,創建瞭一個線程隊列,使用單線程來模擬)。

 

        5.1 DownloadManagerActivity

 

        對應的layout:

 

[html]  

<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"  

    xmlns:tools="https://schemas.android.com/tools"  

    android:layout_width="match_parent"  

    android:layout_height="match_parent"  

    tools:context=".DownloadManagerActivity" >  

  

    <TextView  

        android:layout_width="wrap_content"  

        android:layout_height="wrap_content"  

        android:layout_centerHorizontal="true"  

        android:layout_centerVertical="true"  

        android:text="@string/hello_world" />  

      

    <Button   

        android:id="@+id/add_task"  

        android:layout_width="wrap_content"  

        android:layout_height="wrap_content"  

        android:text="@string/addTask"/>  

      

    <Button   

        android:id="@+id/cancel_task"  

        android:layout_toRightOf="@id/add_task"  

        android:layout_width="wrap_content"  

        android:layout_height="wrap_content"  

        android:text="@string/cancelTask"/>  

  

</RelativeLayout>  

         裡面主要有兩個Button,一個告訴Service添加任務,一個告訴Service取消指定的任務。

 

[java]  

public final static String TAG = "DownloadService";  

private DownloadService mService = null;  

private static int task_count = 0;  

private final static String ACTION_UPDATE = "com.chris.download.service.UPDATE";  

private final static String ACTION_FINISHED = "com.chris.download.service.FINISHED";  

         幾個對象,mService就是當bindService成功時,通過IBinder返回Service對象,ACTION_XXX用來接收Service發送的廣播,在Activity中動態註冊廣播。

 

[java]  

@Override  

protected void onCreate(Bundle savedInstanceState) {  

    super.onCreate(savedInstanceState);  

    setContentView(R.layout.activity_download_manager);  

      

    IntentFilter filter = new IntentFilter();  

    filter.addAction(ACTION_UPDATE);  

    filter.addAction(ACTION_FINISHED);  

    registerReceiver(myReceiver, filter);  

      

    Intent it = new Intent(this, DownloadService.class);  

    startService(it);  

      

    Button add_task = (Button) findViewById(R.id.add_task);  

    add_task.setOnClickListener(new OnClickListener(){  

        @Override  

        public void onClick(View arg0) {  

            TaskInfo ti = new TaskInfo();  

            ti.setTaskId(task_count++);  

            ti.setTaskName(TAG + ti.getTaskId());  

            ti.setProgress(0);  

            ti.setStatus(TaskInfo.WAITING);  

            mService.addTaskInQueue(ti);  

        }  

    });  

      

    Button cancel_task = (Button) findViewById(R.id.cancel_task);  

    cancel_task.setOnClickListener(new OnClickListener(){  

        @Override  

        public void onClick(View arg0) {  

            int index = (int) (Math.random() * task_count);  

            mService.cancelTaskById(index);  

        }  

    });  

}  

        一開始,動態註冊一下BroadcastReceiver,指定接收兩個ACTION;然後,startService啟動一個Service。自定義BroadcastReceiver:

 

[java] 

private BroadcastReceiver myReceiver = new BroadcastReceiver(){  

    @Override  

    public void onReceive(Context context, Intent intent) {  

        if(intent.getAction().equals(ACTION_UPDATE)){  

            int progress = intent.getIntExtra("progress", 0);  

            Log.d(TAG, "myReceiver – progress = " + progress);  

        }else if(intent.getAction().equals(ACTION_FINISHED)){  

            boolean isSuccess = intent.getBooleanExtra("success", false);  

            Log.d(TAG, "myReceiver – success = " + isSuccess);  

        }  

    }  

};  

        在onResume時,去bindService:

 

[java]  

@Override  

protected void onResume() {  

    super.onResume();  

    Log.d(TAG, "Activity onResume");  

      

    Intent it = new Intent(this, DownloadService.class);  

    bindService(it, mServiceConn, BIND_AUTO_CREATE);  

}  

        並在onDestroy時,unbindService,以及unregisterReceiver:

 

[java] 

@Override  

protected void onDestroy() {  

    super.onDestroy();  

    unbindService(mServiceConn);  

    //stopService(new Intent(this, DownloadService.class));  

    unregisterReceiver(myReceiver);  

}  

        ServiceConnection代碼:

 

[java]  

public ServiceConnection mServiceConn = new ServiceConnection(){  

    @Override  

    public void onServiceConnected(ComponentName name, IBinder service) {  

        mService = ((DownloadService.ServiceBinder)service).getService();  

        Log.d(TAG, "onServiceConnected: mService = " + mService);  

          

        if(mService != null){  

            mService.notifyToActivity(false, true);  

        }  

    }  

  

    @Override  

    public void onServiceDisconnected(ComponentName name) {  

        mService = null;  

    }  

};  

        如果成功瞭,就通過IBinder接口,獲得Service對象。

 

        5.2 DownloadService

 

        繼承Service類,override一些方法:

 

[java]  

@Override  

public IBinder onBind(Intent intent) {  

    Log.d(TAG, "onBind");  

    return mBinder;  

}  

  

@Override   

public int onStartCommand(Intent intent, int flags, int startId) {       

    Log.d(TAG, "onStartCommand");  

    return START_STICKY;  

}  

  

@Override  

public void onCreate() {  

    super.onCreate();  

    Log.d(TAG, "onCreate");  

    mBinder = new ServiceBinder();  

    mDownloadQueue = new ArrayList<TaskInfo>();  

    mNotificationManager = (NotificationManager) getSystemService(  

            android.content.Context.NOTIFICATION_SERVICE);  

    mNotification = new Notification();  

    mRemoteView = new RemoteViews(this.getPackageName(), R.layout.remote_view_layout);  

}  

  

@Override  

public void onDestroy() {  

    super.onDestroy();  

    mBinder = null;  

    mDownloadQueue = null;  

    mNotificationManager = null;  

    mNotification = null;  

    mRemoteView = null;  

    Log.d(TAG, "onDestroy");  

}  

        我們通過startService來啟動,因此,啟動流程為:onCreate -> onStartCommand(註:onStart在API5以後,就不在用瞭,取而代之的是onStartCommand)。

 

        然後,我們bindService,此時service已經啟動,所以,隻會調用onBind。

 

        通常,我們應該在onCreate中,去完成一些初始化,而在onDestroy中,去釋放這些內存,因為一但Service運行起來,再去掉startService或bindService,系統就不會再去調用onCreate瞭,但是onStartCommand或onBind仍舊會被調用。

 

        內部類ServiceBinder,隻有一個公有方法,用來返回當前的Service對象:

 

[java]  

public class ServiceBinder extends Binder{  

    public DownloadService getService(){  

        return DownloadService.this;  

    }  

}  

        提供給外部組件的公有方法:

 

[java]  

public void notifyToActivity(boolean update, boolean finished){  

    bNotifyWhenUpdate = update;  

    bNotifyWhenFinished = finished;  

}  

  

public void addTaskInQueue(TaskInfo ti){  

    if(mDownloadQueue != null){  

        mDownloadQueue.add(ti);  

        Log.d(TAG, "addTaskInQueue id = " + ti.getTaskId());  

    }  

      

    if(isRunning == false && mDownloadQueue.size() > 0){  

        startDownload();  

    }  

}  

  

public void cancelTaskById(int id){  

    Log.d(TAG, "cancelTaskById id = " + id);  

    for(int i = 0; i < mDownloadQueue.size(); i ++){  

        TaskInfo ti = mDownloadQueue.get(i);  

        if(ti.getTaskId() == id){  

            if(ti.getStatus() == TaskInfo.RUNNING){  

                ti.setStatus(TaskInfo.CANCELED);  

            }else{  

                mDownloadQueue.remove(i);  

            }  

            break;  

        }  

    }  

}  

        三個方法:添加任務,取消任務,是否需要通知給已經綁定的組件。

 

        接下來,就是我們的線程瞭,這裡的線程是單線程,使用私有的線程隊列

 

[java] 

private void startDownload(){  

    if(isRunning){  

        return;  

    }  

      

    new Thread(new Runnable(){  

        @Override  

        public void run() {  

            while(mDownloadQueue != null && mDownloadQueue.size() > 0){  

                isRunning = true;  

                  

                TaskInfo ti = mDownloadQueue.get(0);  

                while(ti.getProgress() < 100 && ti.getStatus() != TaskInfo.CANCELED){  

                    Message msg = mHandler.obtainMessage(DOWNLOAD_STATUS_UPDATE, ti);  

                    mHandler.sendMessage(msg);  

                    try {  

                        Thread.sleep(1000);  

                    } catch (InterruptedException e) {  

                        e.printStackTrace();  

                    }  

                    ti.setProgress(ti.getProgress()+10);  

                }  

                  

                if(ti.getProgress() == 100 && mDownloadQueue.size() == 1){  

                    Log.d(TAG, ti.getTaskName() + " is finished!");  

                    Message msg = mHandler.obtainMessage(DOWNLOAD_STATUS_SUCCESS, ti);  

                    mHandler.sendMessage(msg);  

                }else if(ti.getStatus() == TaskInfo.CANCELED){  

                    Log.d(TAG, ti.getTaskName() + " is canceled!");  

                }  

                if(mDownloadQueue != null){  

                    mDownloadQueue.remove(ti);  

                }  

            }  

            isRunning = false;  

        }  

    }).start();  

}  

        通過Thread.sleep(1000)來模擬網絡,並使用Thread / Handler的模式,來更新Notification的RemoteViews。

 

       Handler的實現:

 

[java]  

private Handler mHandler = new Handler(){  

    @Override  

    public void handleMessage(Message msg) {  

        switch(msg.what){  

        case DOWNLOAD_STATUS_UPDATE:  

        {  

            mNotification.icon = R.drawable.ic_launcher;  

            mNotification.when = System.currentTimeMillis();  

            mNotification.tickerText = "開始下載…";  

            // 放置在"正在運行"欄目中     

            mNotification.flags = Notification.FLAG_ONGOING_EVENT;  

              

            TaskInfo ti = (TaskInfo) msg.obj;  

            Log.d(TAG, "update : progress = " + ti.getProgress());  

            mRemoteView.setImageViewResource(R.id.ivIcon, R.drawable.ic_launcher);  

            mRemoteView.setTextViewText(R.id.tvName, ti.getTaskName());  

            mRemoteView.setProgressBar(R.id.pbProgress, 100, ti.getProgress(), false);  

            mRemoteView.setTextViewText(R.id.tvProgress, ti.getProgress() + "%");  

            mNotification.contentView = mRemoteView;  

            mNotificationManager.notify(NOTIFY_ID, mNotification);  

              

            notifyUpdate(ti);  

            break;  

        }  

          

        case DOWNLOAD_STATUS_SUCCESS:  

        {  

            mNotification.flags = Notification.FLAG_AUTO_CANCEL;  

            mNotification.contentView = null;  

            Intent it = new Intent(DownloadService.this, DownloadManagerActivity.class);  

            PendingIntent pi = PendingIntent.getActivity(DownloadService.this, 0, it, PendingIntent.FLAG_UPDATE_CURRENT);  

            mNotification.setLatestEventInfo(DownloadService.this, "下載完成", "文件已下載完畢", pi);  

            mNotificationManager.notify(NOTIFY_ID, mNotification);  

              

            notifyFinished(true);  

            break;  

        }  

          

        case DOWNLOAD_STATUS_FAILED:  

        {  

            mNotification.flags = Notification.FLAG_AUTO_CANCEL;  

            mNotification.contentView = null;  

            Intent it = new Intent(DownloadService.this, DownloadManagerActivity.class);  

            PendingIntent pi = PendingIntent.getActivity(DownloadService.this, 0, it, PendingIntent.FLAG_UPDATE_CURRENT);  

            mNotification.setLatestEventInfo(DownloadService.this, "下載失敗", "", pi);  

            mNotificationManager.notify(NOTIFY_ID, mNotification);  

              

            notifyFinished(false);  

            break;  

        }  

          

        default:  

            break;  

        }  

    }  

};  

        通知組件新的情況:

 

[java]  

private void notifyUpdate(TaskInfo ti){  

    if(bNotifyWhenUpdate){  

        Intent it = new Intent(ACTION_UPDATE);  

        it.putExtra("progress", ti.getProgress());  

        DownloadService.this.sendBroadcast(it);  

    }  

}  

  

private void notifyFinished(boolean isSuccess){  

    if(bNotifyWhenFinished){  

        Intent it = new Intent(ACTION_FINISHED);  

        it.putExtra("success", isSuccess);  

        DownloadService.this.sendBroadcast(it);  

    }  

}  

        5.3 TaskInfo類

 

[java]  

package com.chris.download.service.Bean;  

  

import java.io.Serializable;  

  

public class TaskInfo implements Serializable {  

  

    private static final long serialVersionUID = -2810508248527772902L;  

  

    public static final int WAITING = 0;  

    public static final int RUNNING = 1;  

    public static final int CANCELED = 2;  

      

    private int taskId;  

    private String taskName;  

    private int progress;  

    private int status;  

      

    public int getTaskId() {  

        return taskId;  

    }  

    public void setTaskId(int taskId) {  

        this.taskId = taskId;  

    }  

    public String getTaskName() {  

        return taskName;  

    }  

    public void setTaskName(String taskName) {  

        this.taskName = taskName;  

    }  

    public int getProgress() {  

        return progress;  

    }  

    public void setProgress(int progress) {  

        this.progress = progress;  

    }  

    public int getStatus() {  

        return status;  

    }  

    public void setStatus(int status) {  

        this.status = status;  

    }  

}  

 

 

六、總結

 

        本篇隻是帶大傢入門,仍有許多可以改進的地方,如:使用多線程以及如何同步線程隊列,多線程對應在狀態欄上的多個RemoteViews更新,Activity中顯示下載任務隊列及其各任務的狀態等。

發佈留言