Android基础回顾(九)| Android多线程编程

参考书籍:《第一行代码》 第二版 郭霖
如有错漏,请批评指出!

Android多线程

在开发过程中,我们经常需要进行网络请求,而这一过程显然是一个耗时操作。如果直接在主线程(UI线程)中进行耗时操作,可能会导致主线程被阻塞,造成ANR(Application Not Responding)异常,因此,我们需要在子线程中进行耗时操作。那么,在Android中有哪些开辟子线程的方式呢?

1. 继承Thread类,重写run()方法
public class MyThread extends Thread{

    MyThread() {
    }

    @Override
    public void run() {
        //TODO
    }
}

使用方法:

MyThread thread = new MyThread();
thread.start();
2. 实现Runnable接口,并实现其run()方法
public class MyThread implements Runnable{

    MyThread() {
    }

    @Override
    public void run() {
        //TODO
    }
}

使用方法:

MyThread myThread = new MyThread();
new Thread(myThread).start();

上面两种方式都需要去定义一个类,不过在第二种种方式下,我们可以使用匿名类的方式来实现(这种方式也更加常见):

new Thread(new Runnable() {
    @Override
    public void run() {
        //TODO
    }
}).start();
3. 使用Handler
  • Handler配合sendMessage()使用
public class MainActivity extends AppCompatActivity {

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    //这里可以进行UI操作
                    break;
                default:
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        new Thread(new Runnable() {
            @Override
            public void run() {
                Message msg = new Message();
                msg.what = 1;
                handler.sendMessage(msg);
            }
        }).start();

    }
}
  • Handler配合post()方法使用
public class MainActivity extends AppCompatActivity {

    private Handler handler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        handler.post(new Runnable() {
            @Override
            public void run() {
                //这里可进行UI操作
            }
        });
    }
}

这两种方式都会将消息加入消息队列,如果是post() / sendMessage(),即将消息加入到消息队列末尾,当消息队列没有消息时会立即执行;如果是postDelayed() / sendMessageDelayed()或postAtTime() / sendMessageAtTime(),则会在延迟时间后执行。这两种方式中,方式二的优先级高于方式一。

解析异步消息处理机制

Android中的异步消息处理主要由4个部分组成:Message、Handler、MessageQueue、Looper。
1. Message
Message 是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。Message中包含四个字段,what、arg1、arg2这三个字段可以携带整型数据,obj字段可以携带一个Object对象。
2. Handler
Handler主要用于发送和处理消息,发送消息一般是使用Handler的sendMessage()方法,最终消息会传递到Handler的handleMessage()方法中。
3. MessageQueen
MessageQueen(即消息队列),主要用于存放所有通过Handler发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueen对象。
4. Looper
Looper是每个线程中的MessageQueen的管家,调用Looper的loop()方法后,就会进入一个无限循环,每当发现MessageQueen中存在消息,就会依次取出,并传递到Handler的handleMessage()方法中。每个线程也只会有一个Looper对象。
以上是基本概念,接下来梳理一下异步消息处理的流程:首先在主线程中创建一个Handler对象,并重写handleMessage方法。当子线程中需要进行UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息会被添加到MessageQueen的队列中等待被处理,而Looper会一直尝试从MessageQueen中取出待处理消息,然后分发回Handler的handleMessage()方法中,所以此时handleMessage()方法中的代码也会在主线程中运行。也就是说我们的UI操作实际上还是在主线程中进行的。
下面是异步消息处理机制流程图:

AsyncTask 的使用

AsyncTask(异步任务),是Android提供的一个处理异步任务的类,它的实现原理也是基于异步消息处理机制的,只是Android对其进行了很好的封装。下面来看看关于AsyncTask的基本用法。
AsyncTask是一个抽象类,因此,我们需要创建一个类来继承它。在继承时我们可以为AsyncTask类指定3个泛型参数,这三个参数的用途如下:
1. Params:在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。
2. Progress:后台任务执行中,如果需要在界面上显示当前的进度,则使用这里指定的泛型参数作为进度单位。
3. Result:当任务执行完成后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。
因此,一个简单的自定义AsyncTask可以写成下面这样:

  • public class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
    
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }
    
        @Override
        protected Boolean doInBackground(Void... voids) {
            return null;
        }
    
        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
        }
    
        @Override
        protected void onPostExecute(Boolean result) {
            super.onPostExecute(result);
        }
    }
    

这里我们将AsyncTask 的三个泛型参数依次指定为 Void、Integer、Boolean,因为后面我们会实现一个下载功能,因此我们需要返回实时进度(用整型作为进度显示单位),最后还要返回下载结果(用布尔型表示)。
为了实现这样的一个功能,我们还需要重写上面这四个方法。
1. onPreExecute():这个方法一般用来进行一些初始化操作,比如初始化对象,显示dialog等。
2. doInBackground(Params...):这个方法会在子线程中执行,因此,我们要在这里执行耗时任务,任务完成后就将执行结果返回,如果AsyncTask的第三个泛型参数指定为Void类型,就不需要返回。由于这个方法是在子线程中执行,因此我们不能在这里进行UI操作(比如更新下载进度),不过我们可以通过下面这个方法来实现。
3. onProgressUpdate(Progress...):在后台任务中调用 publishProgress(Progress...) 这个方法后,onProgressUpdate(Progress...) 方法就会被调用,这个方法中的参数,就是在 doInBackground(Params...) 方法中调用 publishProgress(Progress...) 方法时传递的参数,利用参数中的数值可以进行更新界面中的进度条。
4. onPostExecute(Result):当后台任务执行完并通过return语句返回数据时,这个方法就会被调用。返回的数据会作为参数传递到这个方法中,可以在这里进行一些关于后台任务执行结果的处理,比如说提示任务执行情况,关闭dialog等。
上面的自定义AsyncTask我们会在后面进行完善。下面先来看一下关于Service的内容。

服务的基本用法

作为Android四大组件之一,Service 是一个可以在后台执行长时间运行操作而不提供用户界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。 此外,组件可以绑定到服务,以与之进行交互,甚至是执行进程间通信 (IPC)。 例如,服务可以处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序交互,而所有这一切均可在后台进行。

1.服务的定义

AndroidStudio提供了自动创建Service的方法, 打开项目之后,点击 File->New->Service->Service,就可以创建自己的Service了。其中有两个属性:
Exported 表示是否允许除了当前程序之外的其他程序访问这个服务;
Enabled 表示是否启用这个服务。
这两个属性是默认勾选的,保持不变就行。
创建完成后,我们会发现,其实就是定义了一个类,继承了Service类,但是还有一点,那就是AS自动在AndroidManifest文件中对这个Service进行了注册:

这是Android四大组件的共性,都需要在AndroidManifest文件中进行注册,也是我们手动创建Service时需要注意的事情。
在我们创建的MyService里面,系统已经自动为我们添加了一个构造方法和一个onBind() 方法,因为onBind()方法是Service类中的一个抽象方法,因此我们必须重写(这里只是创建了一个空的服务,什么逻辑度没有写)。下面先来看看关于Service生命周期的内容,后面再看Service如何使用。

2. Service的生命周期

先来看一张图:

根据上图可以看到,服务基本上分两种方式,并且它们的的生命周期是不同的。

  • 启动
    当应用组件(如 Activity)通过调用 startService() 启动服务时,服务即处于“启动”状态。一旦启动,服务即可在后台无限期运行,即使启动服务的组件已被销毁也不受影响。 直到 stopService() 方法(组件调用)或 stopSelf() 方法(Service自身调用)被调用,服务停止并回调onDestroy()方法销毁,或由于系统资源不足而被回收。需要注意的是,服务只会存在一个实例,因此无论调用多少次 startService() 方法,只需要调用一次 stopService() 方法或 stopSelf() 方法,服务就会停止。
    调用 startService() 方法启动服务时,生命周期是上图中的第一种,并且 onCreate() 方法只会回调一次,在服务已经被创建的情况下,再次调用 startService() 方法,会回调 onStartCommand() 方法。下面来打印生命周期看看:
    在前面创建的MyService中重写所有的生命周期,添加Log打印。

    public class MyService extends Service {
        public MyService() {
        }
    
        @Override
        public IBinder onBind(Intent intent) {
            Log.d("jh", "onBind()");
            throw new UnsupportedOperationException("Not yet implemented");
        }
    
        @Override
        public boolean onUnbind(Intent intent) {
            Log.d("jh", "onUnbind()");
            return super.onUnbind(intent);
        }
    
        @Override
        public void onCreate() {
            super.onCreate();
            Log.d("jh", "onCreate()");
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            Log.d("jh", "onStartCommand()");
            return super.onStartCommand(intent, flags, startId);
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
            Log.d("jh", "onDestroy()");
        }
    }
    

    然后在Activity中添加两个Button并添加点击事件,启动和停止服务:

        case R.id.but_start:
            intent = new Intent(this, MyService.class);
            startService(intent);
            break;
        case R.id.but_stop:
            intent = new Intent(this, MyService.class);
            stopService(intent);
            break;
    

    运行demo,点击三次 StartService Button,点击两次 StopService Button,下面是log:

    由此可以知道,当我们首次创建Service时,会回调onCreate()方法和onStartCommand()方法,在Service运行过程中,再次调用startService() 方法,只会回调onStartCommand()方法。当我们调用 stopService()方法或stopSelf() 方法时,会回调onDestroy()方法销毁服务。

  • 绑定
    当应用组件通过调用 bindService() 绑定到服务时,服务即处于“绑定”状态。绑定服务提供了一个客户端-服务器接口,允许组件与服务进行交互、发送请求、获取结果,甚至是利用进程间通信 (IPC) 跨进程执行这些操作。 仅当与另一个应用组件绑定时,绑定服务才会运行。 多个组件可以同时绑定到该服务,但全部取消绑定(unBindService()方法)后,该服务即会被销毁。
    仅仅通过startService() 方法启动服务后,Activity与服务之间是基本没有什么联系的,自然也无法进行通信。如果要实现Activity与Service之间进行通信,我们就需要借助 onBind() 方法将Activity绑定到Service了。下面我们通过前面提到过的下载功能的思路来实现Activity与Service之间的通信。想要实现Activity与Service之间的通信,我们需要借助Binder对象。

    1. 首先,在服务MyService中创建一个DownLoadBinder内部类,并继承Binder类。
    public class MyService extends Service {
    
        private DownloadBinder mBinder;
    
        public MyService() {
            mBinder = new DownloadBinder();
        }
    
        @Override
        public IBinder onBind(Intent intent) {
            Log.d("jh", "onBind()");
            return mBinder;
        }
    
        ···
    
        public class DownloadBinder extends Binder {
            public void startDownliad() {
                Log.d("jh", "startDownload()");
            }
    
            public int getProgress() {
                Log.d("jh", "getProgress()");
                return 0;
            }
        }
    }
    

    我们可以在Activity中获取到这个DownloadBinder对象,通过这个 DownloadBinder 对象来对Service中的任务进行控制。下面来看Activity的代码:

    public class SerMainActivity extends AppCompatActivity {
    
        private MyService.DownloadBinder mDownloadBinder;
    
        private ServiceConnection connection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                Log.d("jh", "onServiceConnected()");
                mDownloadBinder = (MyService.DownloadBinder) service;
                mDownloadBinder.startDownliad();
                mDownloadBinder.getProgress();
            }
    
            @Override
            public void onServiceDisconnected(ComponentName name) {
                Log.d("jh", "onServiceDisconnected()");
            }
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.ser_activity_main);
            ButterKnife.bind(this);
        }
    
        @OnClick({R.id.but_start, R.id.but_stop, R.id.but_bind, R.id.but_unbind})
        public void click(View v) {
            Intent intent;
            switch (v.getId()){
    
                ···
    
                case R.id.but_bind:
                    intent = new Intent(this, MyService.class);
                    bindService(intent, connection, BIND_AUTO_CREATE);
                    break;
                case R.id.but_unbind:
                    unbindService(connection);
                    break;
                default:
                    break;
            }
        }
    }
    

    从上面的代码可以看到,当我们bind服务的时候,需要传递一个ServiceConnection对象。方便起见,我们创建一个ServiceConnection的匿名类,在里面重写 onServiceConnected() 和 onServiceDisconnected() 方法。onServiceConnection() 方法会在服务成功绑定的时候被调用。这里需要注意:只有当onBind()方法返回IBinder对象时,这个方法才会被调用,如果onBind()方法返回空,这个方法就不会被调用。这个方法有一个IBinder参数,也就是unBind()方法返回的那个IBinder对象,所以在这里我们需要向下转型,将这个IBinder对象类型强制转换为我们定义的DownloadBinder对象,然后在这个方法里面对Service中执行的任务进行控制,比如说开始下载任务、获取下载进度等(当然,这里定义的这两个方法都是空方法,还没有实现)。
    bindService() 方法的第三个参数是一个标志位,这里我们传入 BIND_AUTO_CREATE ,表示在活动和服务进行绑定后自动创建服务,这会使得MyService 中的 onCreate() 方法得到执行,而 onStartCommand() 方法不会执行。
    下面运行看看:
    bindService() -> unBindService()

    从上面的log可以看出,当我们将一个Activity绑定到Service时,如果Service还未创建,会先回调onCreate() 方法创建服务,然后回调onBind()方法,如果这个onBind()方法返回一个IBinder对象,在绑定完成后就会调用onServiceConnected()方法,这个方法是在Activity中实现的,可以通过这个方法来管理Service;然后,当任务执行完后,我们调用unBindService()方法,会依次回调onUnbind()方法解绑,回调onDestroy()方法销毁服务。

    startService() -> bindService() -> unBindService() -> stopService()

    这里我们创建启动Service后,又调用bindService()方法将Activity绑定到Service,一直到这里,生命周期并没有什么特殊,但是当我们unBindService()时,只是回调onUnbind()方法将Service解绑,并没有销毁服务,直到我们stopService(),服务才被销毁,其实就是 onStartCommand()方法被回调的原因,只要回调了onStartCommand()方法,服务就会一直在后台运行,除非我们调用onStopService()方法或stopSelf()方法将其销毁或者服务被系统杀死。

    bindService() -> startService() -> stopService() -> unBindService()

    这个执行顺序和上面又有一些不同,当我们绑定服务并且回调onStartCommand()方法后,直接调用onStopService()方法,会发现没有哪个生命周期方法回调,这说明服务还在后台运行,其实这也很好理解,因为有一个Activity还与Service绑定着,所以服务会等待这个Activity去和它解绑,当我们调用unBindService()方法解绑时,服务和Activity解绑后还会回调onDestroy()方法销毁服务。

    个人理解:onStartCommand()方法会使服务一直在后台运行,并且只有onStopService()方法和stopSelf()方法可以让服务停下来(由于系统资源不足导致Service被kill的情况除外);而仅仅将服务与别的组件绑定时,一旦所有组件都与Service解绑,服务就会被销毁。

3. 关于前台服务

我们知道,服务几乎都是在后台运行的,但是服务的系统优先级还是比较低的,因此,当系统内存不足时,就有可能会回收掉后台运行的服务。但是有时候我们需要用服务执行一些很重要的任务,比如说播放音乐,这时就可以使用前台服务了。
前台服务其实是通过构建一个通知,显示在下拉状态栏,就像我们平时使用的音乐播放器,当我们播放音乐时,就会在状态栏创建一个通知,用来控制音乐的播放。下面来看看如何创建一个前台服务:

  • @Override
    public void onCreate() {
        super.onCreate();
        Log.d("jh", "onCreate()");
        Intent intent = new Intent(this, SerMainActivity.class);
        PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
        Notification notification = new NotificationCompat.Builder(this, "default")
                .setLargeIcon(BitmapFactory.decodeResource(getResources(), 
                              R.drawable.ic_huaji))
                .setSmallIcon(R.mipmap.ic_android)
                .setContentTitle("第一行代码")
                .setContentText("这是一个前台服务")
                .setContentIntent(pi)
                .build();
        startForeground(1, notification);
    }
    

    只需要重写onCreate()方法就行了,构建一个Notificaton对象后,没有通过NotificationManager来将通知显示出来,而是调用setForeground()方法,这个方法接收两个参数,第一个参数是通知的id,第二个参数就是Notification对象。这样,我们的Service就是前台服务了。看下效果:
4.使用IntentService

我们知道,服务中的代码都是默认运行在主线程中的,如果我们处理一些耗时的逻辑,就容易出现ANR异常,因此,我们需要在每个具体的方法中开启一个子线程,用来执行耗时操作,并且需要我们调用stopService()方法或者stopSelf()方法,来停止服务。为了可以简单的创建一个异步的、会自动停止的服务,Android提供了一个IntentSrvice类,下面我们来创建一个IntentService来看看它和Service到底有什么不同:
File -> New -> Service -> Service(IntentService)
创建一个MyIntentservice

  • public class MyIntentService extends IntentService {
    
        public MyIntentService() {
            super("MyIntentService");
        }
    
        @Override
        protected void onHandleIntent(Intent intent) {
            Log.d("jh", "Thread id:" + Thread.currentThread().getId());
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
            Log.d("jh", "onDestroy()");
        }
    }
    

onHandleIntent()方法是一个抽象方法,我们必须实现,这个方法是运行在子线程中的,因此,我们可以直接在这个方法里处理一些耗时逻辑,我们来打印这个方法所在线程的id,然后重写onDestroy()方法并打印。
下面我们还需要添加一个Button用来开启这个IntentService,并给它添加点击事件:

  • case R.id.but_start_intent_service:
        Log.d("jh", "Thread id:" + Thread.currentThread().getId());
        intent = new Intent(this, MyIntentService.class);
        startService(intent);
        break;
    

    我们在Activity中开启这个IntentService,并且打印出Activity所在线程的id,用于和IntentService对比,下面来看运行之后的log:

可以看到,打印出来的两个线程id是不同的,activity运行在id为1的主线程中,也就是说,IntentService中是默认会开启子线程的,并且当任务执行完成后,onDestroy()方法也自动回调了,这就说明IntentService确实集自动开启子线程和自动停止于一身,并且用法和Service大同小异,可以说是Service的加强版了。

关于Service的基本内容就到这里,后面会另写一篇博客,实现前面提到的下载功能,作为服务的实践。


上一篇:Android基础回顾(八)| 使用HTTP协议访问网络


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