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:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://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取消指定的任务。

	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中动态注册广播。

	@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:

	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:

	@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:

	@Override
	protected void onDestroy() {
		super.onDestroy();
		unbindService(mServiceConn);
		//stopService(new Intent(this, DownloadService.class));
		unregisterReceiver(myReceiver);
	}

        ServiceConnection代码:

	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一些方法:

	@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对象:

	public class ServiceBinder extends Binder{
		public DownloadService getService(){
			return DownloadService.this;
		}
	}

        提供给外部组件的公有方法:

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

        三个方法:添加任务,取消任务,是否需要通知给已经绑定的组件。

        接下来,就是我们的线程了,这里的线程是单线程,使用私有的线程队列

	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的实现:

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

        通知组件新的情况:

	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类

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中显示下载任务队列及其各任务的状态等。

       以上这些问题,先留给大家自己思考如何实现,本篇标题是(一),下篇即(二)中,将会用强化本篇,使用以上技术。

       本篇例子代码:《DownloadService实战(一)》

没有更多推荐了,返回首页