服务非常适合执行那些不需要和用户交互还要求长期运行的任务。要注意的是,服务并不是运行在一个独立的进程当中,而是依赖于创建服务时所在的应用程序进程,当应用程序被杀掉时,所有依赖该进程的服务也会停止。不要被服务的后台概念迷惑,实际上服务并不会自动开启子线程,所有的代码都是默认运行在主线程中。因此在使用服务时要注意主线程被阻塞的情况。
服务的基本用法
新建类MyService继承Service,要求重写其唯一一个抽象方法 onBind 。一般又要重写onCreate(服务创建时调用)、onStartCommand(每次服务启动的时候调用)以及onDestroy(服务销毁时调用)方法,它们是服务中最常用的3个方法。如果我们希望服务已启动就立刻执行某个动作,就可以将逻辑写在onStartCommand方法里。另外,还需要在AndroidManifest文件中做如下声明(四大组件都得声明):
1 2 3 4 5
| <service android:name=".MyService" android:enabled="true" android:exported="true" </service
|
其中export属性表示是否允许除了当前程序之外的其他程序访问这个服务,enable表示是否启用这个服务。
启动和停止服务
通过以下代码可以简单地实现启动和停止服务:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Override public void onClick(View v){ switch(v.getId()){ case R.id.start_service: Intent startIntent = new Intent(this,MyService.class); startService(startIntent); break; case R.id.stop_service: Intent startIntent = new Intent(this,MyService.class); stopService(startIntent); break; } }
|
构建出Intent对象,使用startService即可启动MyService,会依次执行onCreate->onStartCommand;同理,使用stopService即可停止,onDestroy就会执行。如果在service里面,那么使用stopSelf即可停止自己。由于刚才点击start的时候,onCreate和onStartCommand都执行了,也许你会疑惑,这两个方法到底有什么区别呢?其实onCreate方法在服务第一次创建时调用,而onStartCommand则在每次启动服务时调用,由于是第一次创建,所以两个方法都执行了,如果多次点击start service按钮,那就只有onStartCommand方法执行了。
活动和服务通信
上一节虽然在活动中启动和停止了服务,但是启动服务之后,活动与服务基本上就没关系了,没法控制服务。如果希望控制服务执行,比如MyService提供下载功能,则希望可以决定什么时候开始下载,以及查看下载进度等,主要的思路是创建一个专门的Binder对象来对下载功能进行管理,然后通过ServiceConnection来实现通信。简易代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class MyService extends Service{ private DownloadBinder mBinder = new DownloadBinder();
class DownloadBinder extends Binder{ public void startDownload(){
}
public int getProgress(){ return 0; } }
@Override public IBinder onBind(Intent intent){ return mBinder; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| private ServiceConnection connection = new ServiceConnection(){ @Override public void onServiceDisconnected(ComponentName name){ }
@Override public void onServiceConnected(ComponentName name,Ibinder service){ downloadBinder = (MyService.DownloadBinder)service; downloadBinder.startDownload(); downloadBinder.getProgress(); } } @Override public void onClick(View v){ switch(v.getId()){ case R.id.bind_service: Intent bindIntent = new Intent(this,MyService.class); bindService(bindIntent,connection,BIND_AUTO_CREATE); break; case R.id.unbind_service: unbindService(connection); break; } }
|
服务生命周期
一旦在任何位置调用了Context的startService方法,相应的服务就会启动,如果服务还没创建过,onCrete先执行,在执行onStartCommand,如果已经创建了,则只会执行onStartCommand了,启动后该服务一直运行,直到调用stopService或者stopSelf为止。虽然多次启动服务onStartCommand会被多次执行,但是服务只会存在一个实例。
另外,还可以调用Context的bindService来获取一个服务的持久连接,这时会回调服务的onBind方法,类似地,如果之前没创建过该服务,就会先执行onCreate在执行onBind,之后,调用方可以获取到onBind方法里返回的IBinder对象实例,就能自由和服务进行通信。调用unbind方法,也会执行Myservice的onDestroy方法,即销毁服务。
服务的更多技巧
使用前台服务
由于服务运行在后台,因此其系统优先级比较低,当出现内存不足的情况时,容易被回收,这就考虑使用前台服务。前台服务和普通服务最大的区别在于,它会一直有一个正在运行的图标在系统的状态栏显示,下拉可以看到更详细的信息,非常类似于通知效果。其主要实现方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class MyService extends Service{ ...
@Override public void onCreate(){ super.onCreate(); Intent intent = new Intent(this,MainActivity.class); PendingIntent pi = PendingIntent.getActivity(this,0,intent,0); Notification notification = new NotificationCompat.Builder(this) .setContentTitle("title") .*** .build();
**startForeground(1,notification); } }
|
使用IntentService
服务中的代码都是默认运行在主线程当中的,如果直接在服务里处理耗时操作,很容易出现ANR。当然,为了避免这种情况,你可以在onStartCommand方法中new 一个Thread来处理耗时逻辑,写法并不复杂,但是程序员容易忘记开启线程或者忘记调用stopSelf。为了简单地创建一个异步的、能够自动停止的服务,Android专门提供了IntentService,在继承Intentservice之后,你可以直接在它的 onHandleIntent方法中执行耗时操作(这是在新的线程里面,可以通过Thread.currentThread
().getId()方法查看线程ID,跟主线程不是同一个),在耗时操作执行完毕之后,这个IntentService会自动调用onDestroy停止。
服务的最佳实践——完整版下载示例
直接上代码吧,多说无益。用于执行异步任务的AsyncTask:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
|
public class DownloadTask extends AsyncTask<String,Integer,Integer>{ public static final int TYPE_SUCCESS = 1; public static final int TYPE_FAILED = 2; public static final int TYPE_PAUSED = 3; public static final int TYPE_CANCELED = 4;
private DownloadListener downloadListener = null; private boolean isPaused = false; private boolean isCancled = false; private int lastProgress;
public DownloadTask(DownloadListener listener){ downloadListener = listener; }
@Override protected Integer doInBackground(String... params) { InputStream is = null; RandomAccessFile savedFile = null; File file = null; try{ long downloadedLength = 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()){ 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","bytes=" + 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(isCancled){ return TYPE_CANCELED; }else if (isPaused){ return TYPE_PAUSED; }else { total += len; savedFile.write(b,0,len); int progress = (int) ((total + downloadedLength) * 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 (isCancled && 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){ downloadListener.onProgress(progress); lastProgress = progress; Log.e("进度",progress+""); } }
@Override protected void onPostExecute(Integer status) { switch (status){ case TYPE_SUCCESS: downloadListener.onSuccess(); break; case TYPE_FAILED: downloadListener.onFailed(); break; case TYPE_PAUSED: downloadListener.onPaused(); break; case TYPE_CANCELED: downloadListener.onCancled(); break; default:
break; } }
public void pauseDownload(){ isPaused = true; }
public void cancelDownload(){ isCancled = true; }
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 contentLength = response.body().contentLength(); response.close(); return contentLength; } return 0; } }
|
用于执行后台任务的Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
| public class DownloadService extends Service{ private DownloadTask downloadTask; private String downloadUrl;
private DownloadListener listener = new DownloadListener(){
@Override public void onProgress(int progress) { getNotificationManager().notify(1,getNotification("Downloading...",progress)); }
@Override public void onSuccess() { downloadTask = null; stopForeground(true); getNotificationManager().notify(1,getNotification("Download Success",-1)); Toast.makeText(DownloadService.this,"Download success",Toast.LENGTH_LONG).show();
}
@Override public void onFailed() { downloadTask = null; stopForeground(true); getNotificationManager().notify(1,getNotification("Download Failed",-1)); Toast.makeText(DownloadService.this,"Download failed",Toast.LENGTH_LONG).show();
}
@Override public void onPaused() { downloadTask = null; Toast.makeText(DownloadService.this,"Paused",Toast.LENGTH_LONG).show();
}
@Override public void onCancled() { downloadTask = null; stopForeground(true); Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_LONG).show(); } };
class DownloadBinder extends Binder{ public void startDownload(String url){ if(downloadTask == null){ downloadUrl = url; downloadTask = new DownloadTask(listener); downloadTask.execute(downloadUrl); startForeground(1,getNotification("Downloading...",0)); Toast.makeText(DownloadService.this,"Downloading...",Toast.LENGTH_LONG).show(); } }
public void pauseDownload(){ if (downloadTask != null){ downloadTask.pauseDownload(); } }
public void cancelDownload(){ if (downloadTask != null){ downloadTask.cancelDownload(); }else{ 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,"Canceled",Toast.LENGTH_LONG).show(); } } } }
private DownloadBinder mBinder = new DownloadBinder(); @Nullable @Override public IBinder onBind(Intent intent) { return mBinder; }
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);
NotificationCompat.Builder builder;
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ){ NotificationChannel channel = new NotificationChannel("im_channel_id","System", NotificationManager.IMPORTANCE_LOW); NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); if(manager != null){ **manager.createNotificationChannel(channel); } }
builder = new NotificationCompat.Builder(this,"im_channel_id"); builder.setSmallIcon(R.mipmap.ic_launcher); builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher)); builder.setContentIntent(pi); builder.setContentTitle(title); if (progress > 0){ builder.setContentText(progress + "%"); builder.setProgress(100,progress,false); } return builder.build(); } }
|
Activity中的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private DownloadService.DownloadBinder downloadBinder; 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 onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
findViewById(R.id.start).setOnClickListener(this); findViewById(R.id.pause).setOnClickListener(this); findViewById(R.id.cancel).setOnClickListener(this);
Intent intent = new Intent(this,DownloadService.class); startService(intent); bindService(intent,connection,BIND_AUTO_CREATE);
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);
} }
@Override public void onClick(View v) { if (downloadBinder == null){ return; } switch (v.getId()){ case R.id.start: String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe"; downloadBinder.startDownload(url); break; case R.id.pause: downloadBinder.pauseDownload(); break; case R.id.cancel: downloadBinder.cancelDownload(); break; } }
@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode){ case 1: if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED){ Toast.makeText(MainActivity.this,"拒绝权限无法使用程序",Toast.LENGTH_SHORT).show(); finish(); } break; } }
@Override protected void onDestroy() { super.onDestroy(); unbindService(connection); }
|
当然,还有要注意的是,要在AndroidManifest.xml中注册Service:
service android:name=”.DownloadService”
还有,在AndroidManifest.xml中需要标明网络权限和存储权限:
uses-permission android:name=”android.permission.INTERNET”
uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE”
最后,针对本例子,例子中使用了okhttp,需要在buildgradle中添加依赖:
implementation ‘com.squareup.okhttp3:okhttp:3.9.0’