第二行代码学习笔记——第十章:后台默默的劳动者——探究服务
来源:互联网 发布:krpano全景漫游软件 编辑:程序博客网 时间:2024/04/30 01:28
本章要点
Android沿用了诺基亚系统的Symbian操作系统的老习惯,从一开始就支持后台功能,这使得应用程序即使在关闭的情况下仍然可以在后台继续运行。后台功能属于四大组件之一,重要程度言不可寓。
10.1 服务是什么
服务(Service)是Android中是实现程序后台运行的解决方案,它非常适合执行那些不需要与用户进行交互还需要长期运行的任务。服务的界面不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了一个应用程序,服务仍然能保持正常运行。
服务并不是运行在一个独立的进程中的,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时,所以依赖于该进程的服务也会停止运行。
默认在主线程,可能会造成主线程阻塞的问题。
10.2 Android多线程编程
当我们执行耗时操作,就必须放在子线程中运行,避免线程阻塞的问题,增强用户的体验度。
10.2.1 线程的基本用法
Android多线程与Java多线程使用语法相同。比如说,定义一个线程只需要定义一个类继承自Thread,然后重写父类的run()方法,并在里面编写耗时逻辑即可,如下:
class MyThread extends Thread { @Override public void run(){ //处理具体的逻辑 }}
启动这个线程,new出MyThread的实例,然后调用start()方法,这样run()方法就运行在子线程中了,如下:
new MyThread().start();
当然,使用继承的方式高耦合,更多的时候我们选择实现Runable接口的方式来定义一个线程,如下:
class MyThread implements Runable { @Override public void run() { //处理具体的逻辑 }}
那么启动方式如下:
MyThread myThread = new MyThread();new Thread(myThread).start();
常见的实现方式如下:
new Thread(new Runable() { @Override public void run() { //处理具体的逻辑 }}).start();
以上就是线程的基本用法。在Java中创建和启动线程的方式是一样的。
10.2.2 在子线程中更新UI
更新应用程序中的UI元素,必须放在主线程。否则就会出现异常。
那我们就来验证下吧。新建AndroidThreadTest项目,修改activity_main.xml中的代码如下:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/btn_change_tv" android:text="Change Text" android:textAllCaps="false" android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/tv" android:layout_centerInParent="true" android:text="Hello World" android:textSize="20sp" android:layout_width="wrap_content" android:layout_height="wrap_content" /></RelativeLayout>
接下来我们对Hello World 进行修改,修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private Button btn_change_tv; private TextView tv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn_change_tv= (Button) findViewById(R.id.btn_change_tv); tv= (TextView) findViewById(R.id.tv); btn_change_tv.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()){ case R.id.btn_change_tv: new Thread(new Runnable() { @Override public void run() { tv.setText("Nice to meet you"); } }).start(); break; default: break; } }}
我们在子线程中更新UI,运行程序,点击Change Text按钮,我们会发现程序崩溃。观察logcat日志,可以看到原因是由于在子线程中更新UI操作,如下:
由此我们证明了Android不能在子线程中更新UI操作。有时候我们会在耗时操作返回的结果需要进行相应的UI更新。那么我们通过Android提供的异步消息处理机制,完美解决在子线程中更新UI操作。
使用异步处理消息的方法。
修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private static final int UPDATE_TEXT=1; //表示更新TextView的动作 private Button btn_change_tv; private TextView tv; private Handler handler=new Handler(){ @Override public void handleMessage(Message msg) { //4.主线程 switch (msg.what){ case UPDATE_TEXT: //5.判断是否相等,进行操作 //在这里进行UI操作 tv.setText("Nice to meet you"); break; default: break; } } }; ... @Override public void onClick(View v) { switch (v.getId()){ case R.id.btn_change_tv: new Thread(new Runnable() { @Override public void run() { Message message=new Message(); //1.创建Message对象 message.what=UPDATE_TEXT; //2.指定what值 handler.sendMessage(message); //3.将Message对象发送过去 } }).start(); break; default: break; } }}
重新运行程序,点击Change Text按钮,Hello World就会变成Nice to meet you。如图:
使用Handler机制顺利的解决了在子线程中更新UI的问题。
10.2.3 解析异步消息处理机制
Android中的异步消息处理主要有4部分组成:Message,Handler,MessageQueue,Looper。
- Message
Message是在线程之间传递的消息,它可以携带少量的信息,用于在不同线程之间交换数据。Message的what字段,arg1和arg2字段携带整型数据,obj字段携带一个Object对象。 - Handler
Handler处理者,主要用于发送和处理消息。发送消息Handler的sendMessage()方法,发送的消息最终会传递到Handler的handleMessage()方法中。 - MessageQueue
MessageQueue消息列队,主要用于存放所有通过Handler发送的消息。这部分消息一直会存在于消息列队中,等待被处理。每个线程中只会有一个MessageQueue对象。 - Looper
Looper是每个线程中的MessageQueue的管家,调用Looper的lop方法后,就会进入到无线循环中,然后每当发送MessageQueue中存在的一条消息,就会将它取出,并传递到Handler中的handleMessage()方法中。每个线程中也只会有一个Looper对象。
异步消息处理的整个流程:首先需要在主线程中创建一个Handler对象,并重写handleMessage()方法。然后在子线程中进行需要UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息被添加在MessageQueue消息列队中等待被处理,而Looper会一直尝试者从MessageQueue中取出待处理的消息,最后发送给Handel的handleMessage方法。由于Handler是在主线程中创建的,所以此时handleMessage()方法也会在主线程中执行,就可以进行UI操作了。整个异步消息处理机制流程图:
整个异步消息处理的核心思想:一条Message经过一个流程的辗转调用后,也就从子线程进入到了主线程,从不能更新UI操作,变成了可以更新UI操作。
之前我们使用的runOnUiThread()方法就是一个异步消息处理机制的接口封装。原理跟上图描述的一样。
10.2.4 使用AsyncTask
Android为了更加方便在子线程中更新UI操作,使用AsyncTask。它的实现原理也是基于异步消息处理机制的封装。
AsyncTask的基本用法,AsyncTask是一个抽象类,我们必须创建一个类继承它。继承时可以为AsyncTask指定3个泛型参数。
- Params。在执行AsyncTask时传入的参数,可用于在后台的使用。
- Progress。 后台执行任务时,如果需要在界面显示当前的进度条,则使用这里指定的泛型作为进度单位。
- Result。 任务执行完毕,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。
一个完整的自定义AsyncTask如下:
class DownloadTask extends AsyncTask<Void, Integer, Boolean> { /** * 开始执行任务之前,用于初始化见界面。例如:显示一个进度条 */ @Override protected void onPreExecute() { progressDialog.show();//显示进度对话框 } /** * 在子线程中执行,处理耗时操作。 * 如果需要更新UI元素,则调用publishProgress() * * @param params * @return */ @Override protected Boolean doInBackground(Void... params) { try { while (true) { int downloadPercent = doDownlod(); //这是一个虚方法 publishProgress(downloadPercent); if (downloadPercent >= 100) { break; } } } catch (Exception e) { return false; } return true; } /** * 当在后台中调用了publishProgress()方法后,onProgressUpdate()很快就会调用, * 该方法携带的参数就是从后台传过来的。在这里执行对UI操作。 * * @param values */ @Override protected void onProgressUpdate(Integer... values) { //在这里执行下载进度 progressDialog.setMessage("Download" + values[0] + "%"); } /** * 当后台执行完毕返回进行返回时。利用返回数据更新UI操作。 * 比如:提醒任务执行的结果,以及关闭对话框等。 * * @param result */ @Override protected void onPostExecute(Boolean result) { progressDialog.dismiss(); //关闭进度对话框 //在这里提醒下载结果 if (result) { Toast.makeText(content, "Download succeeded", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(content, "Download failed", Toast.LENGTH_SHORT).show(); } }}
第一个泛型参数Void,表示不需要传入参数给后台任务。
第二个泛型参数指定为Integer,表示使用整型数据类型作为进度显示单位。
第三个泛型参数指定为Boolean,表示返回回馈的结果。
在这个DownloadTask中,在doInBackground()方法执行具体的下载任务(子线程)。doDownload()方法是一个虚方法,计算当前的下载进度并返回,并让它显示到界面中。我们通过调用 publishProgress()方法并将下载进度传进来。这样onProgressUpdate()就会调用,进行UI操作。下载完成后,doInBackground()返回的是布尔值,然后通过onPostExecute()弹出相应的Toast提示。完成异步加载任务。
简单的来说,在AsyncTask中,doInBackground()进行耗时操作;onProgressUpdate()更新UI操作,onPostExecute()执行收尾工作。
启动这个任务,代码如下:
new DownloadTask().execute();
本章最佳实践,完善下载这个功能。
10.3 服务的基本用法
Android四大组件之一 —— 服务。
10.3.1 定义一个服务
新建ServiceTest项目,点击包—>New—>Service—>Service。弹出如下窗口:
Enabled:表示是否启动这个服务。 Exported:是否允许除了当前程序之外的程序进行访问这个服务。全部勾选,点击Finish完成创建。
MyService的代码如下:
public class MyService extends Service { public MyService() { } @Override public IBinder onBind(Intent intent) { throw new UnsupportedOperationException("Not yet implemented"); }}
MyService类继承自Service。onBind()方法是Service中唯一的一个抽象方法。
处理事情,定义Service其他方法,代码如下:
public class MyService extends Service { ... /** * 创建服务的时候调用 */ @Override public void onCreate() { super.onCreate(); } /** * 每次启动的服务的时候调用 * @param intent * @param flags * @param startId * @return */ @Override public int onStartCommand(Intent intent, int flags, int startId) { return super.onStartCommand(intent, flags, startId); } /** * 销毁服务的时候调用 */ @Override public void onDestroy() { super.onDestroy(); }}
每个服务都需要在AndroidManifest.xml注册是才能够使用,这是Android四大组件的共同点。创建服务的时候Android Studio已经帮我们智能的创建完成了。打开AndroidManifest.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.hjw.servicetest"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> ... <service android:name=".MyService" android:enabled="true" android:exported="true"></service> </application></manifest>
这样,我们就定义好了一个服务。
10.3.2 启动和停止服务
借助Intent来启动和停止这个服务,在ServiceTest项目中来实现吧。
修改activity_main.xml中的代码如下:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="@+id/btn_start_service" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Start Service" android:textAllCaps="false" /> <Button android:id="@+id/btn_stop_service" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Stop Service" android:textAllCaps="false" /></LinearLayout>
修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private Button btn_start_service, btn_stop_service; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn_start_service = (Button) findViewById(R.id.btn_start_service); btn_stop_service = (Button) findViewById(R.id.btn_stop_service); btn_start_service.setOnClickListener(this); btn_stop_service.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_start_service: Intent startIntent = new Intent(this, MyService.class); startService(startIntent); //启动服务 break; case R.id.btn_stop_service: Intent stopIntent = new Intent(this, MyService.class); stopService(stopIntent); //停止服务 break; default: break; } }}
测试服务启动或停止,我们在MyService中的方法加入日志,如下所示:
public class MyService extends Service { private static final String TAG = "MyService"; ... /** * 创建服务的时候调用 */ @Override public void onCreate() { super.onCreate(); Log.d(TAG, "onCreate: executed"); } /** * 每次启动的服务的时候调用 * * @param intent * @param flags * @param startId * @return */ @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "onStartCommand: executed"); return super.onStartCommand(intent, flags, startId); } /** * 销毁服务的时候调用 */ @Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy: executed"); }}
接下来我们运行程序,如下:
点击Start Service按钮,观察logcat日志如下:
这时这个服务已经启动了,我们打开设置—>开发者选项—>正在运行的服务,如图:
然后我们点击Stop Service按钮,观察日志如下:
可以看出MyService的确停止服务了。
onCreate()和onStartCommand()区别,onCreate()会在第一次创建的时候调用,而onStartCommand()则在每次创建服务的时候调用。当我们点击多次Start Service按钮,第一次会执行两个方法,而以后只会执行onStartCommand()方法。
10.3.3 活动和服务进行通信
借助onBind()方法,使我们的服务与活动关联起来。
我们希望实现MyService实现一个下载功能,在活动中决定合适开始下载,以及查看进度。那我们创建一个Binder对象对下载功能进行管理。修改MyService中的代码如下:
public class MyService extends Service { private static final String TAG = "MyService"; public MyService() { } private DownloadBinder mBinder=new DownloadBinder(); @Override public IBinder onBind(Intent intent) { return mBinder; } class DownloadBinder extends Binder{ /** * 模拟方法 */ public void startDownload(){ Log.d(TAG, "startDownload: executed"); } public int getProgress(){ Log.d(TAG, "getProgress: executed"); return 0; } } ...}
新建DownloadBinder类,并继承Binder,再里面实现下载和查看进度的模拟方法(这里我们分别打印一行日志)。
在MyService中创建DownloadBinder的实例,然后在onBind()返回实例。做完MyService工作了。
在活动中调用服务里的方法。新增两个按钮(绑定服务,解绑服务),修改activity_main中的文件:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> ... <Button android:id="@+id/btn_bind_service" android:textAllCaps="false" android:text="Bind Service" android:layout_width="match_parent" android:layout_height="wrap_content" /> <Button android:id="@+id/btn_unbind_service" android:textAllCaps="false" android:text="unBind Service" android:layout_width="match_parent" android:layout_height="wrap_content" /></LinearLayout>
当一个活动和服务绑定之后,就可以调用服务里的Binder提供的方法,修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private Button btn_start_service, btn_stop_service,btn_bind_service,btn_unbind_service; 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); ... btn_bind_service= (Button) findViewById(R.id.btn_bind_service); btn_unbind_service= (Button) findViewById(R.id.btn_unbind_service); ... btn_bind_service.setOnClickListener(this); btn_unbind_service.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { ... case R.id.btn_bind_service: Intent bindIntent = new Intent(this, MyService.class); bindService(bindIntent,connection,BIND_AUTO_CREATE); //绑定服务 break; case R.id.btn_unbind_service: unbindService(connection); //解绑服务 break; default: break; } }}
首先创建一个ServiceConnection匿名类,重写了onServiceConnected()和onServiceDisconnected()方法,与服务成功绑定和解绑的时候调用。通过向下转型得到了DownloadBinder的实例,接下来在onServiceConnected()中调用DownloadBinder的startDownload()和getProgress()方法。
绑定服务:binderService()接受三个参数,第一个参数Intent,第二个参数ServiceConnection,第三个参数BIND_AUTO_CREATE(表示在活动和服务进行绑定后自动创建服务)。这样MyService中的onCreate()会得到执行,但onStartCommand()方法不会执行。
解除绑定:unBinderService()方法。
运行程序,点击Bind Service中的按钮,观察logcat日志如下:
这样就完成了我们的绑定服务,任何一个服务在应用范围内都是通用的(一个服务可以和任意一个活动绑定)。
10.4 服务的生命周期
服务也有自己的生命周期,官方给出两种服务的生命周期,一目了然,如图:
10.5 服务的更多技巧
接下来我们学习服务高级使用技巧。
10.5.1 使用前台服务
希望服务一直保持运行,而不希望系统内存不足而被回收,我们可以使用前台服务。前台服务和普通服务的最大的区别就在与,它会一直有一个正在运行的图标在系统的状态栏显示,下拉菜单显示详情内容,类似于通知。
创建前台服务,修改MyService中的代码如下:
public class MyService extends Service { ... /** * 创建服务的时候调用 */ @Override public void onCreate() { super.onCreate(); Log.d(TAG, "onCreate: executed"); Intent intent=new Intent(this,MainActivity.class); PendingIntent pi=PendingIntent.getActivity(this,0,intent,0); Notification notification=new NotificationCompat.Builder(this) .setContentTitle("This is content title") .setContentText("This is content text") .setWhen(System.currentTimeMillis()) .setSmallIcon(R.mipmap.ic_launcher) .setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher)) .setContentIntent(pi) .build(); startForeground(1,notification); } ...}
调用startForeground()方法来显示通知,就会让MyService变成一个前台服务,显示在系统的状态栏。
运行程序,并点击Start Service或Bind Service按钮,就会启动前台服务。如图:
10.5.2 使用IntentService
服务的代码默认在主线程,如果在服务里进行一些耗时操作,很容易出现ANR(Application Not Responding)的情况。
为了解决ANR,我们必须在每个服务的每个具体方法里开启一个子线程,去处理一些耗时的操作。一个标准的服务的代码如下:
public class MyService extends Service { ... /** * 每次启动的服务的时候调用 * * @param intent * @param flags * @param startId * @return */ @Override public int onStartCommand(Intent intent, int flags, int startId) { new Thread(new Runnable() { @Override public void run() { //处理具体的逻辑 stopSelf(); } }).start(); return super.onStartCommand(intent, flags, startId); } ...}
为了解决我们忘记开启子线程,或忘记调用stopSelf()方法。Android提供了一个异步的,会自动停止服务的IntentService类。
新建MyIntentService类继承自IntentService,代码如下:
public class MyIntentService extends IntentService { public MyIntentService() { super("MyIntentService"); //调用父类的有参构造函数 } @Override protected void onHandleIntent(@Nullable Intent intent) { //打印当前线程的id Log.d("MyIntentService", "Thread id is "+ Thread.currentThread().getId()); } @Override public void onDestroy() { super.onDestroy(); Log.d("MyIntentService", "onDestroy: executed "); }}
首先我们提供一个无参的构造函数,并且必须调用内部父类的有参构造函数。然后在子类中去实现onHandleIntent()抽象方法,在这个方法中处理具体的逻辑,这个方法在子线程中运行(解决了ARN问题)。
接下来修改activity_main.xml中的代码,加入一个启动MyiIntentStart按钮,如下所示:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> ... <Button android:id="@+id/btn_start_intent_service" android:textAllCaps="false" android:text="Start IntentService" android:layout_width="match_parent" android:layout_height="wrap_content" /></LinearLayout>
修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private Button btn_start_service, btn_stop_service,btn_bind_service,btn_unbind_service,btn_start_intent_service; ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ... btn_start_intent_service= (Button) findViewById(R.id.btn_start_intent_service); ... btn_start_intent_service.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { ... case R.id.btn_start_intent_service: Log.d("MainActivity", "Thread id is " + Thread.currentThread().getId());//打印主线程的id Intent intentService=new Intent(this,MyIntentService.class); startService(intentService); break; default: break; } }}
在AndroidManifest.xml中注册服务,代码如下:
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.hjw.servicetest"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> ... <service android:name=".MyIntentService"/> </application></manifest>
运行程序,点击Start IntentService按钮,观察logcat日志,如下:
我们可以看到MyIntentService和MainActivity中的所在线程的id不一样,而且onDestory()也得到了执行。集开启线程和自动停止与一身,IntentService是我们的最爱。
10.6 服务的最佳实践 — 完整版的下载
实现服务中经常使用到的功能个——下载功能。
创建一个ServiceBestPractice项目。
添加OkHttp依赖库,如下:
compile 'com.squareup.okhttp3:okhttp:3.8.0'
接下来定义一个DownloadListener接口回调,用于对下载过程中的各种状态进行监听。如下:
public interface DownloadListener { void onProgress(int progress); void onSuccess(); void onFailed(); void onPaused(); void onCanceled();}
编写下载功能,新建DownloadTask继承自AysncTask,代码如下:
public class DownloadTask extends AsyncTask<String, Integer, Integer> { private static final int TYPE_SUCCESS = 0; private static final int TYPE_FAILED = 1; private static final int TYPE_PAUSED = 2; private static final int TYPE_CANCELED = 3; private DownloadListener listener; private boolean isCanceled = false; private boolean isPaused = false; private int lastProgress; public DownloadTask(DownloadListener listener) { this.listener = listener; } @Override protected Integer doInBackground(String... params) { InputStream is = null; RandomAccessFile savedFile = null; File file = null; try { long downloadLength = 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()) { downloadLength = file.length(); } long contentLength = getContentLength(downloadUrl); if (contentLength == 0) { return TYPE_FAILED; } else if (contentLength == downloadLength) { //已下载字节和文件总字节相等,就等于已经下载完了 return TYPE_SUCCESS; } OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() //断点下载,指定从哪个子节点开始下载 .addHeader("RANGE", "bytes=" + downloadLength + "-") .url(downloadUrl).build(); Response response = client.newCall(request).execute(); if (request != null) { is = response.body().byteStream(); savedFile = new RandomAccessFile(file, "rw"); savedFile.seek(downloadLength); //跳过已下载的字节 byte[] bytes = new byte[1024]; int total = 0; int len; while ((len = is.read(bytes)) != -1) { if (isCanceled) { return TYPE_CANCELED; } else if (isPaused) { return TYPE_PAUSED; } else { total += len; savedFile.write(bytes, 0, len); //计算已下载的百分比 int progress = (int) ((total + downloadLength) * 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 (isCanceled && file != null) { file.delete(); } } catch (IOException 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 status) { switch (status) { case TYPE_SUCCESS: listener.onSuccess(); break; case TYPE_FAILED: listener.onFailed(); break; case TYPE_PAUSED: listener.onPaused(); break; case TYPE_CANCELED: listener.onCanceled(); break; default: break; } } public void pauseDownload() { isPaused = true; } public void cancelDownload() { isCanceled = 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; }}
分析以上代码,首先AsyncTask中的3个泛型参数:第一个泛型参数指定为String,表示在执行AsyncTask时需要传入字符串参数给后台;第二个泛型参数指定为Integer,表示使用整型数据来作为进度显示单位;第三个泛型参数指定为Integer,表示使用整型数据来反馈执行结果。
接下类我们定义了4个整型常量表示下载的状态。然后在DownloadTask的构造函数中要求传入刚刚定义的DownloadListener参数,通过这个参数将下载的状态进行回调。
接下来我们重写doInBackground(),onProgressUpdate()和onPostExecute()这3个方法。
doInBackground()方法用于在后台执行具体下载的逻辑,首先我们从参数中获取了下载路径的URL地址,并根据URL地址解析出来的下载名,然后将文件下载到SD卡的Download路径下(Environment.DIRECTORY_DOWNLOADS)。判断目录下是否存在要下载的文件,存在则读取下载的字节数,这样就可以在后面启动断电下载传的功能。接下来先是调用了getContentLength()方法来获取待下载文件的长度,如果长度等于0,则说明文件有问题,返回TPE_FAILD,如果文件长度等于已经下载文件的长度,则说明下载完成,返回TYPE_SUCCESS。紧接着我们使用OKHttp发送网络请求,在请求中添加了header,指用于告诉服务器我们想从哪个节点开始下载,因为下载过的就不需要下载了(断点下载)。接下来读取服务器响应的数据,并使用Java的文件流的方式,不断从网络上读取数据,不断写入本地,直到文件全部下载完。如果用户在下载过程中没有执行暂停或取消的操作,则实时计算当前下载的进度,然后调用publishProgress()方法进行通知。暂停或取消都是用布尔值进行控制的,调用pauseDownload()或cancelDownload()方法即可更改变量的值。
onProgressUpdate()用于在界面中更新当前的下载进度,首先获取当前的下载进度,与上次下载的进行对比,如果有变化则调用DownloadListener的onProgress()方法来通知下载进度更新。
最后onPostExecute()用于通知最终下载的结果,根据参数中传入的下载状态的参数进行回调。
这样我们就实现了下载的功能,接下来我们为了保证DownloadTask一直运行在后台,则需要创建DownloadService(服务),修改代码如下:
public class DownloadService extends Service { public DownloadService() { } 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_SHORT).show(); } @Override public void onFailed() { downloadTask = null; //下载成功时将前台服务关闭 stopForeground(true); getNotificationManager().notify(1, getNotification("Download Failed", -1)); Toast.makeText(DownloadService.this, "Download Failed", Toast.LENGTH_SHORT).show(); } @Override public void onPaused() { downloadTask = null; Toast.makeText(DownloadService.this, "Pause", Toast.LENGTH_SHORT).show(); } @Override public void onCanceled() { downloadTask = null; stopForeground(true); Toast.makeText(DownloadService.this, "Canceled", 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); startForeground(1, getNotification("Downloading...", 0)); Toast.makeText(DownloadService.this, "Downloading...", Toast.LENGTH_SHORT).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_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); NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setSmallIcon(R.mipmap.ic_launcher); builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)); builder.setContentIntent(pi); builder.setContentTitle(title); if (progress > 0) { //当progress大于或等于0显示加载进度 builder.setContentText(progress + "%"); builder.setProgress(100, progress, false); } return builder.build(); }}
首先我们创建了DownloadListener的匿名实例,实现下载状态的5个方法。在onProgress方法在,调用getNotification()构建了一个用于显示下载进度的通知,然后调用NotificationManager的notify()方法出发通知,就可在下载状态中查看下载进度。再其他几个方法中将正在下载的前台服务通知关闭,创建一个新的通知告诉用于返回的结果。
为了让DownloadService和活动进行通信,我们创建了一个DownloadBinder。它提供了startDownload(),pauseDownload()和cancelDownload()方法分别是开始,暂停和取消下载的。
DownloadService类中的所有使用的通知都调用getNotification。
实现了下载的服务。编写activity_main.xml中的代码如下:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="@+id/btn_start_download" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Start Download" android:textAllCaps="false" /> <Button android:id="@+id/btn_pause_download" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Pause Download" android:textAllCaps="false" /> <Button android:id="@+id/btn_cancel_download" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Cancel Download" android:textAllCaps="false" /></LinearLayout>
接下来修改MainActivity中的代码,如下:
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) { } }; private Button btn_start_download, btn_pause_download, btn_cancel_download; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn_start_download = (Button) findViewById(R.id.btn_start_download); btn_pause_download = (Button) findViewById(R.id.btn_pause_download); btn_cancel_download = (Button) findViewById(R.id.btn_cancel_download); btn_start_download.setOnClickListener(this); btn_pause_download.setOnClickListener(this); btn_cancel_download.setOnClickListener(this); Intent intent = new Intent(this, DownloadService.class); startService(intent); //启动服务 bindService(intent, connection, BIND_AUTO_CREATE);//绑定服务 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(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.btn_start_download: String downloadUrl = "http://eclipse.stu.edu.tw/oomph/epp/neon/R3/eclipse-inst-win64.exe"; downloadBinder.startDownload(downloadUrl); break; case R.id.btn_pause_download: downloadBinder.pauseDownload(); break; case R.id.btn_cancel_download: downloadBinder.cancelDownload(); break; default: break; } } @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(); } break; default: break; } } @Override protected void onDestroy() { super.onDestroy(); unbindService(connection); }}
首先创建了一个ServiceConnection的匿名类,然后在onServiceConnected()获取DownloadBinder的实例。然后在活动中调用服务提供过的各种方法。
在onCreate()方法中进行初始化操作,调用了startService()和bindService()来启动和绑定服务。这点相当重要,因为启动服务就可以保证DownloadService运行在后台,绑定服务可以让与MainActivity进行通信(两个方法必不可少)。我们还进行了申请权限的操作。
在onClick()方法中对点击事件进行判断,点击开始就调用DownloadBinder中的startDowmload()方法;暂停调用pauseDownload()。取消调用cancelDownload()。
销毁活动,一定要对服务进行解绑,否则造成内存泄露。我们在onDestroy()完成解绑。
在AndroidManifest.xml中声明权限,以及DownloadService,代码如下:
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.hjw.servicebestpractice"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".DownloadService" android:enabled="true" android:exported="true"></service> </application></manifest>
运行程序,会弹出授权对话框,如下:
点击允许,然后点击Start Download按钮就开始下载了,下载过程下拉系统状态栏进行查看,如下:
我们还可以点击Pause Download或Cancel Download,甚至断网操作来测试这个程序的健壮性。下载完会弹出Download Success,提示用于下载完成。我们打开SD卡的Download查看,如下:
我们可以看到文件已经下载完成了。你可以任意的测试,这个下载实例的健壮性,综合性都是很强的。
10.7 小结与点评
学习了Android多线程,服务的基本用法,服务的生命周期,前台服务和IntentServiced等。
- 第二行代码学习笔记——第十章:后台默默的劳动者——探究服务
- 阅读郭林《第一行代码》的笔记——第9章 后台默默的劳动者,探究服务
- 后台默默的劳动者,探究服务
- 后台默默的劳动者,探究服务
- 后台默默的劳动者,探究服务
- 第九章 后台默默的劳动者,探究服务
- 后台劳动者--探究服务
- 第二行代码学习笔记——第二章:先从看得到的入手——探究活动
- 第二行代码学习笔记——第二章:先从看得到的入手——探究活动
- 第二行代码学习笔记——第四章:手机平板要兼容——探究碎片
- 第二行代码学习笔记——第七章:跨程序共享数据——探究内容提供器
- 第二行代码学习笔记——第十一章:Android特色开发——基于位置的服务
- Android编程权威指南(第二版)学习笔记(二十六)—— 第26章 后台服务
- 第一行代码学习笔记-第二章 探究活动-1.活动的基本用法
- 第一行代码学习笔记-第二章 探究活动-2.Intent的使用
- 第一行代码学习笔记-第二章 探究活动-3.活动的生命周期
- 第一行代码学习笔记-第二章 探究活动-4.活动的启动模式
- 第一行代码学习笔记-第二章 探究活动-5. 活动的最佳实现
- 计蒜之道初赛第一场-阿里的新游戏
- 简单的注册与登陆
- Python 输出json到文件,让json.dumps输出中文 实例代码
- spring搭建,为什么用spring,对控制反转,依赖注入的理解
- 交叉编译OpenCV2.4.9
- 第二行代码学习笔记——第十章:后台默默的劳动者——探究服务
- Sublime 配置
- django 上传文件
- 数据结构实验之排序七:选课名单
- 算法导论 练习题 15.1-3
- JavaScript学习-操作样式的DOM API
- C++异常处理机制
- MySQL5.7.18自解压Zip 官网下载、安装与配置
- 基于条件随机场(CRF)的组织机构实体识别