Android 异步任务和消息机制面试题分析
作者:星邪Ara
1.1 HandlerThread 的使用场景和用法?
HandlerThread 本质上是一个在子线程的handler (HandlerThread=Handler+Thread);
它的使用:
步骤1:创建HandlerThread实例对象
HandlerThread mHandlerThread = new HandlerThread("handlerThread");
步骤2:启动线程
mHandlerThread.start();
步骤3:创建工作线程Handler & 复写handleMessage()
Handler workHandler = new Handler( handlerThread.getLooper() ) { @OverRide public boolean handleMessage(Message msg) { ...//消息处理 return true; } });
步骤4:使用工作线程Handler向工作线程的消息队列发送消息
Message msg = Message.obtain(); msg.what = 2; //消息的标识 msg.obj = "B"; // 消息的存放 // b. 通过Handler发送消息到其绑定的消息队列 workHandler.sendMessage(msg);
步骤5:结束线程,即停止线程的消息循环
mHandlerThread.quit();
优势:
- 将loop运行在子线程中处理,减轻了主线程的压力,使主线程更流畅
- 串行执行,开启一个线程起到多个线程的作用
- 有自己的消息队列,不会干扰UI线程
劣势:
- 由于每一个任务队列逐步执行,一旦队列耗时过长,消息延时
- 对于IO等操作,线程等待,不能并发
1.2 IntentService 的应用场景和使用姿势?
IntentService 是 Service 的子类,默认为我们开启了一个工作线程,使用这个工作线程逐一处理所有启动请求,在任务执行完毕后会自动停止服务,使用简单,只要实现一个方法 onHandleIntent,该方法会接收每个启动请求的 Intent,能够执行后台工作和耗时操作。可以启动IntentService 多次,而每一个耗时操作会以队列的方式在 IntentService 的 onHandlerIntent 回调方法中执行,并且,每一次只会执行一个工作线程,执行完第一个再执行第二个。并且等待所有消息都执行完后才终止服务。
IntentService 适用于 APP 在不影响当前用户的操作的前提下,在后台默默的做一些操作。
IntentService源码:
- 通过 HandlerThread 单独开启一个名为IntentService 的线程
- 创建一个名叫 ServiceHandler 的内部 Handler
- 把内部Handler与HandlerThread所对应的子线程进行绑定
- 通过 onStartCommand() 传递给服务 intent,依次插入到工作队列中,并逐个发送给 onHandleIntent()
- 通过 onHandleIntent() 来依次处理所有 Intent 请求对象所对应的任务
使用示例:
public class MyIntentService extends IntentService { public static final String TAG = "MyIntentService"; public MyIntentService() { super("MyIntentService"); } @Override protected void onHandleIntent(@Nullable Intent intent) { boolean isMainThread = Thread.currentThread() == Looper.getMainLooper().getThread(); Log.i(TAG, "is main thread:" + isMainThread); // 这里会打印false,说明不是主线程 // 模拟耗时操作 download(); } /** * 模拟执行下载 */ private void download() { try { Thread.sleep(5000); Log.i(TAG, "下载完成..."); } catch (Exception e) { e.printStackTrace(); } } }
1.3 AsyncTask的优点和缺点?
AsyncTask的实现原理:
- AsyncTask是一个抽象类,主要由Handler+2个线程池构成,SERIAL_EXECUTOR是任务队列线程池,用于调度任务,按顺序排列执行,THREAD_POOL_EXECUTOR是执行线程池,真正执行具体的线程任务。Handler用于工作线程和主线程的异步通信。
- AsyncTask<Params,Progress,Result>,其中Params是doInBackground()方法的参数类型,Result是doInBackground()方法的返回值类型,Progress是onProgressUpdate()方法的参数类型。
- 当执行execute()方法的时候,其实就是调用SERIAL_EXECUTOR的execute()方法,就是把任务添加到队列的尾部,然后从头开始取出队列中的任务,调用THREAD_POOL_EXECUTOR的execute()方法依次执行,当队列中没有任务时就停止。
- AsyncTask只能执行一次execute(params)方法,否则会报错。但是SERIAL_EXECUTOR和
THREAD_POOL_EXECUTOR线程池都是静态的,所以可以形成队列。
Q:AsyncTask只能执行一次execute()方法,那么为什么用线程池队列管理 ?
因为SERIAL_EXECUTOR和THREAD_POOL_EXECUTOR线程池都是静态的,所有的AsyncTask实例都共享这2个线程池,因此形成了队列。
Q:AsyncTask的onPreExecute()、doInBackground()、onPostExecute()方法的调用流程?
AsyncTask在创建对象的时候,会在构造函数中创建mWorker(workerRunnable) mFuture(FutureTask)对象。
mWorker实现了Callable接口的call()方法,在call()方法中,调用了doInBackground()方法,并在最后调用了postResult()方法,也就是通过Handler发送消息给主线程,在主线程中调用AsyncTask的finish()方法,决定是调用onCancelled()还是onPostExecute().
mFuture实现了Runnable和Future接口,在创建对象时,初始化成员变量mWorker,在run()方法中,调用mWorker的call()方法。
当asyncTask执行execute()方法的时候,会先调用onPreExecute()方法,然后调用SERIAL_EXECUTOR的execute(mFuture),把任务加入到队列的尾部等待执行。执行的时候调用THREAD_POOL_EXECUTOR的execute(mFuture).
1.4 谈谈你对 Activity.runOnUiThread 的理解?
一般是用来将一个Runnable绑定到主线程,在runOnUiThread源码里面会判断当前Runnable是否是主线程,如果是直接run,如果不是,通过一个默认的空构造函数Handler将Runnable post 到looper里面,创建构造函数Handler,会默认绑定一个主线程的looper对象
1.5 子线程能否更新UI?为什么?
子线程是不能直接更新UI的注意这句话,是不能直接更新,不是不能更新(极端情况
下可更新)
绘制过程要保持同步(否则页面不流畅),而我们的主线程负责绘制UI,极端情况就是,在Activity的onResume(含)之前的生命周期中子线程都可以进行更新UI,也就是 onCreate,onStart和onResume,此时主线程的绘制还没开始。
1.6 谈谈 Handler 机制和原理?
首先在UI线程我们创建了一个Handler实例对象,无论是匿名内部类还是自定义类生成的Handler实例对象,我们都需要对handleMessage方法进行重写,在handleMessage方法中我们可以通过参数msg来写接受消息过后UI线程的逻辑处理,接着我们创建子线程,在子线程中需要更新UI的时候,新建一个Message对象,并且将消息的数据记录在这个消息对象Message的内部,比如arg1,arg2,obj等,然后通过前面的Handler实例对象调用sendMessge方法把这个Message实例对象发送出去,之后这个消息会被存放于MessageQueue中等待被处理,此时MessageQueue的管家Looper正在不停的把MessageQueue存在的消息取出来,通过回调dispatchMessage方法将消息传递给Handler的handleMessage方法,最终前面提到的消息会被Looper从MessageQueue中取出来传递给handleMessage方法。
1.7 为什么在子线程中创建Handler会抛异常?
不能在还没有调用 Looper.prepare() 方法的线程中创建Handler。
因为抛出异常的地方,在mLooper 对象为null的时候,会抛出异常。说明这里的Looper.myLooper();的返回值是null。 只有调用了Looper.prepare()方法,才会构造一个Looper对象并在 ThreadLocal 存储当前线程的Looper 对象。
这样在调用 Looper.myLooper() 时,获取的结果就不会为null。
1.8 试从源码角度分析Handler的post和sendMessage方法的区别和应用场景?
handler.post和handler.sendMessage方法最后都会调用sendMessageAtTime方法进行消息的发送,但是在post方法中message是通过getPostMessage(Runnable r)这个方法获取的message,在这个方法中有这样一句代码m.callback = r ,给message的callback赋值为runnable对象,而在dispatchMessage这个方法中对消息进行分发的时候,先进行了msg.callback != null的判断,如果不为null,消息是通过handleCallback(msg);这个方法处理的,在这个方法中message.callback.run();调用的是post方法传递过来的runnable内的run方法处理消息,如果为空,再进行handler内部的callback判断mCallback != null,如果handler内的callback不为空,执行mCallback.handleMessage(msg)这个处理消息并判断返回是否为true,如果返回true,消息处理结束,如果返回false,消息交给handler的handleMessage(msg)处理。
所以区别就是调用post方法的消息是在post传递的Runnable对象的run方法中处理,而调用sendMessage方法需要重写handleMessage方法或者给handler设置callback,在callback的handleMessage中处理并返回true。
1.9 Handler中有Loop死循环,为什么没有阻塞主线程,原理是什么?
主线程挂起
Looper 是一个死循环, 不断的读取MessageQueue中的消息, loop 方法会调用 MessageQueue 的 next 方法来获取新的消息,next 操作是一个阻塞操作,当没有消息的时候 next 方法会一直阻塞, 进而导致 loop 一直阻塞,理论上 messageQueue.nativePollOnce 会让线程挂起-阻塞-block 住, 但是为什么, 在发送 delay 10s 的消息, 假设消息队列中, 目前只有这一个消息;
那么为什么在这 10s 内, UI是可操作的, 或者列表页是可滑动的, 或者动画还是可以执行的?
先不讲 nativePollOnce 是怎么实现的阻塞, 我们还知道, 另外一个 nativeWake, 是实现线程唤醒的;
那么什么时候会, 触发这个方法的调用呢, 就是在有新消息添加进来的时候, 可是并没有手动添加消息啊?
display 每隔16毫秒, 刷新一次屏幕;
SurfaceFlingerVsyncChoreographer 每隔16毫秒, 发送一个 vSync 信号;
FrameDisplayEventReceiver 收到信号后, 调用 onVsync方法, 通过 handler 消息发送到主线程处理, 所以就会有消息添加进来, UI线程就会被唤醒;
事实上, 安卓系统, 不止有一个屏幕刷新的信号, 还有其他的机制, 比如输入法和系统广播, 也会往主线程的MessageQueue 添加消息;
所以, 可以理解为, 主线程也是随时挂起, 随时被阻塞的;
系统怎么实现的阻塞与唤醒
这种机制是通过pipe(管道)机制实现的;
简单来说, 管道就是一个文件在管道的两端, 分别是两个打开文件的, 文件描述符, 这两个打开文件描述符, 都是对应同一个文件, 其中一个是用来读的, 别一个是用来写的;
一般的使用方式就是, 一个线程通过读文件描述符, 来读管道的内容, 当管道没有内容时, 这个线程就会进入等待状态,
而另外一个线程, 通过写文件描述符, 来向管道中写入内容,写入内容的时候, 如果另一端正有线程, 正在等待管道中的内容, 那么这个线程就会被唤醒;
这个等待和唤醒的操作是如何进行的呢, 这就要借助 Linux系统中的 epoll 机制了, Linux 系统中的 epoll 机制为处理 大批量句柄而作了改进的 poll,是 Linux 下多路复用 IO 接口 select/poll 的增强版本, 它能显著减少程序, 在大量并发连接中, 只有少量活跃的情况下的系统 CPU 利用率;
即当管道中有内容可读时, 就唤醒当前正在等待管道中的内容的线程;
怎么证明, 线程被挂起了
@Override public void onCreateData(@Nullable Bundle bundle) { new Thread() { @SuppressLint("HandlerLeak") @Override public void run() { super.run(); LogTrack.v("thread.id = " + Thread.currentThread().getId()); Looper.prepare(); Handler handler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { super.handleMessage(msg); LogTrack.v("thread.id = " + Thread.currentThread().getId() + ", what = " + msg.what); } }; LogTrack.w("loop.之前"); // 执行了 Looper.loop(); // 执行了 LogTrack.w("loop.之后"); // 无法执行 } }.start(); }
以上就是Android 异步任务和消息机制面试题分析的详细内容,更多关于Android 异步任务消息机制的资料请关注脚本之家其它相关文章!