Android进阶——正确选择Android后台处理方案,你需要了解的一些知识总结

引言

每个Android应用程序都有一个主线程负责处理UI(包括测量和绘图视图),协调用户交互以及处理生命周期事件。如果主线程上处理过多工作就会导致应用程序会响应缓慢甚至挂起触发ANR,带给用户极其不好的体验。所以任何长期(Google官方推荐的是超过a few milliseconds)运行的计算和操作(例如解码位图,访问磁盘或执行网络请求)都应该在单独的后台线程上独立完成,为了高效运行后台任务且尽最大程度减少电池的消耗,有一些知识你需要去了解一下。

一、后台处理概述

通常Android的活动(Activities)可以有多种状态,具体取决于用户的行为以及对操作系统的要求。虽然Activity的生命周期会随着状态的变化而改变,比如创建活动、转入后台、回到前台、销毁活动等,但是实际上Acivity并不能在后台做任何实质性的UI交互处理。而很多情况下并不仅仅是简单的交互,更多的是依赖与一些网络操作或复杂的运算来配合完成或者在Activity退至幕后的时候,也需要完成一些工作,如果把这些全部交给主线程的话是极其不可取的,于是引入后台处理机制。在目前现有Android 后台处理机制有:Service(前台服务、后台服务)线程池WorkManager等,而针对不同类型的后台任务你需要选择不同的方案,通常在选择使用后台处理某项工作时,Google推荐从三个因素考量:

  • 此项工作是否可以延迟或者需要在预定时间内完成(Can the work be deferred, or does it need to happen exactly when scheduled?), 如果需要从网络获取一些数据以响应用户单击按钮,则必须立即完成该工作;但是如果想将日志上传到服务器,那么可以推迟这项工作,因为这不会影响应用程序的性能或用户期望。

  • 此项工作是否需要采取保活措施长存于系统中(Once the work starts executing, should the OS try to keep the app process alive?),比如对于解码和显示图片来说解码和显示位图只需要在应用程序处于前台并且进程处于活动状态时运行;但是对于地图定位、音乐播放来说即使应用程序处于后台并且未被主动使用也需要持续运行。

  • 此项工作是否为了响应系统某种机制(网络状态,电池状态,存储级别、开关机等等)而触发运行的(Does the work start in response to system triggers?),例如退出飞行模式后马上与服务器通信,在这种情况下,如果应用程序进程已死,你需要重新启动并完成通信

这里写图片描述

如上图所示,简单对比了各种方案的特点及使用场景。

一、线程池ThreadPool

线程池ThreadPool适用于当且仅当应用程序处于前台时才需要完成的工作。线程池机制会提供一组后台线程来接收并把提交的工作保存到阻塞队列中,按照线程池的机制进行处理。但如果需要在此期间监视系统触发器并完成一项工作,请使用动态注册的广播接收器来监视操作系统状态和触发器。欲了解线程池的更多知识请移步另一篇文章。

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 3; i++) {
            final int index = i;
            
            cachedThreadPool.execute(new Runnable() {
         
                public void run() {
                    try {
                        Thread.sleep(index * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(index+"一"+"time:"+formatDate("HH:mm:ss:ms",new Date(System.currentTimeMillis())));
                }
            });
        }

二、服务Service

服务作为Android的四大组件之一,服务Service是不提供用户界面却可以在后台执行长时间运行的应用程序组件。可以被另一个应用程序组件启动,而且即使用户切换到其他应用程序,它仍然可以在后台运行,此外,组件还可以绑定到服务上并与之交互,甚至实现进程间通信。 服务默认由主线程创建并且运行在主线程中(默认情况下它既不创建自己的线程,也不在单独的进程中运行),所以使用服务的时候需要避免处理耗时过长的工作(超过20s有可能导致ANR)。

1、后台服务

按照启动方式的不同可以把服务分为:

1.1、启动服务startService

当应用组件(如 Activity)通过调用 startService() 启动服务时,服务即处于“启动”状态。一旦启动,服务即可在后台无限期运行,即使启动服务的组件已被销毁也不受影响。 已启动的服务通常是执行单一操作,而且不会将结果返回给调用方。启动服务由另一个组件通过**调用 startService() 启动,并触发服务的 onStartCommand() **方法,并且返回的是整数型的固定常量之一:

  • START_NOT_STICKY——在 onStartCommand() 返回后终止服务,则除非有挂起 Intent 要传递,否则系统不会重建服务。这是最安全的选项,可以避免在不必要时以及应用能够轻松重启所有未完成的作业时运行服务。
  • START_STICKY——在onStartCommand() 返回后终止服务,则会重建服务并调用 onStartCommand(),但不会重新传递最后一个 Intent。反之除非有挂起 Intent 要启动服务(在这种情况下,将传递这些 Intent ),否则系统会通过空 Intent 调用 onStartCommand()。适用于不执行命令、但无限期运行并等待作业的媒体播放器(或类似服务)
  • START_REDELIVER_INTENT——在 onStartCommand() 返回后终止服务,则会重建服务,并通过传递给服务的最后一个 Intent 调用 onStartCommand(),任何挂起 Intent 均依次传递。这适用于主动执行应该立即恢复的作业的服务,例如下载文件

服务启动之后,其生命周期就独立于启动它的组件,并且可以在后台无限期地运行,即使启动服务的组件已被销毁也不受影响(不被系统回收的情况下)。如果需要停止服务应通过调用 stopSelf() 结束工作来自行停止运行,或者由另一个组件通过调用 stopService() 来停止它

1.2、绑定服务bindService

当应用组件通过调用 bindService() 绑定到服务时,服务即处于“绑定”状态。绑定服务提供了一个客户端-服务器接口,允许组件与服务进行交互、发送请求、获取结果,甚至是利用进程间通信 (IPC) 跨进程执行这些操作。 仅当与另一个应用组件绑定时,绑定服务才会运行, 多个组件可以同时绑定到该服务,在全部取消绑定后,该服务就会被销毁。通过调用bindService() 来创建服务且未调用 onStartCommand(),则服务只会在该组件与其绑定时运行。一旦该服务与所有客户端之间的绑定全部取消,系统便会销毁它。否则仅当内存过低且必须回收系统资源以供具有用户焦点的 Activity 使用时,Android 系统才会强制停止服务。如果将服务绑定到具有用户焦点的 Activity,则它不太可能会终止;而如果将服务声明为在前台服务,则它几乎永远不会终止(事实这也不是绝对仅仅是后台服务保活的一个手段)。如果服务已启动并要长时间运行,则系统会随着时间的推移会降低服务在后台任务列表中的位置,而服务也将随之变得非常容易被终止;如果服务是启动服务,则必须考虑妥善处理系统对它的重启;如果系统终止服务,那么一旦资源变得再次可用,系统便会重启服务(取决于从 onStartCommand() 返回的值)。为了确保应用的安全性,请始终使用显式 Intent 启动或绑定 Service,且不要为服务声明 Intent 过滤器。虽然分开概括这两种形式启动服务,但是服务是可以同时以这两种方式运行,即它既可以是启动服务(以无限期运行),也允许绑定,关键区别于是否实现了对应的回调方法:onStartCommand()(允许组件启动服务)和 onBind()(允许绑定服务)。其实无论应用是处于启动状态还是绑定状态,抑或处于启动并且绑定状态,任何应用组件均可像使用 Activity 那样通过调用 Intent 来使用服务(即使此服务来自另一应用), 不过,可以通过清单文件将服务声明为私有服务( android:exported 属性并将其设置为 "false"),并阻止其他应用访问,增强安全性。

1.3、IntentService

IntentService 是继承自 Service 并处理异步请求的一个类,可以通过startService(Intent)方法传递请求给IntentService,并在onCreate()方法里通过HandlerThread单独开启一个线程来处理所有Intent请求对象(通过startService的方式发送过来的)所对应的耗时工作,如果启动 IntentService 多次,那么每一个耗时操作会以工作队列的方式在 IntentService 的 onHandleIntent 回调方法中执行,依次去执行,执行完自动结束。这样来避免事务处理阻塞主线程。执行完所一个Intent请求对象所对应的工作之后,如果没有新的Intent请求达到,则自动停止Service;否则执行下一个Intent请求所对应的任务。IntentService在采用的Handler方式(** Handler、Looper、MessageQueue 机制)处理事务时,创建一个名叫ServiceHandler的内部Handler,并把它直接绑定到HandlerThread所对应的子线程 ,ServiceHandler把处理一个intent所对应的事务都封装到onHandleIntent**的虚方法,因此我们直接实现虚方法onHandleIntent,再在里面根据Intent的不同进行不同的事务处理就可以了,总结起来IntentService执行了以下操作:

  • 创建默认的工作线程,用于在应用的主线程外执行传递给 onStartCommand() 的所有 Intent。
  • 创建工作队列,用于将 Intent 逐一传递给 onHandleIntent() 实现,这样您就永远不必担心多线程问题。
  • 在处理完所有启动请求后停止服务,因此您永远不必调用 stopSelf()。
  • 提供 onBind() 的默认实现(返回 null)。
  • 提供 onStartCommand() 的默认实现,可将 Intent 依次发送到工作队列和 onHandleIntent() 实现。
static class MyIntentService extends IntentService {
    public MyIntentService() {
        super("MyIntentService"); 
    }
    @Override
    protected void onHandleIntent(Intent intent) {
        //执行耗时操作
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}
    
//使用IntentService
Intent intentService = new Intent(this, MyIntentService.class);
startService(intentService);

2、前台服务

前台服务被认为是用户主动意识到的一种服务,因此在内存不足时,系统也不会考虑将其终止。适用于必须执行完成的工作,如果需要立即执行工作,请使用前台服务。使用前台服务告诉系统,该应用程序正在做一些重要的事情,他们不应该被杀死。通常设置为前台服务都应该在状态栏下提供提供通知,这意味着除非服务停止或从前台移除,否则不能清除通知(如果不想显示通知,只要把参数里的int设为0即可)。例如,应该将通过服务播放音乐的音乐播放器设置为在前台运行,这是因为用户明确意识到其操作,状态栏中的通知可能表示正在播放的歌曲,并允许用户启动 Activity 来与音乐播放器进行交互要请求让服务运行于前台,本质上前台服务也是Service和普通后台服务区别在于,通过startService启动服务之时在onStartCommand中回调中的实现,在onStartCommand方法中调用 startForeground()方法就是前台服务

public class MyForgroundService extends Service {

    @Override
    public void onCreate() {
        Log.d("CircleView", "onCreate方法当服务被创建时调用");
        super.onCreate();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("CircleView", "onStartCommand方法当服务被创建时调用");
        // 在API11之后构建Notification的方式
        Notification.Builder builder = new Notification.Builder(this.getApplicationContext()); //获取一个Notification构造器
        Intent nfIntent = new Intent(this, MainActivity.class);
        builder.setContentIntent(PendingIntent.
                getActivity(this, 0, nfIntent, 0)) // 设置PendingIntent
                .setLargeIcon(BitmapFactory.decodeResource(this.getResources(),
                        R.mipmap.ic_lion)) // 设置下拉列表中的图标(大图标)
                .setContentTitle("下拉列表中的Title") // 设置下拉列表里的标题
                .setSmallIcon(R.mipmap.ic_launcher) // 设置状态栏内的小图标
                .setContentText("要显示的内容") // 设置上下文内容
                .setWhen(System.currentTimeMillis()); // 设置该通知发生的时间

        Notification notification = builder.build(); // 获取构建好的Notification
        notification.defaults = Notification.DEFAULT_SOUND; //设置为默认的声音
        startForeground(1,notification);
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        stopForeground(true);
    }
}
//启动服务
startService(new Intent(MainActivity.this,MyForgroundService.class));

要从前台移除服务,请调用 stopForeground(),此方法采用一个布尔值,指示是否也移除状态栏通知。但此方法不会停止服务(需要调用stopSelf方法来停止)。如果您在服务正在前台运行时将其停止,则通知也会被移除。

三、WorkManager

适用于必须执行完成且可延期的工作,WorkManager是一个Android库,当一些任务因为系统的一些触发器需要运行时(如适当的网络状态和电池条件),它可以优雅地运行可延迟的后台工作。尤其是在Android 5.0(API级别21)之后,Google建议使用诸如JobScheduler之类的API来执行后台任务,您可以通过注册作业并指定其网络和时间要求来使用JobScheduler,系统会在适当的时候优雅地安排作业执行。 而且JobScheduler提供许多方法来定义服务执行条件。英国尽量多使用该框架,以帮助优化电池寿命和批量作业。当在Android 6.0(API级别23)以下的设备上,WorkManager尝试使用 Firebase JobDispatcher( 如果它已经是应用程序的附属依赖项); 否则WorkManager将返回到自定义AlarmManager 实现以优雅地处理您的后台工作。

1、JobScheduler机制概述

JobScheduler是Android L(API21)新增的机制,用于定义满足某些条件下执行的任务。它的宗旨是把一些不是特别紧急的任务放到更合适的时机批量处理,这样可以有效的避免频繁的唤醒硬件模块,造成不必要的电量消耗,同时避免在不合适的时间(例如低电量情况下、弱网络或者移动网络情况下的)执行过多的任务消耗电量,因为在唤醒设备、发送数据以及接受数据的瞬间都会造成大量的电量消耗,同时系统为了你的下一次网络请求不用再次唤醒设备,会等待一段时间再让设备进入休眠。但如果你的请求是间歇性的,那么这些等待休眠的时间内造成的电量消耗其实也是多余的。基于此,JobSchedule机制应运而生——一个很好的优化方案就是将这些间歇性的网络请求任务推迟到某个特定的时间点(“时间点”是指的根据设备当前状况选择合适的时机)来集中处理。

2、JobScheduler机制使用场景

  • 可以推迟的非面向用户的任务(如定期数据库数据更新)
  • 当充电时才希望执行的工作(如备份数据)
  • 需要访问网络或 Wi-Fi 连接的任务(如向服务器拉取内置数据)
  • 希望作为一个批次定期运行的许多任务

3、符合JobScheduler启动的条件

代码上所需做的工作是将相关任务放置到JobService, 并根据应用情况设置Service启动条件, 可以是如下条件:

  • 当设备充电时启动
  • 当设备连接到不限流量网络(wifi)时启动
  • 当设备空闲时启动
  • 在特定的截止期限之前或以最小的延迟完成

只有当条件得到满足, 系统才会启动计划中的任务.只要一切执行到位, 对手机续航的提高当然显而易见.

4、JobScheduler机制的核心角色

JobScheduler机制其实可以简单理解成数据库中作业触发器的机制,先定义一个在数据库中定义一个触发器和对应的作业,然后当满足触发器条件的时候自动触发执行作业里的工作,JobScheduler机制也大同小异,核心角色有三:JobInfoJobServiceJobScheduler

4.1、JobInfo

本质上就是一个序列化的Buidler模式的JavaBean,是用于配置JobScheduler 启动的条件及时机等信息。如果类比Http通信的话 这个就是描述HttpRequest,总之想创建定时任务时,可以使用JobInfo.Builder来构建一个JobInfo对象,然后传递给你的JobService。

4.1、JobService

用于接收JobInfo的Service,当我们配置的任务被启动时候,会自动触发JobService里的回调方法onStartJob(JobParameters),是提供给我们处理先前调度的异步请求的基类。

4.1、JobScheduler

一个系统Service 用于给我们发送、启动、停止和删除所有任务。

5、JobScheduler的简单使用

  • 继承JobService重写onStartJob和onStopJob方法实现具体的任务逻辑
public class CrazyJobService extends JobService {
    private final static String TAG = "CrazyMo_";

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand");
        return START_STICKY;
    }

    @Override
    public boolean onStartJob(JobParameters params) {
        //当作业被启动时候自动回调这个方法,所以这里就是你的实现所有业务逻辑的用武之地
        Log.d(TAG, Thread.currentThread().getName() + "onStartJob");
        //jobFinished(param, true); 手动调用停止当前JobScheduler
        //返回false表示执行完毕,返回true表示需要开发者自己调用jobFinished方法通知系统已执行完成
        return true;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        /**停止,不是结束。jobFinished不会直接触发onStopJob
         *必须在“onStartJob之后,jobFinished之前”取消任务,才会在jobFinished之后触发onStopJob
         */
        Log.d(TAG, Thread.currentThread().getName() + "onStopJob");
        return true;
    }
}

  • 在清单文件中声明对应的JobService并指定权限
<service
            android:name=".service.CrazyJobService"
            android:permission="android.permission.BIND_JOB_SERVICE"/>
  • 构造JobInfo,配置对应的条件

  • 通过Context.getSystemService(JobScheduler.class)或者 Context.getSystemService(Context.JOB_SCHEDULER_SERVICE) 获取对应的JobScheduler实例

  • 调用JobScheduler实例操作

public class MainActivity extends AppCompatActivity {
    public final static String TAG="CrazyMo_";
    private final JobScheduler jobScheduler= (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void direStartJob(View view) {
        scheduleJob();
    }

    public void stopJobService(View view) {
        jobScheduler.cancelAll();
        //jobScheduler.cancel(jobId);
    }

    //将任务作业发送到作业调度中去
    public void scheduleJob(){

        JobInfo.Builder builder = new JobInfo.Builder(0,new ComponentName(this, CrazyJobService.class));//绑定到对应的服务,必须是Service类型的,否则报 java.lang.IllegalArgumentException: No such service
        builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); //设置需要的网络条件,默认为JobInfo.NETWORK_TYPE_NONE即无网络时执行,还有很多常量分别代表不同的条件
        //builder.setPersisted(true); //重启后是否还要继续执行
        builder.setRequiresCharging(false); //是否在充电时执行
        builder.setRequiresDeviceIdle(false); //是否在空闲时执行
        //builder.setPeriodic(1000); //设置时间间隔,单位毫秒
        /**
        *注意setPeriodic不能和setMinimumLatency、setOverrideDeadline这两个同时调用
        *否则会报错“java.lang.IllegalArgumentException: Can't call setMinimumLatency() on a periodic job”
        *“java.lang.IllegalArgumentException: Can't call setOverrideDeadline() on a periodic job”
        */
        builder.setMinimumLatency(500); //设置至少延迟多久后执行,单位毫秒
        builder.setOverrideDeadline(3000); //设置最多延迟多久后执行,单位毫秒
        JobInfo jobInfo = builder.build();//2、创建JObInfo
        JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);//3、获取JobScheduler 系统服务
        jobScheduler.schedule(jobInfo);
    }
}

一般说来当调用了schedule方法发送到作业之后,系统就会自动缓存对应的作业,如果没有自己调用移除任务的方法,当满足条件的时候就会自动周期性触发,但是这是理想情况,实际应用中可能很久都不能触发,这都取决于ROM的机制。


这里写图片描述
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 【Android Service】 Service 简介(★★★) 很多情况下,一些与用户很少需要产生交互的应用程...
    Rtia阅读 3,109评论 1 21
  • 转载注明出处:http://www.jianshu.com/p/a1d3d9693e91 1. 简介 与前一篇An...
    王三的猫阿德阅读 1,851评论 1 9
  • 前言:本文所写的是博主的个人见解,如有错误或者不恰当之处,欢迎私信博主,加以改正!原文链接,demo链接 Serv...
    PassersHowe阅读 1,335评论 0 5
  • 服务基本上分为两种形式 启动 当应用组件(如 Activity)通过调用 startService() 启动服务时...
    pifoo阅读 1,202评论 0 8
  • 2.1 Activity 2.1.1 Activity的生命周期全面分析 典型情况下的生命周期:在用户参与的情况下...
    AndroidMaster阅读 2,972评论 0 8