第九章 后台默默的劳动者,探究服务

9.1服务是什么

服务( Service)是 Android 中实现程序后台运行的解决方案,它适合用于去执行不需要和用户交互而且还要求长期运行的任务。服务的运行不依赖于任何用户界面,当程序被切换到后台,服务仍然能够保持正常运行。但服务并不是运行在一个独立的进程当中的,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的服务也会停止运行。另外,服务并不会自动开启线程,所有的代码都是默认运行在主线程当中的。也就是说,我们需要在服务的内部手动创建子线程,并在这里执行具体的任务,否则就有可能出现主线程被阻塞住的情况。

Android多线程编程

Android多线程编程与Java类似.新建一个类继承Thread,重写run(),其中编写耗时逻辑.启动时只需new出实例,调用start()方法.或者实现Runable接口,实现run()方法.启动时通过

new Thread(myThread).start();

实现.或者通过匿名类实现

new Thread(new Runnable() {
  @Override
  public void run() {
    // 处理具体的逻辑
  }
}).start();

9.2.2在子线程中更新UI

在子线程中更新UI是不安全的,因此必须在主线程中进行.
子线程更新UI一次:

Process: com.wjoker.androidthreadtest, PID: 8037
android.view.ViewRootImpl$CalledFromWrongThreadException:
Only the original thread that created a view hierarchy can touch its views.

Android提供了异步消息处理机制,用于解决子线程中进行UI操作的问题.

9.2.3解析异步消息处理机制

Android 中的异步消息处理主要由四个部分组成, Message、 Handler、 MessageQueue 和Looper。

  1. Message 是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。上一小节中我们使用到了 Message 的 what 字段,除此之外还可以使用 arg1 和 arg2 字段来携带一些整型数据,使用 obj 字段携带一个 Object 对象。
  2. Handler 顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。发送消息一般是使用 Handler 的 sendMessage()方法,而发出的消息经过一系列地辗转处理后,最终会传递到 Handler 的 handleMessage()方法中。
  3. MessageQueue 是消息队列的意思,它主要用于存放所有通过 Handler 发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个 MessageQueue对象。
  4. Looper 是每个线程中的 MessageQueue 的管家,调用 Looper 的 loop()方法后,就会进入到一个无限循环当中,然后每当发现 MessageQueue 中存在一条消息,就会将它取出,并传递到 Handler 的 handleMessage()方法中。每个线程中也只会有一个 Looper 对象。

首先需要在主线程当中创建一个 Handler 对象,并重写handleMessage()方法。
然后当子线程中需要进行 UI 操作时,就创建一个 Message 对象,并通过 Handler 将这条消息发送出去。
之后这条消息会被添加到 MessageQueue 的队列中等待被处理,而 Looper 则会一直尝试从 MessageQueue 中取出待处理消息,最后分发回 Handler的 handleMessage()方法中。
由于 Handler 是在主线程中创建的,所以此时 handleMessage()方法中的代码也会在主线程中运行,于是我们在这里就可以安心地进行 UI 操作了。

9.2.4使用AsyncTask

Android提供AsyncTask在子线程中对UI进行操作,其原理是基于异步消息处理机制.

由于 AsyncTask 是一个抽象类,所以如果我们想使用它,就必须要创建一个子类去继承它。在继承时我们可以为 AsyncTask 类指定三个泛型参数,这三个参数的用途如下。

  1. Params:在执行 AsyncTask 时需要传入的参数,可用于在后台任务中使用。
  2. Progress:后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
  3. Result:当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。
    示例:
class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
    ……
}

这里我们把 AsyncTask 的第一个泛型参数指定为 Void,表示在执行 AsyncTask 的时候不需要传入参数给后台任务。第二个泛型参数指定为 Integer,表示使用整型数据来作为进度显示单位。第三个泛型参数指定为 Boolean,则表示使用布尔型数据来反馈执行结果。

当然,目前我们自定义的 DownloadTask 还是一个空任务,并不能进行任何实际的操作,我们还需要去重写 AsyncTask 中的几个方法才能完成对任务的定制。经常需要去重写的方法有以下四个。

  1. onPreExecute()
    这个方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。
  2. doInBackground(Params...)
    这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成就可以通过 return 语句来将任务的执行结果返回,如果 AsyncTask 的第三个泛型参数指定的是 Void,就可以不返回任务执行结果。注意,在这个方法中是不可以进行 UI 操作的,如果需要更新 UI 元素,比如说反馈当前任务的执行进度,可以调用 publishProgress(Progress...)方法来完成。
  3. onProgressUpdate(Progress...)
    当在后台任务中调用了 publishProgress(Progress...)方法后,这个方法就会很快被调用,方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对 UI 进行操作,利用参数中的数值就可以对界面元素进行相应地更新。
  4. onPostExecute(Result)
    当后台任务执行完毕并通过 return 语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些 UI 操作,比如说提醒任务执行的结果,以及关闭掉进度条对话框等。

示例:

class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
  @Override
  protected void onPreExecute() {
    progressDialog.show(); // 显示进度对话框
  }
  @Override
  protected Boolean doInBackground(Void... params) {
    try {
      while (true) {
        int downloadPercent = doDownload(); // 这是一个虚构的方法
        publishProgress(downloadPercent);
        if (downloadPercent >= 100) {
          break;
        }
      }
    } catch (Exception e) {
      return false;
    }
    return true;
  }
  @Override
  protected void onProgressUpdate(Integer... values) {
    // 在这里更新下载进度
    progressDialog.setMessage("Downloaded " + values[0] + "%");
  }
  @Override
  protected void onPostExecute(Boolean result) {
    progressDialog.dismiss(); // 关闭进度对话框
    // 在这里提示下载结果
    if (result) {
      Toast.makeText(context, "Download                 succeeded",Toast.LENGTH_SHORT).show();
     } else {
      Toast.makeText(context, " Download failed",Toast.LENGTH_SHORT).show();
    }
  }
}

在这个 DownloadTask 中,我们在 doInBackground()方法里去执行具体的下载任务。这个方法里的代码都是在子线程中运行的,因而不会影响到主线程的运行。

注意这里虚构了一个doDownload()方法,这个方法用于计算当前的下载进度并返回,我们假设这个方法已经存在了。在得到了当前的下载进度后,下面就该考虑如何把它显示到界面上了.

由于doInBackground()方法是在子线程中运行的,在这里肯定不能进行 UI 操作,所以我们可以调用 publishProgress()方法并将当前的下载进度传进来,这样 onProgressUpdate()方法就会很快被调用,在这里就可以进行 UI 操作了。

当下载完成后, doInBackground()方法会返回一个布尔型变量,这样 onPostExecute()方法就会很快被调用,这个方法也是在主线程中运行的。然后在这里我们会根据下载的结果来弹出相应的 Toast 提示,从而完成整个 DownloadTask 任务。

简单来说,使用 AsyncTask 的诀窍就是,在 doInBackground()方法中去执行具体的耗时任务,在 onProgressUpdate()方法中进行 UI 操作,在 onPostExecute()方法中执行一些任务的收尾工作。如果想要启动这个任务,只需编写以下代码即可:

new DownloadTask().execute();

9.3服务的基本用法

9.3.1定义一个服务

定义一个类,继承Service.实现onBind,onCreate,onStartCommand//启动,onDestroy方法,

public class MyService extends Service {
  @Override
  public IBinder onBind(Intent intent) {
    return null;
  }
  @Override
  public void onCreate({
    super.onCreate();
  }
  @Override
  public int onStartCommand(Intent intent, int flags, int startId){
    return super.onStartCommand(intent, flags, startId);
  }
  @Override
  public void onDestroy() {
    super.onDestroy();
  }
}

要注册服务后才可使用:

9.3.2启动和停止服务.

启动:

Intent startIntent = new Intent(this, MyService.class);
startService(startIntent); // 启动服务

停止:

Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent); // 停止服务

9.3.3活动和服务进行通信

活动和服务之间的通信可以通过onBind方法进行

首先创建了一个 ServiceConnection 的匿名类,在里面重写onServiceConnected()方法和 onServiceDisconnected()方法,这两个方法分别会在活动与服务成功绑定以及解除绑定的时候调用。
在 onServiceConnected()方法中,我们又通过向下转型得到了 DownloadBinder 的实例,有了这个实例, 活动和服务之间的关系就变得非常紧密了。现在我们可以在活动中根据具体的场景来调用 DownloadBinder 中的任何 public 方法,即实现了指挥服务干什么,服务就去干什么的功能。 这里仍然只是做了个简单的测试, 在onServiceConnected()方法中调用了 DownloadBinder 的 startDownload()和 getProgress()方法。当然,现在活动和服务其实还没进行绑定呢,这个功能是在 Bind Service 按钮的点击事件里完成的。可以看到,这里我们仍然是构建出了一个 Intent 对象,然后调用 bindService()方法将 MainActivity 和 MyService 进行绑定。 bindService()方法接收三个参数,第一个参数就是刚刚构建出的 Intent 对象,第二个参数是前面创建出的 ServiceConnection 的实例,第三个参数则是一个标志位,这里传入 BIND_AUTO_CREATE 表示在活动和服务进行绑定后自动创建服务。 这会使得 MyService 中的 onCreate()方法得到执行,但 onStartCommand()方法不会执行。然后如果我们想解除活动和服务之间的绑定该怎么办呢?调用一下 unbindService()方法就可以了,这也是 Unbind Service 按钮的点击事件里实现的功能。

9.4服务的生命周期

前面我们使用到的 onCreate()、 onStartCommand()、 onBind()和 onDestroy()等方法都是在服务的生命周期内可能回调的方法。

一旦在项目的任何位置调用了 Context 的 startService()方法,相应的服务就会启动起来,并回调 onStartCommand()方法。如果这个服务之前还没有创建过, onCreate()方法会先于onStartCommand()方法执行。服务启动了之后会一直保持运行状态,直到 stopService()或stopSelf()方法被调用。注意虽然每调用一次 startService()方法, onStartCommand()就会执行一次,但实际上每个服务都只会存在一个实例。所以不管你调用了多少次 startService()方法,只需调用一次 stopService()或 stopSelf()方法,服务就会停止下来了。

另外,还可以调用 Context 的 bindService()来获取一个服务的持久连接,这时就会回调服务中的 onBind()方法。类似地,如果这个服务之前还没有创建过, onCreate()方法会先于onBind()方法执行。之后,调用方可以获取到 onBind()方法里返回的 IBinder 对象的实例,这样就能自由地和服务进行通信了。只要调用方和服务之间的连接没有断开,服务就会一直保持运行状态。

当调用了 startService()方法后,又去调用 stopService()方法,这时服务中的 onDestroy()方法就会执行,表示服务已经销毁了。类似地,当调用了 bindService()方法后,又去调用unbindService()方法, onDestroy()方法也会执行,这两种情况都很好理解。但是需要注意,我们是完全有可能对一个服务既调用了 startService()方法,又调用了 bindService()方法的,这种情况下该如何才能让服务销毁掉呢?根据 Android 系统的机制,一个服务只要被启动或者被绑定了之后,就会一直处于运行状态,必须要让以上两种条件同时不满足,服务才能被销毁。所以,这种情况下要同时调用 stopService()和 unbindService()方法, onDestroy()方法才会执行。

9.5服务的更多技巧

9.5.1使用前台服务

大部分服务都在后台运行,但服务的系统优先级较低,当内存不足时可能回收后台运行的服务,如果需要服务一直保持运行,可以使用前台服务.
前台服务会有一个正在运行的图标在系统的状态栏显示,下拉后可以看到更加详细的信息.
在Service的onCreate()中使用通知,最后使用startForeground()方法.该方法接受两个参数,一个是通知的ID,另一个接受构建的Notification对象.

public void onCreate(){
    Log.e("wyxjoker", "onCreate executed");
    super.onCreate();
    Intent notificationIntent = new Intent(this,MainActivity.class);   
    PendingIntent pendingIntent = PendingIntent.getActivity(this,0,notificationIntent,0);
    Notification notification = new Notification.Builder(this)
            .setAutoCancel(true)
            .setContentTitle("This is a title")
            .setContentText("This is content")
            .setContentIntent(pendingIntent)
            .setSmallIcon(R.mipmap.ic_launcher)
            .setWhen(System.currentTimeMillis())
            .build();
    startForeground(1,notification);
}

9.5.2使用IntentService

服务中的嗲吗都是默认运行在主线程中,如果服务里处理耗时操作,容易出现ANR(Application Not Responding).因此需要服务的每个具体方法里开启子线程,处理耗时操作.

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

Android提供了IntentService类用于创建异步,会自动停止的服务.
新建类,继承IntentService在onHandleIntent(Intent intent)中写逻辑.

public class MyIntentService extends IntentService {
  public MyIntentService() {
    super("MyIntentService"); // 调用父类的有参构造函数
  }
  @Override
  protected void onHandleIntent(Intent intent) {
    // 打印当前线程的id
    Log.d("MyIntentService", "Thread id is " +     Thread.currentThread().getId());
  }
  @Override
  public void onDestroy() {
    super.onDestroy();
    Log.d("MyIntentService", "onDestroy executed");
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容