Android开发学习 -- Day17-19 多线程&
Service(服务)是Android 中实现程序后台运行的解决方案,它非常适合去执行那些不需要和用户交互而且还要求长期运行的任务。服务的运行不依赖于任何用户界面,即使程序被切换到后台或者用户打开了另外一个应用程序,服务仍然能够保持正常运行。
不过需要注意的是,服务并不是运行在一个独立的进程当中的,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时, 所有依赖于该进程的服务也会停止运行。
另外,也不耍被服务的后台概念所迷惑,实际上服务并不会自动开启线程,所有的代码都是默认运行在主线程当中的。也就是说,我们需要在服务的内部手动创建子线程,并在这里执行具体的任务,否则就有可能出现主线程被阻塞住的情况。所以我们先来学习一下多线程:
一、Android多线程编程
当我们需要执行一些耗时的操作时,比如发送网络请求等,如果不将这类任务放到子线程中去时,很容易就会导致主线程被阻塞,从而影响用户对软件的正常使用。
1、线程的基本用法
Android多线程编程跟Java多线程编程类似,都使用相同的语法。例如,定义一个线程只需要新建一个类并继承自Thread,然后重写父类的run()方法,在里面添加耗时操作就可以了。或者使用内部类的写法:
new Thread(new Runnable() {
@Override
public void run() {
// 具体逻辑
}
}).start();
2、在子线程中更新UI
Android的UI线程是不安全的,如果想要更新程序里的UI,就必须在主线程中进行。不信的话可以试试如果在子线程更新UI会发生什么:
public class ServiceMulThreadActivity extends BaseActivity implements View.OnClickListener{
private TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_service_mul_thread);
setTitle("多线程编程");
Button crash = findViewById(R.id.service_multhread_crash);
tv = findViewById(R.id.service_tv);
crash.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.service_multhread_crash:
new Thread(new Runnable() {
@Override
public void run() {
tv.setText(R.string.change_text);
}
}).start();
break;
default:
break;
}
}
}
我们在crash按钮里面开启了一个子线程,然后在子线程中调用了TextView的setText()方法,将TextView默认显示的文案变成我们重新定义的文案,这里引用了string中的一个字符串。重新运行程序,点击按钮,一会发现程序立马崩溃了,通过logcat日志观察,可以知道crash来自于在子线程中更新UI。
但是有时候,我们需要在子线程做一些耗时操作,然后根据执行结果来更新UI控件,对于这种情况,Android提供了一套异步消息处理机制,解决了在子线程中进行UI操作的问题。修改代码:
public class ServiceMulThreadActivity extends BaseActivity implements View.OnClickListener{
public static final int UPDATE_TEXT = 1;
private TextView tv;
@SuppressLint("HandlerLeak")
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case UPDATE_TEXT:
tv.setText(R.string.change_text);
break;
default:
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_service_mul_thread);
setTitle("多线程编程");
Button crash = findViewById(R.id.service_multhread_crash);
Button btn_handler = findViewById(R.id.service_multhread_handle);
tv = findViewById(R.id.service_tv);
crash.setOnClickListener(this);
btn_handler.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
...
case R.id.service_multhread_handle:
new Thread(new Runnable() {
@Override
public void run() {
Message message = new Message();
message.what = UPDATE_TEXT;
handler.sendMessage(message);
}
}).start();
break;
default:
break;
}
}
}
首先我们定义了一个整型的常量UPDATE_TEXT用来表示更新TextView的动作。然后新增了一个Handler对象,并重写了父类的handleMessage()方法,在这里对具体的Message进行处理。如果发现Message的what字段为UPDATE_TEXT,那么就调用TextView的setText()方法来更新显示内容。
再来看点击事件内部逻辑,首先开启一个子线程,然后新建一个Message对象(android.os.Message),并将它的what字段值指定为UPDATE_TEXT,然后调用Handler的sendMessage()方法将这条Message发送出去。很快Handler就会收到这条Message,并在handlerMessage()方法中处理消息。此时,handlerMessage()方法中的代码是运行在主线程上,所以可以进行UI操作,当what字段值为UPDATE_TEXT,我们就开始更新UI。
这样就已经初步掌握了Android的异步消息处理的基本用法。下面在分析一下异步消息处理到底是如何工作的。
3、解析异步消息处理机制
Android中异步消息处理主要由4个部分组成:Message、Handler、MessageQueue、Looper。其中Message、Handler我们已经应用过了,MessageQueue、Looper则隐藏在它们之下。
□ Message:在线程间传递的消息,可以在内部携带少量的信息,用于在不同线程之间交换数据。我们之前使用到了Message的what字段,除此之外,还能使用arg1和arg2字段携带一些整型数据,使用obj字段携带一个Object对象。
□ Handler:它主要用于发送和处理消息。发送的消息一般是使用Handler的sendMessage()方法,发出去的消息经过一系列的辗转之后,最终会传递到Handler的handlerMessage()方法中。
□ MessageQueue:是消息队列,主要用于存放所有通过Handler发送的消息。这部分消息一直存在消息队列汇总,等待被处理。每个线程只会有一个MessageQueue对象。
□ Looper:是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入到一个无限循环之中,每当发现MessageQueue中存在一条消息,就会将它取出,并传递到Handler的handlerMessage()方法中。每个线程中也只会有一个Looper对象。
所以梳理下来,首先需要在主线程创建一个Handler对象,并重写handlerMessage()方法。然后当子线程需要更新UI时,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息会进入到MessageQueue中等待被处理,而Looper会一直尝试从MessageQueue取出待处理的消息,最后分发回Handler的handlerMessage()方法中。由于Handler是在主线程中创建的,所以handlerMessage()方法中的代码也是在主线程中运行,接下来就可以进行相应的UI操作了。
借用郭神的图我们在之前web的练习中使用过的runOnUIThread()方法,其实也是对异步消息处理机制的一个接口封装,它们背后的原理都是一样的:
private void showResponse(final String response) {
runOnUiThread(new Runnable() {
@Override
public void run() {
tv.setText(response);
}
});
}
4、使用AsyncTask
为了更方便的在子线程对UI进行操作,Android还提供了另外一些好用的工具,比如AsyncTask。借助AsyncTask,即使对异步消息处理机制完全不了解,也可以十分简单的从子线程切换到主线程。
AsyncTask是一个抽象类,要想使用它就得创建一个子类继承它。在继承时,我们可以为AsyncTask指定3个泛型参数<Params, Progress, Result>
□ Params:在执行AsyncTask时需要传入的参数,可用于后台任务中使用。
□ Progress:在后台任务执行时,如果需要在UI上显示进度,则使用这里指定的泛型作为进度单位。
□ Result:当任务执行完毕后,如果需要对结果进行返回,则使用这个指定的泛型作为返回值类型。
例如如下代码,这里我们把第一个泛型参数指定为String,代表执行AsyncTask时,传入String参数给后台任务。第二泛型参数指定为整型,表示用整型数据作为进度显示单位。第三个泛型参数指定为整型,表示当任务结束后,使用整型数据反馈执行结果。
public class YourSimpleTask extends AsyncTask<String, Integer, Integer> {
...
}
使用AsyncTask时,我们通常需要重写它几个的方法才能完成对任务的定制。
□ onPreExecute():@MainThread
这个方法在执行后台任务前调用,用于进行一些界面上的准备初始化操作,比如显示一个进度条对话框等
□ doInBackground(Params...):@WorkerThread
这个方法的所有代码都会在子线程中运行,我们在这里去执行耗时的任务。任务完成后,就会通过return语句将任务的执行结果返回,如果AsyncTask的第三个泛型参数指定为Void,则可以不用返回执行结果。由于这个这里是在子线程中运行,所以不能进行UI操作。如果需要更新UI元素,可以调用publishProgress(Progress...)方法来完成。
□ onProgressUpdate(Progress...):@MainThread
当在后台任务中调用了publishProgress(Progress...)方法后,onProgressUpdate(Progress...)会很快被调用,该方法中携带的参数就是后台任务中传递过来的。因为这个方法在主线程中运行,所以可以根据参数中的值来进行UI操作。
□ onPostExecute(Result...):@MainThread
当后台任务执行完毕并通过return语句返回时,这个方法就会很快被调用。返回的数据会作为参数传递到这个方法中,利用这些参数就可以进行相应的操作。因为也是在主线程中运行,可以操作UI控件,例如关闭进度条或者提醒任务结果等。
通过介绍我们能看出来,使用AsyncTask的诀窍就是,在doInBackground()方法执行具体的耗时操作,在onProgressUpdate()方法进行UI操做,在onPostExecute()进行任务的收尾工作。开启这个AsyncTask也很简单,调用execute()方法,或者executeOnExecutor()。
new YourSimpleTask().execute();
后面我们将在实践中贴出代码。
二、Service的基本用法
Service之前,我们已经掌握了Android四大组件中的3个:Activity、BroadcastReceiver和ContentProvider。跟以前的思路一样,从最基本的用法开始学习。
1、定义一个Service
我们可以通过Android Studio的快捷方式创建,New -> Service -> Service
Exported表示是否允许除了当前应用之外的其他应用程序访问这个服务,Enabled表示是否启动这个服务。都勾选后点击finish完成自动创建。
public class MyService extends Service {
public MyService() {
}
@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}
}
可以看到MyService继承自Service,说明这是一个服务。另外还有一个onBind()方法,这是Service中唯一的一个抽象方法,所以必须在子类中实现。记得我们之前说过,Android的四大组件都需要在Manifest中注册后才能使用,不过这一步Android Studio已经帮我们做好了。
创建了服务,我们还需要它去做一些事情,这时我们就可以重写Service中另一些方法了,继续修改代码:
public class MyService extends Service {
...
@Override
public void onCreate() {
super.onCreate();
Log.d("MyService", "onCreate executed");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d("MyService", "onStartCommand executed");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("MyService", "onDestroy executed");
}
}
这里我们重写了onCreate()、onStartCommand()、onDestroy()方法。其中onCreate()方法会在服务创建的时候调用,onStartCommand()会在每次启动服务的时候调用,onDestroy()方法在服务销毁的时候调用。
2、启动和停止服务
定义好服务后,接下来就是如何启动和停止服务了。方法还是我们熟悉的Intent,代码:
public class ServiceTestActivity extends BaseActivity implements View.OnClickListener{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_service_test);
setTitle("Service Test");
Button startService = findViewById(R.id.service_start);
Button stopService = findViewById(R.id.service_stop);
startService.setOnClickListener(this);
stopService.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.service_start:
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent);
break;
case R.id.service_stop:
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent);
break;
default:
break;
}
}
}
我们直接点击事件,开始服务我们先是构建一个Intent对象,然后调用startService()方法来启动MyService这个服务。停止服务也一样,先构建Intent对象,然后调用stopService()方法结束服务。我们之前已经在MyService中打下了日志,重新运行应用程序:
同时,当开启服务后,我们从系统的运行状态中也可以看到详细的信息:
3、Activity和Service进行通信
前面我们通过Activity启动Service后,二者就几乎没什么关系了,Activity也不知道Service都去干了些什么。那么如何让它们的关系更紧密一些呢,记得我们在创建Service的时候,有一个待实现的onBind()方法,现在我们就要借助它的力量了。
例如我们需要在MyService中提供一个下载功能,然后在Activity中可以决定什么时候开始下载,以及下载的进度如何,实现的思路就是创建一个专门的Binder对象来对下载功能进行管理,修改代码:
public class MyService extends Service {
private DownloadBinder mBinder = new DownloadBinder();
class DownloadBinder extends Binder {
public void startDownload() {
Log.d("MyService", "startDownload executed");
}
public int getProgress() {
Log.d("MyService", "getProgress executed");
return 0;
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
可以看到我们构建了一个DownloadBinder类并让它继承自Binder,然后内部提供了开始下载和下载进度的方法(这里只是模拟功能)。接着在MyService中创建DownloadBinder的实例,并在onBind()方法中返回该实例。这样MyService中的代码就完成了。
接下来修改Activity中的代码,在布局文件添加绑定服务按钮和解绑按钮:
public class ServiceTestActivity extends BaseActivity implements View.OnClickListener{
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
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;
default:
break;
}
}
}
我们先创建了一个ServiceConnection匿名类,里面重写了onServiceConnected()和onServiceDisconnected()方法,这两个方法会分别在Activity与Service绑定和解绑的时候调用。在onServiceConnected()中,我们通过向下转型得到了DownloadBinder实例,有了这个实例,Activity与Service的联系就紧密的多了。现在我们可以根据场景来使用DownloadBinder中任何public的方法了。
然后我们在点击事件中,仍然是先构建了Intent对象,然后调用bindService()方法。该方法接收3个参数,第一个参数是intent,传我们刚刚创建的Intent对象;第二个参数为ServiceConnection的实例;第三个参数是一个flag标志位,我们传入BIND_AUTO_CREATE表示Activity和Service绑定后自动创建Service。这样会使MyService中的onCreate()得到执行,而onStartCommand()不会执行。同样的,解绑需要调用unbindService,同样是传入ServiceConnection的实例。重新运行应用程序:
点击绑定服务后,就会发现MyService的onCreate()方法得到了执行,然后startDownload()和getProgress()也得到了执行。点击解绑按钮后,onDestroy()方法得到了执行。
4、Service的生命周期
之前我们学习过了活动以及碎片的生命周期。类似地,服务也有自己的生命周期,前面我们使用到的 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()方法才会执行。
这样,Service的生命周期我们就了解了。
关注获取更多内容